diff --git a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index be4a2c01..18f6e455 100644 --- a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,7 +41,7 @@ "kind" : "remoteSourceControl", "location" : "git@github.com:passepartoutvpn/passepartoutkit-source", "state" : { - "revision" : "c0a615bc7a85d68a9b00d3703d0dae6efab9bdd2" + "revision" : "db02de5247d0231ff06fb3c4d166645a434255be" } }, { diff --git a/Passepartout/Library/Package.swift b/Passepartout/Library/Package.swift index e27bb2a4..52441ebc 100644 --- a/Passepartout/Library/Package.swift +++ b/Passepartout/Library/Package.swift @@ -44,7 +44,7 @@ let package = Package( ], dependencies: [ // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.11.0"), - .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "c0a615bc7a85d68a9b00d3703d0dae6efab9bdd2"), + .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "db02de5247d0231ff06fb3c4d166645a434255be"), // .package(path: "../../../passepartoutkit-source"), .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"), // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"), diff --git a/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift b/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift index ab87d5c6..d317a5b3 100644 --- a/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift +++ b/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonLibrary import Foundation import PassepartoutKit @_exported import UILibrary @@ -42,19 +43,23 @@ private extension AppUIMain { .openVPN ] ModuleType.allCases.forEach { moduleType in - let builder = moduleType.newModule(with: registry) - guard builder is any ModuleViewProviding else { - fatalError("\(moduleType): is not ModuleViewProviding") - } - if providerModuleTypes.contains(moduleType) { - do { - let module = try builder.tryBuild() + do { + let builder = moduleType.newModule(with: registry) + let module = try builder.tryBuild() + + // ModuleViewProviding + guard builder is any ModuleViewProviding else { + fatalError("\(moduleType): is not ModuleViewProviding") + } + + // ProviderEntityViewProviding + if providerModuleTypes.contains(moduleType) { guard module is any ProviderEntityViewProviding else { fatalError("\(moduleType): is not ProviderEntityViewProviding") } - } catch { - fatalError("\(moduleType): empty module is not buildable") } + } catch { + fatalError("\(moduleType): empty module is not buildable: \(error)") } } } diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift index 799cd6ec..8e00bd99 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift @@ -54,6 +54,9 @@ public struct AppCoordinator: View, AppCoordinatorConforming { @State private var migrationPath = NavigationPath() + @State + private var paywallReason: PaywallReason? + @StateObject private var errorHandler: ErrorHandler = .default() @@ -72,6 +75,7 @@ public struct AppCoordinator: View, AppCoordinatorConforming { contentView .toolbar(content: toolbarContent) } + .modifier(PaywallModifier(reason: $paywallReason)) .themeModal( item: $modalRoute, size: modalRoute?.size ?? .large, @@ -87,6 +91,8 @@ public struct AppCoordinator: View, AppCoordinatorConforming { extension AppCoordinator { enum ModalRoute: Identifiable { + case about + case editProfile case editProviderEntity(Profile, Module, SerializedProvider) @@ -95,15 +101,13 @@ extension AppCoordinator { case settings - case about - var id: Int { switch self { - case .editProfile: return 1 - case .editProviderEntity: return 2 - case .migrateProfiles: return 3 - case .settings: return 4 - case .about: return 5 + case .about: return 1 + case .editProfile: return 2 + case .editProviderEntity: return 3 + case .migrateProfiles: return 4 + case .settings: return 5 } } @@ -171,6 +175,11 @@ extension AppCoordinator { }, onMigrateProfiles: { modalRoute = .migrateProfiles + }, + onPurchaseRequired: { features in + setLater(.purchase(features)) { + paywallReason = $0 + } } ) ) @@ -197,6 +206,12 @@ extension AppCoordinator { @ViewBuilder func modalDestination(for item: ModalRoute?) -> some View { switch item { + case .about: + AboutRouterView( + profileManager: profileManager, + tunnel: tunnel + ) + case .editProfile: ProfileCoordinator( profileManager: profileManager, @@ -205,9 +220,7 @@ extension AppCoordinator { moduleViewFactory: DefaultModuleViewFactory(registry: registry), modally: true, path: $profilePath, - onDismiss: { - present(nil) - } + onDismiss: onDismiss ) case .editProviderEntity(let profile, let module, let provider): @@ -230,12 +243,6 @@ extension AppCoordinator { case .settings: SettingsView(profileManager: profileManager) - case .about: - AboutRouterView( - profileManager: profileManager, - tunnel: tunnel - ) - default: EmptyView() } @@ -256,11 +263,15 @@ extension AppCoordinator { present(.editProfile) } + func onDismiss() { + present(nil) + } + func present(_ route: ModalRoute?) { + // XXX: this is a workaround for #791 on iOS 16 - Task { - try await Task.sleep(for: .milliseconds(50)) - modalRoute = route + setLater(route) { + modalRoute = $0 } } } diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift index e2894acf..3ba3bcfb 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift @@ -201,7 +201,12 @@ private struct ToggleButton: View { nextProfileId: $nextProfileId, interactiveManager: interactiveManager, errorHandler: errorHandler, - onProviderEntityRequired: flow?.onEditProviderEntity, + onProviderEntityRequired: { + flow?.onEditProviderEntity($0) + }, + onPurchaseRequired: { + flow?.onPurchaseRequired($0) + }, label: { _ in ThemeImage(.tunnelToggle) .scaleEffect(1.5, anchor: .trailing) diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift index 34d08e90..2d7929ac 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift @@ -69,13 +69,20 @@ private extension ProfileContextMenu { profile: profile, nextProfileId: .constant(nil), interactiveManager: interactiveManager, - errorHandler: errorHandler - ) { - ThemeImageLabel( - $0 ? Strings.Global.enable : Strings.Global.disable, - $0 ? .tunnelEnable : .tunnelDisable - ) - } + errorHandler: errorHandler, + onProviderEntityRequired: { + flow?.onEditProviderEntity($0) + }, + onPurchaseRequired: { + flow?.onPurchaseRequired($0) + }, + label: { + ThemeImageLabel( + $0 ? Strings.Global.enable : Strings.Global.disable, + $0 ? .tunnelEnable : .tunnelDisable + ) + } + ) } var providerConnectToButton: some View { @@ -92,10 +99,14 @@ private extension ProfileContextMenu { TunnelRestartButton( tunnel: tunnel, profile: profile, - errorHandler: errorHandler - ) { - ThemeImageLabel(Strings.Global.restart, .tunnelRestart) - } + errorHandler: errorHandler, + onPurchaseRequired: { + flow?.onPurchaseRequired($0) + }, + label: { + ThemeImageLabel(Strings.Global.restart, .tunnelRestart) + } + ) } var profileEditButton: some View { diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileFlow.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileFlow.swift index 4739327d..ad5370d8 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileFlow.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileFlow.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonLibrary import Foundation import PassepartoutKit @@ -32,4 +33,6 @@ struct ProfileFlow { let onEditProviderEntity: (Profile) -> Void let onMigrateProfiles: () -> Void + + let onPurchaseRequired: (Set) -> Void } diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift index 7c9f028f..56667b94 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift @@ -122,7 +122,12 @@ private extension ProfileRowView { nextProfileId: $nextProfileId, interactiveManager: interactiveManager, errorHandler: errorHandler, - onProviderEntityRequired: flow?.onEditProviderEntity, + onProviderEntityRequired: { + flow?.onEditProviderEntity($0) + }, + onPurchaseRequired: { + flow?.onPurchaseRequired($0) + }, label: { _ in ProfileCardView( style: style, diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/TunnelRestartButton.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/TunnelRestartButton.swift index 2b27f3a5..a18739be 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/TunnelRestartButton.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/TunnelRestartButton.swift @@ -37,6 +37,8 @@ struct TunnelRestartButton