- PaywallView is the paywall content
- PaywallModifier attaches paywall with optional confirmation
- PurchaseRequiredButton presents paywall explicitly
- PaywallReason is the compound input
Refactoring:
- PurchaseRequiredButton takes a custom view
- PurchaseAlertModifier was merged into PaywallModifier
- PurchaseButtonModifier was merged into PurchaseRequiredButton
- Modal options were packed into a single struct
Confirmation alert presented on:
- Connect to ineligible profile (AppCoordinator)
- Save ineligible profile (ProfileCoordinator)
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
- Move about subviews to UILibrary
- Refactor about to single coordinator + platform views
- Refactor debug log to single view + content views
- Take out debug log routes from about routes
- Rename Settings* to Preferences*
- Reuse empty modifier in debug log
- Fix a visual bug in .themeTrailingValue() (extra Spacer)
Preparation for #914
To get access to modules, try to avoid full Profile objects. Instead,
replace the coupled ProfileHeader occurrences with a new intermediary
ProfilePreview everywhere.
This way, a ProfileProcessor can inject the localized modules
descriptions from above with the preview() method.
First, rename `force` parameter of ProfileManager.save() to `isLocal`,
because it's meant to be used when saving local profiles. In such
scenario, a profile that is also remotely shared _must_ be re-saved to
the remote repository.
This was not being done when selecting a provider server, and it could
be noticed because the other devices would only receive the iCloud
update after editing the profile and re-doing a manual "Save". Only at
that point would the new profile be re-shared on iCloud.
With some housekeeping.
Bugfixing:
- Do NOT skip empty remote profiles, allow removal when mirroring
- Look up profile in all profiles, not just filtered
- Posptone non-included profile removal
Refactoring:
- Rename ProfileProcessor to InAppProcessor
- Provides ProfileProcessor + TunnelProcessor protocols
- willSave -> willRebuild (because not always called on save)
- Notify ProfileManager import events
If the profile is ineligible for features other than .interactiveLogin,
the interactive prompt would still be presented, but the user would hit
the paywall right afterwards.
StoreKit ProductView performs the purchases internally without calling
IAPManager.purchase(), which causes the IAPManager state to be
momentarily outdated.
Leverage PaywallView.onComplete() to reload the receipt and eventually
trigger IAPManager.objectWillChange, so that the app is immediately
unlocked on a successful purchase.
Visually clarify that a profile requires a purchase to be enabled.
- Implement AppFeatureRequiring in Profile
- Refactor IAPManager.verify() accordingly
- Pre-compute required features in ProfileManager via ProfileProcessor
- 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
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
- Drop the .importing / .imported steps
- Animate rows re-sorting during process
- Rephrase some strings better
- Test fake migration with launch argument
Finalize migration flow:
- Add entry to "Add" menu
- Suggest to migrate old profiles when there are no profiles
- Add informational message
- Keep included profiles on top
- Allow deletion of migratable profiles
- Fix duplicated Form in preview
- Rename views and models
Improve some Theme modifiers:
- Empty message with full screen option
- Progress modifier with custom view
- Confirmation dialog with custom message
- 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
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
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
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
Move the following dependencies:
- OpenVPN/OpenSSL
- WireGuard/Go
up the chain until the main App/Tunnel targets, so that UILibrary and
CommonLibrary can abstract from these unnecessary details. Instead, give
module views access to generic implementations via Registry.
Incidentally, this fixes an issue preventing TV previews from working
due to OpenSSL linkage.
- Use StoreKit views when available
- Offer one-time purchase
- Recurring subscriptions for all features
- Restore purchases
Remove .siri (Shortcuts), now free.
Closes#819Closes#469
- 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