Skip to content

Commit

Permalink
Simplify paywall entities (#923)
Browse files Browse the repository at this point in the history
- PaywallView is the paywall content
- PaywallModifier attaches paywall with optional confirmation
- PurchaseRequiredButton presents paywall explicitly
- PaywallReason is the compound input

Refactoring:

- PurchaseRequiredButton takes a custom view
- PurchaseAlertModifier was merged into PaywallModifier
- PurchaseButtonModifier was merged into PurchaseRequiredButton
- Modal options were packed into a single struct

Confirmation alert presented on:

- Connect to ineligible profile (AppCoordinator)
- Save ineligible profile (ProfileCoordinator)
  • Loading branch information
keeshux authored Nov 24, 2024
1 parent 064e834 commit 2aa91ee
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 376 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
</EnvironmentVariable>
<EnvironmentVariable
key = "PP_USER_LEVEL"
value = "2"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ public struct AppCoordinator: View, AppCoordinatorConforming {

private let registry: Registry

@StateObject
private var profileEditor = ProfileEditor()
@State
private var isImporting = false

@State
private var modalRoute: ModalRoute?
private var paywallReason: PaywallReason?

@State
private var isImporting = false
private var modalRoute: ModalRoute?

@State
private var profilePath = NavigationPath()

@State
private var migrationPath = NavigationPath()

@State
private var paywallReason: PaywallReason?
@StateObject
private var profileEditor = ProfileEditor()

@StateObject
private var errorHandler: ErrorHandler = .default()
Expand All @@ -79,10 +79,14 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
.modifier(PaywallModifier(reason: $paywallReason))
.themeModal(
item: $modalRoute,
size: modalRoute?.size ?? .large,
isFixedWidth: modalRoute?.isFixedWidth ?? false,
isFixedHeight: modalRoute?.isFixedHeight ?? false,
isInteractive: modalRoute?.isInteractive ?? true,
options: {
var options = ThemeModalOptions()
options.size = modalRoute?.size ?? .large
options.isFixedWidth = modalRoute?.isFixedWidth ?? false
options.isFixedHeight = modalRoute?.isFixedHeight ?? false
options.isInteractive = modalRoute?.isInteractive ?? true
return options
}(),
content: modalDestination
)
}
Expand Down Expand Up @@ -177,10 +181,8 @@ extension AppCoordinator {
}
present(.editProviderEntity($0, pair.module, pair.selection))
},
onPurchaseRequired: { features in
setLater(.purchase(features)) {
paywallReason = $0
}
onPurchaseRequired: {
paywallReason = .init($0, needsConfirmation: true)
}
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,6 @@ struct ProfileCoordinator: View {

let onDismiss: () -> Void

@State
private var requiresPurchase = false

@State
private var requiredFeatures: Set<AppFeature> = []

@State
private var paywallReason: PaywallReason?

Expand All @@ -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
))
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ public struct AppCoordinator: View, AppCoordinatorConforming {

private let registry: Registry

@State
private var requiresPurchase = false

@State
private var requiredFeatures: Set<AppFeature> = []

@State
private var paywallReason: PaywallReason?

Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -147,8 +136,7 @@ private extension AppCoordinator {
}

func onPurchaseRequired(_ features: Set<AppFeature>) {
requiredFeatures = features
requiresPurchase = true
paywallReason = .init(features, needsConfirmation: true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,36 +58,24 @@ public enum ThemeModalSize {
extension View {
public func themeModal<Content>(
isPresented: Binding<Bool>,
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<Content, T>(
item: Binding<T?>,
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
))
}
Expand Down Expand Up @@ -282,31 +283,25 @@ struct ThemeBooleanModalModifier<Modal>: 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) {
modal()
#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()
}
}
Expand All @@ -320,31 +315,25 @@ struct ThemeItemModalModifier<Modal, T>: 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) {
modal($0)
#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()
}
}
Expand Down
Loading

0 comments on commit 2aa91ee

Please sign in to comment.