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 .onPurchaseCompleted overload with StoreTransaction #3323

Merged
merged 2 commits into from
Oct 24, 2023
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: 2 additions & 2 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ struct LoadedOfferingPaywallView: View {
locale: self.locale)
.environmentObject(self.introEligibility)
.environmentObject(self.purchaseHandler)
.preference(key: PurchasedCustomerInfoPreferenceKey.self,
value: self.purchaseHandler.purchasedCustomerInfo)
.preference(key: PurchasedResultPreferenceKey.self,
value: .init(data: self.purchaseHandler.purchaseResult))
.preference(key: RestoredCustomerInfoPreferenceKey.self,
value: self.purchaseHandler.restoredCustomerInfo)
.disabled(self.purchaseHandler.actionInProgress)
Expand Down
1 change: 1 addition & 0 deletions RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ extension PurchaseHandler {
return self.init(
purchases: MockPurchases { _ in
return (
// No current way to create a mock transaction with RevenueCat's public methods.
transaction: nil,
customerInfo: customerInfo,
userCancelled: false
Expand Down
25 changes: 20 additions & 5 deletions RevenueCatUI/Purchasing/PurchaseHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final class PurchaseHandler: ObservableObject {

/// When `purchased` becomes `true`, this will include the `CustomerInfo` associated to it.
@Published
fileprivate(set) var purchasedCustomerInfo: CustomerInfo?
fileprivate(set) var purchaseResult: PurchaseResultData?

/// Whether a restore was successfully completed.
@Published
Expand Down Expand Up @@ -84,7 +84,7 @@ extension PurchaseHandler {
} else {
withAnimation(Constants.defaultAnimation) {
self.purchased = true
self.purchasedCustomerInfo = result.customerInfo
self.purchaseResult = result
}
}

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

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
struct PurchasedCustomerInfoPreferenceKey: PreferenceKey {
struct PurchasedResultPreferenceKey: PreferenceKey {

static var defaultValue: CustomerInfo?
struct PurchaseResult: Equatable {
var transaction: StoreTransaction?
var customerInfo: CustomerInfo

static func reduce(value: inout CustomerInfo?, nextValue: () -> CustomerInfo?) {
init(data: PurchaseResultData) {
self.transaction = data.transaction
self.customerInfo = data.customerInfo
}

init?(data: PurchaseResultData?) {
guard let data else { return nil }
self.init(data: data)
}
}

static var defaultValue: PurchaseResult?

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

Expand Down
11 changes: 10 additions & 1 deletion RevenueCatUI/UIKit/PaywallViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ public final class PaywallViewController: UIViewController {
let paywallView = self.offering.map { PaywallView(offering: $0) } ?? PaywallView()

let view = paywallView
.onPurchaseCompleted { [weak self] customerInfo in
.onPurchaseCompleted { [weak self] transaction, customerInfo in
guard let self = self else { return }
self.delegate?.paywallViewController?(self, didFinishPurchasingWith: customerInfo)
self.delegate?.paywallViewController?(self,
didFinishPurchasingWith: customerInfo,
transaction: transaction)
}
.onRestoreCompleted { [weak self] customerInfo in
guard let self = self else { return }
Expand Down Expand Up @@ -83,6 +86,12 @@ public protocol PaywallViewControllerDelegate: AnyObject {
optional func paywallViewController(_ controller: PaywallViewController,
didFinishPurchasingWith customerInfo: CustomerInfo)

/// Notifies that a purchase has completed in a ``PaywallViewController``.
@objc(paywallViewController:didFinishPurchasingWithCustomerInfo:transaction:)
optional func paywallViewController(_ controller: PaywallViewController,
didFinishPurchasingWith customerInfo: CustomerInfo,
transaction: StoreTransaction?)

/// Notifies that the restore operation has completed in a ``PaywallViewController``.
///
/// - Warning: Receiving a ``CustomerInfo``does not imply that the user has any entitlements,
Expand Down
49 changes: 44 additions & 5 deletions RevenueCatUI/View+PurchaseRestoreCompleted.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import SwiftUI
/// A closure used for notifying of purchase or restore completion.
public typealias PurchaseOrRestoreCompletedHandler = @MainActor @Sendable (CustomerInfo) -> Void

/// A closure used for notifying of purchase completion.
public typealias PurchaseCompletedHandler = @MainActor @Sendable (_ transaction: StoreTransaction?,
_ customerInfo: CustomerInfo) -> Void

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
@available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet")
extension View {
Expand All @@ -34,7 +38,7 @@ extension View {
/// PaywallView()
/// .onPurchaseCompleted { customerInfo in
/// print("Purchase completed: \(customerInfo.entitlements)")
/// self.didPurchase = false
/// self.displayPaywall = false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was wrong

/// }
/// }
/// }
Expand All @@ -48,6 +52,33 @@ extension View {
return self.modifier(OnPurchaseCompletedModifier(handler: handler))
}

/// Invokes the given closure when a purchase is completed.
/// The closure includes the `CustomerInfo` with unlocked entitlements.
/// Example:
/// ```swift
/// @State
/// private var displayPaywall: Bool = true
///
/// var body: some View {
/// ContentView()
/// .sheet(isPresented: self.$displayPaywall) {
/// PaywallView()
/// .onPurchaseCompleted { transaction, customerInfo in
/// print("Purchase completed: \(customerInfo.entitlements)")
/// self.displayPaywall = false
/// }
/// }
/// }
/// ```
///
/// ### Related Articles
/// [Documentation](https://rev.cat/paywalls)
public func onPurchaseCompleted(
_ handler: @escaping PurchaseCompletedHandler
) -> some View {
return self.modifier(OnPurchaseCompletedModifier(handler: handler))
}

/// Invokes the given closure when restore purchases is completed.
/// The closure includes the `CustomerInfo` after the process is completed.
/// Example:
Expand Down Expand Up @@ -85,13 +116,21 @@ extension View {
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
private struct OnPurchaseCompletedModifier: ViewModifier {

let handler: PurchaseOrRestoreCompletedHandler
let handler: PurchaseCompletedHandler

init(handler: @escaping PurchaseOrRestoreCompletedHandler) {
self.handler = { _, customerInfo in handler(customerInfo) }
}

init(handler: @escaping PurchaseCompletedHandler) {
self.handler = handler
}

func body(content: Content) -> some View {
content
.onPreferenceChange(PurchasedCustomerInfoPreferenceKey.self) { customerInfo in
if let customerInfo {
self.handler(customerInfo)
.onPreferenceChange(PurchasedResultPreferenceKey.self) { result in
if let result {
self.handler(result.transaction, result.customerInfo)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ struct App: View {

private var offering: Offering
private var fonts: PaywallFontProvider
private var completed: PurchaseOrRestoreCompletedHandler = { (_: CustomerInfo) in }
private var purchaseOrRestoreCompleted: PurchaseOrRestoreCompletedHandler = { (_: CustomerInfo) in }
private var purchaseCompleted: PurchaseCompletedHandler = { (_: StoreTransaction?, _: CustomerInfo) in }

var body: some View {
self.content
Expand All @@ -35,24 +36,29 @@ struct App: View {
Text("")
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "")
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", fonts: self.fonts)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", purchaseCompleted: completed)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", restoreCompleted: completed)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", fonts: self.fonts, purchaseCompleted: completed)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", fonts: self.fonts, restoreCompleted: completed)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", fonts: self.fonts, purchaseCompleted: completed,
restoreCompleted: completed)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "",
purchaseCompleted: self.purchaseOrRestoreCompleted)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "",
restoreCompleted: self.purchaseOrRestoreCompleted)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", fonts: self.fonts,
purchaseCompleted: self.purchaseOrRestoreCompleted)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", fonts: self.fonts,
restoreCompleted: self.purchaseOrRestoreCompleted)
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "", fonts: self.fonts,
purchaseCompleted: self.purchaseOrRestoreCompleted,
restoreCompleted: self.purchaseOrRestoreCompleted)
.presentPaywallIfNeeded(fonts: self.fonts) { (_: CustomerInfo) in false }
.presentPaywallIfNeeded(fonts: self.fonts) { (_: CustomerInfo) in
false
} purchaseCompleted: {
completed($0)
self.purchaseOrRestoreCompleted($0)
}
.presentPaywallIfNeeded(fonts: self.fonts) { (_: CustomerInfo) in
false
} purchaseCompleted: {
completed($0)
self.purchaseOrRestoreCompleted($0)
} restoreCompleted: {
completed($0)
self.purchaseOrRestoreCompleted($0)
}
}

Expand All @@ -61,37 +67,42 @@ struct App: View {
Text("")
.paywallFooter()
.paywallFooter(fonts: self.fonts)
.paywallFooter(purchaseCompleted: completed)
.paywallFooter(fonts: self.fonts, purchaseCompleted: completed)
.paywallFooter(purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(fonts: self.fonts, purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(condensed: true)
.paywallFooter(condensed: true, fonts: self.fonts)
.paywallFooter(condensed: true, purchaseCompleted: completed)
.paywallFooter(condensed: true, fonts: self.fonts, purchaseCompleted: completed)
.paywallFooter(condensed: true, purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(condensed: true, fonts: self.fonts, purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(condensed: true, fonts: self.fonts,
purchaseCompleted: completed, restoreCompleted: completed)
purchaseCompleted: self.purchaseOrRestoreCompleted,
restoreCompleted: self.purchaseOrRestoreCompleted)

.paywallFooter(offering: offering)
.paywallFooter(offering: offering, condensed: true)
.paywallFooter(offering: offering, condensed: true, fonts: self.fonts)
.paywallFooter(offering: offering, condensed: true, purchaseCompleted: completed)
.paywallFooter(offering: offering, condensed: true, restoreCompleted: completed)
.paywallFooter(offering: offering, condensed: true, fonts: self.fonts, purchaseCompleted: completed)
.paywallFooter(offering: offering, condensed: true, purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(offering: offering, condensed: true, restoreCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(offering: offering, condensed: true, fonts: self.fonts,
purchaseCompleted: completed, restoreCompleted: completed)
purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(offering: offering, condensed: true, fonts: self.fonts,
purchaseCompleted: self.purchaseOrRestoreCompleted,
restoreCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(offering: offering)
.paywallFooter(offering: offering, fonts: self.fonts)
.paywallFooter(offering: offering, purchaseCompleted: completed)
.paywallFooter(offering: offering, restoreCompleted: completed)
.paywallFooter(offering: offering, fonts: self.fonts, purchaseCompleted: completed)
.paywallFooter(offering: offering, purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(offering: offering, restoreCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(offering: offering, fonts: self.fonts, purchaseCompleted: self.purchaseOrRestoreCompleted)
.paywallFooter(offering: offering, fonts: self.fonts,
purchaseCompleted: completed, restoreCompleted: completed)
purchaseCompleted: self.purchaseOrRestoreCompleted,
restoreCompleted: self.purchaseOrRestoreCompleted)
}

@ViewBuilder
var checkOnPurchaseAndRestoreCompleted: some View {
Text("")
.onPurchaseCompleted(self.completed)
.onRestoreCompleted(self.completed)
.onPurchaseCompleted(self.purchaseOrRestoreCompleted)
.onPurchaseCompleted(self.purchaseCompleted)
.onRestoreCompleted(self.purchaseOrRestoreCompleted)
}

private func fontProviders() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ final class Delegate: PaywallViewControllerDelegate {
func paywallViewController(_ controller: PaywallViewController,
didFinishPurchasingWith customerInfo: CustomerInfo) {}

func paywallViewController(_ controller: PaywallViewController,
didFinishPurchasingWith customerInfo: CustomerInfo,
transaction: StoreTransaction?) {}

func paywallViewController(_ controller: PaywallViewController,
didFinishRestoringWith customerInfo: CustomerInfo) {}

Expand Down
23 changes: 23 additions & 0 deletions Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@ class PurchaseCompletedHandlerTests: TestCase {
expect(customerInfo).toEventually(be(TestData.customerInfo))
}

func testOnPurchaseCompletedWithTransaction() throws {
var result: (transaction: StoreTransaction?, customerInfo: CustomerInfo)?

try PaywallView(
offering: Self.offering.withLocalImages,
customerInfo: TestData.customerInfo,
introEligibility: .producing(eligibility: .eligible),
purchaseHandler: Self.purchaseHandler
)
.onPurchaseCompleted { transaction, customerInfo in
result = (transaction, customerInfo)
}
.addToHierarchy()

Task {
_ = try await Self.purchaseHandler.purchase(package: Self.package)
}

expect(result).toEventuallyNot(beNil())
expect(result?.customerInfo) === TestData.customerInfo
expect(result?.transaction).to(beNil())
}

func testOnRestoreCompleted() throws {
var customerInfo: CustomerInfo?

Expand Down
8 changes: 4 additions & 4 deletions Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class PurchaseHandlerTests: TestCase {
func testInitialState() async throws {
let handler: PurchaseHandler = .mock()

expect(handler.purchasedCustomerInfo).to(beNil())
expect(handler.purchaseResult).to(beNil())
expect(handler.restoredCustomerInfo).to(beNil())
expect(handler.purchased) == false
expect(handler.restored) == false
Expand All @@ -37,7 +37,7 @@ class PurchaseHandlerTests: TestCase {

_ = try await handler.purchase(package: TestData.packageWithIntroOffer)

expect(handler.purchasedCustomerInfo) === TestData.customerInfo
expect(handler.purchaseResult?.customerInfo) === TestData.customerInfo
expect(handler.restoredCustomerInfo).to(beNil())
expect(handler.purchased) == true
expect(handler.actionInProgress) == false
Expand All @@ -47,7 +47,7 @@ class PurchaseHandlerTests: TestCase {
let handler: PurchaseHandler = .cancelling()

_ = try await handler.purchase(package: TestData.packageWithIntroOffer)
expect(handler.purchasedCustomerInfo).to(beNil())
expect(handler.purchaseResult).to(beNil())
expect(handler.purchased) == false
expect(handler.actionInProgress) == false
}
Expand All @@ -61,7 +61,7 @@ class PurchaseHandlerTests: TestCase {
expect(result.success) == false
expect(handler.restored) == true
expect(handler.restoredCustomerInfo) === TestData.customerInfo
expect(handler.purchasedCustomerInfo).to(beNil())
expect(handler.purchaseResult).to(beNil())
expect(handler.actionInProgress) == false
}

Expand Down