diff --git a/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift b/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift index 2b45aae8..8e8d39fd 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift @@ -25,7 +25,7 @@ import SwiftUI -// FIXME: #819, UI for donations +// FIXME: #830, UI for donations struct DonateView: View { var body: some View { diff --git a/Passepartout/Library/Sources/AppUIMain/Views/About/iOS/AboutView+iOS.swift b/Passepartout/Library/Sources/AppUIMain/Views/About/iOS/AboutView+iOS.swift index f554352b..e8f6080a 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/About/iOS/AboutView+iOS.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/About/iOS/AboutView+iOS.swift @@ -33,7 +33,7 @@ extension AboutView { List { SettingsSectionGroup(profileManager: profileManager) Group { - // FIXME: #819, UI for donations + // FIXME: #830, UI for donations // donateLink linksLink creditsLink diff --git a/Passepartout/Library/Sources/AppUIMain/Views/About/macOS/AboutView+macOS.swift b/Passepartout/Library/Sources/AppUIMain/Views/About/macOS/AboutView+macOS.swift index 451d8404..a813af88 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/About/macOS/AboutView+macOS.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/About/macOS/AboutView+macOS.swift @@ -32,7 +32,7 @@ extension AboutView { var listView: some View { List(selection: $navigationRoute) { Section { - // FIXME: #819, UI for donations + // FIXME: #830, UI for donations // donateLink linksLink creditsLink diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Provider/VPNProviderServerCoordinator.swift b/Passepartout/Library/Sources/AppUIMain/Views/Provider/VPNProviderServerCoordinator.swift index 573b81d5..f200bc73 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Provider/VPNProviderServerCoordinator.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Provider/VPNProviderServerCoordinator.swift @@ -58,11 +58,7 @@ struct VPNProviderServerCoordinator: View where Configuration: Pr Button { dismiss() } label: { -#if os(iOS) - ThemeImage(.close) -#else - Text(Strings.Global.cancel) -#endif + ThemeCloseLabel() } } } diff --git a/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeature.swift b/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeature.swift index 90bbca72..3dfa438f 100644 --- a/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeature.swift +++ b/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeature.swift @@ -40,8 +40,6 @@ public enum AppFeature: String, CaseIterable { case routing - case siri - public static let allButAppleTV: [AppFeature] = allCases.filter { $0 != .appleTV } diff --git a/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeatureProviding.swift b/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeatureProviding.swift index 6f5f216d..8e3e6c5f 100644 --- a/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeatureProviding.swift +++ b/Passepartout/Library/Sources/CommonLibrary/IAP/AppFeatureProviding.swift @@ -47,6 +47,9 @@ extension AppUserLevel: AppFeatureProviding { extension AppProduct: AppFeatureProviding { var features: [AppFeature] { switch self { + case .Full.Recurring.monthly, .Full.Recurring.yearly: + return AppFeature.allCases + case .Features.allProviders: return [.providers] @@ -56,9 +59,6 @@ extension AppProduct: AppFeatureProviding { case .Features.networkSettings: return [.dns, .httpProxy, .routing] - case .Features.siriShortcuts: - return [.siri] - case .Features.trustedNetworks: return [.onDemand] diff --git a/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift b/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift index 45174e72..19c3b6da 100644 --- a/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift +++ b/Passepartout/Library/Sources/CommonLibrary/IAP/AppProduct+Features.swift @@ -35,8 +35,6 @@ extension AppProduct { public static let networkSettings = AppProduct(featureId: "network_settings") - public static let siriShortcuts = AppProduct(featureId: "siri") - public static let trustedNetworks = AppProduct(featureId: "trusted_networks") static let all: [AppProduct] = [ @@ -44,7 +42,6 @@ extension AppProduct { .Features.appleTV, .Features.interactiveLogin, .Features.networkSettings, - .Features.siriShortcuts, .Features.trustedNetworks ] } @@ -56,10 +53,18 @@ extension AppProduct { public static let allPlatforms = AppProduct(featureId: "full_multi_version") + public enum Recurring { + public static let monthly = AppProduct(featureId: "full.monthly") + + public static let yearly = AppProduct(featureId: "full.yearly") + } + static let all: [AppProduct] = [ .Full.iOS, .Full.macOS, - .Full.allPlatforms + .Full.allPlatforms, + .Full.Recurring.monthly, + .Full.Recurring.yearly ] } diff --git a/Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift b/Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift index 289ffacd..b6ee0915 100644 --- a/Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift +++ b/Passepartout/Library/Sources/UILibrary/L10n/AppFeature+L10n.swift @@ -29,30 +29,28 @@ import Foundation extension AppFeature: LocalizableEntity { public var localizedDescription: String { + let V = Strings.Features.self switch self { case .appleTV: - return Strings.Unlocalized.appleTV + return V.appleTV(Strings.Unlocalized.appleTV) case .dns: - return Strings.Unlocalized.dns + return V.dns(Strings.Unlocalized.dns) case .httpProxy: - return Strings.Unlocalized.httpProxy + return V.httpProxy(Strings.Unlocalized.httpProxy) case .interactiveLogin: - return Strings.Features.interactiveLogin + return V.interactiveLogin case .onDemand: - return Strings.Global.onDemand + return V.onDemand(Strings.Global.onDemand) case .providers: - return Strings.Features.providers + return V.providers case .routing: - return Strings.Global.routing - - case .siri: - return Strings.Features.siri + return V.routing(Strings.Global.routing) } } } diff --git a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift index 54323f2a..4dc2f200 100644 --- a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift +++ b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift @@ -175,12 +175,30 @@ public enum Strings { } } 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 static func appleTV(_ p1: Any) -> String { + return Strings.tr("Localizable", "features.appleTV", String(describing: p1), fallback: "%@") + } + /// %@ + public static func dns(_ p1: Any) -> String { + return Strings.tr("Localizable", "features.dns", String(describing: p1), fallback: "%@") + } + /// %@ + public static func httpProxy(_ p1: Any) -> String { + return Strings.tr("Localizable", "features.httpProxy", String(describing: p1), fallback: "%@") + } + /// Interactive Login + public static let interactiveLogin = Strings.tr("Localizable", "features.interactiveLogin", fallback: "Interactive Login") + /// %@ + public static func onDemand(_ p1: Any) -> String { + return Strings.tr("Localizable", "features.onDemand", String(describing: p1), fallback: "%@") + } + /// All Providers + public static let providers = Strings.tr("Localizable", "features.providers", fallback: "All Providers") + /// %@ + public static func routing(_ p1: Any) -> String { + return Strings.tr("Localizable", "features.routing", String(describing: p1), fallback: "%@") + } } public enum Global { /// About @@ -492,6 +510,38 @@ public enum Strings { public static let presharedKey = Strings.tr("Localizable", "modules.wireguard.preshared_key", fallback: "Pre-shared key") } } + public enum Paywall { + public enum Alerts { + public enum Pending { + /// The purchase is pending external confirmation. The feature will be credited upon approval. + public static let message = Strings.tr("Localizable", "paywall.alerts.pending.message", fallback: "The purchase is pending external confirmation. The feature will be credited upon approval.") + } + } + public enum Rows { + /// Restore purchases + public static let restorePurchases = Strings.tr("Localizable", "paywall.rows.restore_purchases", fallback: "Restore purchases") + } + public enum Sections { + public enum Features { + /// Subscribe for + public static let header = Strings.tr("Localizable", "paywall.sections.features.header", fallback: "Subscribe for") + } + public enum OneTime { + /// One-time purchase + public static let header = Strings.tr("Localizable", "paywall.sections.one_time.header", fallback: "One-time purchase") + } + public enum Recurring { + /// All features + public static let header = Strings.tr("Localizable", "paywall.sections.recurring.header", fallback: "All features") + } + public enum Restore { + /// If you bought this app or feature in the past, you can restore your purchases. + public static let footer = Strings.tr("Localizable", "paywall.sections.restore.footer", fallback: "If you bought this app or feature in the past, you can restore your purchases.") + /// Restore + public static let header = Strings.tr("Localizable", "paywall.sections.restore.header", fallback: "Restore") + } + } + } public enum Placeholders { /// secret public static let secret = Strings.tr("Localizable", "placeholders.secret", fallback: "secret") diff --git a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings index 3bd55f14..4dbb283b 100644 --- a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings @@ -254,9 +254,21 @@ // MARK: - Paywalls -"features.interactive_login" = "Interactive login"; -"features.providers" = "Providers"; -"features.siri" = "Shortcuts"; +"paywall.sections.one_time.header" = "One-time purchase"; +"paywall.sections.recurring.header" = "All features"; +"paywall.sections.features.header" = "Subscribe for"; +"paywall.sections.restore.header" = "Restore"; +"paywall.sections.restore.footer" = "If you bought this app or feature in the past, you can restore your purchases."; +"paywall.rows.restore_purchases" = "Restore purchases"; +"paywall.alerts.pending.message" = "The purchase is pending external confirmation. The feature will be credited upon approval."; + +"features.appleTV" = "%@"; +"features.dns" = "%@"; +"features.httpProxy" = "%@"; +"features.interactiveLogin" = "Interactive Login"; +"features.onDemand" = "%@"; +"features.providers" = "All Providers"; +"features.routing" = "%@"; "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."; diff --git a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift index b6145d69..16923eca 100644 --- a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift +++ b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift @@ -278,7 +278,7 @@ struct ThemeNavigationStackModifier: ViewModifier { Button { dismiss() } label: { - ThemeImage(.close) + ThemeCloseLabel() } } } diff --git a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift index 0c1d1406..056404d2 100644 --- a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift +++ b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift @@ -91,6 +91,19 @@ public struct ThemeImageLabel: View { } } +public struct ThemeCloseLabel: View { + public init() { + } + + public var body: some View { +#if os(iOS) || os(tvOS) + ThemeImage(.close) +#else + Text(Strings.Global.cancel) +#endif + } +} + public struct ThemeCountryText: View { private let code: String diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift index 2bc220a2..4a166f87 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallModifier.swift @@ -64,6 +64,7 @@ public struct PaywallModifier: ViewModifier { suggestedProduct: args.product ) } + .frame(idealHeight: 400) } .onChange(of: reason) { switch $0 { diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView+Custom.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView+Custom.swift new file mode 100644 index 00000000..2f1cb5d5 --- /dev/null +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView+Custom.swift @@ -0,0 +1,67 @@ +// +// PaywallView+Custom.swift +// Passepartout +// +// Created by Davide De Rosa on 11/7/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 StoreKit +import SwiftUI + +extension PaywallView { + struct CustomProductView: View { + let style: ProductStyle + + @ObservedObject + var iapManager: IAPManager + + let product: InAppProduct + + let onComplete: (String, InAppPurchaseResult) -> Void + + let onError: (Error) -> Void + + var body: some View { + HStack { + Text(verbatim: product.localizedTitle) + Spacer() + Button(action: purchase) { + Text(verbatim: product.localizedPrice) + } + } + } + } +} + +private extension PaywallView.CustomProductView { + func purchase() { + Task { + do { + let result = try await iapManager.purchase(product) + onComplete(product.productIdentifier, result) + } catch { + onError(error) + } + } + } +} diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView+StoreKit.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView+StoreKit.swift new file mode 100644 index 00000000..b74835df --- /dev/null +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView+StoreKit.swift @@ -0,0 +1,72 @@ +// +// PaywallView+StoreKit.swift +// Passepartout +// +// Created by Davide De Rosa on 11/7/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 CommonUtils +import StoreKit +import SwiftUI + +@available(iOS 17, macOS 14, *) +extension PaywallView { + struct StoreKitProductView: View { + let style: ProductStyle + + let product: InAppProduct + + let onComplete: (String, InAppPurchaseResult) -> Void + + let onError: (Error) -> Void + + var body: some View { + ProductView(id: product.productIdentifier) + .productViewStyle(.compact) + .onInAppPurchaseCompletion { skProduct, result in + do { + let skResult = try result.get() + onComplete(skProduct.id, skResult.toResult) + } catch { + onError(error) + } + } + } + } +} + +private extension Product.PurchaseResult { + var toResult: InAppPurchaseResult { + switch self { + case .success: + return .done + + case .pending: + return .pending + + case .userCancelled: + return .cancelled + + default: + return .cancelled + } + } +} diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift index 2c9eea03..5fe8f025 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift @@ -24,9 +24,16 @@ // import CommonLibrary +import CommonUtils +import StoreKit import SwiftUI struct PaywallView: View { + enum ProductStyle { + case oneTime + + case recurring + } @EnvironmentObject private var iapManager: IAPManager @@ -38,15 +45,112 @@ struct PaywallView: View { let suggestedProduct: AppProduct? - // FIXME: #819, UI for paywall + @State + private var isFetchingProducts = true + + @State + private var oneTimeProduct: InAppProduct? + + @State + private var recurringProducts: [InAppProduct] = [] + + @State + private var isPendingPresented = false + + @StateObject + private var errorHandler: ErrorHandler = .default() + var body: some View { - VStack { - Spacer() - Text(String(describing: feature).capitalized) - Spacer() + Form { + if isFetchingProducts { + ProgressView() + .id(UUID()) + } else { + productsView + subscriptionFeaturesView + restoreView + } } + .themeForm() .toolbar(content: toolbarContent) - .navigationTitle(Strings.Global.purchase) + .alert( + Strings.Global.purchase, + isPresented: $isPendingPresented, + actions: pendingActions, + message: pendingMessage + ) + .task(id: feature) { + await fetchAvailableProducts() + } + .withErrorHandler(errorHandler) + } +} + +private extension PaywallView { + var title: String { + Strings.Global.purchase + } + + var subscriptionFeatures: [AppFeature] { + AppFeature.allCases.sorted { + $0.localizedDescription < $1.localizedDescription + } + } + + @ViewBuilder + var productsView: some View { + oneTimeProduct.map { + productView(.oneTime, for: $0) + .themeSection(header: Strings.Paywall.Sections.OneTime.header) + } + ForEach(recurringProducts, id: \.productIdentifier) { + productView(.recurring, for: $0) + } + .themeSection(header: Strings.Paywall.Sections.Recurring.header) + } + +#if os(iOS) || os(tvOS) + var subscriptionFeaturesView: some View { + ForEach(subscriptionFeatures, id: \.id) { feature in + Text(feature.localizedDescription) + } + .themeSection(header: Strings.Paywall.Sections.Features.header) + } +#else + var subscriptionFeaturesView: some View { + Table(subscriptionFeatures) { + TableColumn(Strings.Paywall.Sections.Features.header, value: \.localizedDescription) + } + } +#endif + + @ViewBuilder + func productView(_ style: ProductStyle, for product: InAppProduct) -> some View { + if #available(iOS 17, macOS 14, *) { + StoreKitProductView( + style: style, + product: product, + onComplete: onComplete, + onError: onError + ) + } else { + CustomProductView( + style: style, + iapManager: iapManager, + product: product, + onComplete: onComplete, + onError: onError + ) + } + } + + var restoreView: some View { + RestorePurchasesButton() + .themeSectionWithSingleRow( + header: Strings.Paywall.Sections.Restore.header, + footer: Strings.Paywall.Sections.Restore.footer, + above: true + ) } } @@ -58,11 +162,77 @@ private extension PaywallView { Button { isPresented = false } label: { - ThemeImage(.close) + ThemeCloseLabel() } } } -// var purchaseView: some View { -// } + func pendingActions() -> some View { + Button(Strings.Global.ok) { + isPresented = false + } + } + + func pendingMessage() -> some View { + Text(Strings.Paywall.Alerts.Pending.message) + } +} + +// MARK: - + +private extension PaywallView { + func fetchAvailableProducts() async { + isFetchingProducts = true + + var list: [AppProduct] = [] + if let suggestedProduct { + list.append(suggestedProduct) + } + list.append(.Full.Recurring.yearly) + list.append(.Full.Recurring.monthly) + + let availableProducts = await iapManager.purchasableProducts(for: list) + oneTimeProduct = availableProducts.first { + guard let suggestedProduct else { + return false + } + return $0.productIdentifier.hasSuffix(suggestedProduct.rawValue) + } + recurringProducts = availableProducts.filter { + $0.productIdentifier != oneTimeProduct?.productIdentifier + } + + isFetchingProducts = false + } + + func onComplete(_ productIdentifier: String, result: InAppPurchaseResult) { + switch result { + case .done: + isPresented = false + + case .pending: + isPendingPresented = true + + case .cancelled: + break + + case .notFound: + fatalError("Product not found: \(productIdentifier)") + } + } + + func onError(_ error: Error) { + errorHandler.handle(error, title: Strings.Global.purchase) + } +} + +// MARK: - Previews + +#Preview { + PaywallView( + isPresented: .constant(true), + feature: .appleTV, + suggestedProduct: .Features.appleTV + ) + .withMockEnvironment() } diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/RestorePurchasesButton.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/RestorePurchasesButton.swift new file mode 100644 index 00000000..6eed490b --- /dev/null +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/RestorePurchasesButton.swift @@ -0,0 +1,44 @@ +// +// RestorePurchasesButton.swift +// Passepartout +// +// Created by Davide De Rosa on 11/6/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 SwiftUI + +public struct RestorePurchasesButton: View { + + @EnvironmentObject + private var iapManager: IAPManager + + public init() { + } + + public var body: some View { + Button(Strings.Paywall.Rows.restorePurchases) { + Task { + try await iapManager.restorePurchases() + } + } + } +} diff --git a/Passepartout/Library/Sources/UILibrary/Views/UI/InteractiveCoordinator.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/InteractiveCoordinator.swift index b78685c9..924da18a 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/UI/InteractiveCoordinator.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/UI/InteractiveCoordinator.swift @@ -100,11 +100,7 @@ private extension InteractiveCoordinator { } ToolbarItem(placement: .cancellationAction) { Button(action: cancel) { -#if os(iOS) - ThemeImage(.close) -#else - Text(Strings.Global.cancel) -#endif + ThemeCloseLabel() } } } diff --git a/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift b/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift index 65ce4c46..f00c9788 100644 --- a/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift +++ b/Passepartout/Library/Tests/CommonLibraryTests/IAPManagerTests.swift @@ -87,7 +87,6 @@ extension IAPManagerTests { func test_givenPurchasedFeatures_thenIsOnlyEligibleForFeatures() async { let reader = MockAppReceiptReader() await reader.setReceipt(withBuild: defaultBuildNumber, products: [ - .Features.siriShortcuts, .Features.networkSettings ]) let sut = IAPManager(receiptReader: reader) @@ -97,7 +96,6 @@ extension IAPManagerTests { XCTAssertTrue(sut.isEligible(for: .httpProxy)) XCTAssertFalse(sut.isEligible(for: .onDemand)) XCTAssertTrue(sut.isEligible(for: .routing)) - XCTAssertTrue(sut.isEligible(for: .siri)) XCTAssertFalse(sut.isEligible(for: AppFeature.allButAppleTV)) }