diff --git a/Passepartout/Library/Sources/AppUI/IAP/AppFeature.swift b/Passepartout/Library/Sources/AppUI/IAP/AppFeature.swift index edc5476e..9a812ab4 100644 --- a/Passepartout/Library/Sources/AppUI/IAP/AppFeature.swift +++ b/Passepartout/Library/Sources/AppUI/IAP/AppFeature.swift @@ -28,6 +28,8 @@ import Foundation public enum AppFeature: String, CaseIterable { case appleTV + case interactiveLogin + case networkSettings case onDemand @@ -37,6 +39,7 @@ public enum AppFeature: String, CaseIterable { case siri public static let allCases: [AppFeature] = [ + .interactiveLogin, .networkSettings, .onDemand, .providers, diff --git a/Passepartout/Library/Sources/AppUI/IAP/IAPManager.swift b/Passepartout/Library/Sources/AppUI/IAP/IAPManager.swift index a47de11c..59eecc3b 100644 --- a/Passepartout/Library/Sources/AppUI/IAP/IAPManager.swift +++ b/Passepartout/Library/Sources/AppUI/IAP/IAPManager.swift @@ -35,6 +35,8 @@ public final class IAPManager: ObservableObject { private let receiptReader: any AppReceiptReader + private let unrestrictedFeatures: Set + private let productsAtBuild: BuildProducts? private(set) var userLevel: AppUserLevel @@ -48,10 +50,12 @@ public final class IAPManager: ObservableObject { public init( customUserLevel: AppUserLevel? = nil, receiptReader: any AppReceiptReader, + unrestrictedFeatures: Set = [], productsAtBuild: BuildProducts? = nil ) { self.customUserLevel = customUserLevel self.receiptReader = receiptReader + self.unrestrictedFeatures = unrestrictedFeatures self.productsAtBuild = productsAtBuild userLevel = .undefined purchasedProducts = [] @@ -106,6 +110,10 @@ public final class IAPManager: ObservableObject { eligibleFeatures = Set(userLevel.features) } + unrestrictedFeatures.forEach { + eligibleFeatures.insert($0) + } + pp_log(.app, .notice, "Purchased products: \(purchasedProducts.map(\.rawValue))") pp_log(.app, .notice, "Eligible features: \(eligibleFeatures)") objectWillChange.send() diff --git a/Passepartout/Library/Sources/AppUI/IAP/KvittoReceiptReader.swift b/Passepartout/Library/Sources/AppUI/IAP/KvittoReceiptReader.swift index 2bc84923..ad25c480 100644 --- a/Passepartout/Library/Sources/AppUI/IAP/KvittoReceiptReader.swift +++ b/Passepartout/Library/Sources/AppUI/IAP/KvittoReceiptReader.swift @@ -47,7 +47,7 @@ public final class KvittoReceiptReader: AppReceiptReader { guard let receipt else { let releaseUrl = url.deletingLastPathComponent().appendingPathComponent("receipt") guard releaseUrl != url else { -#if !targetEnvironment(simulator) +#if !os(macOS) && !targetEnvironment(simulator) assertionFailure("How can release URL be equal to sandbox URL in TestFlight?") #endif return nil diff --git a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift index 82344877..bbaa83fd 100644 --- a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift +++ b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift @@ -382,6 +382,10 @@ public enum Strings { } } } + public enum Purchase { + /// Log in interactively + public static let interactive = Strings.tr("Localizable", "modules.openvpn.purchase.interactive", fallback: "Log in interactively") + } } public enum Wireguard { /// Allowed IPs diff --git a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings index 9fe0a722..44301db9 100644 --- a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings @@ -179,6 +179,7 @@ "modules.on_demand.ethernet" = "Ethernet"; "modules.on_demand.ssid.add" = "Add SSID"; +"modules.openvpn.purchase.interactive" = "Log in interactively"; "modules.openvpn.pull" = "Pull"; "modules.openvpn.redirect_gateway" = "Redirect gateway"; "modules.openvpn.credentials" = "Credentials"; diff --git a/Passepartout/Library/Sources/AppUI/Views/About/AboutRouterView.swift b/Passepartout/Library/Sources/AppUI/Views/About/AboutRouterView.swift index cb93de36..41129e06 100644 --- a/Passepartout/Library/Sources/AppUI/Views/About/AboutRouterView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/About/AboutRouterView.swift @@ -97,5 +97,5 @@ extension AboutRouterView { AboutRouterView( tunnel: .mock ) - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift b/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift index ec1386c3..f11a5b09 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/AppInlineCoordinator.swift @@ -161,6 +161,5 @@ private extension AppInlineCoordinator { tunnel: .mock, registry: Registry() ) - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift b/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift index d10b689e..57e0ce56 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/AppModalCoordinator.swift @@ -149,6 +149,5 @@ extension AppModalCoordinator { tunnel: .mock, registry: Registry() ) - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/AppToolbar.swift b/Passepartout/Library/Sources/AppUI/Views/App/AppToolbar.swift index 3e1f02b2..687b12b6 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/AppToolbar.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/AppToolbar.swift @@ -118,5 +118,5 @@ private extension AppToolbar { } .frame(width: 600, height: 400) } - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift b/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift index 3a43d4c8..ac7a2643 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/ProfileContainerView.swift @@ -153,7 +153,6 @@ private struct PreviewView: View { onEdit: { _ in } ) } - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift b/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift index 3b01d0d5..7b35afaf 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/ProfileGridView.swift @@ -148,6 +148,5 @@ private extension ProfileGridView { onEdit: { _ in } ) .themeWindow(width: 600, height: 300) - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift b/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift index f296e0fa..5e51a6c4 100644 --- a/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/App/ProfileListView.swift @@ -137,6 +137,5 @@ private extension ProfileListView { errorHandler: .default(), onEdit: { _ in } ) - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Diagnostics/DiagnosticsView.swift b/Passepartout/Library/Sources/AppUI/Views/Diagnostics/DiagnosticsView.swift index b99292b6..9559c84b 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Diagnostics/DiagnosticsView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Diagnostics/DiagnosticsView.swift @@ -198,7 +198,5 @@ private extension DiagnosticsView { .init(date: Date().addingTimeInterval(-600), url: URL(string: "http://three.com")!) ] } - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) - .environmentObject(IAPManager.mock) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/IPView+Route.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/IPView+Route.swift index aa5b7252..895d3c1c 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/IPView+Route.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/IPView+Route.swift @@ -136,5 +136,5 @@ private extension IPView.RouteView { } return Preview() - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift index 19b18a53..69cfa526 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OnDemandView.swift @@ -65,7 +65,7 @@ private struct OnDemandView: View { var body: some View { Group { enabledSection - restrictedSection + restrictedArea } .asModuleView(with: editor, draft: draft) .modifier(PaywallModifier(reason: $paywallReason)) @@ -86,11 +86,11 @@ private extension OnDemandView { } @ViewBuilder - var restrictedSection: some View { + var restrictedArea: some View { switch iapManager.paywallReason(forFeature: .onDemand) { - case .purchase(let feature): + case .purchase(let appFeature): Button(Strings.Modules.OnDemand.purchase) { - paywallReason = .purchase(feature) + paywallReason = .purchase(appFeature) } case .restricted: diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Credentials.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Credentials.swift index f1bccf68..0593dba8 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Credentials.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Credentials.swift @@ -29,6 +29,9 @@ import SwiftUI extension OpenVPNView { struct CredentialsView: View { + @EnvironmentObject + private var iapManager: IAPManager + @Binding var isInteractive: Bool @@ -43,11 +46,12 @@ extension OpenVPNView { @State private var otp = "" + @State + private var paywallReason: PaywallReason? + var body: some View { Form { - if !isAuthenticating { - interactiveSection - } + restrictedArea inputSection } .themeAnimation(on: isInteractive, category: .modules) @@ -58,7 +62,7 @@ extension OpenVPNView { builder = credentials?.builder() ?? OpenVPN.Credentials.Builder() } .onChange(of: builder) { - if isAuthenticating { + if isEligibleForInteractiveLogin, isAuthenticating { credentials = $0.buildForAuthentication(otp: otp) } else { credentials = $0.build() @@ -67,15 +71,38 @@ extension OpenVPNView { .onChange(of: otp) { credentials = builder.buildForAuthentication(otp: $0) } + .modifier(PaywallModifier(reason: $paywallReason)) } } } private extension OpenVPNView.CredentialsView { + var isEligibleForInteractiveLogin: Bool { + iapManager.isEligible(for: .interactiveLogin) + } + var otpMethods: [OpenVPN.Credentials.OTPMethod] { [.none, .append, .encode] } + @ViewBuilder + var restrictedArea: some View { + switch iapManager.paywallReason(forFeature: .interactiveLogin) { + case .purchase(let appFeature): + Button(Strings.Modules.Openvpn.Purchase.interactive) { + paywallReason = .purchase(appFeature) + } + + case .restricted: + EmptyView() + + default: + if !isAuthenticating { + interactiveSection + } + } + } + var interactiveSection: some View { Group { Toggle(Strings.Modules.Openvpn.Credentials.interactive, isOn: $isInteractive) @@ -108,7 +135,7 @@ private extension OpenVPNView.CredentialsView { ThemeSecureField(title: Strings.Global.password, text: $builder.password, placeholder: Strings.Placeholders.secret) .textContentType(.password) - if isAuthenticating && builder.otpMethod != .none { + if isEligibleForInteractiveLogin, isAuthenticating && builder.otpMethod != .none { ThemeSecureField(title: Strings.Unlocalized.otp, text: $otp, placeholder: Strings.Placeholders.secret) .textContentType(.oneTimeCode) } @@ -137,6 +164,6 @@ private extension OpenVPNView.CredentialsView { isInteractive: $isInteractive, credentials: $credentials ) - .environmentObject(Theme()) + .withMockEnvironment() } } diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/ModuleDetailView.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/ModuleDetailView.swift index 10bb8f28..66d58f27 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/ModuleDetailView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/ModuleDetailView.swift @@ -64,5 +64,5 @@ private extension ModuleDetailView { moduleId: Profile.mock.modules.first?.id, moduleViewFactory: DefaultModuleViewFactory() ) - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/ProfileCoordinator.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/ProfileCoordinator.swift index 52c73719..c050763e 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/ProfileCoordinator.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/ProfileCoordinator.swift @@ -145,5 +145,5 @@ private extension ProfileCoordinator { path: .constant(NavigationPath()), onDismiss: {} ) - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift index 9b84f202..127be364 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/iOS/ProfileEditView+iOS.swift @@ -170,7 +170,7 @@ private extension ProfileEditView { path: .constant(NavigationPath()) ) } - .environmentObject(Theme()) + .withMockEnvironment() } #endif diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ModuleListView+macOS.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ModuleListView+macOS.swift index d604216a..ff9a7484 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ModuleListView+macOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ModuleListView+macOS.swift @@ -141,7 +141,7 @@ private extension ModuleListView { selectedModuleId: .constant(nil), malformedModuleIds: .constant([]) ) - .environmentObject(Theme()) + .withMockEnvironment() } #endif diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileGeneralView+macOS.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileGeneralView+macOS.swift index 02f64bcc..8a99e748 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileGeneralView+macOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileGeneralView+macOS.swift @@ -52,7 +52,7 @@ struct ProfileGeneralView: View { ProfileGeneralView( profileEditor: ProfileEditor() ) - .environmentObject(Theme()) + .withMockEnvironment() } #endif diff --git a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift index de85ef09..2e9eeca0 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Profile/macOS/ProfileSplitView+macOS.swift @@ -121,7 +121,7 @@ private extension ProfileSplitView { profileEditor: ProfileEditor(profile: .newProfile()), moduleViewFactory: DefaultModuleViewFactory() ) - .environmentObject(Theme()) + .withMockEnvironment() } #endif diff --git a/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/EditableModule+Previews.swift b/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/EditableModule+Previews.swift index 69056b20..3533fd51 100644 --- a/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/EditableModule+Previews.swift +++ b/Passepartout/Library/Sources/AppUI/Views/ProfileEditor/EditableModule+Previews.swift @@ -45,12 +45,3 @@ extension EditableModule where Self: ModuleViewProviding { .withMockEnvironment() } } - -@MainActor -private extension View { - func withMockEnvironment() -> some View { - environmentObject(Theme()) - .environmentObject(IAPManager.mock) - .environmentObject(ConnectionObserver.mock) - } -} diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/ConnectionStatusView.swift b/Passepartout/Library/Sources/AppUI/Views/UI/ConnectionStatusView.swift index 0cbae7f5..6a4d72d5 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/ConnectionStatusView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/ConnectionStatusView.swift @@ -78,8 +78,7 @@ private extension ConnectionStatusView { try? await Tunnel.mock.connect(with: .mock, processor: IAPManager.mock) } .frame(width: 100, height: 100) - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } #Preview("On-Demand") { @@ -98,6 +97,5 @@ private extension ConnectionStatusView { try? await Tunnel.mock.connect(with: profile, processor: IAPManager.mock) } .frame(width: 100, height: 100) - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift b/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift index da71ea39..f371ac55 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/InstalledProfileView.swift @@ -195,8 +195,7 @@ private struct CardModifier: ViewModifier { } } .themeForm() - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } #Preview("Grid") { @@ -209,8 +208,7 @@ private struct CardModifier: ViewModifier { } .padding() } - .environmentObject(Theme()) - .environmentObject(ConnectionObserver.mock) + .withMockEnvironment() } private struct HeaderView: View { diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/LogoView.swift b/Passepartout/Library/Sources/AppUI/Views/UI/LogoView.swift index 704554a7..c027e5db 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/LogoView.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/LogoView.swift @@ -42,5 +42,5 @@ struct LogoView: View { #Preview { LogoView() - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/NameSection.swift b/Passepartout/Library/Sources/AppUI/Views/UI/NameSection.swift index 1221f44f..36c5a2bf 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/NameSection.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/NameSection.swift @@ -56,5 +56,5 @@ struct NameSection: View { ) } .themeForm() - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/ProfileContextMenu.swift b/Passepartout/Library/Sources/AppUI/Views/UI/ProfileContextMenu.swift index 152cfa37..9e175e1c 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/ProfileContextMenu.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/ProfileContextMenu.swift @@ -125,5 +125,5 @@ private extension ProfileContextMenu { ) } } - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/StorageSection.swift b/Passepartout/Library/Sources/AppUI/Views/UI/StorageSection.swift index 080d8c75..ab319a1c 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/StorageSection.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/StorageSection.swift @@ -53,5 +53,5 @@ struct StorageSection: View { ) } .themeForm() - .environmentObject(Theme()) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/TunnelToggleButton.swift b/Passepartout/Library/Sources/AppUI/Views/UI/TunnelToggleButton.swift index d95ce316..bedcda67 100644 --- a/Passepartout/Library/Sources/AppUI/Views/UI/TunnelToggleButton.swift +++ b/Passepartout/Library/Sources/AppUI/Views/UI/TunnelToggleButton.swift @@ -109,10 +109,15 @@ private extension TunnelToggleButton { } } if canConnect && profile.isInteractive { - interactiveManager.present(with: profile) { - await perform(with: $0) + if iapManager.isEligible(for: .interactiveLogin) { + pp_log(.app, .notice, "Present interactive login") + interactiveManager.present(with: profile) { + await perform(with: $0) + } + return + } else { + pp_log(.app, .notice, "Suppress interactive login, not eligible") } - return } await perform(with: profile) } diff --git a/Passepartout/Library/Sources/AppUI/Views/UI/View+Mock.swift b/Passepartout/Library/Sources/AppUI/Views/UI/View+Mock.swift new file mode 100644 index 00000000..712c27cf --- /dev/null +++ b/Passepartout/Library/Sources/AppUI/Views/UI/View+Mock.swift @@ -0,0 +1,35 @@ +// +// View+Mock.swift +// Passepartout +// +// Created by Davide De Rosa on 10/2/24. +// Copyright (c) 2024 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import SwiftUI + +@MainActor +extension View { + func withMockEnvironment() -> some View { + environmentObject(Theme()) + .environmentObject(IAPManager.mock) + .environmentObject(ConnectionObserver.mock) + } +} diff --git a/Passepartout/Library/Tests/AppUITests/IAPManagerTests.swift b/Passepartout/Library/Tests/AppUITests/IAPManagerTests.swift index 2368a569..ae826618 100644 --- a/Passepartout/Library/Tests/AppUITests/IAPManagerTests.swift +++ b/Passepartout/Library/Tests/AppUITests/IAPManagerTests.swift @@ -267,6 +267,20 @@ extension IAPManagerTests { XCTAssertFalse(sut.isEligible(for: AppFeature.allCases)) } + func test_givenBetaApp_thenIsEligibleForUnrestrictedFeature() async { + let reader = MockReceiptReader() + let sut = IAPManager(customUserLevel: .beta, receiptReader: reader, unrestrictedFeatures: [.onDemand]) + + await sut.reloadReceipt() + AppFeature.allCases.forEach { + if $0 == .onDemand { + XCTAssertTrue(sut.isEligible(for: $0)) + } else { + XCTAssertFalse(sut.isEligible(for: $0)) + } + } + } + func test_givenFullApp_thenIsFullVersion() async { let reader = MockReceiptReader() let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader) diff --git a/Passepartout/Shared/Shared+AppUI.swift b/Passepartout/Shared/Shared+AppUI.swift index a77cf30a..418fb90e 100644 --- a/Passepartout/Shared/Shared+AppUI.swift +++ b/Passepartout/Shared/Shared+AppUI.swift @@ -43,6 +43,8 @@ extension IAPManager { static let shared = IAPManager( customUserLevel: customUserLevel, receiptReader: KvittoReceiptReader(), + // FIXME: #662, omit unrestrictedFeatures on release! + unrestrictedFeatures: [.interactiveLogin], productsAtBuild: productsAtBuild )