Commit Graph

27 Commits

Author SHA1 Message Date
Davide
fa293656f4
Fix missing beta check on tvOS (#925)
Reuse the same receipt trick from iOS.

Also, fix a regression in IAPManager.fetchLevelIfNeeded() from #903,
where a guard after an await was dropped leading to reentrancy issues.
2024-11-24 21:19:43 +01:00
Davide
cb530d8a65
Handle TV requirements on connection (#922)
Fixes #913
2024-11-24 01:01:04 +01:00
Davide
bad9e8b58e
Add TV settings tab (#921)
Includes:

- Credits
- Donations
- Diagnostics
- Version

Had to:

- Wrap tab view into a NavigationStack for full-screen navigation
- Take out navigation titles of about subviews
- Customize donations view layout with modifier
- Fix credits and debug log to support scrolling

Closes #914
2024-11-23 23:01:11 +01:00
Davide
3d2a543a54
Fetch products from scratch each time (#905)
Do not risk getting products that may have been removed from sale.
2024-11-21 22:58:24 +01:00
Davide
842375ffce
Test IAPManager (#903)
Refactoring:

- Split tests
- Report purchase to receipt reader (real impl does nothing)
- Abstract BetaChecker from now TestFlightChecker

Bugfixing:

- Fix duplicated code in fetchLevelIfNeeded()
- Fix observeObjects() missing .didUpdate publisher
2024-11-21 18:57:32 +01:00
Davide
b45f9c23fe
Rearrange targets for unit testing (#900)
- Move and rename entities
- Split protocols and default extensions
- Move in-app entities to CommonIAP target
2024-11-20 18:05:47 +01:00
Davide
89d7af4df7
Rethink eligibility checks (#889)
- Allow unrestricted save, but show PurchaseRequiredButton
- Warn however about paid features (FIXME)
- Redesign features in paywall
- Strip already eligible features from paywall
- List required features in restricted alert
- Localize feature descriptions
- Review propagation of paywall modifiers/reasons

Extra:

- Move more domain entities from UILibrary to CommonLibrary
- Default on-demand policy to .any (free feature)
- Fix modals not reappearing after closing with gesture
- Extend UILibrary start-up assertions
2024-11-18 17:43:01 +01:00
Davide
00ba67f135
Refactor buttons/menus
- Add divider in add profile menu
- Reuse trailing dots
2024-11-17 18:45:04 +01:00
Davide
d3e344670b
Resolve excessive profile reloads (#883)
Optimize ProfileManager in several ways:

- Refine control over objectWillChange
- Observe search separately
- Store subscriptions separately (local, remote, search)
- Fix multiple local updates on save/remove/foreground (updating
allProfiles manually)
- Update the library with more optimized NE reloads
- Cancel pending remote import before a new one
- Yield 100ms between imports
- Reorganize code

Extras:

- Only use background context in provider repositories
- Externalize tunnel receipt URL, do not hardcode BundleConfiguration
- Improve some logging

Self-reminder: NEVER use a Core Data background context to observe
changes in CloudKit containers. They just won't be notified (e.g. in
NSFetchedResultsController).

Fixes #857
2024-11-17 11:34:43 +01:00
Davide
589f2f62e0
Revert "Modernize check for TestFlight build" (#880)
Reverts passepartoutvpn/passepartout#875
2024-11-16 13:54:11 +01:00
Davide
55bb2e79c9
Modernize check for TestFlight build (#875)
Closes #874
2024-11-15 15:34:47 +01:00
Davide
5818b36cda
Copy release receipt to tunnel (#871)
Not just the provided one, because it could be the sandbox receipt.

Fixes #870 further.
2024-11-14 19:42:38 +01:00
Davide
f6361ebf06
Fix "Purchase required" in TestFlight (#870)
- Define separate IAPManager instances for app and tunnel (different
receipt URLs)
- Copy app receipt URL over to tunnel before install/connect
- Use AppTransaction to get original build number so that
FallbackReceiptReader is also much simpler now

Fixes #869
2024-11-14 19:12:51 +01:00
Davide
008b78cc7c
Upgrade to Xcode 16 (#858)
- Address further restrictions on actor-isolation by using `nonisolated`
on:
  - Combine subjects
  - Core Data context/controller
  - Blocks
- In previews using inline `@State`, create a custom view instead
- Use `@retroactive` in l10n extensions
- Fix compile error in WireGuardKit
2024-11-13 12:07:30 +01:00
Davide
e514ade036
Add logic to migrate v2 profiles (#854)
Will add UI separately.

Fixes #642
2024-11-12 16:42:19 +01:00
Davide
bd4aeed97a
Disable animations on installed profile view
Fixes #836
2024-11-10 19:33:47 +01:00
Davide
44468b5d1f
Fix regression about handling of Core Data duplicates (#841)
Regression in #839 due to how NSFetchedResultsController was refactored.
Duplicated entities were not excluded from mapping.

Could "crash" the app with these easy steps:

- Pick a profile
- Unshare the profile on iOS
- Unshare the profile on macOS
- Re-share the profile on iCloud on both iOS and macOS
- Save the profile simultaneously on iOS/macOS
- Assertion failure due to duplicates in
ProfileManager.reloadRemoteProfiles() → "Remote repository must not have
duplicates"
2024-11-10 18:13:59 +01:00
Davide
21340e9f56
Rewrite AppContext event handlers (#839)
Loading remote profiles before local profiles may cause duplicated NE
managers. This happened because if local profiles are empty, any remote
profile is imported regardless of their former existence in the local
store. The importer just doesn't know.

Therefore, revisit the sequence of AppContext registrations:

- First off
- Skip Tunnel prepare() because NEProfileRepository.fetch() does it
already
- NE is both Tunnel and ProfileRepository, so calling tunnel.prepare()
loads local NE profiles twice
- onLaunch() - **run this once and before anything else**
  - Read local profiles
  - Reload in-app receipt
  - Observe in-app eligibility → Triggers onEligibleFeatures()
  - Observe profile save → Triggers onSaveProfile()
  - Fetch providers index
- onForeground()
  - Read local profiles
  - Read remote profiles, and toggle CloudKit sync based on eligibility
- onEligibleFeatures()
  - Read remote profiles, and toggle CloudKit sync based on eligibility
- onSaveProfile()
  - Reconnect if necessary
2024-11-10 17:51:28 +01:00
Davide
3a5e3889d3
Add more view modifiers (#838)
1. ThemeProgressViewModifier to replace content with a progress view
while a condition is active
2. ThemeEmptyContentModifier to replace content with a message if an
empty condition is met
3. Replace .opacity(bool ? 1.0 : 0.0) with .opaque(bool)

Reuse:

- 1 in PaywallView and DonateView
- 2 in ProfileContainerView
2024-11-10 12:00:07 +01:00
Davide
e07833b2a4
Revisit in-app eligibility for iCloud sharing (#837)
Restore .sharing feature:

- Merge "Apple TV" into "iCloud" section
  - "Enabled", disabled if ineligible for .sharing
  - "Apple TV", disabled if ineligible for .appleTV || !isShared
- Footer about TV restrictions

Paywalls:

- "Share on iCloud" if ineligible for .sharing
- "Drop TV restriction" if eligible for .sharing but not for .appleTV
  - Applies to full version products (user level 2)
  - Suggest Apple TV product

Restrictions:

- Toggle CloudKit sync on remote repository based on .sharing
eligibility
- Do not start tunnel on Apple TV if ineligible for .appleTV

Fixes:

- Incorrect zip() publishers in remote repository
- Resolve duplicates in Core Data, first profile wins sorted by
lastUpdate descending
- Reload receipt on OOB IAPManager events
2024-11-09 15:20:59 +01:00
Davide
b128b3bf13
Erase shared profiles from iCloud correctly (#827)
Do not delete CloudKit zone. Instead, delete Core Data entities and let
sync do the rest. It's also a "more standard" approach.

Deleting the zone right after the entities legitimately makes deletion
ineffective, because it probably spoils sync.
2024-11-07 12:43:24 +01:00
Davide
68df6066ba
Improve configuration on app launch/active (#821)
- Centralize context initialization/refresh in platform-specific app
delegates
- Prevent multiple calls to .onApplicationActive()
- Simplify local/remote profile fingerprint comparison
- Revert to always replacing Core Data entities
- The remote store somehow ended up having duplicates, which caused
repeated imports of remote profiles due to randomly different
fingerprints
- Optimize reload of in-app receipt
2024-11-06 18:42:42 +01:00
Davide
d8c4e87239
Refactor in-app entities for StoreKit/Kvitto integration (#820)
Refactoring:

- Get receipts from StoreKit Transaction.currentEntitlements
- Search for the originally purchased build in the local receipt anyway
(Kvitto)
- Fall back to release receipt (Kvitto), if any, for feature eligibility
in TestFlight builds
- Parse and verify expiration date in subscriptions
- Decouple in-app identifier composition from BundleConfiguration
- Fix user level features only applied when a receipt was not found

Testing:

- Add StoreKit configuration
- Fake purchases with PP_FAKE_IAP
- Fake user level with PP_USER_LEVEL

Then for reactive receipt reload, detect app activation differently:

- iOS/tvOS on .scenePhase
- macOS on launch and NSWorkspace.didActivateApplicationNotification

As to features:

- Credit former "Full version" purchasers with all current AND future
features, except the Apple TV
2024-11-06 13:20:12 +01:00
Davide
d5ac785bb8
Simulate in-app purchases (#818)
Integrate in-app helper into IAPManager and simulate purchases with an
in-memory receipt.
2024-11-05 18:55:57 +01:00
Davide
f3d13d0cdf
Refactor AppContext creation and profile processing (#810)
Streamline initialization of AppContext objects without singletons,
especially because some are interconnected.

Rethink ProfileProcessor to be the only gateway of profile processing
for:

- Include
- Save
- Connect

Provide closures with access to the IAPManager for eligibility checks.

Finally, take a ProfileProcessor parameter in:

- ProfileManager (for isIncluded and willSave)
- ExtendedTunnel (for willConnect)

so that it's used implicitly without having to put it into the SwiftUI
environment.

Other than that:

- Move AppError to CommonLibrary
- Skip decoding of attributes from Core Data because they are already
part of the profile
2024-11-04 23:34:22 +01:00
Davide
a22584c630
Fine-tune profile management with additional attributes (#807)
Additions to the domain:

- Update rather than replace existing Core Data profile
- Attach ProfileAttributes to Profile.userInfo
- Store one-off `fingerprint` UUID on each save

With the above in place, fix and improve ProfileManager to:

- Use `fingerprint` to compare local/remote profiles in history and thus
avoid local re-import of shared profiles
- Use `deletingRemotely` to delete local profiles when removed from the
remote repository (default false)
- Use `isIncluded` filter to exclude certain profiles from the local
repository (default nil)
2024-11-03 23:35:45 +01:00
Davide
070b661c43
Refactor library targets (#799)
Renames:

- AppUI → UILibrary
- AppUIPlatform → AppUI (conditional umbrella) + AppUI[Main|TV]
- APILibrary/CommonLibrary/UtilsLibrary → Common[API|Library|Utils]

Dependencies:

- AppUI → UILibrary
- UILibrary → AppLibrary, CommonAPI
- AppLibrary → CommonLibrary, CommonUtils
2024-11-02 10:11:59 +01:00