From 2538c2a8e181266b6af60a5025a89d92bb624198 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Fri, 24 May 2024 16:07:27 +0200 Subject: [PATCH 01/76] Initial commit --- RevenueCatUI/PaywallView.swift | 2 + RevenueCatUI/Purchasing/MockPurchases.swift | 14 +++- .../Purchasing/PaywallPurchasesType.swift | 3 + .../Purchasing/PurchaseHandler+TestData.swift | 4 + RevenueCatUI/Purchasing/PurchaseHandler.swift | 74 +++++++++++++++++++ .../View+PurchaseRestoreCompleted.swift | 51 +++++++++++++ RevenueCatUI/Views/LoadingPaywallView.swift | 4 + Sources/Purchasing/Purchases/Purchases.swift | 3 + 8 files changed, 154 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index b1d948bfe0..3d0a712134 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -310,6 +310,8 @@ struct LoadedOfferingPaywallView: View { value: self.purchaseHandler.packageBeingPurchased) .preference(key: PurchasedResultPreferenceKey.self, value: .init(data: self.purchaseHandler.purchaseResult)) + .preference(key: InitiatePurchasedRequestedPreferenceKey.self, + value: .init(data: self.purchaseHandler.initiatePurchaseRequest)) .preference(key: RestoredCustomerInfoPreferenceKey.self, value: self.purchaseHandler.restoredCustomerInfo) .preference(key: RestoreInProgressPreferenceKey.self, diff --git a/RevenueCatUI/Purchasing/MockPurchases.swift b/RevenueCatUI/Purchasing/MockPurchases.swift index 6a9afaadd6..5a8f72e2b2 100644 --- a/RevenueCatUI/Purchasing/MockPurchases.swift +++ b/RevenueCatUI/Purchasing/MockPurchases.swift @@ -19,10 +19,12 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) final class MockPurchases: PaywallPurchasesType { + typealias CustomerInfoBlock = @Sendable () async throws -> CustomerInfo typealias PurchaseBlock = @Sendable (Package) async throws -> PurchaseResultData typealias RestoreBlock = @Sendable () async throws -> CustomerInfo typealias TrackEventBlock = @Sendable (PaywallEvent) async -> Void + private let customerInfoBlock: CustomerInfoBlock private let purchaseBlock: PurchaseBlock private let restoreBlock: RestoreBlock private let trackEventBlock: TrackEventBlock @@ -30,11 +32,17 @@ final class MockPurchases: PaywallPurchasesType { init( purchase: @escaping PurchaseBlock, restorePurchases: @escaping RestoreBlock, - trackEvent: @escaping TrackEventBlock + trackEvent: @escaping TrackEventBlock, + customerInfo: @escaping CustomerInfoBlock ) { self.purchaseBlock = purchase self.restoreBlock = restorePurchases self.trackEventBlock = trackEvent + self.customerInfoBlock = customerInfo + } + + func customerInfo() async throws -> RevenueCat.CustomerInfo { + return try await self.customerInfoBlock() } func purchase(package: Package) async throws -> PurchaseResultData { @@ -65,6 +73,8 @@ extension PaywallPurchasesType { try await restore(self.restorePurchases)() } trackEvent: { event in await self.track(paywallEvent: event) + } customerInfo: { + try await self.customerInfo() } } @@ -78,6 +88,8 @@ extension PaywallPurchasesType { try await self.restorePurchases() } trackEvent: { event in await trackEvent(self.track(paywallEvent:))(event) + } customerInfo: { + try await self.customerInfo() } } diff --git a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift index 448c23325d..82062410bf 100644 --- a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift +++ b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift @@ -23,6 +23,9 @@ protocol PaywallPurchasesType: Sendable { @Sendable func restorePurchases() async throws -> CustomerInfo + @Sendable + func customerInfo() async throws -> CustomerInfo + @Sendable func track(paywallEvent: PaywallEvent) async diff --git a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift index 9f5c111f98..a30b6851a6 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift @@ -32,6 +32,8 @@ extension PurchaseHandler { return customerInfo } trackEvent: { event in Logger.debug("Tracking event: \(event)") + } customerInfo: { + return customerInfo } ) } @@ -55,6 +57,8 @@ extension PurchaseHandler { throw error } trackEvent: { event in Logger.debug("Tracking event: \(event)") + } customerInfo: { + throw error } ) } diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index e08bb25198..6261c43880 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -40,6 +40,9 @@ final class PurchaseHandler: ObservableObject { @Published fileprivate(set) var purchaseResult: PurchaseResultData? + @Published + fileprivate(set) var initiatePurchaseRequest: InitiatePurchaseRequestData? + /// Whether a restore is currently in progress @Published fileprivate(set) var restoreInProgress: Bool = false @@ -84,8 +87,42 @@ final class PurchaseHandler: ObservableObject { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PurchaseHandler { + func doneWithObserverModeThing(_ error: Error?) { + if let error { + self.purchaseError = error + } else { + withAnimation(Constants.defaultAnimation) { + self.purchased = true + } +// if result.userCancelled { +// self.trackCancelledPurchase() +// } else { +// withAnimation(Constants.defaultAnimation) { +// self.purchased = true +// } +// } + } + } + @MainActor func purchase(package: Package) async throws -> PurchaseResultData { + let isObserverMode = true + if isObserverMode { + self.packageBeingPurchased = package + self.purchaseResult = nil + self.purchaseError = nil + self.initiatePurchaseRequest = (package.storeProduct, self.doneWithObserverModeThing) + + self.startAction() + + return PurchaseResultData(nil, try await self.purchases.customerInfo(), false) + } else { + return try await purchaseNormal(package: package) + } + } + + @MainActor + func purchaseNormal(package: Package) async throws -> PurchaseResultData { self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil @@ -97,6 +134,7 @@ extension PurchaseHandler { } do { + // JOSH let result = try await self.purchases.purchase(package: package) self.purchaseResult = result @@ -231,6 +269,10 @@ private extension PurchaseHandler { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private final class NotConfiguredPurchases: PaywallPurchasesType { + func customerInfo() async throws -> RevenueCat.CustomerInfo { + throw ErrorCode.configurationError + } + func purchase(package: Package) async throws -> PurchaseResultData { throw ErrorCode.configurationError } @@ -267,6 +309,38 @@ struct RestoreInProgressPreferenceKey: PreferenceKey { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct InitiatePurchasedRequestedPreferenceKey: PreferenceKey { + + typealias Callback = (Error?) -> Void + + struct PurchaseResult: Equatable { + static func == (lhs: InitiatePurchasedRequestedPreferenceKey.PurchaseResult, rhs: InitiatePurchasedRequestedPreferenceKey.PurchaseResult) -> Bool { + return lhs.storeProduct == rhs.storeProduct + } + + var storeProduct: StoreProduct? + var callback: Callback? + + init(data: InitiatePurchaseRequestData) { + self.storeProduct = data.storeProduct + self.callback = data.callback + } + + init?(data: InitiatePurchaseRequestData?) { + guard let data else { return nil } + self.init(data: data) + } + } + + static var defaultValue: PurchaseResult? + + static func reduce(value: inout PurchaseResult?, nextValue: () -> PurchaseResult?) { + value = nextValue() + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct PurchasedResultPreferenceKey: PreferenceKey { diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 9b679f477e..ac57e535ad 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -14,6 +14,8 @@ import RevenueCat import SwiftUI +import StoreKit + /// A closure used for notifying of purchase or restore completion. public typealias PurchaseOrRestoreCompletedHandler = @MainActor @Sendable (CustomerInfo) -> Void @@ -35,6 +37,11 @@ public typealias PurchaseOfPackageStartedHandler = @MainActor @Sendable (_ packa /// A closure used for notifying of purchase cancellation. public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void +public typealias InitatePurchaseRequestComplete = (Error?) -> Void + +/// A closure used for notifying of purchase initiation is requred. +public typealias InitiatePurchaseRequestedHandler = @MainActor @Sendable (_ storeProduct: StoreProduct, _ callback: @escaping InitatePurchaseRequestComplete) -> Void + /// A closure used for notifying of failures during purchases or restores. public typealias PurchaseFailureHandler = @MainActor @Sendable (NSError) -> Void @@ -270,6 +277,20 @@ extension View { self.environment(\.onRequestedDismissal, action) } + /// Invokes the given closure when a purchase needs initiated + /// + /// Example: + /// ```swift + /// PaywallView() + /// .onObserverModePurchaseRequested { + /// print("TODO") + /// } + /// ``` + public func handleObserverModePurchase( + _ handler: @escaping InitiatePurchaseRequestedHandler + ) -> some View { + return self.modifier(OnInitiatePurchaseRequestedModifier(handler: handler)) + } } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -280,6 +301,11 @@ private struct OnPurchaseStartedModifier: ViewModifier { func body(content: Content) -> some View { content .onPreferenceChange(PurchaseInProgressPreferenceKey.self) { package in + let isObserverMode = true + guard !isObserverMode else { + return + } + if let package { self.handler(package) } @@ -332,6 +358,31 @@ private struct OnPurchaseCancelledModifier: ViewModifier { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private struct OnInitiatePurchaseRequestedModifier: ViewModifier { + + let handler: InitiatePurchaseRequestedHandler + + init(handler: @escaping InitiatePurchaseRequestedHandler) { + self.handler = handler + } + + func body(content: Content) -> some View { + content + .onPreferenceChange(InitiatePurchasedRequestedPreferenceKey.self) { data in + let isObserverMode = true + guard isObserverMode else { + return + } + + if let storeProduct = data?.storeProduct, let callback = data?.callback { + self.handler(storeProduct, callback) + } + } + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private struct OnRestoreStartedModifier: ViewModifier { diff --git a/RevenueCatUI/Views/LoadingPaywallView.swift b/RevenueCatUI/Views/LoadingPaywallView.swift index 49c802fc55..027a55567b 100644 --- a/RevenueCatUI/Views/LoadingPaywallView.swift +++ b/RevenueCatUI/Views/LoadingPaywallView.swift @@ -142,6 +142,10 @@ private extension LoadingPaywallView { @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) private final class LoadingPaywallPurchases: PaywallPurchasesType { + + func customerInfo() async throws -> RevenueCat.CustomerInfo { + fatalError("Should not be able to purchase") + } func purchase(package: Package) async throws -> PurchaseResultData { fatalError("Should not be able to purchase") diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 6b7297b5c8..a027622808 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -30,6 +30,9 @@ public typealias PurchaseResultData = (transaction: StoreTransaction?, customerInfo: CustomerInfo, userCancelled: Bool) +public typealias InitiatePurchaseRequestData = (storeProduct: StoreProduct, + callback: (Error?) -> Void) + /** Completion block for ``Purchases/purchase(product:completion:)`` */ From 4b3d0e721f92c2c707aa1e64aa9b3145b912a928 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Fri, 24 May 2024 19:10:03 +0200 Subject: [PATCH 02/76] Paywalls - Allow developers to handle purchase logic --- RevenueCatUI/PaywallView.swift | 4 +- RevenueCatUI/Purchasing/MockPurchases.swift | 5 ++ .../Purchasing/PaywallPurchasesType.swift | 2 + RevenueCatUI/Purchasing/PurchaseHandler.swift | 89 +++++++++++-------- .../View+PurchaseRestoreCompleted.swift | 47 +++++----- RevenueCatUI/Views/LoadingPaywallView.swift | 7 +- Sources/Purchasing/Purchases/Purchases.swift | 4 +- 7 files changed, 95 insertions(+), 63 deletions(-) diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 3d0a712134..c465f2b362 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -310,8 +310,8 @@ struct LoadedOfferingPaywallView: View { value: self.purchaseHandler.packageBeingPurchased) .preference(key: PurchasedResultPreferenceKey.self, value: .init(data: self.purchaseHandler.purchaseResult)) - .preference(key: InitiatePurchasedRequestedPreferenceKey.self, - value: .init(data: self.purchaseHandler.initiatePurchaseRequest)) + .preference(key: HandlePurchasePreferenceKey.self, + value: .init(data: self.purchaseHandler.handlePurchase)) .preference(key: RestoredCustomerInfoPreferenceKey.self, value: self.purchaseHandler.restoredCustomerInfo) .preference(key: RestoreInProgressPreferenceKey.self, diff --git a/RevenueCatUI/Purchasing/MockPurchases.swift b/RevenueCatUI/Purchasing/MockPurchases.swift index 5a8f72e2b2..bde8798716 100644 --- a/RevenueCatUI/Purchasing/MockPurchases.swift +++ b/RevenueCatUI/Purchasing/MockPurchases.swift @@ -29,6 +29,11 @@ final class MockPurchases: PaywallPurchasesType { private let restoreBlock: RestoreBlock private let trackEventBlock: TrackEventBlock + var finishTransactions: Bool { + get { return false } + set { _ = newValue } + } + init( purchase: @escaping PurchaseBlock, restorePurchases: @escaping RestoreBlock, diff --git a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift index 82062410bf..d51e71a7eb 100644 --- a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift +++ b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift @@ -17,6 +17,8 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) protocol PaywallPurchasesType: Sendable { + var finishTransactions: Bool { get set } + @Sendable func purchase(package: Package) async throws -> PurchaseResultData diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 6261c43880..c06490fc03 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -15,6 +15,8 @@ import RevenueCat import StoreKit import SwiftUI +// swiftlint:disable file_length + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) // @PublicForExternalTesting final class PurchaseHandler: ObservableObject { @@ -41,7 +43,7 @@ final class PurchaseHandler: ObservableObject { fileprivate(set) var purchaseResult: PurchaseResultData? @Published - fileprivate(set) var initiatePurchaseRequest: InitiatePurchaseRequestData? + fileprivate(set) var handlePurchase: HandlePurchaseData? /// Whether a restore is currently in progress @Published @@ -87,42 +89,17 @@ final class PurchaseHandler: ObservableObject { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PurchaseHandler { - func doneWithObserverModeThing(_ error: Error?) { - if let error { - self.purchaseError = error - } else { - withAnimation(Constants.defaultAnimation) { - self.purchased = true - } -// if result.userCancelled { -// self.trackCancelledPurchase() -// } else { -// withAnimation(Constants.defaultAnimation) { -// self.purchased = true -// } -// } - } - } - @MainActor func purchase(package: Package) async throws -> PurchaseResultData { - let isObserverMode = true - if isObserverMode { - self.packageBeingPurchased = package - self.purchaseResult = nil - self.purchaseError = nil - self.initiatePurchaseRequest = (package.storeProduct, self.doneWithObserverModeThing) - - self.startAction() - - return PurchaseResultData(nil, try await self.purchases.customerInfo(), false) + if self.purchases.finishTransactions { + return try await performPurchase(package: package) } else { - return try await purchaseNormal(package: package) + return try await performDeveloperPurchaseLogic(package: package) } } @MainActor - func purchaseNormal(package: Package) async throws -> PurchaseResultData { + func performPurchase(package: Package) async throws -> PurchaseResultData { self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil @@ -134,7 +111,6 @@ extension PurchaseHandler { } do { - // JOSH let result = try await self.purchases.purchase(package: package) self.purchaseResult = result @@ -153,6 +129,35 @@ extension PurchaseHandler { } } + @MainActor + func performDeveloperPurchaseLogic(package: Package) async throws -> PurchaseResultData { + self.packageBeingPurchased = package + self.purchaseResult = nil + self.purchaseError = nil + self.handlePurchase = (package.storeProduct, self.completeHandlePurchase) + + self.startAction() + + return PurchaseResultData(nil, try await self.purchases.customerInfo(), false) + } + + func completeHandlePurchase(_ userCancelled: Bool, _ error: Error?) { + self.actionInProgress = false + self.handlePurchase = nil + + if let error { + self.purchaseError = error + } else { + if userCancelled { + self.trackCancelledPurchase() + } else { + withAnimation(Constants.defaultAnimation) { + self.purchased = true + } + } + } + } + /// - Returns: `success` is `true` only when the resulting `CustomerInfo` /// had any transactions /// - Note: `restoredCustomerInfo` will be not be set after this method, @@ -269,6 +274,11 @@ private extension PurchaseHandler { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private final class NotConfiguredPurchases: PaywallPurchasesType { + var finishTransactions: Bool { + get { return false } + set { _ = newValue } + } + func customerInfo() async throws -> RevenueCat.CustomerInfo { throw ErrorCode.configurationError } @@ -310,27 +320,32 @@ struct RestoreInProgressPreferenceKey: PreferenceKey { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct InitiatePurchasedRequestedPreferenceKey: PreferenceKey { +struct HandlePurchasePreferenceKey: PreferenceKey { - typealias Callback = (Error?) -> Void + typealias Callback = (_ userCancelled: Bool, _ error: Error?) -> Void struct PurchaseResult: Equatable { - static func == (lhs: InitiatePurchasedRequestedPreferenceKey.PurchaseResult, rhs: InitiatePurchasedRequestedPreferenceKey.PurchaseResult) -> Bool { + + static func == ( + lhs: HandlePurchasePreferenceKey.PurchaseResult, + rhs: HandlePurchasePreferenceKey.PurchaseResult + ) -> Bool { return lhs.storeProduct == rhs.storeProduct } - + var storeProduct: StoreProduct? var callback: Callback? - init(data: InitiatePurchaseRequestData) { + init(data: HandlePurchaseData) { self.storeProduct = data.storeProduct self.callback = data.callback } - init?(data: InitiatePurchaseRequestData?) { + init?(data: HandlePurchaseData?) { guard let data else { return nil } self.init(data: data) } + } static var defaultValue: PurchaseResult? diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index ac57e535ad..0e0803e919 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -16,6 +16,8 @@ import SwiftUI import StoreKit +// swiftlint:disable file_length + /// A closure used for notifying of purchase or restore completion. public typealias PurchaseOrRestoreCompletedHandler = @MainActor @Sendable (CustomerInfo) -> Void @@ -37,10 +39,17 @@ public typealias PurchaseOfPackageStartedHandler = @MainActor @Sendable (_ packa /// A closure used for notifying of purchase cancellation. public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void -public typealias InitatePurchaseRequestComplete = (Error?) -> Void +/// A closure used for notifying when custom purchase logic is finished in ``HandlePurchaseHandler`` +public typealias HandlePurchaseComplete = ( + _ userCancelled: Bool, + _ error: Error? +) -> Void /// A closure used for notifying of purchase initiation is requred. -public typealias InitiatePurchaseRequestedHandler = @MainActor @Sendable (_ storeProduct: StoreProduct, _ callback: @escaping InitatePurchaseRequestComplete) -> Void +public typealias HandlePurchaseHandler = @MainActor @Sendable ( + _ storeProduct: StoreProduct, + _ callback: @escaping HandlePurchaseComplete +) -> Void /// A closure used for notifying of failures during purchases or restores. public typealias PurchaseFailureHandler = @MainActor @Sendable (NSError) -> Void @@ -282,14 +291,20 @@ extension View { /// Example: /// ```swift /// PaywallView() - /// .onObserverModePurchaseRequested { - /// print("TODO") + /// .handlePurchase { storeProduct, callback in + /// var userCancelled: Bool = false + /// var error: Error? = nil + /// + /// // Manually call StoreKit purchasing logic + /// // and update userCancelled and error + /// + /// callback(userCancelled, error) /// } /// ``` - public func handleObserverModePurchase( - _ handler: @escaping InitiatePurchaseRequestedHandler + public func handlePurchase( + _ handler: @escaping HandlePurchaseHandler ) -> some View { - return self.modifier(OnInitiatePurchaseRequestedModifier(handler: handler)) + return self.modifier(HandlePurchaseModifier(handler: handler)) } } @@ -301,11 +316,6 @@ private struct OnPurchaseStartedModifier: ViewModifier { func body(content: Content) -> some View { content .onPreferenceChange(PurchaseInProgressPreferenceKey.self) { package in - let isObserverMode = true - guard !isObserverMode else { - return - } - if let package { self.handler(package) } @@ -359,22 +369,17 @@ private struct OnPurchaseCancelledModifier: ViewModifier { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -private struct OnInitiatePurchaseRequestedModifier: ViewModifier { +private struct HandlePurchaseModifier: ViewModifier { - let handler: InitiatePurchaseRequestedHandler + let handler: HandlePurchaseHandler - init(handler: @escaping InitiatePurchaseRequestedHandler) { + init(handler: @escaping HandlePurchaseHandler) { self.handler = handler } func body(content: Content) -> some View { content - .onPreferenceChange(InitiatePurchasedRequestedPreferenceKey.self) { data in - let isObserverMode = true - guard isObserverMode else { - return - } - + .onPreferenceChange(HandlePurchasePreferenceKey.self) { data in if let storeProduct = data?.storeProduct, let callback = data?.callback { self.handler(storeProduct, callback) } diff --git a/RevenueCatUI/Views/LoadingPaywallView.swift b/RevenueCatUI/Views/LoadingPaywallView.swift index 027a55567b..732ba9ba73 100644 --- a/RevenueCatUI/Views/LoadingPaywallView.swift +++ b/RevenueCatUI/Views/LoadingPaywallView.swift @@ -142,7 +142,12 @@ private extension LoadingPaywallView { @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) private final class LoadingPaywallPurchases: PaywallPurchasesType { - + + var finishTransactions: Bool { + get { return false } + set { _ = newValue } + } + func customerInfo() async throws -> RevenueCat.CustomerInfo { fatalError("Should not be able to purchase") } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index a027622808..2dec99ae46 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -30,8 +30,8 @@ public typealias PurchaseResultData = (transaction: StoreTransaction?, customerInfo: CustomerInfo, userCancelled: Bool) -public typealias InitiatePurchaseRequestData = (storeProduct: StoreProduct, - callback: (Error?) -> Void) +public typealias HandlePurchaseData = (storeProduct: StoreProduct, + callback: (_ userCancelled: Bool, _ error: Error?) -> Void) /** Completion block for ``Purchases/purchase(product:completion:)`` From 1e3b3f8964502508f137736d59ec7672fa81f79f Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Mon, 27 May 2024 15:20:52 -0700 Subject: [PATCH 03/76] Use `HandlePurchaseComplete` directly To enable useful code completion. --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 0e0803e919..5e264f3cd8 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -39,16 +39,14 @@ public typealias PurchaseOfPackageStartedHandler = @MainActor @Sendable (_ packa /// A closure used for notifying of purchase cancellation. public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void -/// A closure used for notifying when custom purchase logic is finished in ``HandlePurchaseHandler`` -public typealias HandlePurchaseComplete = ( - _ userCancelled: Bool, - _ error: Error? -) -> Void /// A closure used for notifying of purchase initiation is requred. public typealias HandlePurchaseHandler = @MainActor @Sendable ( _ storeProduct: StoreProduct, - _ callback: @escaping HandlePurchaseComplete + _ handlePurchaseComplete: @escaping ( + _ userCancelled: Bool, + _ error: Error? + ) -> Void ) -> Void /// A closure used for notifying of failures during purchases or restores. From 472a1777ca0d6b91e0914f9609af0d54b1e018a0 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Tue, 28 May 2024 11:50:51 -0700 Subject: [PATCH 04/76] Rename callback; Better documentation --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 5e264f3cd8..dd9b855c6f 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -43,7 +43,7 @@ public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void /// A closure used for notifying of purchase initiation is requred. public typealias HandlePurchaseHandler = @MainActor @Sendable ( _ storeProduct: StoreProduct, - _ handlePurchaseComplete: @escaping ( + _ purchaseCompletedHandler: @escaping ( _ userCancelled: Bool, _ error: Error? ) -> Void @@ -284,19 +284,25 @@ extension View { self.environment(\.onRequestedDismissal, action) } - /// Invokes the given closure when a purchase needs initiated + /// Use this method if you wish to execute your own StoreKit purchase logic, + /// skipping RevenueCat's. This method is **only** called if `Purchases` is + /// confiugured with `finishTransactions` set to `false`. This is typically used + /// when migrating from a direct StoreKit implementation to RevenueCat in stages. + /// + /// After executing your StoreKit purchaecode, you must call `purchaseCompletedHandler` + /// for accurate statistics. /// /// Example: /// ```swift /// PaywallView() - /// .handlePurchase { storeProduct, callback in + /// .handlePurchase { storeProduct, purchaseCompletedHandler in /// var userCancelled: Bool = false /// var error: Error? = nil /// /// // Manually call StoreKit purchasing logic /// // and update userCancelled and error /// - /// callback(userCancelled, error) + /// purchaseCompletedHandler(userCancelled, error) /// } /// ``` public func handlePurchase( From fb29c58b7e3485923b2dab4b5c31d2843f3316ad Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Tue, 28 May 2024 12:21:21 -0700 Subject: [PATCH 05/76] Debug log --- RevenueCatUI/Data/Strings.swift | 6 ++++++ RevenueCatUI/Purchasing/PurchaseHandler.swift | 3 +++ 2 files changed, 9 insertions(+) diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index 4922e2db16..1edcad44a0 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -40,6 +40,8 @@ enum Strings { case restore_purchases_with_empty_result case setting_restored_customer_info + case executing_external_purchase_logic + } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @@ -98,6 +100,10 @@ extension Strings: CustomStringConvertible { case .setting_restored_customer_info: return "Setting restored customer info" + + case .executing_external_purchase_logic: + return "Will execute custom StoreKit purchase logic provided by the SDK adopter. " + + "No StoreKit purchasing logic will be performed by RevenueCat." } } diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index c06490fc03..267ba047b0 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -131,6 +131,9 @@ extension PurchaseHandler { @MainActor func performDeveloperPurchaseLogic(package: Package) async throws -> PurchaseResultData { + + Logger.debug(Strings.executing_external_purchase_logic) + self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil From 6a383dcc9d3c98f5e7a96a3b49e38385c02e06fd Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Tue, 28 May 2024 13:30:29 -0700 Subject: [PATCH 06/76] Fix tests; make `finishTransactions` configurable in mock `PurchaseHandler` --- RevenueCatUI/Purchasing/MockPurchases.swift | 6 +++++- RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift | 8 ++++---- .../Purchasing/PurchaseHandlerTests.swift | 5 +++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RevenueCatUI/Purchasing/MockPurchases.swift b/RevenueCatUI/Purchasing/MockPurchases.swift index bde8798716..f4cc9b921e 100644 --- a/RevenueCatUI/Purchasing/MockPurchases.swift +++ b/RevenueCatUI/Purchasing/MockPurchases.swift @@ -12,6 +12,7 @@ // Created by Nacho Soto on 9/12/23. import RevenueCat +import Foundation #if DEBUG @@ -28,13 +29,15 @@ final class MockPurchases: PaywallPurchasesType { private let purchaseBlock: PurchaseBlock private let restoreBlock: RestoreBlock private let trackEventBlock: TrackEventBlock + private let _finishTransactions: Bool var finishTransactions: Bool { - get { return false } + get { return _finishTransactions } set { _ = newValue } } init( + finishTransactions: Bool = true, purchase: @escaping PurchaseBlock, restorePurchases: @escaping RestoreBlock, trackEvent: @escaping TrackEventBlock, @@ -44,6 +47,7 @@ final class MockPurchases: PaywallPurchasesType { self.restoreBlock = restorePurchases self.trackEventBlock = trackEvent self.customerInfoBlock = customerInfo + self._finishTransactions = finishTransactions } func customerInfo() async throws -> RevenueCat.CustomerInfo { diff --git a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift index a30b6851a6..cd2857b919 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift @@ -19,9 +19,9 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PurchaseHandler { - static func mock(_ customerInfo: CustomerInfo = TestData.customerInfo) -> Self { + static func mock(_ customerInfo: CustomerInfo = TestData.customerInfo, finishTransactions: Bool = true) -> Self { return self.init( - purchases: MockPurchases { _ in + purchases: MockPurchases(finishTransactions: finishTransactions) { _ in return ( // No current way to create a mock transaction with RevenueCat's public methods. transaction: nil, @@ -38,8 +38,8 @@ extension PurchaseHandler { ) } - static func cancelling() -> Self { - return .mock() + static func cancelling(finishTransactions: Bool = true) -> Self { + return .mock(finishTransactions: finishTransactions) .map { block in { var result = try await block($0) result.userCancelled = true diff --git a/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift b/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift index 03c0b28ef2..38d179a427 100644 --- a/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift +++ b/Tests/RevenueCatUITests/Purchasing/PurchaseHandlerTests.swift @@ -246,6 +246,11 @@ private final class AsyncPurchaseHandler { return TestData.customerInfo } trackEvent: { event in Logger.debug("Tracking event: \(event)") + } customerInfo: { [weak instance = self] in + let instance = try XCTUnwrap(instance) + await instance.createAndWaitForContinuation() + + return TestData.customerInfo } ) } From 839d83a142cf104ae351184d9ae02e13cd1d3567 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Tue, 28 May 2024 13:38:17 -0700 Subject: [PATCH 07/76] Remove foundation import --- RevenueCatUI/Purchasing/MockPurchases.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RevenueCatUI/Purchasing/MockPurchases.swift b/RevenueCatUI/Purchasing/MockPurchases.swift index f4cc9b921e..ccaec03f5d 100644 --- a/RevenueCatUI/Purchasing/MockPurchases.swift +++ b/RevenueCatUI/Purchasing/MockPurchases.swift @@ -12,7 +12,6 @@ // Created by Nacho Soto on 9/12/23. import RevenueCat -import Foundation #if DEBUG From 1ee81108a64ac63596be669f8662d5c48f2f7b03 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Tue, 28 May 2024 13:53:49 -0700 Subject: [PATCH 08/76] =?UTF-8?q?Test=20PaywallView().handlePurchase=20{?= =?UTF-8?q?=20=E2=80=A6=20}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PurchaseCompletedHandlerTests.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index 26bfe3cb61..13bf138a72 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -190,6 +190,30 @@ class PurchaseCompletedHandlerTests: TestCase { expect(error).toEventually(matchError(Self.failureError)) } + func testHandleExternalPurchase() throws { + var completed = false + var customPurchaseCodeExecuted = false + + try PaywallView( + offering: Self.offering.withLocalImages, + customerInfo: TestData.customerInfo, + introEligibility: .producing(eligibility: .eligible), + purchaseHandler: Self.externalPurchaseHandler + ) + .handlePurchase { storeProduct, purchaseCompletedHandler in + customPurchaseCodeExecuted = true + } + .addToHierarchy() + + Task { + _ = try await Self.externalPurchaseHandler.purchase(package: Self.package) + completed = true + } + + expect(completed).toEventually(beTrue()) + expect(customPurchaseCodeExecuted) == true + } + func testOnRestoreStarted() throws { var started = false @@ -255,6 +279,7 @@ class PurchaseCompletedHandlerTests: TestCase { expect(error).toEventually(matchError(Self.failureError)) } + private static let externalPurchaseHandler: PurchaseHandler = .mock(finishTransactions: false) private static let purchaseHandler: PurchaseHandler = .mock() private static let failingHandler: PurchaseHandler = .failing(failureError) private static let offering = TestData.offeringWithNoIntroOffer From 18a6db3888922c32b60bf1f9e005c5883b23cf97 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 11:28:48 -0700 Subject: [PATCH 09/76] WIP handle external restore logic --- Gemfile.lock | 1 + RevenueCatUI/PaywallView.swift | 2 ++ .../View+PurchaseRestoreCompleted.swift | 33 +++++++++++++++++++ Sources/Purchasing/Purchases/Purchases.swift | 2 ++ 4 files changed, 38 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index ea1e6a0f33..97361ca7c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -338,6 +338,7 @@ PLATFORMS arm64-darwin-21 arm64-darwin-22 arm64-darwin-23 + ruby x86_64-darwin-22 x86_64-linux diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index c465f2b362..57242c1152 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -312,6 +312,8 @@ struct LoadedOfferingPaywallView: View { value: .init(data: self.purchaseHandler.purchaseResult)) .preference(key: HandlePurchasePreferenceKey.self, value: .init(data: self.purchaseHandler.handlePurchase)) + .preference(key: HandleRestorePreferenceKey.self, + value: .init(callback: self.purchaseHandler.handleRestore)) .preference(key: RestoredCustomerInfoPreferenceKey.self, value: self.purchaseHandler.restoredCustomerInfo) .preference(key: RestoreInProgressPreferenceKey.self, diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index dd9b855c6f..6fccc95220 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -49,6 +49,13 @@ public typealias HandlePurchaseHandler = @MainActor @Sendable ( ) -> Void ) -> Void +public typealias HandleRestoreHandler = @MainActor @Sendable ( + _ purchaseRestoreHandler: @escaping ( + _ userCancelled: Bool, + _ error: Error? + ) -> Void +) -> Void + /// A closure used for notifying of failures during purchases or restores. public typealias PurchaseFailureHandler = @MainActor @Sendable (NSError) -> Void @@ -310,6 +317,12 @@ extension View { ) -> some View { return self.modifier(HandlePurchaseModifier(handler: handler)) } + + public func handleRestore( + _ handler: @escaping HandleRestoreHandler + ) -> some View { + return self.modifier(HandleRestoreModifier(handler: handler)) + } } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -392,6 +405,26 @@ private struct HandlePurchaseModifier: ViewModifier { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private struct HandleRestoreModifier: ViewModifier { + + let handler: HandleRestoreHandler + + init(handler: @escaping HandleRestoreHandler) { + self.handler = handler + } + + func body(content: Content) -> some View { + content + .onPreferenceChange(HandleRestorePreferenceKey.self) { result in + if let callback = result?.callback { + self.handler(callback) + } + } + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private struct OnRestoreStartedModifier: ViewModifier { diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 2dec99ae46..1f5994ead2 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -33,6 +33,8 @@ public typealias PurchaseResultData = (transaction: StoreTransaction?, public typealias HandlePurchaseData = (storeProduct: StoreProduct, callback: (_ userCancelled: Bool, _ error: Error?) -> Void) +public typealias HandleRestoreCallback = (_ userCancelled: Bool, _ error: Error?) -> Void + /** Completion block for ``Purchases/purchase(product:completion:)`` */ From a5214c86007635160b96f40b6e8438d54ab2ff5b Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 11:47:07 -0700 Subject: [PATCH 10/76] Missing from last commit --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 267ba047b0..8c72ccdfed 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -45,6 +45,9 @@ final class PurchaseHandler: ObservableObject { @Published fileprivate(set) var handlePurchase: HandlePurchaseData? + @Published + fileprivate(set) var handleRestore: HandleRestoreCallback? + /// Whether a restore is currently in progress @Published fileprivate(set) var restoreInProgress: Bool = false @@ -161,13 +164,21 @@ extension PurchaseHandler { } } + func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { + if self.purchases.finishTransactions { + return try await performRestorePurchases() + } else { + return try await performDeveloperRestoreLogic() + } + } + /// - Returns: `success` is `true` only when the resulting `CustomerInfo` /// had any transactions /// - Note: `restoredCustomerInfo` will be not be set after this method, /// instead `setRestored(_:)` must be manually called afterwards. /// This allows the UI to display an alert before dismissing the paywall. @MainActor - func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { + func performRestorePurchases() async throws -> (info: CustomerInfo, success: Bool) { self.restoreInProgress = true self.restoredCustomerInfo = nil self.restoreError = nil @@ -189,6 +200,18 @@ extension PurchaseHandler { } } + @MainActor + func performDeveloperRestoreLogic() async throws -> (info: CustomerInfo, success: Bool) { + self.restoreInProgress = true + self.restoredCustomerInfo = nil + self.restoreError = nil + self.handleRestore = self.completeHandlePurchase //TODO: This needs a different correct callback + + self.startAction() + + return (info: try await self.purchases.customerInfo(), true) + } + @MainActor func setRestored(_ customerInfo: CustomerInfo) { self.restoredCustomerInfo = customerInfo @@ -359,6 +382,40 @@ struct HandlePurchasePreferenceKey: PreferenceKey { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct HandleRestorePreferenceKey: PreferenceKey { + + struct RestoreResult: Equatable { + + static func == ( + lhs: HandleRestorePreferenceKey.RestoreResult, + rhs: HandleRestorePreferenceKey.RestoreResult + ) -> Bool { + return lhs.callback as AnyObject === rhs.callback as AnyObject + } + + var callback: HandleRestoreCallback? + + init(callback: @escaping HandleRestoreCallback) { + self.callback = callback + } + + init?(callback: HandleRestoreCallback?) { + guard let callback else { return nil } + self.init(callback: callback) + } + + } + + static var defaultValue: RestoreResult? + + static func reduce(value: inout RestoreResult?, nextValue: () -> RestoreResult?) { + value = nextValue() + } + +} + + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct PurchasedResultPreferenceKey: PreferenceKey { From f642ffa142316835717ecbfca1c0ad524fd2d764 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 12:52:51 -0700 Subject: [PATCH 11/76] Only call restore once! --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 49 +++++++++---------- .../View+PurchaseRestoreCompleted.swift | 2 +- Sources/Purchasing/Purchases/Purchases.swift | 5 -- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 8c72ccdfed..ba307c60bf 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -17,6 +17,25 @@ import SwiftUI // swiftlint:disable file_length + +typealias HandlePurchaseData = (storeProduct: StoreProduct, + callback: (_ userCancelled: Bool, _ error: Error?) -> Void) + +class HandleRestoreCallbackContainer: Equatable { + + let handleRestoreCallback: ((_ userCancelled: Bool, _ error: Error?) -> Void)? + + init(callback: ((Bool, Error?) -> Void)?) { + self.handleRestoreCallback = callback + } + + static func ==(lhs: HandleRestoreCallbackContainer, rhs: HandleRestoreCallbackContainer) -> Bool { + return lhs === rhs + } + +} + + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) // @PublicForExternalTesting final class PurchaseHandler: ObservableObject { @@ -46,7 +65,7 @@ final class PurchaseHandler: ObservableObject { fileprivate(set) var handlePurchase: HandlePurchaseData? @Published - fileprivate(set) var handleRestore: HandleRestoreCallback? + fileprivate(set) var handleRestore: HandleRestoreCallbackContainer? /// Whether a restore is currently in progress @Published @@ -205,7 +224,7 @@ extension PurchaseHandler { self.restoreInProgress = true self.restoredCustomerInfo = nil self.restoreError = nil - self.handleRestore = self.completeHandlePurchase //TODO: This needs a different correct callback + self.handleRestore = HandleRestoreCallbackContainer(callback: self.completeHandlePurchase) //TODO: This needs a different correct callback self.startAction() @@ -385,31 +404,9 @@ struct HandlePurchasePreferenceKey: PreferenceKey { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct HandleRestorePreferenceKey: PreferenceKey { - struct RestoreResult: Equatable { - - static func == ( - lhs: HandleRestorePreferenceKey.RestoreResult, - rhs: HandleRestorePreferenceKey.RestoreResult - ) -> Bool { - return lhs.callback as AnyObject === rhs.callback as AnyObject - } - - var callback: HandleRestoreCallback? - - init(callback: @escaping HandleRestoreCallback) { - self.callback = callback - } - - init?(callback: HandleRestoreCallback?) { - guard let callback else { return nil } - self.init(callback: callback) - } - - } - - static var defaultValue: RestoreResult? + static var defaultValue: HandleRestoreCallbackContainer? - static func reduce(value: inout RestoreResult?, nextValue: () -> RestoreResult?) { + static func reduce(value: inout HandleRestoreCallbackContainer?, nextValue: () -> HandleRestoreCallbackContainer?) { value = nextValue() } diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 6fccc95220..738351366a 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -417,7 +417,7 @@ private struct HandleRestoreModifier: ViewModifier { func body(content: Content) -> some View { content .onPreferenceChange(HandleRestorePreferenceKey.self) { result in - if let callback = result?.callback { + if let callback = result?.handleRestoreCallback { self.handler(callback) } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 1f5994ead2..6b7297b5c8 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -30,11 +30,6 @@ public typealias PurchaseResultData = (transaction: StoreTransaction?, customerInfo: CustomerInfo, userCancelled: Bool) -public typealias HandlePurchaseData = (storeProduct: StoreProduct, - callback: (_ userCancelled: Bool, _ error: Error?) -> Void) - -public typealias HandleRestoreCallback = (_ userCancelled: Bool, _ error: Error?) -> Void - /** Completion block for ``Purchases/purchase(product:completion:)`` */ From 95cafb7b8ef0d7700ec65f9da32c6586e656aff5 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 12:55:04 -0700 Subject: [PATCH 12/76] No need to be optional --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index ba307c60bf..2fe3b2d6f0 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -23,9 +23,9 @@ typealias HandlePurchaseData = (storeProduct: StoreProduct, class HandleRestoreCallbackContainer: Equatable { - let handleRestoreCallback: ((_ userCancelled: Bool, _ error: Error?) -> Void)? + let handleRestoreCallback: (_ userCancelled: Bool, _ error: Error?) -> Void - init(callback: ((Bool, Error?) -> Void)?) { + init(callback: @escaping (Bool, Error?) -> Void) { self.handleRestoreCallback = callback } From 52faa87ac5563645cc94166b4fe1574bdee8e983 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 12:55:45 -0700 Subject: [PATCH 13/76] No need for explicit initializer --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 738351366a..3656a4df7f 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -410,10 +410,6 @@ private struct HandleRestoreModifier: ViewModifier { let handler: HandleRestoreHandler - init(handler: @escaping HandleRestoreHandler) { - self.handler = handler - } - func body(content: Content) -> some View { content .onPreferenceChange(HandleRestorePreferenceKey.self) { result in From 3a282e8af89694c9656bcf7bd80b4cd2b124d963 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 13:39:13 -0700 Subject: [PATCH 14/76] Ability to pass restore result back --- RevenueCatUI/PaywallView.swift | 2 +- RevenueCatUI/Purchasing/PurchaseHandler.swift | 30 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 57242c1152..fd827ba8a9 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -313,7 +313,7 @@ struct LoadedOfferingPaywallView: View { .preference(key: HandlePurchasePreferenceKey.self, value: .init(data: self.purchaseHandler.handlePurchase)) .preference(key: HandleRestorePreferenceKey.self, - value: .init(callback: self.purchaseHandler.handleRestore)) + value: self.purchaseHandler.handleRestore) .preference(key: RestoredCustomerInfoPreferenceKey.self, value: self.purchaseHandler.restoredCustomerInfo) .preference(key: RestoreInProgressPreferenceKey.self, diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 2fe3b2d6f0..8a5c6e884e 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -85,6 +85,8 @@ final class PurchaseHandler: ObservableObject { private var eventData: PaywallEvent.Data? + private var continuation: CheckedContinuation<(success: Bool, error: Error?), Never>? + convenience init(purchases: Purchases = .shared) { self.init(isConfigured: true, purchases: purchases) } @@ -183,6 +185,11 @@ extension PurchaseHandler { } } + func completeRestorePurchases(success: Bool, error: Error?) { + continuation?.resume(returning: (success, error)) + continuation = nil + } + func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { if self.purchases.finishTransactions { return try await performRestorePurchases() @@ -224,11 +231,30 @@ extension PurchaseHandler { self.restoreInProgress = true self.restoredCustomerInfo = nil self.restoreError = nil - self.handleRestore = HandleRestoreCallbackContainer(callback: self.completeHandlePurchase) //TODO: This needs a different correct callback + + DispatchQueue.main.async { + // this triggers the view `.handleRestore` function, and its callback must be called + // after the continuation is set below + self.handleRestore = HandleRestoreCallbackContainer(callback: self.completeRestorePurchases) + } self.startAction() - return (info: try await self.purchases.customerInfo(), true) + defer { + self.restoreInProgress = false + self.actionInProgress = false + } + + let result = await withCheckedContinuation { cont in + continuation = cont + } + + if let error = result.error { + self.restoreError = error + throw error + } + + return (info: try await self.purchases.customerInfo(), result.success) } @MainActor From e5e0e076c7af93188ec5f519cbcb92384bfacf38 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 13:50:38 -0700 Subject: [PATCH 15/76] Better error handling --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 8a5c6e884e..44784c3f2c 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -85,7 +85,7 @@ final class PurchaseHandler: ObservableObject { private var eventData: PaywallEvent.Data? - private var continuation: CheckedContinuation<(success: Bool, error: Error?), Never>? + private var continuation: CheckedContinuation? convenience init(purchases: Purchases = .shared) { self.init(isConfigured: true, purchases: purchases) @@ -186,7 +186,12 @@ extension PurchaseHandler { } func completeRestorePurchases(success: Bool, error: Error?) { - continuation?.resume(returning: (success, error)) + if let error { + self.restoreError = error + continuation?.resume(throwing: error) + } else { + continuation?.resume(returning: success) + } continuation = nil } @@ -245,16 +250,11 @@ extension PurchaseHandler { self.actionInProgress = false } - let result = await withCheckedContinuation { cont in + let success = try await withCheckedThrowingContinuation { cont in continuation = cont } - if let error = result.error { - self.restoreError = error - throw error - } - - return (info: try await self.purchases.customerInfo(), result.success) + return (info: try await self.purchases.customerInfo(), success) } @MainActor From 2244b8e1210a7e3ddfe07366a88bf14b5c7246cb Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 13:52:51 -0700 Subject: [PATCH 16/76] Naming --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 44784c3f2c..b140d85665 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -85,7 +85,7 @@ final class PurchaseHandler: ObservableObject { private var eventData: PaywallEvent.Data? - private var continuation: CheckedContinuation? + private var externalRestorePurchaseContinuation: CheckedContinuation? convenience init(purchases: Purchases = .shared) { self.init(isConfigured: true, purchases: purchases) @@ -188,11 +188,11 @@ extension PurchaseHandler { func completeRestorePurchases(success: Bool, error: Error?) { if let error { self.restoreError = error - continuation?.resume(throwing: error) + externalRestorePurchaseContinuation?.resume(throwing: error) } else { - continuation?.resume(returning: success) + externalRestorePurchaseContinuation?.resume(returning: success) } - continuation = nil + externalRestorePurchaseContinuation = nil } func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { @@ -238,7 +238,7 @@ extension PurchaseHandler { self.restoreError = nil DispatchQueue.main.async { - // this triggers the view `.handleRestore` function, and its callback must be called + // this triggers the view's `.handleRestore` function, and its callback must be called // after the continuation is set below self.handleRestore = HandleRestoreCallbackContainer(callback: self.completeRestorePurchases) } @@ -250,8 +250,8 @@ extension PurchaseHandler { self.actionInProgress = false } - let success = try await withCheckedThrowingContinuation { cont in - continuation = cont + let success = try await withCheckedThrowingContinuation { continuation in + externalRestorePurchaseContinuation = continuation } return (info: try await self.purchases.customerInfo(), success) From 094722c0bc1f50bb527035fca19cf9cd90a499b6 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 14:14:12 -0700 Subject: [PATCH 17/76] correct label --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 3656a4df7f..f18e1299d6 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -51,7 +51,7 @@ public typealias HandlePurchaseHandler = @MainActor @Sendable ( public typealias HandleRestoreHandler = @MainActor @Sendable ( _ purchaseRestoreHandler: @escaping ( - _ userCancelled: Bool, + _ success: Bool, _ error: Error? ) -> Void ) -> Void From 1b8390a8bf3fcf33a52f871e939b113a5b26d367 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 14:14:26 -0700 Subject: [PATCH 18/76] Test restore --- .../PurchaseCompletedHandlerTests.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index 13bf138a72..9db558d106 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -201,6 +201,7 @@ class PurchaseCompletedHandlerTests: TestCase { purchaseHandler: Self.externalPurchaseHandler ) .handlePurchase { storeProduct, purchaseCompletedHandler in + purchaseCompletedHandler(false, nil) customPurchaseCodeExecuted = true } .addToHierarchy() @@ -214,6 +215,31 @@ class PurchaseCompletedHandlerTests: TestCase { expect(customPurchaseCodeExecuted) == true } + func testHandleExternalRestore() throws { + var completed = false + var customRestoreCodeExecuted = false + + try PaywallView( + offering: Self.offering.withLocalImages, + customerInfo: TestData.customerInfo, + introEligibility: .producing(eligibility: .eligible), + purchaseHandler: Self.externalPurchaseHandler + ) + .handleRestore { purchaseRestoreHandler in + purchaseRestoreHandler(true, nil) + customRestoreCodeExecuted = true + } + .addToHierarchy() + + Task { + _ = try await Self.externalPurchaseHandler.restorePurchases() + completed = true + } + + expect(completed).toEventually(beTrue()) + expect(customRestoreCodeExecuted) == true + } + func testOnRestoreStarted() throws { var started = false From 3cdc95f45bfc9600a3237a3bdaeb968e808d0d1a Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 14:21:41 -0700 Subject: [PATCH 19/76] Documentation --- .../View+PurchaseRestoreCompleted.swift | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index f18e1299d6..f6e59fe2c2 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -296,7 +296,7 @@ extension View { /// confiugured with `finishTransactions` set to `false`. This is typically used /// when migrating from a direct StoreKit implementation to RevenueCat in stages. /// - /// After executing your StoreKit purchaecode, you must call `purchaseCompletedHandler` + /// After executing your StoreKit purchae code, you must call `purchaseCompletedHandler` /// for accurate statistics. /// /// Example: @@ -318,6 +318,26 @@ extension View { return self.modifier(HandlePurchaseModifier(handler: handler)) } + /// Use this method if you wish to execute your own StoreKit restore purchases logic, + /// skipping RevenueCat's. This method is **only** called if `Purchases` is + /// confiugured with `finishTransactions` set to `false`. This is typically used + /// when migrating from a direct StoreKit implementation to RevenueCat in stages. + /// + /// After executing your StoreKit purchae code, you must call `purchaseRestoreHandler`. + /// + /// Example: + /// ```swift + /// PaywallView() + /// .handleRestore { purchaseRestoreHandler in + /// var success: Bool = false + /// var error: Error? = nil + /// + /// // Manually call StoreKit purchasing logic + /// // and update success and error + /// + /// purchaseRestoreHandler(success, error) + /// } + /// ``` public func handleRestore( _ handler: @escaping HandleRestoreHandler ) -> some View { From 8532e33ad0e29312eb967babea650ff5c5bffcdb Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 14:32:31 -0700 Subject: [PATCH 20/76] More logging --- RevenueCatUI/Data/Strings.swift | 14 ++++++++++++++ RevenueCatUI/Purchasing/PurchaseHandler.swift | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index 1edcad44a0..a1e89ca63f 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -40,7 +40,11 @@ enum Strings { case restore_purchases_with_empty_result case setting_restored_customer_info + case executing_purchase_logic case executing_external_purchase_logic + case executing_restore_logic + case executing_external_restore_logic + } @@ -104,6 +108,16 @@ extension Strings: CustomStringConvertible { case .executing_external_purchase_logic: return "Will execute custom StoreKit purchase logic provided by the SDK adopter. " + "No StoreKit purchasing logic will be performed by RevenueCat." + + case .executing_purchase_logic: + return "Will execute purchase logic provided by RevenueCat." + + case .executing_restore_logic: + return "Will execute restore purchases logic provided by RevenueCat." + + case .executing_external_restore_logic: + return "Will execute custom StoreKit restore purchases logic provided by the SDK adopter. " + + "No StoreKit restore purchases logic will be performed by RevenueCat." } } diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index b140d85665..71fb584e31 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -124,6 +124,7 @@ extension PurchaseHandler { @MainActor func performPurchase(package: Package) async throws -> PurchaseResultData { + Logger.debug(Strings.executing_purchase_logic) self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil @@ -210,6 +211,7 @@ extension PurchaseHandler { /// This allows the UI to display an alert before dismissing the paywall. @MainActor func performRestorePurchases() async throws -> (info: CustomerInfo, success: Bool) { + Logger.debug(Strings.executing_restore_logic) self.restoreInProgress = true self.restoredCustomerInfo = nil self.restoreError = nil @@ -233,6 +235,8 @@ extension PurchaseHandler { @MainActor func performDeveloperRestoreLogic() async throws -> (info: CustomerInfo, success: Bool) { + Logger.debug(Strings.executing_external_restore_logic) + self.restoreInProgress = true self.restoredCustomerInfo = nil self.restoreError = nil From b7adeb0eb53b7eb7b5a317d7979f6f6afc3adfb9 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 15:57:40 -0700 Subject: [PATCH 21/76] Linting --- RevenueCatUI/Data/Strings.swift | 1 - RevenueCatUI/Purchasing/PurchaseHandler.swift | 4 +--- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 4 ++-- Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index a1e89ca63f..6648e83cf9 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -45,7 +45,6 @@ enum Strings { case executing_restore_logic case executing_external_restore_logic - } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 71fb584e31..345f3f8bb8 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -17,7 +17,6 @@ import SwiftUI // swiftlint:disable file_length - typealias HandlePurchaseData = (storeProduct: StoreProduct, callback: (_ userCancelled: Bool, _ error: Error?) -> Void) @@ -29,7 +28,7 @@ class HandleRestoreCallbackContainer: Equatable { self.handleRestoreCallback = callback } - static func ==(lhs: HandleRestoreCallbackContainer, rhs: HandleRestoreCallbackContainer) -> Bool { + static func == (lhs: HandleRestoreCallbackContainer, rhs: HandleRestoreCallbackContainer) -> Bool { return lhs === rhs } @@ -442,7 +441,6 @@ struct HandleRestorePreferenceKey: PreferenceKey { } - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct PurchasedResultPreferenceKey: PreferenceKey { diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index f6e59fe2c2..2ce6793d7b 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -39,8 +39,7 @@ public typealias PurchaseOfPackageStartedHandler = @MainActor @Sendable (_ packa /// A closure used for notifying of purchase cancellation. public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void - -/// A closure used for notifying of purchase initiation is requred. +/// A closure used for notifying that custom purchase logic has completed. public typealias HandlePurchaseHandler = @MainActor @Sendable ( _ storeProduct: StoreProduct, _ purchaseCompletedHandler: @escaping ( @@ -49,6 +48,7 @@ public typealias HandlePurchaseHandler = @MainActor @Sendable ( ) -> Void ) -> Void +/// A closure used for notifying that custom restore logic has completed. public typealias HandleRestoreHandler = @MainActor @Sendable ( _ purchaseRestoreHandler: @escaping ( _ success: Bool, diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index 9db558d106..a14ccd743d 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -200,7 +200,7 @@ class PurchaseCompletedHandlerTests: TestCase { introEligibility: .producing(eligibility: .eligible), purchaseHandler: Self.externalPurchaseHandler ) - .handlePurchase { storeProduct, purchaseCompletedHandler in + .handlePurchase { _, purchaseCompletedHandler in purchaseCompletedHandler(false, nil) customPurchaseCodeExecuted = true } From 9d7099eb73880c69c90ccd87f309e098653ff8c1 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 16:11:10 -0700 Subject: [PATCH 22/76] Code organization --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 345f3f8bb8..0425563dbc 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -25,8 +25,8 @@ class HandleRestoreCallbackContainer: Equatable { let handleRestoreCallback: (_ userCancelled: Bool, _ error: Error?) -> Void init(callback: @escaping (Bool, Error?) -> Void) { - self.handleRestoreCallback = callback - } + self.handleRestoreCallback = callback + } static func == (lhs: HandleRestoreCallbackContainer, rhs: HandleRestoreCallbackContainer) -> Bool { return lhs === rhs @@ -112,12 +112,14 @@ final class PurchaseHandler: ObservableObject { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PurchaseHandler { + // MARK: - Purchase + @MainActor func purchase(package: Package) async throws -> PurchaseResultData { if self.purchases.finishTransactions { return try await performPurchase(package: package) } else { - return try await performDeveloperPurchaseLogic(package: package) + return try await performExternalPurchaseLogic(package: package) } } @@ -154,21 +156,23 @@ extension PurchaseHandler { } @MainActor - func performDeveloperPurchaseLogic(package: Package) async throws -> PurchaseResultData { - + func performExternalPurchaseLogic(package: Package) async throws -> PurchaseResultData { Logger.debug(Strings.executing_external_purchase_logic) self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil - self.handlePurchase = (package.storeProduct, self.completeHandlePurchase) + self.handlePurchase = (package.storeProduct, self.completeExternalHandlePurchase) self.startAction() return PurchaseResultData(nil, try await self.purchases.customerInfo(), false) } - func completeHandlePurchase(_ userCancelled: Bool, _ error: Error?) { + + + @MainActor + func completeExternalHandlePurchase(_ userCancelled: Bool, _ error: Error?) { self.actionInProgress = false self.handlePurchase = nil @@ -185,21 +189,13 @@ extension PurchaseHandler { } } - func completeRestorePurchases(success: Bool, error: Error?) { - if let error { - self.restoreError = error - externalRestorePurchaseContinuation?.resume(throwing: error) - } else { - externalRestorePurchaseContinuation?.resume(returning: success) - } - externalRestorePurchaseContinuation = nil - } + // MARK: - Restore func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { if self.purchases.finishTransactions { return try await performRestorePurchases() } else { - return try await performDeveloperRestoreLogic() + return try await performExternalRestoreLogic() } } @@ -233,7 +229,7 @@ extension PurchaseHandler { } @MainActor - func performDeveloperRestoreLogic() async throws -> (info: CustomerInfo, success: Bool) { + func performExternalRestoreLogic() async throws -> (info: CustomerInfo, success: Bool) { Logger.debug(Strings.executing_external_restore_logic) self.restoreInProgress = true @@ -243,7 +239,7 @@ extension PurchaseHandler { DispatchQueue.main.async { // this triggers the view's `.handleRestore` function, and its callback must be called // after the continuation is set below - self.handleRestore = HandleRestoreCallbackContainer(callback: self.completeRestorePurchases) + self.handleRestore = HandleRestoreCallbackContainer(callback: self.completeExternalRestorePurchases) } self.startAction() @@ -260,6 +256,17 @@ extension PurchaseHandler { return (info: try await self.purchases.customerInfo(), success) } + @MainActor + func completeExternalRestorePurchases(success: Bool, error: Error?) { + if let error { + self.restoreError = error + externalRestorePurchaseContinuation?.resume(throwing: error) + } else { + externalRestorePurchaseContinuation?.resume(returning: success) + } + externalRestorePurchaseContinuation = nil + } + @MainActor func setRestored(_ customerInfo: CustomerInfo) { self.restoredCustomerInfo = customerInfo From c205d5fb747364690c639017ab4cbba5e5130316 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 16:27:31 -0700 Subject: [PATCH 23/76] Tweaks --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 2ce6793d7b..7193dffe6b 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -50,7 +50,7 @@ public typealias HandlePurchaseHandler = @MainActor @Sendable ( /// A closure used for notifying that custom restore logic has completed. public typealias HandleRestoreHandler = @MainActor @Sendable ( - _ purchaseRestoreHandler: @escaping ( + _ restorePurchasesCompletedHandler: @escaping ( _ success: Bool, _ error: Error? ) -> Void @@ -335,10 +335,10 @@ extension View { /// // Manually call StoreKit purchasing logic /// // and update success and error /// - /// purchaseRestoreHandler(success, error) + /// restorePurchasesCompletedHandler(success, error) /// } /// ``` - public func handleRestore( + public func handleRestorePurchases( _ handler: @escaping HandleRestoreHandler ) -> some View { return self.modifier(HandleRestoreModifier(handler: handler)) @@ -410,10 +410,6 @@ private struct HandlePurchaseModifier: ViewModifier { let handler: HandlePurchaseHandler - init(handler: @escaping HandlePurchaseHandler) { - self.handler = handler - } - func body(content: Content) -> some View { content .onPreferenceChange(HandlePurchasePreferenceKey.self) { data in @@ -432,8 +428,8 @@ private struct HandleRestoreModifier: ViewModifier { func body(content: Content) -> some View { content - .onPreferenceChange(HandleRestorePreferenceKey.self) { result in - if let callback = result?.handleRestoreCallback { + .onPreferenceChange(HandleRestorePreferenceKey.self) { callbackContainer in + if let callback = callbackContainer?.handleRestoreCallback { self.handler(callback) } } From 45b442558b1077ee930a241cd276c994162fa1d5 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 16:34:54 -0700 Subject: [PATCH 24/76] Lint --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 0425563dbc..edb7a85087 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -34,7 +34,6 @@ class HandleRestoreCallbackContainer: Equatable { } - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) // @PublicForExternalTesting final class PurchaseHandler: ObservableObject { @@ -169,8 +168,6 @@ extension PurchaseHandler { return PurchaseResultData(nil, try await self.purchases.customerInfo(), false) } - - @MainActor func completeExternalHandlePurchase(_ userCancelled: Bool, _ error: Error?) { self.actionInProgress = false From 534cc835ccf2ea5160d9b8184e7975c7968f743f Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Wed, 29 May 2024 16:36:19 -0700 Subject: [PATCH 25/76] Update test method name --- Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index a14ccd743d..f218e73bcd 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -225,7 +225,7 @@ class PurchaseCompletedHandlerTests: TestCase { introEligibility: .producing(eligibility: .eligible), purchaseHandler: Self.externalPurchaseHandler ) - .handleRestore { purchaseRestoreHandler in + .handleRestorePurchases { purchaseRestoreHandler in purchaseRestoreHandler(true, nil) customRestoreCodeExecuted = true } From 122d2c2cdea1a3e788baf28ae6592cc77af680bd Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 09:56:43 -0700 Subject: [PATCH 26/76] Change name from `finishTransactions` to `purchasesAreCompletedBy` --- RevenueCatUI/Purchasing/MockPurchases.swift | 2 +- RevenueCatUI/Purchasing/PaywallPurchasesType.swift | 2 +- RevenueCatUI/Purchasing/PurchaseHandler.swift | 6 +++--- RevenueCatUI/Views/LoadingPaywallView.swift | 2 +- Sources/Purchasing/Purchases/Purchases.swift | 7 +++++++ Sources/Purchasing/Purchases/PurchasesType.swift | 3 +++ 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/Purchasing/MockPurchases.swift b/RevenueCatUI/Purchasing/MockPurchases.swift index ccaec03f5d..b17673be82 100644 --- a/RevenueCatUI/Purchasing/MockPurchases.swift +++ b/RevenueCatUI/Purchasing/MockPurchases.swift @@ -30,7 +30,7 @@ final class MockPurchases: PaywallPurchasesType { private let trackEventBlock: TrackEventBlock private let _finishTransactions: Bool - var finishTransactions: Bool { + var purchasesAreCompletedBy: Bool { get { return _finishTransactions } set { _ = newValue } } diff --git a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift index d51e71a7eb..1dd8740258 100644 --- a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift +++ b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift @@ -17,7 +17,7 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) protocol PaywallPurchasesType: Sendable { - var finishTransactions: Bool { get set } + var purchasesAreCompletedBy: Bool { get set } @Sendable func purchase(package: Package) async throws -> PurchaseResultData diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index edb7a85087..38a5a058ff 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -115,7 +115,7 @@ extension PurchaseHandler { @MainActor func purchase(package: Package) async throws -> PurchaseResultData { - if self.purchases.finishTransactions { + if self.purchases.purchasesAreCompletedBy { return try await performPurchase(package: package) } else { return try await performExternalPurchaseLogic(package: package) @@ -189,7 +189,7 @@ extension PurchaseHandler { // MARK: - Restore func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { - if self.purchases.finishTransactions { + if self.purchases.purchasesAreCompletedBy { return try await performRestorePurchases() } else { return try await performExternalRestoreLogic() @@ -352,7 +352,7 @@ private extension PurchaseHandler { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private final class NotConfiguredPurchases: PaywallPurchasesType { - var finishTransactions: Bool { + var purchasesAreCompletedBy: Bool { get { return false } set { _ = newValue } } diff --git a/RevenueCatUI/Views/LoadingPaywallView.swift b/RevenueCatUI/Views/LoadingPaywallView.swift index 9b4bafddcb..e0f5658c8a 100644 --- a/RevenueCatUI/Views/LoadingPaywallView.swift +++ b/RevenueCatUI/Views/LoadingPaywallView.swift @@ -144,7 +144,7 @@ private extension LoadingPaywallView { @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) private final class LoadingPaywallPurchases: PaywallPurchasesType { - var finishTransactions: Bool { + var purchasesAreCompletedBy: Bool { get { return false } set { _ = newValue } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 6b7297b5c8..b9df3fc216 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -228,11 +228,18 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void @objc public let attribution: Attribution + + @available(*, deprecated, message: "Use purchasesAreCompletedBy instead") @objc public var finishTransactions: Bool { get { self.systemInfo.finishTransactions } set { self.systemInfo.finishTransactions = newValue } } + @objc public var purchasesAreCompletedBy: Bool { + get { self.systemInfo.finishTransactions } + set { self.systemInfo.finishTransactions = newValue } + } + private let attributionFetcher: AttributionFetcher private let attributionPoster: AttributionPoster private let backend: Backend diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 9c151f7dc1..dcfb9e00a4 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -37,8 +37,11 @@ public protocol PurchasesType: AnyObject { * will turn up every time the app is opened. * More information on finishing transactions manually [is available here](https://rev.cat/finish-transactions). */ + @available(*, deprecated, message: "Use purchasesAreCompletedBy instead") var finishTransactions: Bool { get set } + var purchasesAreCompletedBy: Bool { get set } + /** * Delegate for ``Purchases`` instance. The delegate is responsible for handling promotional product purchases and * changes to customer information. From 5c4809e9fd1a0f80c8fcd3f4a07cea50c0fa69b4 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 10:13:38 -0700 Subject: [PATCH 27/76] Switch `purchasesAreCompletedBy` to enum type. --- RevenueCat.xcodeproj/project.pbxproj | 4 +++ Sources/Purchasing/Purchases/Purchases.swift | 6 ++-- .../Purchases/PurchasesAreCompletedBy.swift | 28 +++++++++++++++++++ .../Purchasing/Purchases/PurchasesType.swift | 2 +- 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 Sources/Purchasing/Purchases/PurchasesAreCompletedBy.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 8be7b58e05..4faaf6e691 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -620,6 +620,7 @@ 6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; }; 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; }; 80E80EF226970E04008F245A /* ReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */; }; + 884D3CE62C08E86400412198 /* PurchasesAreCompletedBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884D3CE52C08E86400412198 /* PurchasesAreCompletedBy.swift */; }; 9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */; }; 9A65E03625918B0500DE00B0 /* ConfigureStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */; }; 9A65E03B25918B0900DE00B0 /* CustomerInfoStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E03A25918B0900DE00B0 /* CustomerInfoStrings.swift */; }; @@ -1364,6 +1365,7 @@ 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = ""; }; 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = ""; }; 84C3F1AC1D7E1E64341D3936 /* Pods_RevenueCat_PurchasesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RevenueCat_PurchasesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 884D3CE52C08E86400412198 /* PurchasesAreCompletedBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesAreCompletedBy.swift; sourceTree = ""; }; 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogIntent.swift; sourceTree = ""; }; 9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigureStrings.swift; sourceTree = ""; }; 9A65E03A25918B0900DE00B0 /* CustomerInfoStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerInfoStrings.swift; sourceTree = ""; }; @@ -2853,6 +2855,7 @@ B3843BCA285149A0009F4854 /* Attribution.swift */, 57FD7B1428DA4037009CA4E4 /* PurchasesType.swift */, B35042C326CDB79A00905B95 /* Purchases.swift */, + 884D3CE52C08E86400412198 /* PurchasesAreCompletedBy.swift */, B35042C526CDD3B100905B95 /* PurchasesDelegate.swift */, 2D9F4A5426C30CA800B07B43 /* PurchasesOrchestrator.swift */, 4F8038322A1EA7C300D21039 /* TransactionPoster.swift */, @@ -3535,6 +3538,7 @@ 35AAEB452BBB14D000A12548 /* DiagnosticsFileHandler.swift in Sources */, B34D2AA626976FC700D88C3A /* ErrorCode.swift in Sources */, 4F15B4A12A6774C9005BEFE8 /* CustomerInfo+NonSubscriptions.swift in Sources */, + 884D3CE62C08E86400412198 /* PurchasesAreCompletedBy.swift in Sources */, B39E811D268E887500D31189 /* SubscriberAttribute.swift in Sources */, A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */, B378156D285A9772000A7B93 /* OfferingsAPI.swift in Sources */, diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index b9df3fc216..b5ad275b42 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -235,9 +235,9 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void set { self.systemInfo.finishTransactions = newValue } } - @objc public var purchasesAreCompletedBy: Bool { - get { self.systemInfo.finishTransactions } - set { self.systemInfo.finishTransactions = newValue } + @objc public var purchasesAreCompletedBy: PurchasesAreCompletedBy { + get { self.systemInfo.finishTransactions ? .revenueCat : .myApp } + set { self.systemInfo.finishTransactions = (newValue == .revenueCat ? true : false) } } private let attributionFetcher: AttributionFetcher diff --git a/Sources/Purchasing/Purchases/PurchasesAreCompletedBy.swift b/Sources/Purchasing/Purchases/PurchasesAreCompletedBy.swift new file mode 100644 index 0000000000..193a209148 --- /dev/null +++ b/Sources/Purchasing/Purchases/PurchasesAreCompletedBy.swift @@ -0,0 +1,28 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// PurchasesAreCompletedBy.swift +// +// Created by James Borthwick on 2024-05-30. + +import Foundation + +/// Where responsibility for completing purchase transactions lies. +@objc(RCPurchasesAreCompletedBy) +public enum PurchasesAreCompletedBy: Int { + + /// Purchase transactions are to be finished by RevenueCat. + case revenueCat + + /// Purchase transactions are to be finished by the app. + case myApp + +} + +extension PurchasesAreCompletedBy: Sendable {} diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index dcfb9e00a4..85a3ad22ee 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -40,7 +40,7 @@ public protocol PurchasesType: AnyObject { @available(*, deprecated, message: "Use purchasesAreCompletedBy instead") var finishTransactions: Bool { get set } - var purchasesAreCompletedBy: Bool { get set } + var purchasesAreCompletedBy: PurchasesAreCompletedBy { get set } /** * Delegate for ``Purchases`` instance. The delegate is responsible for handling promotional product purchases and From d8168561e846b67e5dd749327c0956fd1d005f90 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 10:20:48 -0700 Subject: [PATCH 28/76] Use new enum --- RevenueCatUI/Purchasing/MockPurchases.swift | 10 +++++----- RevenueCatUI/Purchasing/PaywallPurchasesType.swift | 2 +- RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift | 10 ++++++---- RevenueCatUI/Purchasing/PurchaseHandler.swift | 8 ++++---- RevenueCatUI/Views/LoadingPaywallView.swift | 4 ++-- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/RevenueCatUI/Purchasing/MockPurchases.swift b/RevenueCatUI/Purchasing/MockPurchases.swift index b17673be82..200c52be4c 100644 --- a/RevenueCatUI/Purchasing/MockPurchases.swift +++ b/RevenueCatUI/Purchasing/MockPurchases.swift @@ -28,15 +28,15 @@ final class MockPurchases: PaywallPurchasesType { private let purchaseBlock: PurchaseBlock private let restoreBlock: RestoreBlock private let trackEventBlock: TrackEventBlock - private let _finishTransactions: Bool + private let _purchasesAreCompletedBy: PurchasesAreCompletedBy - var purchasesAreCompletedBy: Bool { - get { return _finishTransactions } + var purchasesAreCompletedBy: PurchasesAreCompletedBy { + get { return _purchasesAreCompletedBy } set { _ = newValue } } init( - finishTransactions: Bool = true, + purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat, purchase: @escaping PurchaseBlock, restorePurchases: @escaping RestoreBlock, trackEvent: @escaping TrackEventBlock, @@ -46,7 +46,7 @@ final class MockPurchases: PaywallPurchasesType { self.restoreBlock = restorePurchases self.trackEventBlock = trackEvent self.customerInfoBlock = customerInfo - self._finishTransactions = finishTransactions + self._purchasesAreCompletedBy = purchasesAreCompletedBy } func customerInfo() async throws -> RevenueCat.CustomerInfo { diff --git a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift index 1dd8740258..ba88e09d5f 100644 --- a/RevenueCatUI/Purchasing/PaywallPurchasesType.swift +++ b/RevenueCatUI/Purchasing/PaywallPurchasesType.swift @@ -17,7 +17,7 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) protocol PaywallPurchasesType: Sendable { - var purchasesAreCompletedBy: Bool { get set } + var purchasesAreCompletedBy: PurchasesAreCompletedBy { get set } @Sendable func purchase(package: Package) async throws -> PurchaseResultData diff --git a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift index cd2857b919..4159585859 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift @@ -19,9 +19,11 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PurchaseHandler { - static func mock(_ customerInfo: CustomerInfo = TestData.customerInfo, finishTransactions: Bool = true) -> Self { + static func mock(_ customerInfo: CustomerInfo = TestData.customerInfo, + purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat) + -> Self { return self.init( - purchases: MockPurchases(finishTransactions: finishTransactions) { _ in + purchases: MockPurchases(purchasesAreCompletedBy: purchasesAreCompletedBy) { _ in return ( // No current way to create a mock transaction with RevenueCat's public methods. transaction: nil, @@ -38,8 +40,8 @@ extension PurchaseHandler { ) } - static func cancelling(finishTransactions: Bool = true) -> Self { - return .mock(finishTransactions: finishTransactions) + static func cancelling(purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat) -> Self { + return .mock(purchasesAreCompletedBy: purchasesAreCompletedBy) .map { block in { var result = try await block($0) result.userCancelled = true diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 38a5a058ff..20605eb0c7 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -115,7 +115,7 @@ extension PurchaseHandler { @MainActor func purchase(package: Package) async throws -> PurchaseResultData { - if self.purchases.purchasesAreCompletedBy { + if self.purchases.purchasesAreCompletedBy == .revenueCat { return try await performPurchase(package: package) } else { return try await performExternalPurchaseLogic(package: package) @@ -189,7 +189,7 @@ extension PurchaseHandler { // MARK: - Restore func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { - if self.purchases.purchasesAreCompletedBy { + if self.purchases.purchasesAreCompletedBy == .revenueCat { return try await performRestorePurchases() } else { return try await performExternalRestoreLogic() @@ -352,8 +352,8 @@ private extension PurchaseHandler { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private final class NotConfiguredPurchases: PaywallPurchasesType { - var purchasesAreCompletedBy: Bool { - get { return false } + var purchasesAreCompletedBy: PurchasesAreCompletedBy { + get { return .myApp } set { _ = newValue } } diff --git a/RevenueCatUI/Views/LoadingPaywallView.swift b/RevenueCatUI/Views/LoadingPaywallView.swift index e0f5658c8a..09161fe0fc 100644 --- a/RevenueCatUI/Views/LoadingPaywallView.swift +++ b/RevenueCatUI/Views/LoadingPaywallView.swift @@ -144,8 +144,8 @@ private extension LoadingPaywallView { @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) private final class LoadingPaywallPurchases: PaywallPurchasesType { - var purchasesAreCompletedBy: Bool { - get { return false } + var purchasesAreCompletedBy: PurchasesAreCompletedBy { + get { return .myApp } set { _ = newValue } } From 7047521c8dd164f19486a65bb49df2f798dd762f Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 10:24:29 -0700 Subject: [PATCH 29/76] Update test purchase handler --- Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index f218e73bcd..262ddf058f 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -305,7 +305,7 @@ class PurchaseCompletedHandlerTests: TestCase { expect(error).toEventually(matchError(Self.failureError)) } - private static let externalPurchaseHandler: PurchaseHandler = .mock(finishTransactions: false) + private static let externalPurchaseHandler: PurchaseHandler = .mock(purchasesAreCompletedBy: .myApp) private static let purchaseHandler: PurchaseHandler = .mock() private static let failingHandler: PurchaseHandler = .failing(failureError) private static let offering = TestData.offeringWithNoIntroOffer From 5a0718ded163a1518d2c403f316e9aff3fe17df9 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 10:38:21 -0700 Subject: [PATCH 30/76] Documentation update --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 7193dffe6b..02242bf475 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -293,7 +293,7 @@ extension View { /// Use this method if you wish to execute your own StoreKit purchase logic, /// skipping RevenueCat's. This method is **only** called if `Purchases` is - /// confiugured with `finishTransactions` set to `false`. This is typically used + /// confiugured with `purchasesAreCompletedBy` set to `.myApp`. This is typically used /// when migrating from a direct StoreKit implementation to RevenueCat in stages. /// /// After executing your StoreKit purchae code, you must call `purchaseCompletedHandler` @@ -320,7 +320,7 @@ extension View { /// Use this method if you wish to execute your own StoreKit restore purchases logic, /// skipping RevenueCat's. This method is **only** called if `Purchases` is - /// confiugured with `finishTransactions` set to `false`. This is typically used + /// confiugured with `purchasesAreCompletedBy` set to `.myApp`. This is typically used /// when migrating from a direct StoreKit implementation to RevenueCat in stages. /// /// After executing your StoreKit purchae code, you must call `purchaseRestoreHandler`. From 57ec3abdf0ad74999eed4e30046d736c69334b39 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 10:39:07 -0700 Subject: [PATCH 31/76] More informative debug log. --- RevenueCatUI/Data/Strings.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index 6648e83cf9..ce207d1a54 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -105,8 +105,9 @@ extension Strings: CustomStringConvertible { return "Setting restored customer info" case .executing_external_purchase_logic: - return "Will execute custom StoreKit purchase logic provided by the SDK adopter. " + - "No StoreKit purchasing logic will be performed by RevenueCat." + return "Will execute custom StoreKit purchase logic provided by yourapp. " + + "No StoreKit purchasing logic will be performed by RevenueCat. " + + "You must use `.handlePurchase` on your `PaywallView`" case .executing_purchase_logic: return "Will execute purchase logic provided by RevenueCat." @@ -115,8 +116,9 @@ extension Strings: CustomStringConvertible { return "Will execute restore purchases logic provided by RevenueCat." case .executing_external_restore_logic: - return "Will execute custom StoreKit restore purchases logic provided by the SDK adopter. " + - "No StoreKit restore purchases logic will be performed by RevenueCat." + return "Will execute custom StoreKit restore purchases logic provided by your app. " + + "No StoreKit restore purchases logic will be performed by RevenueCat. " + + "You must use `.handleRestorePurchases` on your `PaywallView`" } } From f526e838aadb8951576e8a8a5fb7f027060b287b Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 10:41:28 -0700 Subject: [PATCH 32/76] =?UTF-8?q?Add=20a=20=E2=80=9C.=E2=80=9D=20to=20the?= =?UTF-8?q?=20end=20of=20the=20sentence=E2=80=A6.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RevenueCatUI/Data/Strings.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index ce207d1a54..2e3f668d1a 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -107,7 +107,7 @@ extension Strings: CustomStringConvertible { case .executing_external_purchase_logic: return "Will execute custom StoreKit purchase logic provided by yourapp. " + "No StoreKit purchasing logic will be performed by RevenueCat. " + - "You must use `.handlePurchase` on your `PaywallView`" + "You must use `.handlePurchase` on your `PaywallView`." case .executing_purchase_logic: return "Will execute purchase logic provided by RevenueCat." @@ -118,7 +118,7 @@ extension Strings: CustomStringConvertible { case .executing_external_restore_logic: return "Will execute custom StoreKit restore purchases logic provided by your app. " + "No StoreKit restore purchases logic will be performed by RevenueCat. " + - "You must use `.handleRestorePurchases` on your `PaywallView`" + "You must use `.handleRestorePurchases` on your `PaywallView`." } } From 2d2f2eeaecfe2c44534fdd972b4baf17574aab2d Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 10:44:30 -0700 Subject: [PATCH 33/76] Documentation. --- Sources/Purchasing/Purchases/Purchases.swift | 2 +- Sources/Purchasing/Purchases/PurchasesType.swift | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index b5ad275b42..c1a2143cc3 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -229,7 +229,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void @objc public let attribution: Attribution - @available(*, deprecated, message: "Use purchasesAreCompletedBy instead") + @available(*, deprecated, message: "Use purchasesAreCompletedBy instead.") @objc public var finishTransactions: Bool { get { self.systemInfo.finishTransactions } set { self.systemInfo.finishTransactions = newValue } diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 85a3ad22ee..4847777bc1 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -37,9 +37,15 @@ public protocol PurchasesType: AnyObject { * will turn up every time the app is opened. * More information on finishing transactions manually [is available here](https://rev.cat/finish-transactions). */ - @available(*, deprecated, message: "Use purchasesAreCompletedBy instead") + @available(*, deprecated, message: "Use purchasesAreCompletedBy instead.") var finishTransactions: Bool { get set } + /** Whether transactions should be finished automatically. `.revenueCat` by default. + * - Warning: Setting this value to `.myApp` will prevent the SDK from finishing transactions. + * In this case, you *must* finish transactions in your app, otherwise they will remain in the queue and + * will turn up every time the app is opened. + * More information on finishing transactions manually [is available here](https://rev.cat/finish-transactions). + */ var purchasesAreCompletedBy: PurchasesAreCompletedBy { get set } /** From df3e1d9c2a9855d3bc85e743505d290fcc815062 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 11:00:11 -0700 Subject: [PATCH 34/76] Lint --- Sources/Purchasing/Purchases/Purchases.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index c1a2143cc3..3c55a72822 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -228,7 +228,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void @objc public let attribution: Attribution - @available(*, deprecated, message: "Use purchasesAreCompletedBy instead.") @objc public var finishTransactions: Bool { get { self.systemInfo.finishTransactions } From 6de881e8c3782fc3bab9adc5c68ff6223ccf2e5f Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 11:00:17 -0700 Subject: [PATCH 35/76] API Test --- .../SwiftAPITester/SwiftAPITester/PurchasesAPI.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 57f7ebbcb6..5c7b1f7964 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -19,12 +19,12 @@ func checkPurchasesAPI() { let purch = checkConfigure()! // initializers - let finishTransactions: Bool = purch.finishTransactions + let purchasesAreCompletedBy: Bool = purch.purchasesAreCompletedBy let delegate: PurchasesDelegate? = purch.delegate let appUserID: String = purch.appUserID let isAnonymous: Bool = purch.isAnonymous - print(finishTransactions, delegate!, appUserID, isAnonymous) + print(purchasesAreCompletedBy, delegate!, appUserID, isAnonymous) checkStaticMethods() checkIdentity(purchases: purch) From 262daf6a0ab1b264363554e64c30f15505b71da9 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 11:11:11 -0700 Subject: [PATCH 36/76] Test fixes --- .../APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift | 2 +- Tests/UnitTests/Mocks/MockPurchases.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 5c7b1f7964..3f6cbbfa8d 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -19,7 +19,7 @@ func checkPurchasesAPI() { let purch = checkConfigure()! // initializers - let purchasesAreCompletedBy: Bool = purch.purchasesAreCompletedBy + let purchasesAreCompletedBy: PurchasesAreCompletedBy = purch.purchasesAreCompletedBy let delegate: PurchasesDelegate? = purch.delegate let appUserID: String = purch.appUserID let isAnonymous: Bool = purch.isAnonymous diff --git a/Tests/UnitTests/Mocks/MockPurchases.swift b/Tests/UnitTests/Mocks/MockPurchases.swift index 1c22140f28..f32c0e742e 100644 --- a/Tests/UnitTests/Mocks/MockPurchases.swift +++ b/Tests/UnitTests/Mocks/MockPurchases.swift @@ -126,7 +126,7 @@ extension MockPurchases: PurchasesType { self.unimplemented() } - var finishTransactions: Bool { + var purchasesAreCompletedBy: PurchasesAreCompletedBy { get { self.unimplemented() } // swiftlint:disable:next unused_setter_value set { self.unimplemented() } From 46c84e03565c27646369d5c3a913510d778c6e47 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 11:13:52 -0700 Subject: [PATCH 37/76] Test updates --- Tests/UnitTests/Mocks/MockPurchases.swift | 6 ++++++ .../Purchasing/Purchases/PurchasesPurchasingTests.swift | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTests/Mocks/MockPurchases.swift b/Tests/UnitTests/Mocks/MockPurchases.swift index f32c0e742e..39d00ca45e 100644 --- a/Tests/UnitTests/Mocks/MockPurchases.swift +++ b/Tests/UnitTests/Mocks/MockPurchases.swift @@ -126,6 +126,12 @@ extension MockPurchases: PurchasesType { self.unimplemented() } + var finishTransactions: Bool { + get { self.unimplemented() } + // swiftlint:disable:next unused_setter_value + set { self.unimplemented() } + } + var purchasesAreCompletedBy: PurchasesAreCompletedBy { get { self.unimplemented() } // swiftlint:disable:next unused_setter_value diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift index 3c95356752..ea38b3fd5d 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesPurchasingTests.swift @@ -214,7 +214,7 @@ class PurchasesPurchasingTests: BasePurchasesTests { } func testDoesntFinishTransactionsIfFinishingDisabled() throws { - self.purchases.finishTransactions = false + self.purchases.purchasesAreCompletedBy = .myApp let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1")) self.purchases.purchase(product: product) { (_, _, _, _) in } From 5bd6065367059429caa844573f8537b72d2aba5c Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 30 May 2024 11:34:57 -0700 Subject: [PATCH 38/76] API test --- .../SwiftAPITester/PurchasesAPI.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 0260fdeeed..5dda28905f 100644 --- a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -19,12 +19,12 @@ func checkPurchasesAPI() { let purch = checkConfigure()! // initializers - let finishTransactions: Bool = purch.finishTransactions + let purchasesAreCompletedBy: PurchasesAreCompletedBy = purch.purchasesAreCompletedBy let delegate: PurchasesDelegate? = purch.delegate let appUserID: String = purch.appUserID let isAnonymous: Bool = purch.isAnonymous - print(finishTransactions, delegate!, appUserID, isAnonymous) + print(purchasesAreCompletedBy, delegate!, appUserID, isAnonymous) checkStaticMethods() checkIdentity(purchases: purch) From 2f59d3dcc30c2eff1e29468e51a739a37c6d3855 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 11:23:28 -0700 Subject: [PATCH 39/76] Functional cmbination single handler; needs cleanup --- .../View+PurchaseRestoreCompleted.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 02242bf475..2c37d71bf3 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -56,6 +56,23 @@ public typealias HandleRestoreHandler = @MainActor @Sendable ( ) -> Void ) -> Void +/// A closure used for notifying that custom purchase logic has completed. +public typealias HandlePurchaseAndRestoreHandler = ( + purchaseHandler: @MainActor @Sendable ( + _ storeProduct: StoreProduct, + _ purchaseCompletedHandler: @escaping ( + _ userCancelled: Bool, + _ error: Error? + ) -> Void + ) -> Void, + restoreHandler: @MainActor @Sendable ( + _ restorePurchasesCompletedHandler: @escaping ( + _ success: Bool, + _ error: Error? + ) -> Void + ) -> Void +) + /// A closure used for notifying of failures during purchases or restores. public typealias PurchaseFailureHandler = @MainActor @Sendable (NSError) -> Void @@ -291,6 +308,19 @@ extension View { self.environment(\.onRequestedDismissal, action) } + public func handlePurchaseAndRestore( + purchaseHandler: @escaping HandlePurchaseHandler, + restoreHandler: @escaping HandleRestoreHandler + ) -> some View { + return self.modifier( + HandlePurchaseAndRestoreModifier( + purchaseHandler: purchaseHandler, + restoreHandler: restoreHandler + ) + ) + } + + /// Use this method if you wish to execute your own StoreKit purchase logic, /// skipping RevenueCat's. This method is **only** called if `Purchases` is /// confiugured with `purchasesAreCompletedBy` set to `.myApp`. This is typically used @@ -405,6 +435,18 @@ private struct OnPurchaseCancelledModifier: ViewModifier { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct HandlePurchaseAndRestoreModifier: ViewModifier { + let purchaseHandler: HandlePurchaseHandler + let restoreHandler: HandleRestoreHandler + + func body(content: Content) -> some View { + content + .modifier(HandlePurchaseModifier(handler: purchaseHandler)) + .modifier(HandleRestoreModifier(handler: restoreHandler)) + } +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private struct HandlePurchaseModifier: ViewModifier { From 5ef84faf6f3aaf0b7846413bbc94f94b3fb92e58 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 11:50:08 -0700 Subject: [PATCH 40/76] Tidy, consolidate, rename --- .../View+PurchaseRestoreCompleted.swift | 118 ++++++------------ 1 file changed, 39 insertions(+), 79 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 2c37d71bf3..a606eb6b11 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -40,39 +40,22 @@ public typealias PurchaseOfPackageStartedHandler = @MainActor @Sendable (_ packa public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void /// A closure used for notifying that custom purchase logic has completed. -public typealias HandlePurchaseHandler = @MainActor @Sendable ( +public typealias PerformPurchase = @MainActor @Sendable ( _ storeProduct: StoreProduct, - _ purchaseCompletedHandler: @escaping ( + _ reportPurchaseResult: @escaping ( _ userCancelled: Bool, _ error: Error? ) -> Void ) -> Void /// A closure used for notifying that custom restore logic has completed. -public typealias HandleRestoreHandler = @MainActor @Sendable ( - _ restorePurchasesCompletedHandler: @escaping ( +public typealias PerformRestore = @MainActor @Sendable ( + _ reportRestoreResult: @escaping ( _ success: Bool, _ error: Error? ) -> Void ) -> Void -/// A closure used for notifying that custom purchase logic has completed. -public typealias HandlePurchaseAndRestoreHandler = ( - purchaseHandler: @MainActor @Sendable ( - _ storeProduct: StoreProduct, - _ purchaseCompletedHandler: @escaping ( - _ userCancelled: Bool, - _ error: Error? - ) -> Void - ) -> Void, - restoreHandler: @MainActor @Sendable ( - _ restorePurchasesCompletedHandler: @escaping ( - _ success: Bool, - _ error: Error? - ) -> Void - ) -> Void -) - /// A closure used for notifying of failures during purchases or restores. public typealias PurchaseFailureHandler = @MainActor @Sendable (NSError) -> Void @@ -308,70 +291,47 @@ extension View { self.environment(\.onRequestedDismissal, action) } - public func handlePurchaseAndRestore( - purchaseHandler: @escaping HandlePurchaseHandler, - restoreHandler: @escaping HandleRestoreHandler - ) -> some View { - return self.modifier( - HandlePurchaseAndRestoreModifier( - purchaseHandler: purchaseHandler, - restoreHandler: restoreHandler - ) - ) - } - - - /// Use this method if you wish to execute your own StoreKit purchase logic, + /// Use this method if you wish to execute your own StoreKit purchase and restore logic, /// skipping RevenueCat's. This method is **only** called if `Purchases` is /// confiugured with `purchasesAreCompletedBy` set to `.myApp`. This is typically used - /// when migrating from a direct StoreKit implementation to RevenueCat in stages. + /// when migrating from a direct StoreKit implementation to RevenueCat in stages, or when using + /// RevenueCat for experiments and growth tools. /// - /// After executing your StoreKit purchae code, you must call `purchaseCompletedHandler` - /// for accurate statistics. + /// After executing your StoreKit purchae code, you **must** communicate the result of your purchase + /// code by calling `reportPurchaseResult` and `reportRestoreResult` when your code + /// has finished executing. Failure to do so will result in undefined behavior. /// /// Example: /// ```swift - /// PaywallView() - /// .handlePurchase { storeProduct, purchaseCompletedHandler in - /// var userCancelled: Bool = false - /// var error: Error? = nil + /// PaywallView() + /// .handlePurchaseAndRestore( + /// performPurchase: { storeProduct, reportPurchaseResult in + /// var userDidCancel = false + /// var error: Error? /// - /// // Manually call StoreKit purchasing logic - /// // and update userCancelled and error + /// // your app's purchase logic /// - /// purchaseCompletedHandler(userCancelled, error) - /// } - /// ``` - public func handlePurchase( - _ handler: @escaping HandlePurchaseHandler - ) -> some View { - return self.modifier(HandlePurchaseModifier(handler: handler)) - } - - /// Use this method if you wish to execute your own StoreKit restore purchases logic, - /// skipping RevenueCat's. This method is **only** called if `Purchases` is - /// confiugured with `purchasesAreCompletedBy` set to `.myApp`. This is typically used - /// when migrating from a direct StoreKit implementation to RevenueCat in stages. - /// - /// After executing your StoreKit purchae code, you must call `purchaseRestoreHandler`. - /// - /// Example: - /// ```swift - /// PaywallView() - /// .handleRestore { purchaseRestoreHandler in - /// var success: Bool = false - /// var error: Error? = nil + /// reportPurchaseResult(userDidCancel, error) + /// }, performRestore: { reportRestoreResult in + /// var success = false + /// var error: Error? /// - /// // Manually call StoreKit purchasing logic - /// // and update success and error + /// // your app's restore logic /// - /// restorePurchasesCompletedHandler(success, error) - /// } + /// reportRestoreResult(success, error) + /// }) /// ``` - public func handleRestorePurchases( - _ handler: @escaping HandleRestoreHandler + /// + public func handlePurchaseAndRestore( + performPurchase: @escaping PerformPurchase, + performRestore: @escaping PerformRestore ) -> some View { - return self.modifier(HandleRestoreModifier(handler: handler)) + return self.modifier( + HandlePurchaseAndRestoreModifier( + performPurchase: performPurchase, + performRestore: performRestore + ) + ) } } @@ -437,20 +397,20 @@ private struct OnPurchaseCancelledModifier: ViewModifier { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct HandlePurchaseAndRestoreModifier: ViewModifier { - let purchaseHandler: HandlePurchaseHandler - let restoreHandler: HandleRestoreHandler + let performPurchase: PerformPurchase + let performRestore: PerformRestore func body(content: Content) -> some View { content - .modifier(HandlePurchaseModifier(handler: purchaseHandler)) - .modifier(HandleRestoreModifier(handler: restoreHandler)) + .modifier(HandlePurchaseModifier(handler: performPurchase)) + .modifier(HandleRestoreModifier(handler: performRestore)) } } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private struct HandlePurchaseModifier: ViewModifier { - let handler: HandlePurchaseHandler + let handler: PerformPurchase func body(content: Content) -> some View { content @@ -466,7 +426,7 @@ private struct HandlePurchaseModifier: ViewModifier { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private struct HandleRestoreModifier: ViewModifier { - let handler: HandleRestoreHandler + let handler: PerformRestore func body(content: Content) -> some View { content From 4a02670d47e50920f4f3a9cc91ba372831ee6d49 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 11:54:52 -0700 Subject: [PATCH 41/76] typo --- RevenueCatUI/Data/Strings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index 2e3f668d1a..26fd381e7f 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -105,7 +105,7 @@ extension Strings: CustomStringConvertible { return "Setting restored customer info" case .executing_external_purchase_logic: - return "Will execute custom StoreKit purchase logic provided by yourapp. " + + return "Will execute custom StoreKit purchase logic provided by your app. " + "No StoreKit purchasing logic will be performed by RevenueCat. " + "You must use `.handlePurchase` on your `PaywallView`." From be00392ee369468ab4dd9221e3878b0055650f83 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 12:37:02 -0700 Subject: [PATCH 42/76] Documentation --- Sources/Purchasing/Purchases/PurchasesType.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 4847777bc1..6c5e68a852 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -41,9 +41,9 @@ public protocol PurchasesType: AnyObject { var finishTransactions: Bool { get set } /** Whether transactions should be finished automatically. `.revenueCat` by default. - * - Warning: Setting this value to `.myApp` will prevent the SDK from finishing transactions. - * In this case, you *must* finish transactions in your app, otherwise they will remain in the queue and - * will turn up every time the app is opened. + * - Warning: Setting this value to `.myApp` will prevent the SDK from making purchaes and finishing transactions. + * In this case, you *must* perform all of this logic in your app. If using a `PaywallView`, use the modifier + * `.handlePurchaseAndRestore`. * More information on finishing transactions manually [is available here](https://rev.cat/finish-transactions). */ var purchasesAreCompletedBy: PurchasesAreCompletedBy { get set } From c51d92ef9fcda441e69020ec870d47d9ce79e0a0 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 12:38:50 -0700 Subject: [PATCH 43/76] Documentation --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index a606eb6b11..861bc753f0 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -294,8 +294,8 @@ extension View { /// Use this method if you wish to execute your own StoreKit purchase and restore logic, /// skipping RevenueCat's. This method is **only** called if `Purchases` is /// confiugured with `purchasesAreCompletedBy` set to `.myApp`. This is typically used - /// when migrating from a direct StoreKit implementation to RevenueCat in stages, or when using - /// RevenueCat for experiments and growth tools. + /// when migrating from a direct StoreKit implementation to RevenueCat in stages, or if integrating + /// RevenueCat for experiments and growth tools only. /// /// After executing your StoreKit purchae code, you **must** communicate the result of your purchase /// code by calling `reportPurchaseResult` and `reportRestoreResult` when your code From 9ed38e3377c0d7505fcdbb8a5390f7107c6aebf7 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 13:28:50 -0700 Subject: [PATCH 44/76] naming --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 20605eb0c7..9b9321832c 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -20,7 +20,7 @@ import SwiftUI typealias HandlePurchaseData = (storeProduct: StoreProduct, callback: (_ userCancelled: Bool, _ error: Error?) -> Void) -class HandleRestoreCallbackContainer: Equatable { +class PerformRestoreInfo: Equatable { let handleRestoreCallback: (_ userCancelled: Bool, _ error: Error?) -> Void @@ -28,7 +28,7 @@ class HandleRestoreCallbackContainer: Equatable { self.handleRestoreCallback = callback } - static func == (lhs: HandleRestoreCallbackContainer, rhs: HandleRestoreCallbackContainer) -> Bool { + static func == (lhs: PerformRestoreInfo, rhs: PerformRestoreInfo) -> Bool { return lhs === rhs } @@ -63,7 +63,7 @@ final class PurchaseHandler: ObservableObject { fileprivate(set) var handlePurchase: HandlePurchaseData? @Published - fileprivate(set) var handleRestore: HandleRestoreCallbackContainer? + fileprivate(set) var handleRestore: PerformRestoreInfo? /// Whether a restore is currently in progress @Published @@ -236,7 +236,7 @@ extension PurchaseHandler { DispatchQueue.main.async { // this triggers the view's `.handleRestore` function, and its callback must be called // after the continuation is set below - self.handleRestore = HandleRestoreCallbackContainer(callback: self.completeExternalRestorePurchases) + self.handleRestore = PerformRestoreInfo(callback: self.completeExternalRestorePurchases) } self.startAction() @@ -437,9 +437,9 @@ struct HandlePurchasePreferenceKey: PreferenceKey { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct HandleRestorePreferenceKey: PreferenceKey { - static var defaultValue: HandleRestoreCallbackContainer? + static var defaultValue: PerformRestoreInfo? - static func reduce(value: inout HandleRestoreCallbackContainer?, nextValue: () -> HandleRestoreCallbackContainer?) { + static func reduce(value: inout PerformRestoreInfo?, nextValue: () -> PerformRestoreInfo?) { value = nextValue() } From 436ae8648bf39fb8417fb5951250611a4a0ac243 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 13:32:51 -0700 Subject: [PATCH 45/76] Name and type --- RevenueCatUI/PaywallView.swift | 2 +- RevenueCatUI/Purchasing/PurchaseHandler.swift | 53 ++++++++----------- .../View+PurchaseRestoreCompleted.swift | 2 +- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index fd827ba8a9..c1e2d463b0 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -311,7 +311,7 @@ struct LoadedOfferingPaywallView: View { .preference(key: PurchasedResultPreferenceKey.self, value: .init(data: self.purchaseHandler.purchaseResult)) .preference(key: HandlePurchasePreferenceKey.self, - value: .init(data: self.purchaseHandler.handlePurchase)) + value: self.purchaseHandler.handlePurchase) .preference(key: HandleRestorePreferenceKey.self, value: self.purchaseHandler.handleRestore) .preference(key: RestoredCustomerInfoPreferenceKey.self, diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 9b9321832c..f0883c03c3 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -17,8 +17,22 @@ import SwiftUI // swiftlint:disable file_length -typealias HandlePurchaseData = (storeProduct: StoreProduct, - callback: (_ userCancelled: Bool, _ error: Error?) -> Void) +class PerformPurchaseInfo: Equatable { + + let storeProduct: StoreProduct + let reportPurchaseResult: (_ userCancelled: Bool, _ error: Error?) -> Void + + init(storeProduct: StoreProduct, reportPurchaseResult: @escaping (_: Bool, _: Error?) -> Void) { + self.storeProduct = storeProduct + self.reportPurchaseResult = reportPurchaseResult + } + + static func == (lhs: PerformPurchaseInfo, rhs: PerformPurchaseInfo) -> Bool { + return lhs.storeProduct == rhs.storeProduct + } + +} + class PerformRestoreInfo: Equatable { @@ -60,7 +74,7 @@ final class PurchaseHandler: ObservableObject { fileprivate(set) var purchaseResult: PurchaseResultData? @Published - fileprivate(set) var handlePurchase: HandlePurchaseData? + fileprivate(set) var handlePurchase: PerformPurchaseInfo? @Published fileprivate(set) var handleRestore: PerformRestoreInfo? @@ -161,7 +175,8 @@ extension PurchaseHandler { self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil - self.handlePurchase = (package.storeProduct, self.completeExternalHandlePurchase) + self.handlePurchase = PerformPurchaseInfo(storeProduct: package.storeProduct, + reportPurchaseResult: self.completeExternalHandlePurchase) self.startAction() @@ -400,35 +415,9 @@ struct RestoreInProgressPreferenceKey: PreferenceKey { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct HandlePurchasePreferenceKey: PreferenceKey { - typealias Callback = (_ userCancelled: Bool, _ error: Error?) -> Void - - struct PurchaseResult: Equatable { - - static func == ( - lhs: HandlePurchasePreferenceKey.PurchaseResult, - rhs: HandlePurchasePreferenceKey.PurchaseResult - ) -> Bool { - return lhs.storeProduct == rhs.storeProduct - } - - var storeProduct: StoreProduct? - var callback: Callback? - - init(data: HandlePurchaseData) { - self.storeProduct = data.storeProduct - self.callback = data.callback - } - - init?(data: HandlePurchaseData?) { - guard let data else { return nil } - self.init(data: data) - } - - } - - static var defaultValue: PurchaseResult? + static var defaultValue: PerformPurchaseInfo? - static func reduce(value: inout PurchaseResult?, nextValue: () -> PurchaseResult?) { + static func reduce(value: inout PerformPurchaseInfo?, nextValue: () -> PerformPurchaseInfo?) { value = nextValue() } diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 861bc753f0..109e0ee7c4 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -415,7 +415,7 @@ private struct HandlePurchaseModifier: ViewModifier { func body(content: Content) -> some View { content .onPreferenceChange(HandlePurchasePreferenceKey.self) { data in - if let storeProduct = data?.storeProduct, let callback = data?.callback { + if let storeProduct = data?.storeProduct, let callback = data?.reportPurchaseResult { self.handler(storeProduct, callback) } } From 6d0ad5855ccf38fb6bd7d0fc3c1fd03e9584e867 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 13:34:46 -0700 Subject: [PATCH 46/76] naming --- RevenueCatUI/PaywallView.swift | 4 ++-- RevenueCatUI/Purchasing/PurchaseHandler.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index c1e2d463b0..b3ae37cdcc 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -311,9 +311,9 @@ struct LoadedOfferingPaywallView: View { .preference(key: PurchasedResultPreferenceKey.self, value: .init(data: self.purchaseHandler.purchaseResult)) .preference(key: HandlePurchasePreferenceKey.self, - value: self.purchaseHandler.handlePurchase) + value: self.purchaseHandler.performPurchase) .preference(key: HandleRestorePreferenceKey.self, - value: self.purchaseHandler.handleRestore) + value: self.purchaseHandler.performRestore) .preference(key: RestoredCustomerInfoPreferenceKey.self, value: self.purchaseHandler.restoredCustomerInfo) .preference(key: RestoreInProgressPreferenceKey.self, diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index f0883c03c3..2f05d7cc3f 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -74,10 +74,10 @@ final class PurchaseHandler: ObservableObject { fileprivate(set) var purchaseResult: PurchaseResultData? @Published - fileprivate(set) var handlePurchase: PerformPurchaseInfo? + fileprivate(set) var performPurchase: PerformPurchaseInfo? @Published - fileprivate(set) var handleRestore: PerformRestoreInfo? + fileprivate(set) var performRestore: PerformRestoreInfo? /// Whether a restore is currently in progress @Published @@ -175,7 +175,7 @@ extension PurchaseHandler { self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil - self.handlePurchase = PerformPurchaseInfo(storeProduct: package.storeProduct, + self.performPurchase = PerformPurchaseInfo(storeProduct: package.storeProduct, reportPurchaseResult: self.completeExternalHandlePurchase) self.startAction() @@ -186,7 +186,7 @@ extension PurchaseHandler { @MainActor func completeExternalHandlePurchase(_ userCancelled: Bool, _ error: Error?) { self.actionInProgress = false - self.handlePurchase = nil + self.performPurchase = nil if let error { self.purchaseError = error @@ -251,7 +251,7 @@ extension PurchaseHandler { DispatchQueue.main.async { // this triggers the view's `.handleRestore` function, and its callback must be called // after the continuation is set below - self.handleRestore = PerformRestoreInfo(callback: self.completeExternalRestorePurchases) + self.performRestore = PerformRestoreInfo(callback: self.completeExternalRestorePurchases) } self.startAction() From edccb80d0158f96b9da32364d8db96c070b5def4 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 13:40:17 -0700 Subject: [PATCH 47/76] Documentation --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 2f05d7cc3f..c11aa48ee5 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -73,9 +73,11 @@ final class PurchaseHandler: ObservableObject { @Published fileprivate(set) var purchaseResult: PurchaseResultData? + /// Information used to perform a purchase in the app (rather than in RevenueCat) @Published fileprivate(set) var performPurchase: PerformPurchaseInfo? + /// Information used to perform restoring a purchase in the app (rather than in RevenueCat) @Published fileprivate(set) var performRestore: PerformRestoreInfo? From e411c2ef2184f80e83d5a487feb63564bc2699c0 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:03:29 -0700 Subject: [PATCH 48/76] Improve public API with labels --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index c11aa48ee5..2874c9397f 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -19,15 +19,19 @@ import SwiftUI class PerformPurchaseInfo: Equatable { - let storeProduct: StoreProduct - let reportPurchaseResult: (_ userCancelled: Bool, _ error: Error?) -> Void + public let storeProduct: StoreProduct + let reportPurchaseResultCallback: (_ userCancelled: Bool, _ error: Error?) -> Void init(storeProduct: StoreProduct, reportPurchaseResult: @escaping (_: Bool, _: Error?) -> Void) { self.storeProduct = storeProduct - self.reportPurchaseResult = reportPurchaseResult + self.reportPurchaseResultCallback = reportPurchaseResult } - static func == (lhs: PerformPurchaseInfo, rhs: PerformPurchaseInfo) -> Bool { + public func reportPurchaseResult2(userCancelled: Bool, error: Error?) -> Void { + reportPurchaseResultCallback(userCancelled, error) + } + + public static func == (lhs: PerformPurchaseInfo, rhs: PerformPurchaseInfo) -> Bool { return lhs.storeProduct == rhs.storeProduct } @@ -113,8 +117,7 @@ final class PurchaseHandler: ObservableObject { self.purchases = purchases } - // @PublicForExternalTesting - static func `default`() -> Self { + public static func `default`() -> Self { return Purchases.isConfigured ? .init() : Self.notConfigured() } @@ -178,7 +181,7 @@ extension PurchaseHandler { self.purchaseResult = nil self.purchaseError = nil self.performPurchase = PerformPurchaseInfo(storeProduct: package.storeProduct, - reportPurchaseResult: self.completeExternalHandlePurchase) + reportPurchaseResult: self.reportExternalPurchaseResult) self.startAction() @@ -186,7 +189,7 @@ extension PurchaseHandler { } @MainActor - func completeExternalHandlePurchase(_ userCancelled: Bool, _ error: Error?) { + func reportExternalPurchaseResult(_ userCancelled: Bool, _ error: Error?) { self.actionInProgress = false self.performPurchase = nil @@ -253,7 +256,7 @@ extension PurchaseHandler { DispatchQueue.main.async { // this triggers the view's `.handleRestore` function, and its callback must be called // after the continuation is set below - self.performRestore = PerformRestoreInfo(callback: self.completeExternalRestorePurchases) + self.performRestore = PerformRestoreInfo(callback: self.reportExternalRestoreResult) } self.startAction() @@ -271,7 +274,7 @@ extension PurchaseHandler { } @MainActor - func completeExternalRestorePurchases(success: Bool, error: Error?) { + func reportExternalRestoreResult(success: Bool, error: Error?) { if let error { self.restoreError = error externalRestorePurchaseContinuation?.resume(throwing: error) From 02c266daabf85b96cd75dc1e35afd4a000c4a210 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:04:35 -0700 Subject: [PATCH 49/76] Missing from last commit --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 109e0ee7c4..dbf3d68839 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -41,11 +41,7 @@ public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void /// A closure used for notifying that custom purchase logic has completed. public typealias PerformPurchase = @MainActor @Sendable ( - _ storeProduct: StoreProduct, - _ reportPurchaseResult: @escaping ( - _ userCancelled: Bool, - _ error: Error? - ) -> Void + _ reportPurchaseResult: PerformPurchaseInfo ) -> Void /// A closure used for notifying that custom restore logic has completed. @@ -414,9 +410,9 @@ private struct HandlePurchaseModifier: ViewModifier { func body(content: Content) -> some View { content - .onPreferenceChange(HandlePurchasePreferenceKey.self) { data in - if let storeProduct = data?.storeProduct, let callback = data?.reportPurchaseResult { - self.handler(storeProduct, callback) + .onPreferenceChange(HandlePurchasePreferenceKey.self) { performPurchaseInfo in + if let performPurchaseInfo { + self.handler(performPurchaseInfo) } } } From fab75812decd678a565e364ac2af5b5412657e3d Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:20:11 -0700 Subject: [PATCH 50/76] Improve public API --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 34 +++++++++++-------- .../View+PurchaseRestoreCompleted.swift | 16 ++++----- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 2874c9397f..133dc46d5e 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -19,7 +19,7 @@ import SwiftUI class PerformPurchaseInfo: Equatable { - public let storeProduct: StoreProduct + let storeProduct: StoreProduct let reportPurchaseResultCallback: (_ userCancelled: Bool, _ error: Error?) -> Void init(storeProduct: StoreProduct, reportPurchaseResult: @escaping (_: Bool, _: Error?) -> Void) { @@ -27,26 +27,30 @@ class PerformPurchaseInfo: Equatable { self.reportPurchaseResultCallback = reportPurchaseResult } - public func reportPurchaseResult2(userCancelled: Bool, error: Error?) -> Void { + public func reportPurchaseResult(userCancelled: Bool, error: Error?) -> Void { reportPurchaseResultCallback(userCancelled, error) } - public static func == (lhs: PerformPurchaseInfo, rhs: PerformPurchaseInfo) -> Bool { + public static func == (lhs: PurchaseResultReporter, rhs: PurchaseResultReporter) -> Bool { return lhs.storeProduct == rhs.storeProduct } } -class PerformRestoreInfo: Equatable { +public class RestoreResultReporter: Equatable { - let handleRestoreCallback: (_ userCancelled: Bool, _ error: Error?) -> Void + let reportRestoreResultCallback: (_ success: Bool, _ error: Error?) -> Void init(callback: @escaping (Bool, Error?) -> Void) { - self.handleRestoreCallback = callback + self.reportRestoreResultCallback = callback } - static func == (lhs: PerformRestoreInfo, rhs: PerformRestoreInfo) -> Bool { + public func report(success: Bool, error: Error?) -> Void { + reportRestoreResultCallback(success, error) + } + + public static func == (lhs: RestoreResultReporter, rhs: RestoreResultReporter) -> Bool { return lhs === rhs } @@ -79,11 +83,11 @@ final class PurchaseHandler: ObservableObject { /// Information used to perform a purchase in the app (rather than in RevenueCat) @Published - fileprivate(set) var performPurchase: PerformPurchaseInfo? + fileprivate(set) var performPurchase: PurchaseResultReporter? /// Information used to perform restoring a purchase in the app (rather than in RevenueCat) @Published - fileprivate(set) var performRestore: PerformRestoreInfo? + fileprivate(set) var performRestore: RestoreResultReporter? /// Whether a restore is currently in progress @Published @@ -180,7 +184,7 @@ extension PurchaseHandler { self.packageBeingPurchased = package self.purchaseResult = nil self.purchaseError = nil - self.performPurchase = PerformPurchaseInfo(storeProduct: package.storeProduct, + self.performPurchase = PurchaseResultReporter(storeProduct: package.storeProduct, reportPurchaseResult: self.reportExternalPurchaseResult) self.startAction() @@ -256,7 +260,7 @@ extension PurchaseHandler { DispatchQueue.main.async { // this triggers the view's `.handleRestore` function, and its callback must be called // after the continuation is set below - self.performRestore = PerformRestoreInfo(callback: self.reportExternalRestoreResult) + self.performRestore = RestoreResultReporter(callback: self.reportExternalRestoreResult) } self.startAction() @@ -420,9 +424,9 @@ struct RestoreInProgressPreferenceKey: PreferenceKey { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct HandlePurchasePreferenceKey: PreferenceKey { - static var defaultValue: PerformPurchaseInfo? + static var defaultValue: PurchaseResultReporter? - static func reduce(value: inout PerformPurchaseInfo?, nextValue: () -> PerformPurchaseInfo?) { + static func reduce(value: inout PurchaseResultReporter?, nextValue: () -> PurchaseResultReporter?) { value = nextValue() } @@ -431,9 +435,9 @@ struct HandlePurchasePreferenceKey: PreferenceKey { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct HandleRestorePreferenceKey: PreferenceKey { - static var defaultValue: PerformRestoreInfo? + static var defaultValue: RestoreResultReporter? - static func reduce(value: inout PerformRestoreInfo?, nextValue: () -> PerformRestoreInfo?) { + static func reduce(value: inout RestoreResultReporter?, nextValue: () -> RestoreResultReporter?) { value = nextValue() } diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index dbf3d68839..bebd4ba032 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -41,15 +41,13 @@ public typealias PurchaseCancelledHandler = @MainActor @Sendable () -> Void /// A closure used for notifying that custom purchase logic has completed. public typealias PerformPurchase = @MainActor @Sendable ( - _ reportPurchaseResult: PerformPurchaseInfo + _ storeProduct: StoreProduct, + _ purchaseResultReporter: PurchaseResultReporter ) -> Void /// A closure used for notifying that custom restore logic has completed. public typealias PerformRestore = @MainActor @Sendable ( - _ reportRestoreResult: @escaping ( - _ success: Bool, - _ error: Error? - ) -> Void + _ restoreResultReporter: RestoreResultReporter ) -> Void /// A closure used for notifying of failures during purchases or restores. @@ -412,7 +410,7 @@ private struct HandlePurchaseModifier: ViewModifier { content .onPreferenceChange(HandlePurchasePreferenceKey.self) { performPurchaseInfo in if let performPurchaseInfo { - self.handler(performPurchaseInfo) + self.handler(performPurchaseInfo.storeProduct, performPurchaseInfo) } } } @@ -426,9 +424,9 @@ private struct HandleRestoreModifier: ViewModifier { func body(content: Content) -> some View { content - .onPreferenceChange(HandleRestorePreferenceKey.self) { callbackContainer in - if let callback = callbackContainer?.handleRestoreCallback { - self.handler(callback) + .onPreferenceChange(HandleRestorePreferenceKey.self) { performRestoreInfo in + if let performRestoreInfo { + self.handler(performRestoreInfo) } } } From b4ace488587e72d7b10a74bd5ae92646292c3872 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:22:05 -0700 Subject: [PATCH 51/76] Improve Public API --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 133dc46d5e..1579c56484 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -27,7 +27,7 @@ class PerformPurchaseInfo: Equatable { self.reportPurchaseResultCallback = reportPurchaseResult } - public func reportPurchaseResult(userCancelled: Bool, error: Error?) -> Void { + public func reportResult(userCancelled: Bool, error: Error?) -> Void { reportPurchaseResultCallback(userCancelled, error) } @@ -46,7 +46,7 @@ public class RestoreResultReporter: Equatable { self.reportRestoreResultCallback = callback } - public func report(success: Bool, error: Error?) -> Void { + public func reportResult(success: Bool, error: Error?) -> Void { reportRestoreResultCallback(success, error) } From 0899a382193fafb2a8d76accdae2b1b3a39b2bd8 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:28:27 -0700 Subject: [PATCH 52/76] Docs --- .../View+PurchaseRestoreCompleted.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index bebd4ba032..77d62ffcc6 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -299,21 +299,17 @@ extension View { /// ```swift /// PaywallView() /// .handlePurchaseAndRestore( - /// performPurchase: { storeProduct, reportPurchaseResult in - /// var userDidCancel = false - /// var error: Error? + /// performPurchase: { storeProduct, purchaseResultReporter in + /// // make purchase for `storeProduct` /// - /// // your app's purchase logic + /// // report result to RevenueCat + /// purchaseResultReporter.reportResult(userCancelled: false, error: nil) + /// }, performRestore: { restoreResultReporter in + /// // restore purchases /// - /// reportPurchaseResult(userDidCancel, error) - /// }, performRestore: { reportRestoreResult in - /// var success = false - /// var error: Error? - /// - /// // your app's restore logic - /// - /// reportRestoreResult(success, error) - /// }) + /// // report result to RevenueCat + /// restoreResultReporter.reportResult(success: true, error: nil) + /// }) /// ``` /// public func handlePurchaseAndRestore( From 63942c1240d2b9f49014cbd998b5aceb17d9432d Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:30:14 -0700 Subject: [PATCH 53/76] Docs --- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 77d62ffcc6..007574849f 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -292,7 +292,7 @@ extension View { /// RevenueCat for experiments and growth tools only. /// /// After executing your StoreKit purchae code, you **must** communicate the result of your purchase - /// code by calling `reportPurchaseResult` and `reportRestoreResult` when your code + /// code by calling `reportResult`on the passed result reporter object when your code /// has finished executing. Failure to do so will result in undefined behavior. /// /// Example: From dd5d32fb21fbf9ab3446730986a05b72ccfa482c Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:41:12 -0700 Subject: [PATCH 54/76] Docs and naming --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 2 +- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 12 ++++++------ Sources/Purchasing/Purchases/PurchasesType.swift | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 1579c56484..4fa429eed8 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -185,7 +185,7 @@ extension PurchaseHandler { self.purchaseResult = nil self.purchaseError = nil self.performPurchase = PurchaseResultReporter(storeProduct: package.storeProduct, - reportPurchaseResult: self.reportExternalPurchaseResult) + reportPurchaseResult: self.reportExternalPurchaseResult) self.startAction() diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 007574849f..eecb906fb3 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -404,9 +404,9 @@ private struct HandlePurchaseModifier: ViewModifier { func body(content: Content) -> some View { content - .onPreferenceChange(HandlePurchasePreferenceKey.self) { performPurchaseInfo in - if let performPurchaseInfo { - self.handler(performPurchaseInfo.storeProduct, performPurchaseInfo) + .onPreferenceChange(HandlePurchasePreferenceKey.self) { purchaseResultReporter in + if let purchaseResultReporter { + self.handler(purchaseResultReporter.storeProduct, purchaseResultReporter) } } } @@ -420,9 +420,9 @@ private struct HandleRestoreModifier: ViewModifier { func body(content: Content) -> some View { content - .onPreferenceChange(HandleRestorePreferenceKey.self) { performRestoreInfo in - if let performRestoreInfo { - self.handler(performRestoreInfo) + .onPreferenceChange(HandleRestorePreferenceKey.self) { restoreResultReporter in + if let restoreResultReporter { + self.handler(restoreResultReporter) } } } diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 6c5e68a852..dbeb57228a 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -40,7 +40,7 @@ public protocol PurchasesType: AnyObject { @available(*, deprecated, message: "Use purchasesAreCompletedBy instead.") var finishTransactions: Bool { get set } - /** Whether transactions should be finished automatically. `.revenueCat` by default. + /** Whether purchaess should be made and transactions finished automatically by RevenueCat. `.revenueCat` by default. * - Warning: Setting this value to `.myApp` will prevent the SDK from making purchaes and finishing transactions. * In this case, you *must* perform all of this logic in your app. If using a `PaywallView`, use the modifier * `.handlePurchaseAndRestore`. From 539062adba6c6b1b96c94123352f7036c538da0f Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:42:16 -0700 Subject: [PATCH 55/76] =?UTF-8?q?=F0=9F=A4=B7=E2=80=8D=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 97361ca7c2..ea1e6a0f33 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -338,7 +338,6 @@ PLATFORMS arm64-darwin-21 arm64-darwin-22 arm64-darwin-23 - ruby x86_64-darwin-22 x86_64-linux From fa14695c90bafb64d9a4229511b125a09b9fd05a Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 14:47:52 -0700 Subject: [PATCH 56/76] Fix tests --- .../PurchaseCompletedHandlerTests.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index 262ddf058f..08f8d60a3d 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -190,7 +190,7 @@ class PurchaseCompletedHandlerTests: TestCase { expect(error).toEventually(matchError(Self.failureError)) } - func testHandleExternalPurchase() throws { + func testHandleExternalPurchaseAndRestore() throws { var completed = false var customPurchaseCodeExecuted = false @@ -200,10 +200,12 @@ class PurchaseCompletedHandlerTests: TestCase { introEligibility: .producing(eligibility: .eligible), purchaseHandler: Self.externalPurchaseHandler ) - .handlePurchase { _, purchaseCompletedHandler in - purchaseCompletedHandler(false, nil) + .handlePurchaseAndRestore(performPurchase: { storeProduct, purchaseResultReporter in + purchaseResultReporter.reportResult(userCancelled: false, error: nil) customPurchaseCodeExecuted = true - } + }, performRestore: { restoreResultReporter in + + }) .addToHierarchy() Task { @@ -225,10 +227,12 @@ class PurchaseCompletedHandlerTests: TestCase { introEligibility: .producing(eligibility: .eligible), purchaseHandler: Self.externalPurchaseHandler ) - .handleRestorePurchases { purchaseRestoreHandler in - purchaseRestoreHandler(true, nil) + .handlePurchaseAndRestore(performPurchase: { storeProduct, purchaseResultReporter in + + }, performRestore: { restoreResultReporter in + restoreResultReporter.reportResult(success: true, error: nil) customRestoreCodeExecuted = true - } + }) .addToHierarchy() Task { From 9763b72a176a8236c6657da3a086ea5479f91add Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 15:26:42 -0700 Subject: [PATCH 57/76] Linter stuff --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 16 +++++++++++++--- Sources/Purchasing/Purchases/PurchasesType.swift | 3 ++- .../PurchaseCompletedHandlerTests.swift | 6 +++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 4fa429eed8..736c6149cd 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -27,17 +27,23 @@ class PerformPurchaseInfo: Equatable { self.reportPurchaseResultCallback = reportPurchaseResult } - public func reportResult(userCancelled: Bool, error: Error?) -> Void { + /// Use this method to report the result of the purchase. + /// - Parameters: + /// - userCancelled: A boolean indicating whether the user cancelled the purchase. + /// - error: An optional error object if an error occurred during the purchase. + public func reportResult(userCancelled: Bool, error: Error?) { reportPurchaseResultCallback(userCancelled, error) } + /// Checks whether two `PurchaseResultReporter` instances are equal. + /// They are considered equal if the object represents the same `StoreProduct` public static func == (lhs: PurchaseResultReporter, rhs: PurchaseResultReporter) -> Bool { return lhs.storeProduct == rhs.storeProduct } } - +/// A class that can be used to report the result of a restoring purchases. public class RestoreResultReporter: Equatable { let reportRestoreResultCallback: (_ success: Bool, _ error: Error?) -> Void @@ -46,7 +52,11 @@ public class RestoreResultReporter: Equatable { self.reportRestoreResultCallback = callback } - public func reportResult(success: Bool, error: Error?) -> Void { + /// Use this method to report the result of a restore operation. + /// - Parameters: + /// - success: A boolean indicating whether the restore operation was successful. + /// - error: An optional error object if an error occurred during the restore operation. + public func reportResult(success: Bool, error: Error?) { reportRestoreResultCallback(success, error) } diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index dbeb57228a..1fb4419a45 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -40,7 +40,8 @@ public protocol PurchasesType: AnyObject { @available(*, deprecated, message: "Use purchasesAreCompletedBy instead.") var finishTransactions: Bool { get set } - /** Whether purchaess should be made and transactions finished automatically by RevenueCat. `.revenueCat` by default. + /** Controls if purchaess should be made and transactions finished automatically by RevenueCat. + * `.revenueCat` by default. * - Warning: Setting this value to `.myApp` will prevent the SDK from making purchaes and finishing transactions. * In this case, you *must* perform all of this logic in your app. If using a `PaywallView`, use the modifier * `.handlePurchaseAndRestore`. diff --git a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift index 08f8d60a3d..ea3bf57810 100644 --- a/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift +++ b/Tests/RevenueCatUITests/PurchaseCompletedHandlerTests.swift @@ -200,10 +200,10 @@ class PurchaseCompletedHandlerTests: TestCase { introEligibility: .producing(eligibility: .eligible), purchaseHandler: Self.externalPurchaseHandler ) - .handlePurchaseAndRestore(performPurchase: { storeProduct, purchaseResultReporter in + .handlePurchaseAndRestore(performPurchase: { _, purchaseResultReporter in purchaseResultReporter.reportResult(userCancelled: false, error: nil) customPurchaseCodeExecuted = true - }, performRestore: { restoreResultReporter in + }, performRestore: { _ in }) .addToHierarchy() @@ -227,7 +227,7 @@ class PurchaseCompletedHandlerTests: TestCase { introEligibility: .producing(eligibility: .eligible), purchaseHandler: Self.externalPurchaseHandler ) - .handlePurchaseAndRestore(performPurchase: { storeProduct, purchaseResultReporter in + .handlePurchaseAndRestore(performPurchase: { _, _ in }, performRestore: { restoreResultReporter in restoreResultReporter.reportResult(success: true, error: nil) From ac65d2f1c79f382317445d53d273c2a9a200ce10 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 15:28:44 -0700 Subject: [PATCH 58/76] Missing name change --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 736c6149cd..1b8f42d30a 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -17,7 +17,8 @@ import SwiftUI // swiftlint:disable file_length -class PerformPurchaseInfo: Equatable { +/// A class that can be used to report the result of a purchase. +public class PurchaseResultReporter: Equatable { let storeProduct: StoreProduct let reportPurchaseResultCallback: (_ userCancelled: Bool, _ error: Error?) -> Void From be0b23bdb40b06e13bda9f6551a3b9bcb6520d97 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 15:41:34 -0700 Subject: [PATCH 59/76] Documentation --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 1b8f42d30a..ca74df54ab 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -132,6 +132,8 @@ final class PurchaseHandler: ObservableObject { self.purchases = purchases } + /// Returns a new instance of `PurchaseHandler` using `Purchases.shared` if `Purchases` + /// has been configured, and using a PurchaseHandler that cannot be used for purchases otherwise. public static func `default`() -> Self { return Purchases.isConfigured ? .init() : Self.notConfigured() } From 0bfa9db56ad346c1aa4814a58be0e61523a09fef Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Fri, 31 May 2024 15:42:04 -0700 Subject: [PATCH 60/76] More documentation --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index ca74df54ab..1c5df26833 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -61,6 +61,7 @@ public class RestoreResultReporter: Equatable { reportRestoreResultCallback(success, error) } + /// Returns true if objects are the same object (same memory address); false otherwise. public static func == (lhs: RestoreResultReporter, rhs: RestoreResultReporter) -> Bool { return lhs === rhs } From b43f77e77ef57b24f96c9472205a07b31701cdac Mon Sep 17 00:00:00 2001 From: James Borthwick <109382862+jamesrb1@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:41:54 -0700 Subject: [PATCH 61/76] Code review doc fixes Co-authored-by: Josh Holtz --- RevenueCatUI/Data/Strings.swift | 4 ++-- RevenueCatUI/Purchasing/PurchaseHandler.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index 26fd381e7f..f58b7900bc 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -107,7 +107,7 @@ extension Strings: CustomStringConvertible { case .executing_external_purchase_logic: return "Will execute custom StoreKit purchase logic provided by your app. " + "No StoreKit purchasing logic will be performed by RevenueCat. " + - "You must use `.handlePurchase` on your `PaywallView`." + "You must use `.handlePurchaseAndRestore` on your `PaywallView`." case .executing_purchase_logic: return "Will execute purchase logic provided by RevenueCat." @@ -118,7 +118,7 @@ extension Strings: CustomStringConvertible { case .executing_external_restore_logic: return "Will execute custom StoreKit restore purchases logic provided by your app. " + "No StoreKit restore purchases logic will be performed by RevenueCat. " + - "You must use `.handleRestorePurchases` on your `PaywallView`." + "You must use `.handlePurchaseAndRestore` on your `PaywallView`." } } diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 1c5df26833..c2ac11280c 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -272,7 +272,7 @@ extension PurchaseHandler { self.restoreError = nil DispatchQueue.main.async { - // this triggers the view's `.handleRestore` function, and its callback must be called + // this triggers the view's `.handlePurchaseAndRestore` function, and its callback must be called // after the continuation is set below self.performRestore = RestoreResultReporter(callback: self.reportExternalRestoreResult) } From 7da678d0ebfc1234cb909e7d0160be7b562f64f6 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 14:44:49 -0700 Subject: [PATCH 62/76] if -> switch to catch new cases --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index c2ac11280c..2fb2ef77b1 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -152,10 +152,11 @@ extension PurchaseHandler { @MainActor func purchase(package: Package) async throws -> PurchaseResultData { - if self.purchases.purchasesAreCompletedBy == .revenueCat { - return try await performPurchase(package: package) - } else { - return try await performExternalPurchaseLogic(package: package) + switch self.purchases.purchasesAreCompletedBy { + case .revenueCat: + try await performPurchase(package: package) + case .myApp: + try await performExternalPurchaseLogic(package: package) } } @@ -227,10 +228,11 @@ extension PurchaseHandler { // MARK: - Restore func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { - if self.purchases.purchasesAreCompletedBy == .revenueCat { - return try await performRestorePurchases() - } else { - return try await performExternalRestoreLogic() + switch self.purchases.purchasesAreCompletedBy { + case .revenueCat: + try await performRestorePurchases() + case .myApp: + try await performExternalRestoreLogic() } } From 5aa37308c48393e1f4c2e2ac794a2882d7153578 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 14:50:47 -0700 Subject: [PATCH 63/76] Move defer --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 2fb2ef77b1..63b6726fca 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -269,6 +269,11 @@ extension PurchaseHandler { func performExternalRestoreLogic() async throws -> (info: CustomerInfo, success: Bool) { Logger.debug(Strings.executing_external_restore_logic) + defer { + self.restoreInProgress = false + self.actionInProgress = false + } + self.restoreInProgress = true self.restoredCustomerInfo = nil self.restoreError = nil @@ -281,11 +286,6 @@ extension PurchaseHandler { self.startAction() - defer { - self.restoreInProgress = false - self.actionInProgress = false - } - let success = try await withCheckedThrowingContinuation { continuation in externalRestorePurchaseContinuation = continuation } From 604ea4fa99cdbaa50caf735e51da39ef60eab37d Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 14:52:27 -0700 Subject: [PATCH 64/76] Remove public access modifier --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 63b6726fca..ca739b5754 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -135,7 +135,7 @@ final class PurchaseHandler: ObservableObject { /// Returns a new instance of `PurchaseHandler` using `Purchases.shared` if `Purchases` /// has been configured, and using a PurchaseHandler that cannot be used for purchases otherwise. - public static func `default`() -> Self { + static func `default`() -> Self { return Purchases.isConfigured ? .init() : Self.notConfigured() } From ce8b8e05520479867eae8b5e106c3d671a80b81c Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 15:58:12 -0700 Subject: [PATCH 65/76] Add error logging if callback is `nil` --- RevenueCatUI/Helpers/Logger.swift | 15 +++++++++++++++ RevenueCatUI/View+PurchaseRestoreCompleted.swift | 3 +++ 2 files changed, 18 insertions(+) diff --git a/RevenueCatUI/Helpers/Logger.swift b/RevenueCatUI/Helpers/Logger.swift index 6a44ed961c..bc3840d6cc 100644 --- a/RevenueCatUI/Helpers/Logger.swift +++ b/RevenueCatUI/Helpers/Logger.swift @@ -63,6 +63,21 @@ enum Logger { ) } + static func error( + _ text: CustomStringConvertible, + file: String = #file, + function: String = #function, + line: UInt = #line + ) { + Self.log( + text, + .error, + file: file, + function: function, + line: line + ) + } + private static func log( _ text: CustomStringConvertible, _ level: LogLevel, diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index eecb906fb3..2e5da67827 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -423,6 +423,9 @@ private struct HandleRestoreModifier: ViewModifier { .onPreferenceChange(HandleRestorePreferenceKey.self) { restoreResultReporter in if let restoreResultReporter { self.handler(restoreResultReporter) + } else { + Logger.error("Change to `HandleRestorePreferenceKey` with a value of `nil` means " + + "the performPurchase modifier cannot be called.") } } } From 40c0685f27dce6dac415d6798c799abe466fc371 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 17:22:04 -0700 Subject: [PATCH 66/76] Deprecate `observerMode` from public APIs, replace with `purchasesAreCompletedBy`. --- Sources/Purchasing/Configuration.swift | 28 +++++++++-- Sources/Purchasing/Purchases/Purchases.swift | 52 ++++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/Sources/Purchasing/Configuration.swift b/Sources/Purchasing/Configuration.swift index 46724c081a..b977af8802 100644 --- a/Sources/Purchasing/Configuration.swift +++ b/Sources/Purchasing/Configuration.swift @@ -86,7 +86,15 @@ import Foundation private(set) var apiKey: String private(set) var appUserID: String? - private(set) var observerMode: Bool = false + var observerMode: Bool { + switch purchasesAreCompletedBy { + case .revenueCat: + false + case .myApp: + true + } + } + private(set) var purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat private(set) var userDefaults: UserDefaults? private(set) var dangerousSettings: DangerousSettings? private(set) var networkTimeout = Configuration.networkTimeoutDefault @@ -143,12 +151,26 @@ import Foundation * RevenueCat's backend. Default is `false`. * * - Warning: This assumes your IAP implementation uses StoreKit 1. - * Observer mode is not compatible with StoreKit 2. + * `.myApp` is not compatible with StoreKit 2. */ + @available(*, deprecated, message: "Use with(purchasesAreCompletedBy:) instead.") @objc public func with(observerMode: Bool) -> Configuration.Builder { - self.observerMode = observerMode + self.purchasesAreCompletedBy = observerMode ? .myApp : .revenueCat return self } + /** + * Set `purchasesAreCompletedBy`. + * - Parameter purchasesAreCompletedBy: Set this to `.myApp` if you have your own IAP implementation and want to use only + * RevenueCat's backend. Default is `.revenueCat`. + * + * - Warning: This assumes your IAP implementation uses StoreKit 1. + * `.myApp` is not compatible with StoreKit 2. + */ + @objc public func with(purchasesAreCompletedBy: PurchasesAreCompletedBy) -> Configuration.Builder { + self.purchasesAreCompletedBy = purchasesAreCompletedBy + return self + } + /** * Set `userDefaults`. * - Parameter userDefaults: Custom `UserDefaults` to use diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 3c55a72822..672bc02f1f 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1231,7 +1231,7 @@ public extension Purchases { * ```swift * Purchases.configure( * with: Configuration.Builder(withAPIKey: Constants.apiKey) - * .with(observerMode: false) + * .with(purchasesAreCompletedBy: .revenueCat) * .with(appUserID: "") * .build() * ) @@ -1272,7 +1272,7 @@ public extension Purchases { * ```swift * Purchases.configure( * with: .init(withAPIKey: Constants.apiKey) - * .with(observerMode: false) + * .with(purchasesAreCompletedBy: .revenueCat) * .with(appUserID: "") * ) * ``` @@ -1325,7 +1325,7 @@ public extension Purchases { @_disfavoredOverload @objc(configureWithAPIKey:appUserID:) @discardableResult static func configure(withAPIKey apiKey: String, appUserID: String?) -> Purchases { - Self.configure(withAPIKey: apiKey, appUserID: appUserID, observerMode: false) + Self.configure(withAPIKey: apiKey, appUserID: appUserID, purchasesAreCompletedBy: .revenueCat) } @available(*, deprecated, message: """ @@ -1362,19 +1362,59 @@ public extension Purchases { * Observer mode is not compatible with StoreKit 2. */ @_disfavoredOverload + @available(*, deprecated, message: "Use configure(withAPIKey:appUserID:purchasesAreCompletedBy:) instead.") @objc(configureWithAPIKey:appUserID:observerMode:) @discardableResult static func configure(withAPIKey apiKey: String, appUserID: String?, observerMode: Bool) -> Purchases { + let purchasesBy: PurchasesAreCompletedBy = observerMode ? .myApp : .revenueCat + + return Self.configure( + with: Configuration + .builder(withAPIKey: apiKey) + .with(appUserID: appUserID) + .with(purchasesAreCompletedBy: purchasesBy) + .build() + ) + } + + /** + * Configures an instance of the Purchases SDK with a custom `UserDefaults`. + * + * Use this constructor if you want to + * sync status across a shared container, such as between a host app and an extension. The instance of the + * Purchases SDK will be set as a singleton. + * You should access the singleton instance using ``Purchases/shared`` + * + * - Parameter apiKey: The API Key generated for your app from https://app.revenuecat.com/ + * + * - Parameter appUserID: The unique app user id for this user. This user id will allow users to share their + * purchases and subscriptions across devices. Pass `nil` or an empty string if you want ``Purchases`` + * to generate this for you. + * + * - Parameter purchasesAreCompletedBy: Set this to `.myApp` if you have your own IAP implementation and want to use only + * RevenueCat's backend. Default is `.revenueCat`. + * + * - Returns: An instantiated ``Purchases`` object that has been set as a singleton. + * + * - Warning: This assumes your IAP implementation uses StoreKit 1. + * Observer mode is not compatible with StoreKit 2. + */ + @_disfavoredOverload + @objc(configureWithAPIKey:appUserID:purchasesAreCompletedBy:) + @discardableResult static func configure(withAPIKey apiKey: String, + appUserID: String?, + purchasesAreCompletedBy: PurchasesAreCompletedBy) -> Purchases { Self.configure( with: Configuration .builder(withAPIKey: apiKey) .with(appUserID: appUserID) - .with(observerMode: observerMode) + .with(purchasesAreCompletedBy: purchasesAreCompletedBy) .build() ) } + @available(*, deprecated, message: """ The appUserID passed to logIn is a constant string known at compile time. This is likely a programmer error. This ID is used to identify the current user. @@ -1385,11 +1425,13 @@ public extension Purchases { appUserID: StaticString, observerMode: Bool) -> Purchases { Logger.warn(Strings.identity.logging_in_with_static_string) + let purchasesBy: PurchasesAreCompletedBy = observerMode ? .myApp : .revenueCat + return Self.configure( with: Configuration .builder(withAPIKey: apiKey) .with(appUserID: "\(appUserID)") - .with(observerMode: observerMode) + .with(purchasesAreCompletedBy: purchasesBy) .build() ) } From bd7bbf3431e469213e0542a430851b848e1271d5 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 17:27:22 -0700 Subject: [PATCH 67/76] Return pre-processor marker --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index ca739b5754..80a75fc59e 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -136,6 +136,7 @@ final class PurchaseHandler: ObservableObject { /// Returns a new instance of `PurchaseHandler` using `Purchases.shared` if `Purchases` /// has been configured, and using a PurchaseHandler that cannot be used for purchases otherwise. static func `default`() -> Self { + // @PublicForExternalTesting return Purchases.isConfigured ? .init() : Self.notConfigured() } From f30bd1ffac95a71ad9af8951c7455138cd10841b Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 17:52:34 -0700 Subject: [PATCH 68/76] fix preprocessor --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 80a75fc59e..1a7969e8f1 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -135,8 +135,8 @@ final class PurchaseHandler: ObservableObject { /// Returns a new instance of `PurchaseHandler` using `Purchases.shared` if `Purchases` /// has been configured, and using a PurchaseHandler that cannot be used for purchases otherwise. - static func `default`() -> Self { // @PublicForExternalTesting + static func `default`() -> Self { return Purchases.isConfigured ? .init() : Self.notConfigured() } From ef9526f1673acf28c73cd1728924e01e2af621d3 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 18:04:13 -0700 Subject: [PATCH 69/76] Add API tests --- .../SwiftAPITester/SwiftAPITester/ConfigurationAPI.swift | 2 +- .../SwiftAPITester/SwiftAPITester/PurchasesAPI.swift | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/ConfigurationAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/ConfigurationAPI.swift index 9b149eaade..a0eaa6e0bd 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/ConfigurationAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/ConfigurationAPI.swift @@ -13,7 +13,7 @@ func checkConfigurationAPI() { .builder(withAPIKey: "") .with(apiKey: "") .with(appUserID: nil) - .with(observerMode: true) + .with(purchasesAreCompletedBy: .myApp) .with(userDefaults: UserDefaults.standard) .with(dangerousSettings: DangerousSettings()) .with(dangerousSettings: DangerousSettings(autoSyncPurchases: true)) diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift index 3f6cbbfa8d..cc70e7ab4c 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PurchasesAPI.swift @@ -306,7 +306,7 @@ private func checkConfigure() -> Purchases! { Purchases.configure(withAPIKey: "") Purchases.configure(withAPIKey: "", appUserID: nil) - Purchases.configure(withAPIKey: "", appUserID: nil, observerMode: true) + Purchases.configure(withAPIKey: "", appUserID: nil, purchasesAreCompletedBy: .myApp) return nil } @@ -339,6 +339,7 @@ private func checkDeprecatedMethods(_ purchases: Purchases) { Purchases.addAttributionData([String: Any](), from: AttributionNetwork.adjust, forNetworkUserId: nil) let _: Bool = Purchases.automaticAppleSearchAdsAttributionCollection Purchases.automaticAppleSearchAdsAttributionCollection = false + purchases.finishTransactions = true purchases.checkTrialOrIntroDiscountEligibility([String]()) { (_: [String: IntroEligibility]) in } @@ -350,6 +351,7 @@ private func checkDeprecatedMethods(_ purchases: Purchases) { Purchases.configure(withAPIKey: "", appUserID: nil, observerMode: true, userDefaults: UserDefaults()) Purchases.configure(withAPIKey: "", appUserID: "") Purchases.configure(withAPIKey: "", appUserID: "", observerMode: false) + Purchases.configure(withAPIKey: "", appUserID: nil, observerMode: true) Purchases.configure(withAPIKey: "", appUserID: nil, observerMode: true, @@ -373,4 +375,9 @@ private func checkDeprecatedMethods(_ purchases: Purchases) { userDefaults: UserDefaults(), useStoreKit2IfAvailable: true, dangerousSettings: DangerousSettings(autoSyncPurchases: false)) + + _ = Configuration + .builder(withAPIKey: "") + .with(observerMode: true) + } From 1df2679c1333fc37cc0617ea86b9318f176fda5f Mon Sep 17 00:00:00 2001 From: James Borthwick <109382862+jamesrb1@users.noreply.github.com> Date: Thu, 6 Jun 2024 20:12:57 -0700 Subject: [PATCH 70/76] Documentation wording Co-authored-by: Josh Holtz --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 4 ++-- RevenueCatUI/View+PurchaseRestoreCompleted.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 1a7969e8f1..ff1f3d0960 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -93,11 +93,11 @@ final class PurchaseHandler: ObservableObject { @Published fileprivate(set) var purchaseResult: PurchaseResultData? - /// Information used to perform a purchase in the app (rather than in RevenueCat) + /// Information used to perform a purchase by the app (rather than by RevenueCat) @Published fileprivate(set) var performPurchase: PurchaseResultReporter? - /// Information used to perform restoring a purchase in the app (rather than in RevenueCat) + /// Information used to perform restoring a purchase by the app (rather than by RevenueCat) @Published fileprivate(set) var performRestore: RestoreResultReporter? diff --git a/RevenueCatUI/View+PurchaseRestoreCompleted.swift b/RevenueCatUI/View+PurchaseRestoreCompleted.swift index 2e5da67827..f1b03da62d 100644 --- a/RevenueCatUI/View+PurchaseRestoreCompleted.swift +++ b/RevenueCatUI/View+PurchaseRestoreCompleted.swift @@ -291,7 +291,7 @@ extension View { /// when migrating from a direct StoreKit implementation to RevenueCat in stages, or if integrating /// RevenueCat for experiments and growth tools only. /// - /// After executing your StoreKit purchae code, you **must** communicate the result of your purchase + /// After executing your StoreKit purchase code, you **must** communicate the result of your purchase /// code by calling `reportResult`on the passed result reporter object when your code /// has finished executing. Failure to do so will result in undefined behavior. /// From 256a45042e1ae9d6763a8d7c868e02b8e877c1d3 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 20:27:11 -0700 Subject: [PATCH 71/76] Obj-C API Tests --- .../ObjCAPITester/RCConfigurationAPI.m | 4 +++- .../ObjCAPITester/RCPurchasesAPI.m | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCConfigurationAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCConfigurationAPI.m index a7f751a127..a697d4147f 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCConfigurationAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCConfigurationAPI.m @@ -14,7 +14,7 @@ @implementation RCConfigurationAPI + (void)checkAPI { RCConfigurationBuilder *builder = [RCConfiguration builderWithAPIKey:@""]; RCConfiguration *config __unused = [[[[[[[[[[[builder withApiKey:@""] - withObserverMode:false] + withPurchasesAreCompletedBy:RCPurchasesAreCompletedByRevenueCat] withUserDefaults:NSUserDefaults.standardUserDefaults] withAppUserID:@""] withAppUserID:nil] @@ -25,6 +25,8 @@ + (void)checkAPI { withUsesStoreKit2IfAvailable:false] build]; + RCConfiguration *configDeprecated __unused = [[[builder withApiKey:@""] withObserverMode:true] build]; + if (@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)) { RCConfiguration *config __unused = [[builder withEntitlementVerificationMode:RCEntitlementVerificationModeInformational] build]; diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m index 09e4d3a503..62ee73bd97 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCPurchasesAPI.m @@ -25,6 +25,7 @@ @implementation RCPurchasesAPI BOOL isConfigured; BOOL allowSharingAppStoreAccount; BOOL finishTransactions; +RCPurchasesAreCompletedBy purchasesAreCompletedBy; id delegate; NSString *appUserID; BOOL isAnonymous; @@ -41,6 +42,10 @@ + (void)checkAPI { [RCPurchases configureWithAPIKey:@"" appUserID:nil observerMode:false userDefaults:nil]; [RCPurchases configureWithAPIKey:@"" appUserID:@"" observerMode:false userDefaults:[[NSUserDefaults alloc] init]]; [RCPurchases configureWithAPIKey:@"" appUserID:nil observerMode:false userDefaults:[[NSUserDefaults alloc] init]]; + [RCPurchases configureWithAPIKey:@"" appUserID:@"" purchasesAreCompletedBy:RCPurchasesAreCompletedByRevenueCat]; + [RCPurchases configureWithAPIKey:@"" appUserID:nil purchasesAreCompletedBy:RCPurchasesAreCompletedByRevenueCat]; + [RCPurchases configureWithAPIKey:@"" appUserID:@"" purchasesAreCompletedBy:RCPurchasesAreCompletedByMyApp]; + [RCPurchases configureWithAPIKey:@"" appUserID:nil purchasesAreCompletedBy:RCPurchasesAreCompletedByMyApp]; [RCPurchases configureWithAPIKey:@"" appUserID:nil observerMode:false @@ -96,6 +101,8 @@ + (void)checkAPI { allowSharingAppStoreAccount = [p allowSharingAppStoreAccount]; finishTransactions = [p finishTransactions]; + purchasesAreCompletedBy = [p purchasesAreCompletedBy]; + delegate = [p delegate]; appUserID = [p appUserID]; isAnonymous = [p isAnonymous]; @@ -232,7 +239,7 @@ + (void)checkEnums { case RCLogLevelInfo: case RCLogLevelWarn: case RCLogLevelError: - NSLog(@"%ld", (long)o); + NSLog(@"%ld", (long)l); } RCStoreMessageType smt = RCStoreMessageTypeBillingIssue; @@ -240,7 +247,14 @@ + (void)checkEnums { case RCStoreMessageTypeBillingIssue: case RCStoreMessageTypePriceIncreaseConsent: case RCStoreMessageTypeGeneric: - NSLog(@"%ld", (long)o); + NSLog(@"%ld", (long)smt); + } + + RCPurchasesAreCompletedBy pacb = RCPurchasesAreCompletedByRevenueCat; + switch(pacb) { + case RCPurchasesAreCompletedByMyApp: + case RCPurchasesAreCompletedByRevenueCat: + NSLog(@"%ld", (long)pacb); } } From f12f20e412ca79ba2ee0e677e139344172f28e91 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 20:33:36 -0700 Subject: [PATCH 72/76] Lint --- Sources/Purchasing/Configuration.swift | 4 ++-- Sources/Purchasing/Purchases/Purchases.swift | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/Purchasing/Configuration.swift b/Sources/Purchasing/Configuration.swift index b977af8802..2bf765ddd5 100644 --- a/Sources/Purchasing/Configuration.swift +++ b/Sources/Purchasing/Configuration.swift @@ -160,8 +160,8 @@ import Foundation } /** * Set `purchasesAreCompletedBy`. - * - Parameter purchasesAreCompletedBy: Set this to `.myApp` if you have your own IAP implementation and want to use only - * RevenueCat's backend. Default is `.revenueCat`. + * - Parameter purchasesAreCompletedBy: Set this to `.myApp` if you have your own IAP implementation and + * want to use only RevenueCat's backend. Default is `.revenueCat`. * * - Warning: This assumes your IAP implementation uses StoreKit 1. * `.myApp` is not compatible with StoreKit 2. diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 672bc02f1f..65aa1b1e9f 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1392,8 +1392,8 @@ public extension Purchases { * purchases and subscriptions across devices. Pass `nil` or an empty string if you want ``Purchases`` * to generate this for you. * - * - Parameter purchasesAreCompletedBy: Set this to `.myApp` if you have your own IAP implementation and want to use only - * RevenueCat's backend. Default is `.revenueCat`. + * - Parameter purchasesAreCompletedBy: Set this to `.myApp` if you have your own IAP implementation + * and want to use only RevenueCat's backend. Default is `.revenueCat`. * * - Returns: An instantiated ``Purchases`` object that has been set as a singleton. * @@ -1414,7 +1414,6 @@ public extension Purchases { ) } - @available(*, deprecated, message: """ The appUserID passed to logIn is a constant string known at compile time. This is likely a programmer error. This ID is used to identify the current user. From 6911c3875f239d3a4851b3697f90373cfa1036b8 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 20:40:09 -0700 Subject: [PATCH 73/76] Fix unit tests --- Tests/UnitTests/Purchasing/ConfigurationTests.swift | 8 ++++---- .../Purchases/PurchasesConfiguringTests.swift | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/UnitTests/Purchasing/ConfigurationTests.swift b/Tests/UnitTests/Purchasing/ConfigurationTests.swift index 3e0ed70bc7..c8092f384b 100644 --- a/Tests/UnitTests/Purchasing/ConfigurationTests.swift +++ b/Tests/UnitTests/Purchasing/ConfigurationTests.swift @@ -52,9 +52,9 @@ class ConfigurationTests: TestCase { self.logger.verifyMessageWasNotLogged(Strings.configure.observer_mode_with_storekit2) } - func testObserverModeWithStoreKit1() { + func testPurchasesAreCompletedByMyAppWithStoreKit1() { let configuration = Configuration.Builder(withAPIKey: "test") - .with(observerMode: true) + .with(purchasesAreCompletedBy: .myApp) .build() expect(configuration.observerMode) == true @@ -64,9 +64,9 @@ class ConfigurationTests: TestCase { } @available(*, deprecated) - func testObserverModeWithStoreKit2() { + func testPurchasesAreCompletedByMyAppWithStoreKit2() { let configuration = Configuration.Builder(withAPIKey: "test") - .with(observerMode: true) + .with(purchasesAreCompletedBy: .myApp) .with(usesStoreKit2IfAvailable: true) .build() diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesConfiguringTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesConfiguringTests.swift index e7c85755f7..b0bf1ffa51 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesConfiguringTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesConfiguringTests.swift @@ -504,8 +504,8 @@ class PurchasesConfiguringTests: BasePurchasesTests { // MARK: - OfflineCustomerInfoCreator - func testObserverModeDoesNotCreateOfflineCustomerInfoCreator() { - expect(Self.create(observerMode: true).offlineCustomerInfoEnabled) == false + func testPurchaesAreCompletedByMyAppDoesNotCreateOfflineCustomerInfoCreator() { + expect(Self.create(purchasesAreCompletedBy: .myApp).offlineCustomerInfoEnabled) == false } func testOlderVersionsDoNoCreateOfflineCustomerInfo() throws { @@ -513,19 +513,19 @@ class PurchasesConfiguringTests: BasePurchasesTests { throw XCTSkip("Test for older versions") } - expect(Self.create(observerMode: false).offlineCustomerInfoEnabled) == false + expect(Self.create(purchasesAreCompletedBy: .revenueCat).offlineCustomerInfoEnabled) == false } func testOfflineCustomerInfoEnabled() throws { try AvailabilityChecks.iOS15APIAvailableOrSkipTest() - expect(Self.create(observerMode: false).offlineCustomerInfoEnabled) == true + expect(Self.create(purchasesAreCompletedBy: .revenueCat).offlineCustomerInfoEnabled) == true } - private static func create(observerMode: Bool) -> Purchases { + private static func create(purchasesAreCompletedBy: PurchasesAreCompletedBy) -> Purchases { return Purchases.configure( with: .init(withAPIKey: "") - .with(observerMode: observerMode) + .with(purchasesAreCompletedBy: purchasesAreCompletedBy) ) } From faf226ba02c06ecc0a648572210f98f5aba24200 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 20:42:34 -0700 Subject: [PATCH 74/76] Fix build (I guess our build machines use old versions of Swift?) --- Sources/Purchasing/Configuration.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Purchasing/Configuration.swift b/Sources/Purchasing/Configuration.swift index 2bf765ddd5..5dfb77ae11 100644 --- a/Sources/Purchasing/Configuration.swift +++ b/Sources/Purchasing/Configuration.swift @@ -89,9 +89,9 @@ import Foundation var observerMode: Bool { switch purchasesAreCompletedBy { case .revenueCat: - false + return false case .myApp: - true + return true } } private(set) var purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat From 07ef349652cd603c2540f65dee86e367678adf3f Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 20:44:52 -0700 Subject: [PATCH 75/76] More old swift compatibility --- RevenueCatUI/Purchasing/PurchaseHandler.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index ff1f3d0960..6941c04ca4 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -155,9 +155,9 @@ extension PurchaseHandler { func purchase(package: Package) async throws -> PurchaseResultData { switch self.purchases.purchasesAreCompletedBy { case .revenueCat: - try await performPurchase(package: package) + return try await performPurchase(package: package) case .myApp: - try await performExternalPurchaseLogic(package: package) + return try await performExternalPurchaseLogic(package: package) } } @@ -231,9 +231,9 @@ extension PurchaseHandler { func restorePurchases() async throws -> (info: CustomerInfo, success: Bool) { switch self.purchases.purchasesAreCompletedBy { case .revenueCat: - try await performRestorePurchases() + return try await performRestorePurchases() case .myApp: - try await performExternalRestoreLogic() + return try await performExternalRestoreLogic() } } From b2fc2e2addc01c5d5eb3a49fb4bce0aa77261524 Mon Sep 17 00:00:00 2001 From: James Borthwick Date: Thu, 6 Jun 2024 20:51:06 -0700 Subject: [PATCH 76/76] CustomEntitlementComputation API Tester update --- .../SwiftAPITester/ConfigurationAPI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ConfigurationAPI.swift b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ConfigurationAPI.swift index e1f1c2001a..92842bfb26 100644 --- a/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ConfigurationAPI.swift +++ b/Tests/APITesters/CustomEntitlementComputationSwiftAPITester/SwiftAPITester/ConfigurationAPI.swift @@ -13,7 +13,7 @@ func checkConfigurationAPI() { .builder(withAPIKey: "") .with(apiKey: "") .with(appUserID: nil) - .with(observerMode: false) + .with(purchasesAreCompletedBy: .myApp) .with(userDefaults: UserDefaults.standard) .with(dangerousSettings: DangerousSettings()) .with(dangerousSettings: DangerousSettings(autoSyncPurchases: true))