Skip to content

Commit

Permalink
Add package to purchaseStarted
Browse files Browse the repository at this point in the history
  • Loading branch information
vegaro committed Feb 20, 2024
1 parent 7aaffeb commit 2cccdc3
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 109 deletions.
4 changes: 2 additions & 2 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ struct LoadedOfferingPaywallView: View {
var body: some View {
// Note: preferences need to be applied after `.toolbar` call
self.content
.preference(key: PurchasedInProgressPreferenceKey.self,
value: self.purchaseHandler.purchaseInProgress)
.preference(key: PurchaseInProgressPreferenceKey.self,
value: self.purchaseHandler.packageBeingPurchased)
.preference(key: PurchasedResultPreferenceKey.self,
value: .init(data: self.purchaseHandler.purchaseResult))
.preference(key: RestoredCustomerInfoPreferenceKey.self,
Expand Down
12 changes: 6 additions & 6 deletions RevenueCatUI/Purchasing/PurchaseHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class PurchaseHandler: ObservableObject {

/// Whether a purchase is currently in progress
@Published
fileprivate(set) var purchaseInProgress: Bool = false
fileprivate(set) var packageBeingPurchased: Package?

/// Whether a purchase or restore is currently in progress
@Published
Expand Down Expand Up @@ -84,13 +84,13 @@ extension PurchaseHandler {

@MainActor
func purchase(package: Package) async throws -> PurchaseResultData {
self.purchaseInProgress = true
self.packageBeingPurchased = package
self.purchaseResult = nil
self.purchaseError = nil

self.startAction()
defer {
self.purchaseInProgress = false
self.packageBeingPurchased = nil
self.actionInProgress = false
}

Expand Down Expand Up @@ -244,11 +244,11 @@ private final class NotConfiguredPurchases: PaywallPurchasesType {
// MARK: - Preference Keys

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

static var defaultValue: Bool = false
static var defaultValue: Package?

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

Expand Down
10 changes: 8 additions & 2 deletions RevenueCatUI/UIKit/PaywallViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ public protocol PaywallViewControllerDelegate: AnyObject {
@objc(paywallViewControllerDidStartPurchase:)
optional func paywallViewControllerDidStartPurchase(_ controller: PaywallViewController)

/// Notifies that a purchase has started in a ``PaywallViewController``.
@objc(paywallViewController:didStartPurchaseWithPackage:)
optional func paywallViewController(_ controller: PaywallViewController,
didStartPurchaseWith package: Package)

/// Notifies that a purchase has completed in a ``PaywallViewController``.
@objc(paywallViewController:didFinishPurchasingWithCustomerInfo:)
optional func paywallViewController(_ controller: PaywallViewController,
Expand Down Expand Up @@ -238,9 +243,10 @@ private extension PaywallViewController {
func createHostingController() -> UIHostingController<PaywallContainerView> {
let container = PaywallContainerView(
configuration: self.configuration,
purchaseStarted: { [weak self] in
purchaseStarted: { [weak self] package in
guard let self else { return }
self.delegate?.paywallViewControllerDidStartPurchase?(self)
self.delegate?.paywallViewController?(self, didStartPurchaseWith: package)
},
purchaseCompleted: { [weak self] transaction, customerInfo in
guard let self else { return }
Expand Down Expand Up @@ -292,7 +298,7 @@ private struct PaywallContainerView: View {

var configuration: PaywallViewConfiguration

let purchaseStarted: PurchaseStartedHandler
let purchaseStarted: PurchaseOfPackageStartedHandler
let purchaseCompleted: PurchaseCompletedHandler
let purchaseCancelled: PurchaseCancelledHandler
let restoreCompleted: PurchaseOrRestoreCompletedHandler
Expand Down
159 changes: 152 additions & 7 deletions RevenueCatUI/View+PresentPaywall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ extension PaywallPresentationMode {

}

// swiftlint:disable file_length
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet")
@available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet")
extension View {

typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo

// swiftlint:disable line_length
/// Presents a ``PaywallView`` if the given entitlement identifier is not active
/// in the current environment for the current `CustomerInfo`.
/// ```swift
Expand All @@ -65,18 +67,77 @@ extension View {
/// [Documentation](https://rev.cat/paywalls)
///
/// - Tag: presentPaywallIfNeeded
@available(iOS, deprecated: 1, renamed: "presentPaywallIfNeeded(requiredEntitlementIdentifier:offering:fonts:presentationMode:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(tvOS, deprecated: 1, renamed: "presentPaywallIfNeeded(requiredEntitlementIdentifier:offering:fonts:presentationMode:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(watchOS, deprecated: 1, renamed: "presentPaywallIfNeeded(requiredEntitlementIdentifier:offering:fonts:presentationMode:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(macOS, deprecated: 1, renamed: "presentPaywallIfNeeded(requiredEntitlementIdentifier:offering:fonts:presentationMode:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(macCatalyst, deprecated: 1, renamed: "presentPaywallIfNeeded(requiredEntitlementIdentifier:offering:fonts:presentationMode:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
// swiftlint:enable line_length
public func presentPaywallIfNeeded(
requiredEntitlementIdentifier: String,
offering: Offering? = nil,
fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
presentationMode: PaywallPresentationMode = .default,
purchaseStarted: PurchaseStartedHandler? = nil,
purchaseStarted: @escaping PurchaseStartedHandler,
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseFailure: PurchaseFailureHandler? = nil,
restoreFailure: PurchaseFailureHandler? = nil,
onDismiss: (() -> Void)? = nil
) -> some View {
return self.presentPaywallIfNeeded(
requiredEntitlementIdentifier: requiredEntitlementIdentifier,
offering: offering,
fonts: fonts,
presentationMode: presentationMode,
purchaseStarted: { _ in
purchaseStarted()
},
purchaseCompleted: purchaseCompleted,
purchaseCancelled: purchaseCancelled,
restoreStarted: nil,
restoreCompleted: restoreCompleted,
purchaseFailure: purchaseFailure,
restoreFailure: restoreFailure,
onDismiss: onDismiss
)
}

/// Presents a ``PaywallView`` if the given entitlement identifier is not active
/// in the current environment for the current `CustomerInfo`.
/// ```swift
/// var body: some View {
/// YourApp()
/// .presentPaywallIfNeeded(requiredEntitlementIdentifier: "pro")
/// }
/// ```
/// - Note: If loading the `CustomerInfo` fails (for example, if Internet is offline),
/// the paywall won't be displayed.
///
/// - Parameter offering: The `Offering` containing the desired `PaywallData` to display.
/// If `nil` (the default), `Offerings.current` will be used. Note that specifying this parameter means
/// that it will ignore the offering configured in an active experiment.
/// - Parameter fonts: An optional ``PaywallFontProvider``.
/// - Parameter presentationMode: The desired presentation mode of the paywall. Defaults to `.sheet`.
///
/// ### Related Articles
/// [Documentation](https://rev.cat/paywalls)
///
/// - Tag: presentPaywallIfNeeded
public func presentPaywallIfNeeded(
requiredEntitlementIdentifier: String,
offering: Offering? = nil,
fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
presentationMode: PaywallPresentationMode = .default,
purchaseStarted: PurchaseOfPackageStartedHandler? = nil,
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreStarted: RestoreStartedHandler? = nil,
restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseFailure: PurchaseFailureHandler? = nil,
restoreFailure: PurchaseFailureHandler? = nil,
onDismiss: (() -> Void)? = nil
) -> some View {
return self.presentPaywallIfNeeded(
offering: offering,
Expand All @@ -98,6 +159,7 @@ extension View {
)
}

// swiftlint:disable line_length
/// Presents a ``PaywallView`` based a given condition.
/// Example:
/// ```swift
Expand Down Expand Up @@ -134,12 +196,18 @@ extension View {
///
/// ### Related Articles
/// [Documentation](https://rev.cat/paywalls)
@available(iOS, deprecated: 1, renamed: "presentPaywallIfNeeded(offering:fonts:presentationMode:shouldDisplay:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(tvOS, deprecated: 1, renamed: "presentPaywallIfNeeded(offering:fonts:presentationMode:shouldDisplay:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(watchOS, deprecated: 1, renamed: "presentPaywallIfNeeded(offering:fonts:presentationMode:shouldDisplay:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(macOS, deprecated: 1, renamed: "presentPaywallIfNeeded(offering:fonts:presentationMode:shouldDisplay:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
@available(macCatalyst, deprecated: 1, renamed: "presentPaywallIfNeeded(offering:fonts:presentationMode:shouldDisplay:purchaseStarted:purchaseCompleted:purchaseCancelled:restoreStarted:restoreCompleted:purchaseFailure:restoreFailure:onDismiss:)")
// swiftlint:enable line_length
public func presentPaywallIfNeeded(
offering: Offering? = nil,
fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
presentationMode: PaywallPresentationMode = .default,
shouldDisplay: @escaping @Sendable (CustomerInfo) -> Bool,
purchaseStarted: PurchaseStartedHandler? = nil,
purchaseStarted: @escaping PurchaseStartedHandler,
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
Expand All @@ -152,7 +220,9 @@ extension View {
fonts: fonts,
presentationMode: presentationMode,
shouldDisplay: shouldDisplay,
purchaseStarted: purchaseStarted,
purchaseStarted: { _ in
purchaseStarted()
},
purchaseCompleted: purchaseCompleted,
purchaseCancelled: purchaseCancelled,
restoreStarted: nil,
Expand All @@ -170,6 +240,81 @@ extension View {
)
}

/// Presents a ``PaywallView`` based a given condition.
/// Example:
/// ```swift
/// var body: some View {
/// YourApp()
/// .presentPaywallIfNeeded {
/// !$0.entitlements.active.keys.contains("entitlement_identifier")
/// } purchaseStarted: { package in
/// print("Purchase started \(package)")
/// } purchaseCompleted: { customerInfo in
/// print("Customer info unlocked entitlement: \(customerInfo.entitlements)")
/// } purchaseCancelled: {
/// print("Purchase was cancelled")
/// } restoreStarted: {
/// print("Restore started")
/// } 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.")
/// }
/// }
/// ```
/// - Note: If loading the `CustomerInfo` fails (for example, if Internet is offline),
/// the paywall won't be displayed.
///
/// - Parameter offering: The `Offering` containing the desired `PaywallData` to display.
/// If `nil` (the default), `Offerings.current` will be used. Note that specifying this parameter means
/// that it will ignore the offering configured in an active experiment.
/// - Parameter fonts: An optional ``PaywallFontProvider``.
/// - Parameter presentationMode: The desired presentation mode of the paywall. Defaults to `.sheet`.
///
/// ### Related Articles
/// [Documentation](https://rev.cat/paywalls)
public func presentPaywallIfNeeded(
offering: Offering? = nil,
fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
presentationMode: PaywallPresentationMode = .default,
shouldDisplay: @escaping @Sendable (CustomerInfo) -> Bool,
purchaseStarted: PurchaseOfPackageStartedHandler? = nil,
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreStarted: RestoreStartedHandler? = nil,
restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseFailure: PurchaseFailureHandler? = nil,
restoreFailure: PurchaseFailureHandler? = nil,
onDismiss: (() -> Void)? = nil
) -> some View {
return self.presentPaywallIfNeeded(
offering: offering,
fonts: fonts,
presentationMode: presentationMode,
shouldDisplay: shouldDisplay,
purchaseStarted: purchaseStarted,
purchaseCompleted: purchaseCompleted,
purchaseCancelled: purchaseCancelled,
restoreStarted: restoreStarted,
restoreCompleted: restoreCompleted,
purchaseFailure: purchaseFailure,
restoreFailure: restoreFailure,
onDismiss: onDismiss,
customerInfoFetcher: {
guard Purchases.isConfigured else {
throw PaywallError.purchasesNotConfigured
}

return try await Purchases.shared.customerInfo()
}
)
}

// Visible overload for tests
func presentPaywallIfNeeded(
offering: Offering? = nil,
Expand All @@ -178,7 +323,7 @@ extension View {
purchaseHandler: PurchaseHandler? = nil,
presentationMode: PaywallPresentationMode = .default,
shouldDisplay: @escaping @Sendable (CustomerInfo) -> Bool,
purchaseStarted: PurchaseStartedHandler? = nil,
purchaseStarted: PurchaseOfPackageStartedHandler? = nil,
purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
purchaseCancelled: PurchaseCancelledHandler? = nil,
restoreStarted: RestoreStartedHandler? = nil,
Expand Down Expand Up @@ -222,7 +367,7 @@ private struct PresentingPaywallModifier: ViewModifier {

var shouldDisplay: @Sendable (CustomerInfo) -> Bool
var presentationMode: PaywallPresentationMode
var purchaseStarted: PurchaseStartedHandler?
var purchaseStarted: PurchaseOfPackageStartedHandler?
var purchaseCompleted: PurchaseOrRestoreCompletedHandler?
var purchaseCancelled: PurchaseCancelledHandler?
var restoreCompleted: PurchaseOrRestoreCompletedHandler?
Expand All @@ -240,7 +385,7 @@ private struct PresentingPaywallModifier: ViewModifier {
init(
shouldDisplay: @escaping @Sendable (CustomerInfo) -> Bool,
presentationMode: PaywallPresentationMode,
purchaseStarted: PurchaseStartedHandler?,
purchaseStarted: PurchaseOfPackageStartedHandler?,
purchaseCompleted: PurchaseOrRestoreCompletedHandler?,
purchaseCancelled: PurchaseCancelledHandler?,
restoreCompleted: PurchaseOrRestoreCompletedHandler?,
Expand Down Expand Up @@ -319,7 +464,7 @@ private struct PresentingPaywallModifier: ViewModifier {
)
)
.onPurchaseStarted {
self.purchaseStarted?()
self.purchaseStarted?($0)
}
.onPurchaseCompleted {
self.purchaseCompleted?($0)
Expand Down
Loading

0 comments on commit 2cccdc3

Please sign in to comment.