From d5ac785bb861c07967ae9931a2b083ad04e7e3bc Mon Sep 17 00:00:00 2001 From: Davide Date: Tue, 5 Nov 2024 18:55:57 +0100 Subject: [PATCH] Simulate in-app purchases (#818) Integrate in-app helper into IAPManager and simulate purchases with an in-memory receipt. --- .../Views/Modules/OnDemandView.swift | 1 + .../Views/Profile/AppleTVSection.swift | 3 +- .../Views/Profile/ProfileCoordinator.swift | 6 +- .../Provider/ProviderContentModifier.swift | 1 + .../IAP/AppProduct+Features.swift | 4 ++ .../CommonLibrary/IAP/IAPManager.swift | 47 +++++++++++++-- .../CommonLibrary/IAP/PaywallReason.swift | 2 +- .../Mock/MockAppProductHelper.swift | 15 ++++- .../Mock/MockAppReceiptReader.swift | 31 +++++++++- .../CommonUtils/Business/StoreKitHelper.swift | 4 +- .../UILibrary/L10n/AppFeature+L10n.swift | 58 +++++++++++++++++++ .../UILibrary/L10n/SwiftGen+Strings.swift | 8 +++ .../UILibrary/Mock/AppContext+Mock.swift | 1 + .../Resources/en.lproj/Localizable.strings | 4 ++ .../Modules/OpenVPNView+Credentials.swift | 1 + .../Views/Paywall/PaywallModifier.swift | 30 +++++++--- .../UILibrary/Views/Paywall/PaywallView.swift | 2 + .../Views/UI/PurchaseButtonModifier.swift | 8 ++- .../CommonLibraryTests/IAPManagerTests.swift | 18 ++++++ Passepartout/Shared/Shared+App.swift | 11 +++- 20 files changed, 228 insertions(+), 27 deletions(-) create mode 100644 Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift b/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift index af6c6ec9..7bae92b0 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift @@ -63,6 +63,7 @@ struct OnDemandView: View, ModuleDraftEditing { .modifier(PurchaseButtonModifier( Strings.Modules.OnDemand.purchase, feature: .onDemand, + suggesting: nil, showsIfRestricted: false, paywallReason: $paywallReason )) diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Profile/AppleTVSection.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/AppleTVSection.swift index 23d6acd2..4c801ca7 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Profile/AppleTVSection.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Profile/AppleTVSection.swift @@ -59,6 +59,7 @@ private extension AppleTVSection { .modifier(PurchaseButtonModifier( Strings.Modules.General.Rows.AppleTv.purchase, feature: .appleTV, + suggesting: .Features.appleTV, showsIfRestricted: true, paywallReason: $paywallReason )) @@ -72,7 +73,7 @@ private extension AppleTVSection { let purchaseDesc = { Strings.Modules.General.Sections.AppleTv.Footer.Purchase._2 } - switch iapManager.paywallReason(forFeature: .appleTV) { + switch iapManager.paywallReason(forFeature: .appleTV, suggesting: nil) { case .purchase: desc.append(expirationDesc()) desc.append(purchaseDesc()) diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift index 26b7a8ad..5a6b12ed 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Profile/ProfileCoordinator.swift @@ -104,13 +104,13 @@ private extension ProfileCoordinator { func onNewModule(_ moduleType: ModuleType) { switch moduleType { case .dns: - paywallReason = iapManager.paywallReason(forFeature: .dns) + paywallReason = iapManager.paywallReason(forFeature: .dns, suggesting: nil) case .httpProxy: - paywallReason = iapManager.paywallReason(forFeature: .httpProxy) + paywallReason = iapManager.paywallReason(forFeature: .httpProxy, suggesting: nil) case .ip: - paywallReason = iapManager.paywallReason(forFeature: .routing) + paywallReason = iapManager.paywallReason(forFeature: .routing, suggesting: nil) case .openVPN, .wireGuard: break diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift b/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift index 56627af5..ca005012 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Provider/ProviderContentModifier.swift @@ -137,6 +137,7 @@ private extension ProviderContentModifier { .modifier(PurchaseButtonModifier( Strings.Providers.Picker.purchase, feature: .providers, + suggesting: nil, showsIfRestricted: true, paywallReason: $paywallReason )) diff --git a/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift b/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift index 6383bc34..df89471f 100644 --- a/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift +++ b/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift @@ -31,6 +31,9 @@ extension AppProduct { public static let appleTV = AppProduct(featureId: "appletv") + // FIXME: #585, add in-app product + public static let interactiveLogin = AppProduct(featureId: "interactive_login") + public static let networkSettings = AppProduct(featureId: "network_settings") public static let siriShortcuts = AppProduct(featureId: "siri") @@ -40,6 +43,7 @@ extension AppProduct { static let all: [AppProduct] = [ .Features.allProviders, .Features.appleTV, + .Features.interactiveLogin, .Features.networkSettings, .Features.siriShortcuts, .Features.trustedNetworks diff --git a/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift b/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift index c8a821cf..ff7e344f 100644 --- a/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift +++ b/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift @@ -27,12 +27,12 @@ import CommonUtils import Foundation import PassepartoutKit -// FIXME: #424, reload receipt + objectWillChange on purchase/transactions - @MainActor public final class IAPManager: ObservableObject { private let customUserLevel: AppUserLevel? + private let inAppHelper: any AppProductHelper + private let receiptReader: any AppReceiptReader private let unrestrictedFeatures: Set @@ -49,17 +49,56 @@ public final class IAPManager: ObservableObject { public init( customUserLevel: AppUserLevel? = nil, + inAppHelper: any AppProductHelper, receiptReader: any AppReceiptReader, unrestrictedFeatures: Set = [], productsAtBuild: BuildProducts? = nil ) { self.customUserLevel = customUserLevel + self.inAppHelper = inAppHelper self.receiptReader = receiptReader self.unrestrictedFeatures = unrestrictedFeatures self.productsAtBuild = productsAtBuild userLevel = .undefined purchasedProducts = [] eligibleFeatures = [] + + Task { + do { + try await inAppHelper.fetchProducts() + } catch { + pp_log(.app, .error, "Unable to fetch in-app products: \(error)") + } + } + } + + public func products(for identifiers: Set) async -> [InAppProduct] { + let raw = identifiers.map(\.rawValue) + return await inAppHelper.products + .values + .filter { + raw.contains($0.productIdentifier) + } + } + + public func purchase(_ inAppProduct: InAppProduct) async throws -> InAppPurchaseResult { + guard let product = AppProduct(rawValue: inAppProduct.productIdentifier) else { + return .notFound + } + return try await purchase(product) + } + + public func purchase(_ product: AppProduct) async throws -> InAppPurchaseResult { + let result = try await inAppHelper.purchase(productWithIdentifier: product) + if result == .done { + await reloadReceipt() + } + return result + } + + public func restorePurchases() async throws { + try await inAppHelper.restorePurchases() + await reloadReceipt() } public func reloadReceipt() async { @@ -166,11 +205,11 @@ extension IAPManager { #endif } - public func paywallReason(forFeature feature: AppFeature) -> PaywallReason? { + public func paywallReason(forFeature feature: AppFeature, suggesting product: AppProduct?) -> PaywallReason? { if isEligible(for: feature) { return nil } - return isRestricted ? .restricted : .purchase(feature) + return isRestricted ? .restricted : .purchase(feature, product) } public func isPayingUser() -> Bool { diff --git a/Passepartout/Library/Sources/CommonLibrary/IAP/PaywallReason.swift b/Passepartout/Library/Sources/CommonLibrary/IAP/PaywallReason.swift index 05855153..27857bd4 100644 --- a/Passepartout/Library/Sources/CommonLibrary/IAP/PaywallReason.swift +++ b/Passepartout/Library/Sources/CommonLibrary/IAP/PaywallReason.swift @@ -28,5 +28,5 @@ import Foundation public enum PaywallReason: Hashable { case restricted - case purchase(AppFeature) + case purchase(AppFeature, AppProduct?) } diff --git a/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppProductHelper.swift b/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppProductHelper.swift index 9e8d41f9..9d165c5f 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppProductHelper.swift +++ b/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppProductHelper.swift @@ -27,10 +27,17 @@ import CommonUtils import Foundation public actor MockAppProductHelper: AppProductHelper { + private let build: Int + public private(set) var products: [AppProduct: InAppProduct] - public init() { + public nonisolated let receiptReader: MockAppReceiptReader + + // set .max to skip entitled products + public init(build: Int = .max) { + self.build = build products = [:] + receiptReader = MockAppReceiptReader() } public nonisolated var canMakePurchases: Bool { @@ -42,14 +49,16 @@ public actor MockAppProductHelper: AppProductHelper { $0[$1] = InAppProduct( productIdentifier: $1.rawValue, localizedTitle: $1.rawValue, - localizedPrice: "10.0", + localizedPrice: "€10.0", native: $1 ) } + await receiptReader.setReceipt(withBuild: build, products: []) } public func purchase(productWithIdentifier productIdentifier: AppProduct) async throws -> InAppPurchaseResult { - .done + await receiptReader.addPurchase(with: productIdentifier) + return .done } public func restorePurchases() async throws { diff --git a/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppReceiptReader.swift b/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppReceiptReader.swift index 32560888..f49bf08f 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppReceiptReader.swift +++ b/Passepartout/Library/Sources/CommonLibrary/Mock/MockAppReceiptReader.swift @@ -35,13 +35,38 @@ public actor MockAppReceiptReader: AppReceiptReader { public func setReceipt(withBuild build: Int, products: [AppProduct], cancelledProducts: Set = []) { receipt = InAppReceipt(originalBuildNumber: build, purchaseReceipts: products.map { - .init(productIdentifier: $0.rawValue, - cancellationDate: cancelledProducts.contains($0) ? Date() : nil, - originalPurchaseDate: nil) + .init( + productIdentifier: $0.rawValue, + cancellationDate: cancelledProducts.contains($0) ? Date() : nil, + originalPurchaseDate: nil + ) }) } public func receipt(for userLevel: AppUserLevel) -> InAppReceipt? { receipt } + + public func addPurchase(with product: AppProduct) { + guard let receipt else { + fatalError("Call setReceipt() first") + } + var purchaseReceipts = receipt.purchaseReceipts ?? [] + purchaseReceipts.append(product.purchaseReceipt) + let newReceipt = InAppReceipt( + originalBuildNumber: receipt.originalBuildNumber, + purchaseReceipts: purchaseReceipts + ) + self.receipt = newReceipt + } +} + +private extension AppProduct { + var purchaseReceipt: InAppReceipt.PurchaseReceipt { + .init( + productIdentifier: rawValue, + cancellationDate: nil, + originalPurchaseDate: nil + ) + } } diff --git a/Passepartout/Library/Sources/CommonUtils/Business/StoreKitHelper.swift b/Passepartout/Library/Sources/CommonUtils/Business/StoreKitHelper.swift index adeb6124..c49ae1bc 100644 --- a/Passepartout/Library/Sources/CommonUtils/Business/StoreKitHelper.swift +++ b/Passepartout/Library/Sources/CommonUtils/Business/StoreKitHelper.swift @@ -83,12 +83,12 @@ public final class StoreKitHelper: InAppHelper where PID: RawRepresentable } } - // FIXME: #424, implement purchase + // FIXME: #585, implement purchase public func purchase(productWithIdentifier productIdentifier: ProductIdentifier) async throws -> InAppPurchaseResult { fatalError("purchase") } - // FIXME: #424, implement restore purchases + // FIXME: #585, implement restore purchases public func restorePurchases() async throws { fatalError("restorePurchases") } diff --git a/Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift b/Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift new file mode 100644 index 00000000..289ffacd --- /dev/null +++ b/Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift @@ -0,0 +1,58 @@ +// +// AppFeature+L10n.swift +// Passepartout +// +// Created by Davide De Rosa on 11/5/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 CommonLibrary +import CommonUtils +import Foundation + +extension AppFeature: LocalizableEntity { + public var localizedDescription: String { + switch self { + case .appleTV: + return Strings.Unlocalized.appleTV + + case .dns: + return Strings.Unlocalized.dns + + case .httpProxy: + return Strings.Unlocalized.httpProxy + + case .interactiveLogin: + return Strings.Features.interactiveLogin + + case .onDemand: + return Strings.Global.onDemand + + case .providers: + return Strings.Features.providers + + case .routing: + return Strings.Global.routing + + case .siri: + return Strings.Features.siri + } + } +} diff --git a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift index 8196c753..dac89df8 100644 --- a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift +++ b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift @@ -174,6 +174,14 @@ public enum Strings { public static let tls = Strings.tr("Localizable", "errors.tunnel.tls", fallback: "TLS failed") } } + public enum Features { + /// Interactive login + public static let interactiveLogin = Strings.tr("Localizable", "features.interactive_login", fallback: "Interactive login") + /// Providers + public static let providers = Strings.tr("Localizable", "features.providers", fallback: "Providers") + /// Shortcuts + public static let siri = Strings.tr("Localizable", "features.siri", fallback: "Shortcuts") + } public enum Global { /// About public static let about = Strings.tr("Localizable", "global.about", fallback: "About") diff --git a/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift b/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift index 66574759..9055a52e 100644 --- a/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift +++ b/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift @@ -35,6 +35,7 @@ extension AppContext { public static func mock(withRegistry registry: Registry) -> AppContext { let iapManager = IAPManager( customUserLevel: nil, + inAppHelper: MockAppProductHelper(), receiptReader: MockAppReceiptReader(), unrestrictedFeatures: [ .interactiveLogin, diff --git a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings index 589ad6c2..298d875a 100644 --- a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings @@ -252,6 +252,10 @@ // MARK: - Paywalls +"features.interactive_login" = "Interactive login"; +"features.providers" = "Providers"; +"features.siri" = "Shortcuts"; + "modules.general.sections.apple_tv.footer.purchase.1" = "TV profiles expire after %d minutes."; "modules.general.sections.apple_tv.footer.purchase.2" = "Purchase to drop the restriction."; "modules.general.rows.apple_tv.purchase" = "Drop time restriction"; diff --git a/Passepartout/Library/Sources/UILibrary/Views/Modules/OpenVPNView+Credentials.swift b/Passepartout/Library/Sources/UILibrary/Views/Modules/OpenVPNView+Credentials.swift index 47e55c11..d5031b89 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/Modules/OpenVPNView+Credentials.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/Modules/OpenVPNView+Credentials.swift @@ -77,6 +77,7 @@ public struct OpenVPNCredentialsView: View { .modifier(PurchaseButtonModifier( Strings.Modules.Openvpn.Credentials.Interactive.purchase, feature: .interactiveLogin, + suggesting: .Features.interactiveLogin, showsIfRestricted: false, paywallReason: $paywallReason )) diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift index 777ed639..2bc220a2 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift @@ -35,7 +35,7 @@ public struct PaywallModifier: ViewModifier { private var isPresentingRestricted = false @State - private var paywallFeature: AppFeature? + private var paywallArguments: PaywallArguments? public init(reason: Binding) { _reason = reason @@ -56,9 +56,13 @@ public struct PaywallModifier: ViewModifier { Text(Strings.Alerts.Iap.Restricted.message) } ) - .themeModal(item: $paywallFeature) { feature in + .themeModal(item: $paywallArguments) { args in NavigationStack { - PaywallView(isPresented: isPresentingPurchase, feature: feature) + PaywallView( + isPresented: isPresentingPurchase, + feature: args.feature, + suggestedProduct: args.product + ) } } .onChange(of: reason) { @@ -66,8 +70,8 @@ public struct PaywallModifier: ViewModifier { case .restricted: isPresentingRestricted = true - case .purchase(let feature): - paywallFeature = feature + case .purchase(let feature, let product): + paywallArguments = PaywallArguments(feature: feature, product: product) default: break @@ -79,11 +83,23 @@ public struct PaywallModifier: ViewModifier { private extension PaywallModifier { var isPresentingPurchase: Binding { Binding { - paywallFeature != nil + paywallArguments != nil } set: { if !$0 { - paywallFeature = nil + // make sure to reset this to allow paywall to appear again + reason = nil + paywallArguments = nil } } } } + +private struct PaywallArguments: Identifiable { + let feature: AppFeature + + let product: AppProduct? + + var id: String { + feature.id + } +} diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift index 1efef7c2..7e684cd6 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift @@ -36,6 +36,8 @@ struct PaywallView: View { let feature: AppFeature + let suggestedProduct: AppProduct? + // FIXME: #585, implement payments var body: some View { VStack { diff --git a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseButtonModifier.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseButtonModifier.swift index abaed3c9..3bb215fd 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseButtonModifier.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseButtonModifier.swift @@ -37,6 +37,8 @@ public struct PurchaseButtonModifier: ViewModifier { private let feature: AppFeature + private let suggestedProduct: AppProduct? + private let showsIfRestricted: Bool @Binding @@ -46,18 +48,20 @@ public struct PurchaseButtonModifier: ViewModifier { _ title: String, label: String? = nil, feature: AppFeature, + suggesting suggestedProduct: AppProduct?, showsIfRestricted: Bool, paywallReason: Binding ) { self.title = title self.label = label self.feature = feature + self.suggestedProduct = suggestedProduct self.showsIfRestricted = showsIfRestricted _paywallReason = paywallReason } public func body(content: Content) -> some View { - switch iapManager.paywallReason(forFeature: feature) { + switch iapManager.paywallReason(forFeature: feature, suggesting: suggestedProduct) { case .purchase: purchaseView @@ -85,7 +89,7 @@ private extension PurchaseButtonModifier { var purchaseButton: some View { Button(title) { - paywallReason = .purchase(feature) + paywallReason = .purchase(feature, suggestedProduct) } } } diff --git a/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift b/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift index 23c3a497..29a99986 100644 --- a/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift +++ b/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift @@ -24,6 +24,7 @@ // @testable import CommonLibrary +import CommonUtils import Foundation import XCTest @@ -259,3 +260,20 @@ extension IAPManagerTests { XCTAssertTrue(sut.isEligible(for: .appleTV)) } } + +private extension IAPManager { + convenience init( + customUserLevel: AppUserLevel? = nil, + receiptReader: any AppReceiptReader, + unrestrictedFeatures: Set = [], + productsAtBuild: BuildProducts? = nil + ) { + self.init( + customUserLevel: customUserLevel, + inAppHelper: MockAppProductHelper(), + receiptReader: receiptReader, + unrestrictedFeatures: unrestrictedFeatures, + productsAtBuild: productsAtBuild + ) + } +} diff --git a/Passepartout/Shared/Shared+App.swift b/Passepartout/Shared/Shared+App.swift index f82bd690..8a295d1c 100644 --- a/Passepartout/Shared/Shared+App.swift +++ b/Passepartout/Shared/Shared+App.swift @@ -37,9 +37,18 @@ extension AppContext { let tunnelEnvironment: TunnelEnvironment = .shared let registry: Registry = .shared +#if DEBUG + let inAppHelper = MockAppProductHelper() + let receiptReader = inAppHelper.receiptReader +#else + let inAppHelper = StoreKitHelper(identifiers: AppProduct.all) + let receiptReader = KvittoReceiptReader() +#endif + let iapManager = IAPManager( customUserLevel: Configuration.IAPManager.customUserLevel, - receiptReader: KvittoReceiptReader(), + inAppHelper: inAppHelper, + receiptReader: receiptReader, // FIXME: #662, omit unrestrictedFeatures on release! unrestrictedFeatures: [.interactiveLogin], productsAtBuild: Configuration.IAPManager.productsAtBuild