Store module preferences in the Profile.userInfo field for atomicity.
Access and modification are dramatically simplified, and synchronization
comes for free.
On the other side, fix provider preferences synchronization by using
viewContext for the CloudKit container.
Fixes#992
- Save/rollback was done outside of MOC
- Use different contexts for module/provider preferences
- Save providers → also saves modules
- Discard modules → also discards providers
- Use background context because it's not automatically merged (can
rollback)
- Expose ModulePreferences in OpenVPNView as StateObject
- Rework Blacklist to a more reusable ObservableList
- Reapply #988
Exclude endpoints from OpenVPN modules and providers with the generic
Blacklist<T> observable. Eventually, rebuild the Profile in
PacketTunnelProvider (via DefaultTunnelProcessor) with the applied
exclusions from preferences.
Revisit approach to preferences:
- Module preferences
- Tied to the module and therefore to the parent profile
- Load/save in ProfileEditor on request (rather than on
ProfileEditor.load)
- Provider preferences
- Shared globally across profiles
- Load/save in module view if needed
For more consistency with Core Data:
- Revert to observables for both module and provider preferences
- Treat excluded endpoints as relationships rather than a serialized
Array
- Add/remove single relationships over bulk delete + re-add
- Do not map the relationships, Blacklist only needs exists/add/remove:
- isExcludedEndpoint
- addExcludedEndpoint
- removeExcludedEndpoint
Some clean-up:
- Move the importer logic to OpenVPNView.ImportModifier
- Move the preview data to OpenVPN.Configuration.Builder.forPreviews
- Drop objectWillChange.send() on .repository didSet to avoid potential
recursion during SwiftUI updates
Closes#971
Sort out the increasing mess coming from:
- AppContext*
- Dependencies
- Shared*
by doing the following:
- Keep in the "Shared" folder only the entities actually shared by
App/Tunnel
- Create TunnelContext
- Move AppContext and related to the App/Context folder
- Move TunnelContext and related to the Tunnel/Context folder
- Delete Shared+* extensions, use AppContext/TunnelContext singletons
from the app
- Create a Dependencies factory singleton to create entities in a single
place
- Split extensions by domain
- Make it clear with `func` vs `var` when a dependency method returns a
new instance
Formerly via blocks, now with final classes.
App:
- ProfileProcessor
- AppTunnelProcessor
- Implemented by DefaultAppProcessor in app
- Implemented by MockAppProcessor in UILibrary (for previews)
Tunnel:
- PacketTunnelProcessor
- Implemented by DefaultTunnelProcessor
Simplify preferences model by doing a bulk load/save together with
load/save Profile. ModulePreferences is now a struct rather than an
ObservableObject, because it doesn't need ad hoc observation. It's just
a binding to ProfileEditor.preferences
Fix:
- Disable CloudKit in tunnel singleton of PreferencesManager
(.sharedForTunnel)
Additionally:
- Replace MainActor in PreferencesManager with Sendable (immutable)
- Replace MainActor from ProviderPreferencesRepository with Sendable
(syncs on NSManagedObjectContext)
- Drop ModuleMetadata for good
Replace favorites entities with a PreferencesManager, that returns
observables for:
- Module preferences (by module UUID)
- Provider preferences (by ProviderID)
Automate preferences availability in:
- Module views (empty for now)
- VPN server view (favorites)
Synchronize preferences by making this a CloudKit container. Preferences
are also available in the Tunnel by storing the container in the App
Group.
Let the AppCoordinator take care of the connection requirements via
modals:
- onInteractiveLogin() - now presented on AppError
- onProviderEntityRequired()
- onPurchaseRequired()
- Any other connection error
Subviews must not use tunnel.connect(), rather they route connection
requests via the ConnectionFlow callbacks. In particular, migrate to the
AppCoordinator the connection logic from:
- TunnelToggleButton.perform()
- ProviderEntitySelector.onSelect()
onInteractiveLogin() and onPurchaseRequired() are now handled
internally, while onProviderEntityRequired() is kept public because it's
how subviews may present the entity selector.
Extras:
- Avoid modals overlap with a 500ms delay
- Shrink interactive login size on macOS
Clarify the use of contexts:
- **Production** (.shared)
- **Previews** (.mock → .forPreviews)
- ONLY use it in UILibrary for, well, previews
- This context has dumb profiles with UUIDs as names
- Registry is fake
- **UI Tests** (.forUITesting)
- Add new context for UI testing
- Selected based on command line arguments
- This context has mock data tuned for decent screenshots
- Registry is real
Share the same InAppProcessor in .shared and .forTesting contexts
because the app behavior was inconsistent regarding e.g. in-app
purchases.
Ready for screenshots generation, except for the tests themselves and
the TV target.
- More customizations while UI testing
- Act as full version user in IAPManager
- Override layout with default to .grid if isBigDevice
- Show module names in profile list/grid
- Improve mock Profile/ProfileManager
- Meaningful profile names
- iCloud/TV icons
- Initial modules
- Improve XCTest extensions
- Screenshot destination (attachment/temporary)
- Screenshot target (window/sheet)
- Print saved temporary URL at the end (may help with CI)
- Append device name to screenshot filename
- Tests
- Refactor actions with the [Page Object
pattern](https://swiftwithmajid.com/2021/03/24/ui-testing-using-page-object-pattern-in-swift/)
- Perform iPad screenshots in landscape
- Split simple flow tests and screenshots
- Add "Connect to" test
Closes#681
Create UITesting target with:
- AppCommandLine/AppEnvironment: strongly typed refactoring of PP_*
environment values
- AccessibilityInfo: identifies and locates elements for UI testing
Make the app behave differently when launched with `.uiTesting`, and
expose the flag to SwiftUI via `.environment(\.isUITesting)` to:
- Use the mock AppContext
- Skip onboarding
Add PassepartoutUITests target with two screenshot tests:
- Connected screen
- Profile modal
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.
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
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
- 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
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.
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
Based on in-app eligibility, expire TV profiles after 10 minutes.
Refactor/redesign general sections and offer .sharing feature for free,
it makes it simpler to focus on Apple TV product.
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
Add profile attribute `isAvailableForTV` and set specific behavior to:
- Observe shared profiles and delete locally when unshared
- Only keep locally those profiles with the TV attribute enabled
- Add toggle in UI
Also, improve filters by constraining related fields:
- Pick countries from the filtered category
- Pick presets from those available in the currently filtered servers
Closes#705
- Drop logic behind connection button tasks, let the library handle
concurrency
- Drop AppContext observation of saved profiles for reconnection, let
save() actively decide
- NETunnelStrategy and NETunnelManagerRepository are now a single entity
- Avoid flickering when toggling same profile
Resolve some flickering and state inconsistency due to overextended
observation of VPNProviderManager. Narrow down its scope to
VPNProviderServerView.
The downside of that, for now, is that servers are loaded "lazily late",
but this flow will make region selection from home easier.
Finally, show filters in popover on iPad.