diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme index 8649b99f..5824feb5 100644 --- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme +++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme @@ -126,7 +126,7 @@ Bool { + guard productManager.isEligible(forFeature: .networkSettings) else { + pp_log.warning("Ignore network settings, not eligible") + return false + } + return true + } + // eligibility: reset on-demand rules if no trusted networks private func isEligibleForOnDemandRules() -> Bool { guard productManager.isEligible(forFeature: .trustedNetworks) else { diff --git a/Passepartout/App/Shared/Constants/Constants+Extensions.swift b/Passepartout/App/Shared/Constants/Constants+Extensions.swift index 769c6575..05a8a68c 100644 --- a/Passepartout/App/Shared/Constants/Constants+Extensions.swift +++ b/Passepartout/App/Shared/Constants/Constants+Extensions.swift @@ -75,6 +75,8 @@ extension Constants { #else static let lastFullVersionBuild: (Int, LocalProduct) = (0, .fullVersion_macOS) #endif + + static let lastNetworkSettingsBuild = 2999 private static var isBeta: Bool { #if os(iOS) diff --git a/Passepartout/App/Shared/InApp/LocalProduct.swift b/Passepartout/App/Shared/InApp/LocalProduct.swift index 3ae9592d..12dc72a1 100644 --- a/Passepartout/App/Shared/InApp/LocalProduct.swift +++ b/Passepartout/App/Shared/InApp/LocalProduct.swift @@ -68,6 +68,8 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable { // MARK: Features static let allProviders = LocalProduct(featureId: "all_providers") + + static let networkSettings = LocalProduct(featureId: "network_settings") static let trustedNetworks = LocalProduct(featureId: "trusted_networks") @@ -81,6 +83,7 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable { static let allFeatures: [LocalProduct] = [ .allProviders, + .networkSettings, .trustedNetworks, .siriShortcuts, .fullVersion_iOS, diff --git a/Passepartout/App/Shared/InApp/ProductManager.swift b/Passepartout/App/Shared/InApp/ProductManager.swift index 9719108c..a185e16b 100644 --- a/Passepartout/App/Shared/InApp/ProductManager.swift +++ b/Passepartout/App/Shared/InApp/ProductManager.swift @@ -49,12 +49,16 @@ class ProductManager: NSObject, ObservableObject { let lastFullVersionBuild: (Int, LocalProduct) + let lastNetworkSettingsBuild: Int + init( appType: AppType, - lastFullVersionBuild: (Int, LocalProduct) + lastFullVersionBuild: (Int, LocalProduct), + lastNetworkSettingsBuild: Int ) { self.appType = appType self.lastFullVersionBuild = lastFullVersionBuild + self.lastNetworkSettingsBuild = lastNetworkSettingsBuild } } @@ -193,6 +197,11 @@ class ProductManager: NSObject, ObservableObject { } func isEligible(forFeature feature: LocalProduct) -> Bool { + if let purchasedAppBuild = purchasedAppBuild { + if feature == .networkSettings && purchasedAppBuild <= cfg.lastNetworkSettingsBuild { + return true + } + } #if os(iOS) return isFullVersion() || purchasedFeatures.contains(feature) #else diff --git a/Passepartout/App/iOS/Views/ProfileView+Configuration.swift b/Passepartout/App/iOS/Views/ProfileView+Configuration.swift index 23b37200..e819af43 100644 --- a/Passepartout/App/iOS/Views/ProfileView+Configuration.swift +++ b/Passepartout/App/iOS/Views/ProfileView+Configuration.swift @@ -34,6 +34,10 @@ extension ProfileView { @Binding private var modalType: ModalType? + private var isEligibleForNetworkSettings: Bool { + productManager.isEligible(forFeature: .networkSettings) + } + private var isEligibleForTrustedNetworks: Bool { productManager.isEligible(forFeature: .trustedNetworks) } @@ -75,10 +79,20 @@ extension ProfileView { Label(L10n.Account.title, systemImage: themeAccountImage) } } - NavigationLink { - NetworkSettingsView(currentProfile: currentProfile) - } label: { - Label(L10n.NetworkSettings.title, systemImage: themeNetworkSettingsImage) + + // eligibility: enter network settings or present paywall + if isEligibleForNetworkSettings { + NavigationLink { + NetworkSettingsView(currentProfile: currentProfile) + } label: { + networkSettingsRow + } + } else { + Button { + modalType = .paywallNetworkSettings + } label: { + networkSettingsRow + } } // eligibility: enter trusted networks or present paywall @@ -97,6 +111,10 @@ extension ProfileView { } } } + + private var networkSettingsRow: some View { + Label(L10n.NetworkSettings.title, systemImage: themeNetworkSettingsImage) + } private var onDemandRow: some View { Label(L10n.OnDemand.title, systemImage: themeOnDemandImage) diff --git a/Passepartout/App/iOS/Views/ProfileView.swift b/Passepartout/App/iOS/Views/ProfileView.swift index a4dfe52e..6c140743 100644 --- a/Passepartout/App/iOS/Views/ProfileView.swift +++ b/Passepartout/App/iOS/Views/ProfileView.swift @@ -31,6 +31,8 @@ struct ProfileView: View { enum ModalType: Int, Identifiable { case rename + case paywallNetworkSettings + case paywallTrustedNetworks var id: Int { @@ -123,6 +125,14 @@ struct ProfileView: View { RenameView(currentProfile: profileManager.currentProfile) }.themeGlobal() + case .paywallNetworkSettings: + NavigationView { + PaywallView( + modalType: $modalType, + feature: .networkSettings + ) + }.themeGlobal() + case .paywallTrustedNetworks: NavigationView { PaywallView( diff --git a/PassepartoutCore/Sources/PassepartoutCore/Extensions/OpenVPNSettings+VPNConfiguration.swift b/PassepartoutCore/Sources/PassepartoutCore/Extensions/OpenVPNSettings+VPNConfiguration.swift index 1031d9d3..4fe77781 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Extensions/OpenVPNSettings+VPNConfiguration.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Extensions/OpenVPNSettings+VPNConfiguration.swift @@ -41,10 +41,12 @@ extension Profile.OpenVPNSettings: VPNConfigurationProviding { } // network settings - customBuilder.applyGateway(from: parameters.networkSettings.gateway) - customBuilder.applyDNS(from: parameters.networkSettings.dns) - customBuilder.applyProxy(from: parameters.networkSettings.proxy) - customBuilder.applyMTU(from: parameters.networkSettings.mtu) + if parameters.withNetworkSettings { + customBuilder.applyGateway(from: parameters.networkSettings.gateway) + customBuilder.applyDNS(from: parameters.networkSettings.dns) + customBuilder.applyProxy(from: parameters.networkSettings.proxy) + customBuilder.applyMTU(from: parameters.networkSettings.mtu) + } let customConfiguration = customBuilder.build() diff --git a/PassepartoutCore/Sources/PassepartoutCore/Extensions/WireGuardSettings+VPNConfiguration.swift b/PassepartoutCore/Sources/PassepartoutCore/Extensions/WireGuardSettings+VPNConfiguration.swift index 8618c92d..e5683c1d 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Extensions/WireGuardSettings+VPNConfiguration.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Extensions/WireGuardSettings+VPNConfiguration.swift @@ -33,9 +33,11 @@ extension Profile.WireGuardSettings: VPNConfigurationProviding { var customBuilder = configuration.builder() // network settings - customBuilder.applyGateway(from: parameters.networkSettings.gateway) - customBuilder.applyDNS(from: parameters.networkSettings.dns) - customBuilder.applyMTU(from: parameters.networkSettings.mtu) + if parameters.withNetworkSettings { + customBuilder.applyGateway(from: parameters.networkSettings.gateway) + customBuilder.applyDNS(from: parameters.networkSettings.dns) + customBuilder.applyMTU(from: parameters.networkSettings.mtu) + } let customConfiguration = customBuilder.build() diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift index 706f5632..5bc5f633 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift @@ -54,6 +54,7 @@ extension VPNManager { appGroup: profileManager.appGroup, preferences: appManager.preferences, passwordReference: profileManager.passwordReference(forProfile: profile), + withNetworkSettings: isNetworkSettingsSupported(), withCustomRules: isOnDemandRulesSupported() ) diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift index c1454be3..a92e727e 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift @@ -41,6 +41,8 @@ public class VPNManager: ObservableObject, RateLimited { private let strategy: VPNManagerStrategy + public var isNetworkSettingsSupported: () -> Bool + public var isOnDemandRulesSupported: () -> Bool // MARK: State @@ -72,6 +74,7 @@ public class VPNManager: ObservableObject, RateLimited { self.profileManager = profileManager self.providerManager = providerManager self.strategy = strategy + isNetworkSettingsSupported = { true } isOnDemandRulesSupported = { true } currentState = ObservableState() diff --git a/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift b/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift index efc88bdf..8c225b0c 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift @@ -47,6 +47,8 @@ struct VPNConfigurationParameters { let passwordReference: Data? + let withNetworkSettings: Bool + let onDemandRules: [NEOnDemandRule] init( @@ -54,6 +56,7 @@ struct VPNConfigurationParameters { appGroup: String, preferences: AppPreferences, passwordReference: Data?, + withNetworkSettings: Bool, withCustomRules: Bool ) { title = profile.header.name @@ -62,6 +65,7 @@ struct VPNConfigurationParameters { networkSettings = profile.networkSettings username = !profile.account.username.isEmpty ? profile.account.username : nil self.passwordReference = passwordReference + self.withNetworkSettings = withNetworkSettings onDemandRules = profile.onDemand.rules(withCustomRules: withCustomRules) } }