diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme
index 0e471e804..21b0fa825 100644
--- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme
+++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme
@@ -103,7 +103,7 @@
Void
- @State
- private var requiresPurchase = false
-
- @State
- private var requiredFeatures: Set = []
-
@State
private var paywallReason: PaywallReason?
@@ -72,11 +66,8 @@ struct ProfileCoordinator: View {
var body: some View {
contentView
- .modifier(PaywallModifier(reason: $paywallReason))
- .modifier(PurchaseAlertModifier(
- isPresented: $requiresPurchase,
- paywallReason: $paywallReason,
- requiredFeatures: requiredFeatures,
+ .modifier(PaywallModifier(
+ reason: $paywallReason,
okTitle: Strings.Views.Profile.Alerts.Purchase.Buttons.ok,
okAction: onDismiss
))
@@ -144,8 +135,7 @@ private extension ProfileCoordinator {
do {
try iapManager.verify(savedProfile)
} catch AppError.ineligibleProfile(let requiredFeatures) {
- self.requiredFeatures = requiredFeatures
- requiresPurchase = true
+ paywallReason = .init(requiredFeatures, needsConfirmation: true)
return
}
onDismiss()
@@ -156,7 +146,7 @@ private extension ProfileCoordinator {
do {
try iapManager.verify(profileEditor.activeModules)
} catch AppError.ineligibleProfile(let requiredFeatures) {
- paywallReason = .purchase(requiredFeatures)
+ paywallReason = .init(requiredFeatures)
return
}
try await profileEditor.save(to: profileManager)
diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Profile/StorageSection.swift b/Passepartout/Library/Sources/AppUIMain/Views/Profile/StorageSection.swift
index 1b8c5248c..3f32c881f 100644
--- a/Passepartout/Library/Sources/AppUIMain/Views/Profile/StorageSection.swift
+++ b/Passepartout/Library/Sources/AppUIMain/Views/Profile/StorageSection.swift
@@ -74,25 +74,20 @@ private extension StorageSection {
}
var purchaseSharingButton: some View {
- EmptyView()
- .modifier(PurchaseButtonModifier(
- Strings.Modules.General.Rows.Shared.purchase,
- feature: .sharing,
- suggesting: nil,
- showsIfRestricted: false,
- paywallReason: $paywallReason
- ))
+ PurchaseRequiredButton(
+ Strings.Modules.General.Rows.Shared.purchase,
+ features: [.sharing],
+ paywallReason: $paywallReason
+ )
}
var purchaseTVButton: some View {
- EmptyView()
- .modifier(PurchaseButtonModifier(
- Strings.Modules.General.Rows.Appletv.purchase,
- feature: .appleTV,
- suggesting: .Features.appleTV,
- showsIfRestricted: false,
- paywallReason: $paywallReason
- ))
+ PurchaseRequiredButton(
+ Strings.Modules.General.Rows.Appletv.purchase,
+ features: [.appleTV],
+ suggestedProduct: .Features.appleTV,
+ paywallReason: $paywallReason
+ )
}
var header: String {
diff --git a/Passepartout/Library/Sources/AppUITV/Views/App/AppCoordinator.swift b/Passepartout/Library/Sources/AppUITV/Views/App/AppCoordinator.swift
index 8d7330272..82bac97db 100644
--- a/Passepartout/Library/Sources/AppUITV/Views/App/AppCoordinator.swift
+++ b/Passepartout/Library/Sources/AppUITV/Views/App/AppCoordinator.swift
@@ -36,12 +36,6 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
private let registry: Registry
- @State
- private var requiresPurchase = false
-
- @State
- private var requiredFeatures: Set = []
-
@State
private var paywallReason: PaywallReason?
@@ -74,13 +68,8 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
}
}
.navigationDestination(for: AppCoordinatorRoute.self, destination: pushDestination)
- .withErrorHandler(errorHandler)
.modifier(PaywallModifier(reason: $paywallReason))
- .modifier(PurchaseAlertModifier(
- isPresented: $requiresPurchase,
- paywallReason: $paywallReason,
- requiredFeatures: requiredFeatures
- ))
+ .withErrorHandler(errorHandler)
}
}
}
@@ -147,8 +136,7 @@ private extension AppCoordinator {
}
func onPurchaseRequired(_ features: Set) {
- requiredFeatures = features
- requiresPurchase = true
+ paywallReason = .init(features, needsConfirmation: true)
}
}
diff --git a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift
index de4f0804f..d5cc7f2f7 100644
--- a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift
+++ b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift
@@ -705,6 +705,12 @@ public enum Strings {
}
public enum Paywall {
public enum Alerts {
+ public enum Confirmation {
+ /// This profile requires paid features to work.
+ public static let message = Strings.tr("Localizable", "views.paywall.alerts.confirmation.message", fallback: "This profile requires paid features to work.")
+ /// Purchase required
+ public static let title = Strings.tr("Localizable", "views.paywall.alerts.confirmation.title", fallback: "Purchase required")
+ }
public enum Pending {
/// The purchase is pending external confirmation. The feature will be credited upon approval.
public static let message = Strings.tr("Localizable", "views.paywall.alerts.pending.message", fallback: "The purchase is pending external confirmation. The feature will be credited upon approval.")
@@ -830,12 +836,6 @@ public enum Strings {
/// (on-demand)
public static let onDemandSuffix = Strings.tr("Localizable", "views.ui.connection_status.on_demand_suffix", fallback: " (on-demand)")
}
- public enum PurchaseAlert {
- /// This profile requires paid features to work.
- public static let message = Strings.tr("Localizable", "views.ui.purchase_alert.message", fallback: "This profile requires paid features to work.")
- /// Purchase required
- public static let title = Strings.tr("Localizable", "views.ui.purchase_alert.title", fallback: "Purchase required")
- }
public enum PurchaseRequired {
public enum Purchase {
/// Purchase required
diff --git a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings
index 73aa2d4a4..d8c87f667 100644
--- a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings
+++ b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings
@@ -63,6 +63,8 @@
"views.paywall.sections.restore.header" = "Restore";
"views.paywall.sections.restore.footer" = "If you bought this app or feature in the past, you can restore your purchases.";
"views.paywall.rows.restore_purchases" = "Restore purchases";
+"views.paywall.alerts.confirmation.title" = "Purchase required";
+"views.paywall.alerts.confirmation.message" = "This profile requires paid features to work.";
"views.paywall.alerts.restricted.title" = "Restricted";
"views.paywall.alerts.restricted.message" = "Some features are unavailable in this build.";
"views.paywall.alerts.pending.message" = "The purchase is pending external confirmation. The feature will be credited upon approval.";
@@ -93,8 +95,6 @@
"views.providers.vpn.no_servers" = "No servers";
"views.ui.connection_status.on_demand_suffix" = " (on-demand)";
-"views.ui.purchase_alert.title" = "Purchase required";
-"views.ui.purchase_alert.message" = "This profile requires paid features to work.";
"views.ui.purchase_required.purchase.help" = "Purchase required";
"views.ui.purchase_required.restricted.help" = "Feature is restricted";
diff --git a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift
index 702319b28..e44ac7dfe 100644
--- a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift
+++ b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift
@@ -32,7 +32,20 @@ import SwiftUI
// MARK: Shortcuts
-public enum ThemeModalSize {
+public struct ThemeModalOptions: Hashable {
+ public var size: ThemeModalSize = .medium
+
+ public var isFixedWidth = false
+
+ public var isFixedHeight = false
+
+ public var isInteractive = true
+
+ public init() {
+ }
+}
+
+public enum ThemeModalSize: Hashable {
case small
case medium
@@ -45,36 +58,24 @@ public enum ThemeModalSize {
extension View {
public func themeModal(
isPresented: Binding,
- size: ThemeModalSize = .medium,
- isFixedWidth: Bool = false,
- isFixedHeight: Bool = false,
- isInteractive: Bool = true,
+ options: ThemeModalOptions? = nil,
content: @escaping () -> Content
) -> some View where Content: View {
modifier(ThemeBooleanModalModifier(
isPresented: isPresented,
- size: size,
- isFixedWidth: isFixedWidth,
- isFixedHeight: isFixedHeight,
- isInteractive: isInteractive,
+ options: options ?? ThemeModalOptions(),
modal: content
))
}
public func themeModal(
item: Binding,
- size: ThemeModalSize = .medium,
- isFixedWidth: Bool = false,
- isFixedHeight: Bool = false,
- isInteractive: Bool = true,
+ options: ThemeModalOptions? = nil,
content: @escaping (T) -> Content
) -> some View where Content: View, T: Identifiable {
modifier(ThemeItemModalModifier(
item: item,
- size: size,
- isFixedWidth: isFixedWidth,
- isFixedHeight: isFixedHeight,
- isInteractive: isInteractive,
+ options: options ?? ThemeModalOptions(),
modal: content
))
}
@@ -282,18 +283,12 @@ struct ThemeBooleanModalModifier: ViewModifier where Modal: View {
@Binding
var isPresented: Bool
- let size: ThemeModalSize
-
- let isFixedWidth: Bool
-
- let isFixedHeight: Bool
-
- let isInteractive: Bool
+ let options: ThemeModalOptions
let modal: () -> Modal
func body(content: Content) -> some View {
- let modalSize = theme.modalSize(size)
+ let modalSize = theme.modalSize(options.size)
_ = modalSize
return content
.sheet(isPresented: $isPresented) {
@@ -301,12 +296,12 @@ struct ThemeBooleanModalModifier: ViewModifier where Modal: View {
#if os(macOS)
.frame(
minWidth: modalSize.width,
- maxWidth: isFixedWidth ? modalSize.width : nil,
+ maxWidth: options.isFixedWidth ? modalSize.width : nil,
minHeight: modalSize.height,
- maxHeight: isFixedHeight ? modalSize.height : nil
+ maxHeight: options.isFixedHeight ? modalSize.height : nil
)
#endif
- .interactiveDismissDisabled(!isInteractive)
+ .interactiveDismissDisabled(!options.isInteractive)
.themeLockScreen()
}
}
@@ -320,18 +315,12 @@ struct ThemeItemModalModifier: ViewModifier where Modal: View, T: Iden
@Binding
var item: T?
- let size: ThemeModalSize
-
- let isFixedWidth: Bool
-
- let isFixedHeight: Bool
-
- let isInteractive: Bool
+ let options: ThemeModalOptions
let modal: (T) -> Modal
func body(content: Content) -> some View {
- let modalSize = theme.modalSize(size)
+ let modalSize = theme.modalSize(options.size)
_ = modalSize
return content
.sheet(item: $item) {
@@ -339,12 +328,12 @@ struct ThemeItemModalModifier: ViewModifier where Modal: View, T: Iden
#if os(macOS)
.frame(
minWidth: modalSize.width,
- maxWidth: isFixedWidth ? modalSize.width : nil,
+ maxWidth: options.isFixedWidth ? modalSize.width : nil,
minHeight: modalSize.height,
- maxHeight: isFixedHeight ? modalSize.height : nil
+ maxHeight: options.isFixedHeight ? modalSize.height : nil
)
#endif
- .interactiveDismissDisabled(!isInteractive)
+ .interactiveDismissDisabled(!options.isInteractive)
.themeLockScreen()
}
}
diff --git a/Passepartout/Library/Sources/CommonIAP/Domain/PaywallReason.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/PaywallModifier+Reason.swift
similarity index 57%
rename from Passepartout/Library/Sources/CommonIAP/Domain/PaywallReason.swift
rename to Passepartout/Library/Sources/UILibrary/Views/UI/PaywallModifier+Reason.swift
index 57f44175b..ad23b301c 100644
--- a/Passepartout/Library/Sources/CommonIAP/Domain/PaywallReason.swift
+++ b/Passepartout/Library/Sources/UILibrary/Views/UI/PaywallModifier+Reason.swift
@@ -1,5 +1,5 @@
//
-// PaywallReason.swift
+// PaywallModifier+Reason.swift
// Passepartout
//
// Created by Davide De Rosa on 9/14/24.
@@ -23,8 +23,27 @@
// along with Passepartout. If not, see .
//
+import CommonIAP
import Foundation
-public enum PaywallReason: Hashable {
- case purchase(Set, AppProduct? = nil)
+public typealias PaywallReason = PaywallModifier.Reason
+
+extension PaywallModifier {
+ public struct Reason: Hashable {
+ public let requiredFeatures: Set
+
+ public let suggestedProduct: AppProduct?
+
+ public let needsConfirmation: Bool
+
+ public init(
+ _ requiredFeatures: Set,
+ suggestedProduct: AppProduct? = nil,
+ needsConfirmation: Bool = false
+ ) {
+ self.requiredFeatures = requiredFeatures
+ self.suggestedProduct = suggestedProduct
+ self.needsConfirmation = needsConfirmation
+ }
+ }
}
diff --git a/Passepartout/Library/Sources/UILibrary/Views/UI/PaywallModifier.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/PaywallModifier.swift
index 22e3afb4b..b793ffe13 100644
--- a/Passepartout/Library/Sources/UILibrary/Views/UI/PaywallModifier.swift
+++ b/Passepartout/Library/Sources/UILibrary/Views/UI/PaywallModifier.swift
@@ -34,97 +34,161 @@ public struct PaywallModifier: ViewModifier {
@Binding
private var reason: PaywallReason?
+ private let okTitle: String?
+
+ private let okAction: (() -> Void)?
+
+ @State
+ private var isConfirming = false
+
@State
- private var isPresentingRestricted = false
+ private var isRestricted = false
@State
- private var paywallArguments: PaywallArguments?
+ private var isPurchasing = false
- public init(reason: Binding) {
+ public init(
+ reason: Binding,
+ okTitle: String? = nil,
+ okAction: (() -> Void)? = nil
+ ) {
_reason = reason
+ self.okTitle = okTitle
+ self.okAction = okAction
}
public func body(content: Content) -> some View {
content
+ .alert(
+ Strings.Views.Paywall.Alerts.Confirmation.title,
+ isPresented: $isConfirming,
+ actions: confirmationActions,
+ message: confirmationMessage
+ )
.alert(
Strings.Views.Paywall.Alerts.Restricted.title,
- isPresented: $isPresentingRestricted,
- actions: {
- Button(Strings.Global.Nouns.ok) {
- reason = nil
- isPresentingRestricted = false
- }
- },
- message: {
- Text(restrictedMessage)
- }
+ isPresented: $isRestricted,
+ actions: restrictedActions,
+ message: restrictedMessage
)
- .themeModal(item: $paywallArguments) { args in
- NavigationStack {
- PaywallView(
- isPresented: isPresentingPurchase,
- features: iapManager.excludingEligible(from: args.features),
- suggestedProduct: args.product
- )
+ .themeModal(isPresented: $isPurchasing, content: modalDestination)
+ .onChange(of: isRestricted) {
+ if !$0 {
+ reason = nil
+ }
+ }
+ .onChange(of: isPurchasing) {
+ if !$0 {
+ reason = nil
}
- .frame(idealHeight: 500)
}
.onChange(of: reason) {
- switch $0 {
- case .purchase(let features, let product):
- guard !iapManager.isRestricted else {
- isPresentingRestricted = true
- return
+ guard let reason = $0 else {
+ return
+ }
+ if !iapManager.isRestricted {
+ if reason.needsConfirmation {
+ isConfirming = true
+ } else {
+ isPurchasing = true
}
- paywallArguments = PaywallArguments(features: features, product: product)
-
- default:
- break
+ } else {
+ isRestricted = true
}
}
}
}
private extension PaywallModifier {
- var isPresentingPurchase: Binding {
- Binding {
- paywallArguments != nil
- } set: {
- if !$0 {
- // make sure to reset this to allow paywall to appear again
+ var ineligibleFeatures: [String] {
+ guard let reason else {
+ return []
+ }
+ return iapManager
+ .excludingEligible(from: reason.requiredFeatures)
+ .map(\.localizedDescription)
+ .sorted()
+ }
+
+ func alertMessage(startingWith header: String, features: [String]) -> String {
+ header + "\n\n" + features
+ .joined(separator: "\n")
+ }
+}
+
+private extension IAPManager {
+ func excludingEligible(from features: Set) -> Set {
+ features.filter {
+ !isEligible(for: $0)
+ }
+ }
+}
+
+// MARK: - Confirmation alert
+
+private extension PaywallModifier {
+
+ @ViewBuilder
+ func confirmationActions() -> some View {
+ Button(Strings.Global.Actions.purchase) {
+ // IMPORTANT: retain reason because it serves paywall content
+ isPurchasing = true
+ }
+ if let okTitle {
+ Button(okTitle) {
reason = nil
- paywallArguments = nil
+ okAction?()
}
}
+ Button(Strings.Global.Actions.cancel, role: .cancel) {
+ reason = nil
+ }
}
- var restrictedMessage: String {
- guard case .purchase(let features, _) = reason else {
- return ""
- }
- let msg = Strings.Views.Paywall.Alerts.Restricted.message
- return msg + "\n\n" + iapManager
- .excludingEligible(from: features)
- .map(\.localizedDescription)
- .sorted()
- .joined(separator: "\n")
+ func confirmationMessage() -> some View {
+ Text(confirmationMessageString)
+ }
+
+ var confirmationMessageString: String {
+ alertMessage(
+ startingWith: Strings.Views.Paywall.Alerts.Confirmation.message,
+ features: ineligibleFeatures
+ )
}
}
-private struct PaywallArguments: Identifiable {
- let features: Set
+// MARK: - Restricted alert
- let product: AppProduct?
+private extension PaywallModifier {
+ func restrictedActions() -> some View {
+ Button(Strings.Global.Nouns.ok) {
+ //
+ }
+ }
- var id: [String] {
- features.map(\.id)
+ func restrictedMessage() -> some View {
+ Text(restrictedMessageString)
+ }
+
+ var restrictedMessageString: String {
+ alertMessage(
+ startingWith: Strings.Views.Paywall.Alerts.Restricted.message,
+ features: ineligibleFeatures
+ )
}
}
-private extension IAPManager {
- func excludingEligible(from features: Set) -> Set {
- features.filter {
- !isEligible(for: $0)
+// MARK: - Paywall
+
+private extension PaywallModifier {
+ func modalDestination() -> some View {
+ reason.map {
+ PaywallView(
+ isPresented: $isPurchasing,
+ features: iapManager.excludingEligible(from: $0.requiredFeatures),
+ suggestedProduct: $0.suggestedProduct
+ )
+ .themeNavigationStack()
}
}
}
diff --git a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseAlertModifier.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseAlertModifier.swift
deleted file mode 100644
index a4771ab68..000000000
--- a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseAlertModifier.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-//
-// PurchaseAlertModifier.swift
-// Passepartout
-//
-// Created by Davide De Rosa on 11/23/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 CommonIAP
-import CommonUtils
-import SwiftUI
-
-public struct PurchaseAlertModifier: ViewModifier {
-
- @Binding
- private var isPresented: Bool
-
- @Binding
- private var paywallReason: PaywallReason?
-
- private let requiredFeatures: Set
-
- private let okTitle: String?
-
- private let okAction: (() -> Void)?
-
- public init(
- isPresented: Binding,
- paywallReason: Binding,
- requiredFeatures: Set,
- okTitle: String? = nil,
- okAction: (() -> Void)? = nil
- ) {
- _isPresented = isPresented
- _paywallReason = paywallReason
- self.requiredFeatures = requiredFeatures
- self.okTitle = okTitle
- self.okAction = okAction
- }
-
- public func body(content: Content) -> some View {
- content
- .alert(Strings.Views.Ui.PurchaseAlert.title, isPresented: $isPresented) {
- Button(Strings.Global.Actions.purchase) {
- setLater(.purchase(requiredFeatures, nil)) {
- paywallReason = $0
- }
- }
- if let okTitle {
- Button(okTitle) {
- okAction?()
- }
- }
- Button(Strings.Global.Actions.cancel, role: .cancel, action: {})
- } message: {
- Text(purchaseMessage)
- }
- }
-}
-
-private extension PurchaseAlertModifier {
- var purchaseMessage: String {
- let msg = Strings.Views.Ui.PurchaseAlert.message
- return msg + "\n\n" + requiredFeatures
- .map(\.localizedDescription)
- .joined(separator: "\n")
- }
-}
diff --git a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseButtonModifier.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseButtonModifier.swift
deleted file mode 100644
index ed028d82d..000000000
--- a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseButtonModifier.swift
+++ /dev/null
@@ -1,90 +0,0 @@
-//
-// PurchaseButtonModifier.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 SwiftUI
-
-public struct PurchaseButtonModifier: ViewModifier {
-
- @EnvironmentObject
- private var iapManager: IAPManager
-
- private let title: String
-
- private let label: String?
-
- private let feature: AppFeature
-
- private let suggestedProduct: AppProduct?
-
- private let showsIfRestricted: Bool
-
- @Binding
- private var paywallReason: PaywallReason?
-
- public init(
- _ 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 {
- if iapManager.isEligible(for: feature) {
- content
- } else if !iapManager.isRestricted {
- purchaseView
- } else if showsIfRestricted {
- content
- }
- }
-}
-
-private extension PurchaseButtonModifier {
- var purchaseView: some View {
- HStack {
- if let label {
- Text(label)
- Spacer()
- }
- purchaseButton
- }
- }
-
- var purchaseButton: some View {
- Button(title) {
- paywallReason = .purchase([feature], suggestedProduct)
- }
- }
-}
diff --git a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseRequiredButton.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseRequiredButton.swift
index 9c15df69c..740868416 100644
--- a/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseRequiredButton.swift
+++ b/Passepartout/Library/Sources/UILibrary/Views/UI/PurchaseRequiredButton.swift
@@ -28,61 +28,108 @@ import CommonUtils
import PassepartoutKit
import SwiftUI
-public struct PurchaseRequiredButton: View {
-
- @EnvironmentObject
- private var theme: Theme
+public struct PurchaseRequiredButton: View where Content: View {
@EnvironmentObject
private var iapManager: IAPManager
- private let features: Set?
+ let features: Set?
- @Binding
- private var paywallReason: PaywallReason?
+ let suggestedProduct: AppProduct?
- public init(for requiring: AppFeatureRequiring?, paywallReason: Binding) {
- features = requiring?.features
- _paywallReason = paywallReason
- }
+ @Binding
+ var paywallReason: PaywallReason?
- public init(features: Set?, paywallReason: Binding) {
- self.features = features
- _paywallReason = paywallReason
- }
+ @ViewBuilder
+ let content: (_ isRestricted: Bool, _ action: @escaping () -> Void) -> Content
public var body: some View {
- Button {
- guard let features, !isEligible else {
- return
- }
- setLater(.purchase(features)) {
- paywallReason = $0
- }
- } label: {
- ThemeImage(iapManager.isRestricted ? .warning : .upgrade)
- .help(helpMessage)
- }
-#if os(iOS)
- .buttonStyle(.plain)
-#else
- .imageScale(.large)
- .buttonStyle(.borderless)
-#endif
- .foregroundStyle(theme.upgradeColor)
- .opaque(!isEligible)
+ content(iapManager.isRestricted, onTap)
+ .opaque(!isEligible)
}
}
private extension PurchaseRequiredButton {
+ func onTap() {
+ guard let features, !isEligible else {
+ return
+ }
+ setLater(.init(features, suggestedProduct: suggestedProduct)) {
+ paywallReason = $0
+ }
+ }
+
var isEligible: Bool {
if let features {
return iapManager.isEligible(for: features)
}
return true
}
+}
+
+// MARK: - Initializers
- var helpMessage: String {
- iapManager.isRestricted ? Strings.Views.Ui.PurchaseRequired.Restricted.help : Strings.Views.Ui.PurchaseRequired.Purchase.help
+extension PurchaseRequiredButton where Content == Button {
+ public init(
+ _ title: String,
+ features: Set?,
+ suggestedProduct: AppProduct? = nil,
+ paywallReason: Binding
+ ) {
+ self.features = features
+ self.suggestedProduct = suggestedProduct
+ _paywallReason = paywallReason
+ content = { _, action in
+ Button(title, action: action)
+ }
+ }
+}
+
+extension PurchaseRequiredButton where Content == PurchaseRequiredImageButtonContent {
+ public init(
+ for requiring: AppFeatureRequiring?,
+ suggestedProduct: AppProduct? = nil,
+ paywallReason: Binding
+ ) {
+ self.init(
+ features: requiring?.features,
+ suggestedProduct: suggestedProduct,
+ paywallReason: paywallReason
+ )
+ }
+
+ public init(
+ features: Set?,
+ suggestedProduct: AppProduct? = nil,
+ paywallReason: Binding
+ ) {
+ self.features = features
+ self.suggestedProduct = suggestedProduct
+ _paywallReason = paywallReason
+ content = {
+ PurchaseRequiredImageButtonContent(isRestricted: $0, action: $1)
+ }
+ }
+}
+
+public struct PurchaseRequiredImageButtonContent: View {
+
+ @EnvironmentObject
+ private var theme: Theme
+
+ let isRestricted: Bool
+
+ let action: () -> Void
+
+ public var body: some View {
+ Button(action: action) {
+ ThemeImage(isRestricted ? .warning : .upgrade)
+ .foregroundStyle(theme.upgradeColor)
+ .help(isRestricted ? Strings.Views.Ui.PurchaseRequired.Restricted.help : Strings.Views.Ui.PurchaseRequired.Purchase.help)
+ }
+ .buttonStyle(.plain)
+#if os(macOS)
+ .imageScale(.large)
+#endif
}
}