parent
dcdcec4da7
commit
e8d5f2477b
|
@ -28,6 +28,8 @@ import Foundation
|
||||||
public enum AppFeature: String, CaseIterable {
|
public enum AppFeature: String, CaseIterable {
|
||||||
case appleTV
|
case appleTV
|
||||||
|
|
||||||
|
case interactiveLogin
|
||||||
|
|
||||||
case networkSettings
|
case networkSettings
|
||||||
|
|
||||||
case onDemand
|
case onDemand
|
||||||
|
@ -37,6 +39,7 @@ public enum AppFeature: String, CaseIterable {
|
||||||
case siri
|
case siri
|
||||||
|
|
||||||
public static let allCases: [AppFeature] = [
|
public static let allCases: [AppFeature] = [
|
||||||
|
.interactiveLogin,
|
||||||
.networkSettings,
|
.networkSettings,
|
||||||
.onDemand,
|
.onDemand,
|
||||||
.providers,
|
.providers,
|
||||||
|
|
|
@ -35,6 +35,8 @@ public final class IAPManager: ObservableObject {
|
||||||
|
|
||||||
private let receiptReader: any AppReceiptReader
|
private let receiptReader: any AppReceiptReader
|
||||||
|
|
||||||
|
private let unrestrictedFeatures: Set<AppFeature>
|
||||||
|
|
||||||
private let productsAtBuild: BuildProducts<AppProduct>?
|
private let productsAtBuild: BuildProducts<AppProduct>?
|
||||||
|
|
||||||
private(set) var userLevel: AppUserLevel
|
private(set) var userLevel: AppUserLevel
|
||||||
|
@ -48,10 +50,12 @@ public final class IAPManager: ObservableObject {
|
||||||
public init(
|
public init(
|
||||||
customUserLevel: AppUserLevel? = nil,
|
customUserLevel: AppUserLevel? = nil,
|
||||||
receiptReader: any AppReceiptReader,
|
receiptReader: any AppReceiptReader,
|
||||||
|
unrestrictedFeatures: Set<AppFeature> = [],
|
||||||
productsAtBuild: BuildProducts<AppProduct>? = nil
|
productsAtBuild: BuildProducts<AppProduct>? = nil
|
||||||
) {
|
) {
|
||||||
self.customUserLevel = customUserLevel
|
self.customUserLevel = customUserLevel
|
||||||
self.receiptReader = receiptReader
|
self.receiptReader = receiptReader
|
||||||
|
self.unrestrictedFeatures = unrestrictedFeatures
|
||||||
self.productsAtBuild = productsAtBuild
|
self.productsAtBuild = productsAtBuild
|
||||||
userLevel = .undefined
|
userLevel = .undefined
|
||||||
purchasedProducts = []
|
purchasedProducts = []
|
||||||
|
@ -106,6 +110,10 @@ public final class IAPManager: ObservableObject {
|
||||||
eligibleFeatures = Set(userLevel.features)
|
eligibleFeatures = Set(userLevel.features)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unrestrictedFeatures.forEach {
|
||||||
|
eligibleFeatures.insert($0)
|
||||||
|
}
|
||||||
|
|
||||||
pp_log(.app, .notice, "Purchased products: \(purchasedProducts.map(\.rawValue))")
|
pp_log(.app, .notice, "Purchased products: \(purchasedProducts.map(\.rawValue))")
|
||||||
pp_log(.app, .notice, "Eligible features: \(eligibleFeatures)")
|
pp_log(.app, .notice, "Eligible features: \(eligibleFeatures)")
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
|
|
|
@ -47,7 +47,7 @@ public final class KvittoReceiptReader: AppReceiptReader {
|
||||||
guard let receipt else {
|
guard let receipt else {
|
||||||
let releaseUrl = url.deletingLastPathComponent().appendingPathComponent("receipt")
|
let releaseUrl = url.deletingLastPathComponent().appendingPathComponent("receipt")
|
||||||
guard releaseUrl != url else {
|
guard releaseUrl != url else {
|
||||||
#if !targetEnvironment(simulator)
|
#if !os(macOS) && !targetEnvironment(simulator)
|
||||||
assertionFailure("How can release URL be equal to sandbox URL in TestFlight?")
|
assertionFailure("How can release URL be equal to sandbox URL in TestFlight?")
|
||||||
#endif
|
#endif
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -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 {
|
public enum Wireguard {
|
||||||
/// Allowed IPs
|
/// Allowed IPs
|
||||||
|
|
|
@ -179,6 +179,7 @@
|
||||||
"modules.on_demand.ethernet" = "Ethernet";
|
"modules.on_demand.ethernet" = "Ethernet";
|
||||||
"modules.on_demand.ssid.add" = "Add SSID";
|
"modules.on_demand.ssid.add" = "Add SSID";
|
||||||
|
|
||||||
|
"modules.openvpn.purchase.interactive" = "Log in interactively";
|
||||||
"modules.openvpn.pull" = "Pull";
|
"modules.openvpn.pull" = "Pull";
|
||||||
"modules.openvpn.redirect_gateway" = "Redirect gateway";
|
"modules.openvpn.redirect_gateway" = "Redirect gateway";
|
||||||
"modules.openvpn.credentials" = "Credentials";
|
"modules.openvpn.credentials" = "Credentials";
|
||||||
|
|
|
@ -97,5 +97,5 @@ extension AboutRouterView {
|
||||||
AboutRouterView(
|
AboutRouterView(
|
||||||
tunnel: .mock
|
tunnel: .mock
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,6 +161,5 @@ private extension AppInlineCoordinator {
|
||||||
tunnel: .mock,
|
tunnel: .mock,
|
||||||
registry: Registry()
|
registry: Registry()
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,6 +149,5 @@ extension AppModalCoordinator {
|
||||||
tunnel: .mock,
|
tunnel: .mock,
|
||||||
registry: Registry()
|
registry: Registry()
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,5 +118,5 @@ private extension AppToolbar {
|
||||||
}
|
}
|
||||||
.frame(width: 600, height: 400)
|
.frame(width: 600, height: 400)
|
||||||
}
|
}
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,6 @@ private struct PreviewView: View {
|
||||||
onEdit: { _ in }
|
onEdit: { _ in }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,6 +148,5 @@ private extension ProfileGridView {
|
||||||
onEdit: { _ in }
|
onEdit: { _ in }
|
||||||
)
|
)
|
||||||
.themeWindow(width: 600, height: 300)
|
.themeWindow(width: 600, height: 300)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,6 +137,5 @@ private extension ProfileListView {
|
||||||
errorHandler: .default(),
|
errorHandler: .default(),
|
||||||
onEdit: { _ in }
|
onEdit: { _ in }
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,7 +198,5 @@ private extension DiagnosticsView {
|
||||||
.init(date: Date().addingTimeInterval(-600), url: URL(string: "http://three.com")!)
|
.init(date: Date().addingTimeInterval(-600), url: URL(string: "http://three.com")!)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
.environmentObject(IAPManager.mock)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,5 +136,5 @@ private extension IPView.RouteView {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Preview()
|
return Preview()
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ private struct OnDemandView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
enabledSection
|
enabledSection
|
||||||
restrictedSection
|
restrictedArea
|
||||||
}
|
}
|
||||||
.asModuleView(with: editor, draft: draft)
|
.asModuleView(with: editor, draft: draft)
|
||||||
.modifier(PaywallModifier(reason: $paywallReason))
|
.modifier(PaywallModifier(reason: $paywallReason))
|
||||||
|
@ -86,11 +86,11 @@ private extension OnDemandView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var restrictedSection: some View {
|
var restrictedArea: some View {
|
||||||
switch iapManager.paywallReason(forFeature: .onDemand) {
|
switch iapManager.paywallReason(forFeature: .onDemand) {
|
||||||
case .purchase(let feature):
|
case .purchase(let appFeature):
|
||||||
Button(Strings.Modules.OnDemand.purchase) {
|
Button(Strings.Modules.OnDemand.purchase) {
|
||||||
paywallReason = .purchase(feature)
|
paywallReason = .purchase(appFeature)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .restricted:
|
case .restricted:
|
||||||
|
|
|
@ -29,6 +29,9 @@ import SwiftUI
|
||||||
extension OpenVPNView {
|
extension OpenVPNView {
|
||||||
struct CredentialsView: View {
|
struct CredentialsView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var iapManager: IAPManager
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var isInteractive: Bool
|
var isInteractive: Bool
|
||||||
|
|
||||||
|
@ -43,11 +46,12 @@ extension OpenVPNView {
|
||||||
@State
|
@State
|
||||||
private var otp = ""
|
private var otp = ""
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var paywallReason: PaywallReason?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
if !isAuthenticating {
|
restrictedArea
|
||||||
interactiveSection
|
|
||||||
}
|
|
||||||
inputSection
|
inputSection
|
||||||
}
|
}
|
||||||
.themeAnimation(on: isInteractive, category: .modules)
|
.themeAnimation(on: isInteractive, category: .modules)
|
||||||
|
@ -58,7 +62,7 @@ extension OpenVPNView {
|
||||||
builder = credentials?.builder() ?? OpenVPN.Credentials.Builder()
|
builder = credentials?.builder() ?? OpenVPN.Credentials.Builder()
|
||||||
}
|
}
|
||||||
.onChange(of: builder) {
|
.onChange(of: builder) {
|
||||||
if isAuthenticating {
|
if isEligibleForInteractiveLogin, isAuthenticating {
|
||||||
credentials = $0.buildForAuthentication(otp: otp)
|
credentials = $0.buildForAuthentication(otp: otp)
|
||||||
} else {
|
} else {
|
||||||
credentials = $0.build()
|
credentials = $0.build()
|
||||||
|
@ -67,15 +71,38 @@ extension OpenVPNView {
|
||||||
.onChange(of: otp) {
|
.onChange(of: otp) {
|
||||||
credentials = builder.buildForAuthentication(otp: $0)
|
credentials = builder.buildForAuthentication(otp: $0)
|
||||||
}
|
}
|
||||||
|
.modifier(PaywallModifier(reason: $paywallReason))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension OpenVPNView.CredentialsView {
|
private extension OpenVPNView.CredentialsView {
|
||||||
|
var isEligibleForInteractiveLogin: Bool {
|
||||||
|
iapManager.isEligible(for: .interactiveLogin)
|
||||||
|
}
|
||||||
|
|
||||||
var otpMethods: [OpenVPN.Credentials.OTPMethod] {
|
var otpMethods: [OpenVPN.Credentials.OTPMethod] {
|
||||||
[.none, .append, .encode]
|
[.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 {
|
var interactiveSection: some View {
|
||||||
Group {
|
Group {
|
||||||
Toggle(Strings.Modules.Openvpn.Credentials.interactive, isOn: $isInteractive)
|
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)
|
ThemeSecureField(title: Strings.Global.password, text: $builder.password, placeholder: Strings.Placeholders.secret)
|
||||||
.textContentType(.password)
|
.textContentType(.password)
|
||||||
|
|
||||||
if isAuthenticating && builder.otpMethod != .none {
|
if isEligibleForInteractiveLogin, isAuthenticating && builder.otpMethod != .none {
|
||||||
ThemeSecureField(title: Strings.Unlocalized.otp, text: $otp, placeholder: Strings.Placeholders.secret)
|
ThemeSecureField(title: Strings.Unlocalized.otp, text: $otp, placeholder: Strings.Placeholders.secret)
|
||||||
.textContentType(.oneTimeCode)
|
.textContentType(.oneTimeCode)
|
||||||
}
|
}
|
||||||
|
@ -137,6 +164,6 @@ private extension OpenVPNView.CredentialsView {
|
||||||
isInteractive: $isInteractive,
|
isInteractive: $isInteractive,
|
||||||
credentials: $credentials
|
credentials: $credentials
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,5 +64,5 @@ private extension ModuleDetailView {
|
||||||
moduleId: Profile.mock.modules.first?.id,
|
moduleId: Profile.mock.modules.first?.id,
|
||||||
moduleViewFactory: DefaultModuleViewFactory()
|
moduleViewFactory: DefaultModuleViewFactory()
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,5 +145,5 @@ private extension ProfileCoordinator {
|
||||||
path: .constant(NavigationPath()),
|
path: .constant(NavigationPath()),
|
||||||
onDismiss: {}
|
onDismiss: {}
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@ private extension ProfileEditView {
|
||||||
path: .constant(NavigationPath())
|
path: .constant(NavigationPath())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -141,7 +141,7 @@ private extension ModuleListView {
|
||||||
selectedModuleId: .constant(nil),
|
selectedModuleId: .constant(nil),
|
||||||
malformedModuleIds: .constant([])
|
malformedModuleIds: .constant([])
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -52,7 +52,7 @@ struct ProfileGeneralView: View {
|
||||||
ProfileGeneralView(
|
ProfileGeneralView(
|
||||||
profileEditor: ProfileEditor()
|
profileEditor: ProfileEditor()
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -121,7 +121,7 @@ private extension ProfileSplitView {
|
||||||
profileEditor: ProfileEditor(profile: .newProfile()),
|
profileEditor: ProfileEditor(profile: .newProfile()),
|
||||||
moduleViewFactory: DefaultModuleViewFactory()
|
moduleViewFactory: DefaultModuleViewFactory()
|
||||||
)
|
)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -45,12 +45,3 @@ extension EditableModule where Self: ModuleViewProviding {
|
||||||
.withMockEnvironment()
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private extension View {
|
|
||||||
func withMockEnvironment() -> some View {
|
|
||||||
environmentObject(Theme())
|
|
||||||
.environmentObject(IAPManager.mock)
|
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -78,8 +78,7 @@ private extension ConnectionStatusView {
|
||||||
try? await Tunnel.mock.connect(with: .mock, processor: IAPManager.mock)
|
try? await Tunnel.mock.connect(with: .mock, processor: IAPManager.mock)
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("On-Demand") {
|
#Preview("On-Demand") {
|
||||||
|
@ -98,6 +97,5 @@ private extension ConnectionStatusView {
|
||||||
try? await Tunnel.mock.connect(with: profile, processor: IAPManager.mock)
|
try? await Tunnel.mock.connect(with: profile, processor: IAPManager.mock)
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,8 +195,7 @@ private struct CardModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.themeForm()
|
.themeForm()
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Grid") {
|
#Preview("Grid") {
|
||||||
|
@ -209,8 +208,7 @@ private struct CardModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
.environmentObject(ConnectionObserver.mock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct HeaderView: View {
|
private struct HeaderView: View {
|
||||||
|
|
|
@ -42,5 +42,5 @@ struct LogoView: View {
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
LogoView()
|
LogoView()
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,5 +56,5 @@ struct NameSection: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.themeForm()
|
.themeForm()
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,5 +125,5 @@ private extension ProfileContextMenu {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,5 +53,5 @@ struct StorageSection: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.themeForm()
|
.themeForm()
|
||||||
.environmentObject(Theme())
|
.withMockEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,10 +109,15 @@ private extension TunnelToggleButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if canConnect && profile.isInteractive {
|
if canConnect && profile.isInteractive {
|
||||||
interactiveManager.present(with: profile) {
|
if iapManager.isEligible(for: .interactiveLogin) {
|
||||||
await perform(with: $0)
|
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)
|
await perform(with: profile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
extension View {
|
||||||
|
func withMockEnvironment() -> some View {
|
||||||
|
environmentObject(Theme())
|
||||||
|
.environmentObject(IAPManager.mock)
|
||||||
|
.environmentObject(ConnectionObserver.mock)
|
||||||
|
}
|
||||||
|
}
|
|
@ -267,6 +267,20 @@ extension IAPManagerTests {
|
||||||
XCTAssertFalse(sut.isEligible(for: AppFeature.allCases))
|
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 {
|
func test_givenFullApp_thenIsFullVersion() async {
|
||||||
let reader = MockReceiptReader()
|
let reader = MockReceiptReader()
|
||||||
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
|
let sut = IAPManager(customUserLevel: .fullVersion, receiptReader: reader)
|
||||||
|
|
|
@ -43,6 +43,8 @@ extension IAPManager {
|
||||||
static let shared = IAPManager(
|
static let shared = IAPManager(
|
||||||
customUserLevel: customUserLevel,
|
customUserLevel: customUserLevel,
|
||||||
receiptReader: KvittoReceiptReader(),
|
receiptReader: KvittoReceiptReader(),
|
||||||
|
// FIXME: #662, omit unrestrictedFeatures on release!
|
||||||
|
unrestrictedFeatures: [.interactiveLogin],
|
||||||
productsAtBuild: productsAtBuild
|
productsAtBuild: productsAtBuild
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue