From 7d7aaa8b0c11f556e29f0ec89b780415e1c88f2e Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 23 Dec 2023 12:10:34 +0100 Subject: [PATCH] Update paywall (#441) Group features and drop platform purchases. --- Passepartout/App/Constants/Theme.swift | 8 ++ .../App/Views/OrganizerView+ProfileRow.swift | 4 +- .../App/Views/PaywallView+Purchase.swift | 115 +++++++----------- .../Managers/ProductManager.swift | 8 +- 4 files changed, 58 insertions(+), 77 deletions(-) diff --git a/Passepartout/App/Constants/Theme.swift b/Passepartout/App/Constants/Theme.swift index e01d45f0..3aed7cc0 100644 --- a/Passepartout/App/Constants/Theme.swift +++ b/Passepartout/App/Constants/Theme.swift @@ -372,6 +372,14 @@ extension View { .foregroundColor(themeSecondaryColor) } + func themeCellTitleStyle() -> some View { + font(.headline) + } + + func themeCellSubtitleStyle() -> some View { + font(.subheadline) + } + func themeDebugLogStyle() -> some View { font(.system(size: 13, weight: .medium, design: .monospaced)) } diff --git a/Passepartout/App/Views/OrganizerView+ProfileRow.swift b/Passepartout/App/Views/OrganizerView+ProfileRow.swift index 765623b1..1d3b14e9 100644 --- a/Passepartout/App/Views/OrganizerView+ProfileRow.swift +++ b/Passepartout/App/Views/OrganizerView+ProfileRow.swift @@ -45,11 +45,11 @@ extension OrganizerView { return HStack { VStack(alignment: .leading, spacing: 5) { Text(profile.header.name) - .font(.headline) + .themeCellTitleStyle() .themeLongTextStyle() VPNStatusText(isActiveProfile: isActiveProfile) - .font(.subheadline) + .themeCellSubtitleStyle() .themeSecondaryTextStyle() } Spacer() diff --git a/Passepartout/App/Views/PaywallView+Purchase.swift b/Passepartout/App/Views/PaywallView+Purchase.swift index a46d5b95..d27aa4fc 100644 --- a/Passepartout/App/Views/PaywallView+Purchase.swift +++ b/Passepartout/App/Views/PaywallView+Purchase.swift @@ -52,7 +52,10 @@ extension PaywallView { var body: some View { List { - productsSection + featuresSection + purchaseSection + .disabled(purchaseState != nil) + restoreSection .disabled(purchaseState != nil) }.navigationTitle(Unlocalized.appName) @@ -77,38 +80,41 @@ private struct PurchaseRow: View { let title: String - let extra: String? - let action: () -> Void let purchaseState: PaywallView.PurchaseView.PurchaseState? var body: some View { - VStack(alignment: .leading) { - actionButton - .padding(.bottom, 5) - - extra.map { - Text($0) - .frame(maxHeight: .infinity) - } - }.padding([.top, .bottom]) + actionButton } } -private typealias RowModel = (product: InAppProduct, extra: String?) - // MARK: - private extension PaywallView.PurchaseView { - var productsSection: some View { + var featuresSection: some View { + Section { + ForEach(features, id: \.productIdentifier) { product in + VStack(alignment: .leading) { + Text(product.localizedTitle) + .themeCellTitleStyle() + if product.localizedDescription != product.localizedTitle { + Text(product.localizedDescription) + .themeCellSubtitleStyle() + .themeSecondaryTextStyle() + } + } + } + } + } + + var purchaseSection: some View { Section { if !productManager.isRefreshingProducts { - ForEach(productRowModels, id: \.product.productIdentifier, content: productRow) + ForEach(productRowModels, id: \.productIdentifier, content: productRow) } else { ProgressView() } - restoreRow } header: { Text(L10n.Paywall.title) } footer: { @@ -116,13 +122,20 @@ private extension PaywallView.PurchaseView { } } - func productRow(_ model: RowModel) -> some View { + var restoreSection: some View { + Section { + restoreRow + } footer: { + Text(L10n.Paywall.Items.Restore.description) + } + } + + func productRow(_ product: InAppProduct) -> some View { PurchaseRow( - product: model.product, - title: model.product.localizedTitle, - extra: model.extra, + product: product, + title: product.localizedTitle, action: { - purchaseProduct(model.product) + purchaseProduct(product) }, purchaseState: purchaseState ) @@ -131,7 +144,6 @@ private extension PaywallView.PurchaseView { var restoreRow: some View { PurchaseRow( title: L10n.Paywall.Items.Restore.title, - extra: L10n.Paywall.Items.Restore.description, action: restorePurchases, purchaseState: purchaseState ) @@ -139,20 +151,6 @@ private extension PaywallView.PurchaseView { } private extension PaywallView.PurchaseView { - var skFeature: InAppProduct? { - guard let feature = feature else { - return nil - } - return productManager.product(withIdentifier: feature) - } - - var skPlatformVersion: InAppProduct? { - #if targetEnvironment(macCatalyst) - productManager.product(withIdentifier: .fullVersion_macOS) - #else - productManager.product(withIdentifier: .fullVersion_iOS) - #endif - } // hide full version if already bought the other platform version var skFullVersion: InAppProduct? { @@ -168,43 +166,18 @@ private extension PaywallView.PurchaseView { return productManager.product(withIdentifier: .fullVersion) } - var platformVersionExtra: [String] { - productManager.featureProducts(excluding: [ - .fullVersion, - .fullVersion_iOS, - .fullVersion_macOS - ]).map { - $0.localizedTitle - }.sorted { - $0.lowercased() < $1.lowercased() + var features: [InAppProduct] { + productManager.featureProducts(excluding: { + $0 == .fullVersion || $0.isPlatformVersion + }) + .sorted { + $0.localizedTitle.lowercased() < $1.localizedTitle.lowercased() } } - var fullVersionExtra: [String] { - productManager.featureProducts(including: [ - .fullVersion_iOS, - .fullVersion_macOS - ]).map { - $0.localizedTitle - }.sorted { - $0.lowercased() < $1.lowercased() - } - } - - var productRowModels: [RowModel] { - var models: [RowModel] = [] - skPlatformVersion.map { - let extra = platformVersionExtra.joined(separator: "\n") - models.append(($0, extra)) - } - skFullVersion.map { - let extra = fullVersionExtra.joined(separator: "\n") - models.append(($0, extra)) - } - skFeature.map { - models.append(($0, nil)) - } - return models + var productRowModels: [InAppProduct] { + [skFullVersion] + .compactMap { $0 } } } diff --git a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift index 17e5a62b..e2a20a5d 100644 --- a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift @@ -132,12 +132,12 @@ public final class ProductManager: NSObject, ObservableObject { inApp.product(withIdentifier: identifier) } - public func featureProducts(including: [LocalProduct]) -> [InAppProduct] { + public func featureProducts(including: (LocalProduct) -> Bool) -> [InAppProduct] { inApp.products().filter { guard let p = LocalProduct(rawValue: $0.productIdentifier) else { return false } - guard including.contains(p) else { + guard including(p) else { return false } guard p.isFeature else { @@ -147,12 +147,12 @@ public final class ProductManager: NSObject, ObservableObject { } } - public func featureProducts(excluding: [LocalProduct]) -> [InAppProduct] { + public func featureProducts(excluding: (LocalProduct) -> Bool) -> [InAppProduct] { inApp.products().filter { guard let p = LocalProduct(rawValue: $0.productIdentifier) else { return false } - guard !excluding.contains(p) else { + guard !excluding(p) else { return false } guard p.isFeature else {