Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls: new .onPurchaseFailure and .onRestoreFailure modifiers #3622

Merged
merged 1 commit into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,10 @@ struct LoadedOfferingPaywallView: View {
value: .init(data: self.purchaseHandler.purchaseResult))
.preference(key: RestoredCustomerInfoPreferenceKey.self,
value: self.purchaseHandler.restoredCustomerInfo)
.preference(key: PurchaseErrorPreferenceKey.self,
value: self.purchaseHandler.purchaseError as NSError?)
.preference(key: RestoreErrorPreferenceKey.self,
value: self.purchaseHandler.restoreError as NSError?)
}

@ViewBuilder
Expand Down
13 changes: 13 additions & 0 deletions RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ extension PurchaseHandler {
} restore: { $0 }
}

/// - Returns: `PurchaseHandler` that throws `error` for purchases and restores.
static func failing(_ error: Error) -> Self {
return self.init(
purchases: MockPurchases { _ in
throw error
} restorePurchases: {
throw error
} trackEvent: { event in
Logger.debug("Tracking event: \(event)")
}
)
}

/// Creates a copy of this `PurchaseHandler` with a delay.
func with(delay seconds: TimeInterval) -> Self {
return self.map { purchaseBlock in {
Expand Down
80 changes: 64 additions & 16 deletions RevenueCatUI/Purchasing/PurchaseHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ final class PurchaseHandler: ObservableObject {
@Published
fileprivate(set) var restoredCustomerInfo: CustomerInfo?

/// Error produced during a purchase.
@Published
fileprivate(set) var purchaseError: Error?

/// Error produced during restoring..
@Published
fileprivate(set) var restoreError: Error?

private var eventData: PaywallEvent.Data?

convenience init(purchases: Purchases = .shared) {
Expand Down Expand Up @@ -69,24 +77,28 @@ extension PurchaseHandler {
@MainActor
func purchase(package: Package) async throws -> PurchaseResultData {
self.purchaseResult = nil
self.purchaseError = nil

withAnimation(Constants.fastAnimation) {
self.actionInProgress = true
}
self.startAction()
defer { self.actionInProgress = false }

let result = try await self.purchases.purchase(package: package)
self.purchaseResult = result
do {
let result = try await self.purchases.purchase(package: package)
self.purchaseResult = result

if result.userCancelled {
self.trackCancelledPurchase()
} else {
withAnimation(Constants.defaultAnimation) {
self.purchased = true
if result.userCancelled {
self.trackCancelledPurchase()
} else {
withAnimation(Constants.defaultAnimation) {
self.purchased = true
}
}
}

return result
return result
} catch {
self.purchaseError = error
throw error
}
}

/// - Returns: `success` is `true` only when the resulting `CustomerInfo`
Expand All @@ -96,13 +108,21 @@ extension PurchaseHandler {
/// This allows the UI to display an alert before dismissing the paywall.
@MainActor
func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) {
self.actionInProgress = true
self.restoredCustomerInfo = nil
self.restoreError = nil

self.startAction()
defer { self.actionInProgress = false }

let customerInfo = try await self.purchases.restorePurchases()
do {
let customerInfo = try await self.purchases.restorePurchases()

return (info: customerInfo,
success: customerInfo.hasActiveSubscriptionsOrNonSubscriptions)
return (info: customerInfo,
success: customerInfo.hasActiveSubscriptionsOrNonSubscriptions)
} catch {
self.restoreError = error
throw error
}
}

@MainActor
Expand Down Expand Up @@ -140,6 +160,12 @@ extension PurchaseHandler {
return true
}

private func startAction() {
withAnimation(Constants.fastAnimation) {
self.actionInProgress = true
}
}

}

#if DEBUG
Expand Down Expand Up @@ -240,6 +266,28 @@ struct RestoredCustomerInfoPreferenceKey: PreferenceKey {

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct PurchaseErrorPreferenceKey: PreferenceKey {

static var defaultValue: NSError?

static func reduce(value: inout NSError?, nextValue: () -> NSError?) {
value = nextValue()
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct RestoreErrorPreferenceKey: PreferenceKey {

static var defaultValue: NSError?

static func reduce(value: inout NSError?, nextValue: () -> NSError?) {
value = nextValue()
}

}

// MARK: -

private extension CustomerInfo {
Expand Down
22 changes: 22 additions & 0 deletions RevenueCatUI/UIKit/PaywallViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ public protocol PaywallViewControllerDelegate: AnyObject {
@objc(paywallViewControllerDidCancelPurchase:)
optional func paywallViewControllerDidCancelPurchase(_ controller: PaywallViewController)

/// Notifies that the purchase operation has failed in a ``PaywallViewController``.
@objc(paywallViewController:didFailPurchasingWithError:)
optional func paywallViewController(_ controller: PaywallViewController,
didFailPurchasingWith error: NSError)

/// Notifies that the restore operation has completed in a ``PaywallViewController``.
///
/// - Warning: Receiving a ``CustomerInfo``does not imply that the user has any entitlements,
Expand All @@ -195,6 +200,11 @@ public protocol PaywallViewControllerDelegate: AnyObject {
optional func paywallViewController(_ controller: PaywallViewController,
didFinishRestoringWith customerInfo: CustomerInfo)

/// Notifies that the restore operation has failed in a ``PaywallViewController``.
@objc(paywallViewController:didFailRestoringWithError:)
optional func paywallViewController(_ controller: PaywallViewController,
didFailRestoringWith error: NSError)

/// Notifies that the ``PaywallViewController`` was dismissed.
@objc(paywallViewControllerWasDismissed:)
optional func paywallViewControllerWasDismissed(_ controller: PaywallViewController)
Expand Down Expand Up @@ -228,6 +238,14 @@ private extension PaywallViewController {
guard let self else { return }
self.delegate?.paywallViewController?(self, didFinishRestoringWith: customerInfo)
},
purchaseFailure: { [weak self] error in
guard let self else { return }
self.delegate?.paywallViewController?(self, didFailPurchasingWith: error)
},
restoreFailure: { [weak self] error in
guard let self else { return }
self.delegate?.paywallViewController?(self, didFailRestoringWith: error)
},
onSizeChange: { [weak self] in
guard let self else { return }
self.delegate?.paywallViewController?(self, didChangeSizeTo: $0)
Expand All @@ -254,13 +272,17 @@ private struct PaywallContainerView: View {
let purchaseCompleted: PurchaseCompletedHandler
let purchaseCancelled: PurchaseCancelledHandler
let restoreCompleted: PurchaseOrRestoreCompletedHandler
let purchaseFailure: PurchaseFailureHandler
let restoreFailure: PurchaseFailureHandler
let onSizeChange: (CGSize) -> Void

var body: some View {
PaywallView(configuration: self.configuration)
.onPurchaseCompleted(self.purchaseCompleted)
.onPurchaseCancelled(self.purchaseCancelled)
.onRestoreCompleted(self.restoreCompleted)
.onPurchaseFailure(self.purchaseFailure)
.onRestoreFailure(self.restoreFailure)
.onSizeChange(self.onSizeChange)

}
Expand Down
28 changes: 28 additions & 0 deletions RevenueCatUI/View+PresentPaywall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ extension View {
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseFailure: PurchaseFailureHandler? = nil,
restoreFailure: PurchaseFailureHandler? = nil,
onDismiss: (() -> Void)? = nil
) -> some View {
return self.presentPaywallIfNeeded(
Expand All @@ -62,6 +64,8 @@ extension View {
purchaseCompleted: purchaseCompleted,
purchaseCancelled: purchaseCancelled,
restoreCompleted: restoreCompleted,
purchaseFailure: purchaseFailure,
restoreFailure: restoreFailure,
onDismiss: onDismiss
)
}
Expand All @@ -80,6 +84,10 @@ extension View {
/// } restoreCompleted: { customerInfo in
/// // If `entitlement_identifier` is active, paywall will dismiss automatically.
/// print("Purchases restored")
/// } purchaseFailure: { error in
/// print("Error purchasing: \(error)")
/// } restoreFailure: { error in
/// print("Error restoring purchases: \(error)")
/// } onDismiss: {
/// print("Paywall was dismissed either manually or automatically after a purchase.")
/// }
Expand All @@ -99,6 +107,8 @@ extension View {
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseFailure: PurchaseFailureHandler? = nil,
restoreFailure: PurchaseFailureHandler? = nil,
onDismiss: (() -> Void)? = nil
) -> some View {
return self.presentPaywallIfNeeded(
Expand All @@ -108,6 +118,8 @@ extension View {
purchaseCompleted: purchaseCompleted,
purchaseCancelled: purchaseCancelled,
restoreCompleted: restoreCompleted,
purchaseFailure: purchaseFailure,
restoreFailure: restoreFailure,
onDismiss: onDismiss,
customerInfoFetcher: {
guard Purchases.isConfigured else {
Expand All @@ -129,6 +141,8 @@ extension View {
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseFailure: PurchaseFailureHandler? = nil,
restoreFailure: PurchaseFailureHandler? = nil,
onDismiss: (() -> Void)? = nil,
customerInfoFetcher: @escaping CustomerInfoFetcher
) -> some View {
Expand All @@ -138,6 +152,8 @@ extension View {
purchaseCompleted: purchaseCompleted,
purchaseCancelled: purchaseCancelled,
restoreCompleted: restoreCompleted,
purchaseFailure: purchaseFailure,
restoreFailure: restoreFailure,
onDismiss: onDismiss,
content: .optionalOffering(offering),
fontProvider: fonts,
Expand All @@ -163,6 +179,8 @@ private struct PresentingPaywallModifier: ViewModifier {
var purchaseCompleted: PurchaseOrRestoreCompletedHandler?
var purchaseCancelled: PurchaseCancelledHandler?
var restoreCompleted: PurchaseOrRestoreCompletedHandler?
var purchaseFailure: PurchaseFailureHandler?
var restoreFailure: PurchaseFailureHandler?
var onDismiss: (() -> Void)?

var content: PaywallViewConfiguration.Content
Expand All @@ -176,6 +194,8 @@ private struct PresentingPaywallModifier: ViewModifier {
purchaseCompleted: PurchaseOrRestoreCompletedHandler?,
purchaseCancelled: PurchaseCancelledHandler?,
restoreCompleted: PurchaseOrRestoreCompletedHandler?,
purchaseFailure: PurchaseFailureHandler?,
restoreFailure: PurchaseFailureHandler?,
onDismiss: (() -> Void)?,
content: PaywallViewConfiguration.Content,
fontProvider: PaywallFontProvider,
Expand All @@ -187,6 +207,8 @@ private struct PresentingPaywallModifier: ViewModifier {
self.purchaseCompleted = purchaseCompleted
self.purchaseCancelled = purchaseCancelled
self.restoreCompleted = restoreCompleted
self.purchaseFailure = purchaseFailure
self.restoreFailure = restoreFailure
self.onDismiss = onDismiss
self.content = content
self.fontProvider = fontProvider
Expand Down Expand Up @@ -227,6 +249,12 @@ private struct PresentingPaywallModifier: ViewModifier {
self.close()
}
}
.onPurchaseFailure {
self.purchaseFailure?($0)
}
.onRestoreFailure {
self.restoreFailure?($0)
}
.interactiveDismissDisabled(self.purchaseHandler.actionInProgress)
}
.task {
Expand Down
Loading