From dbcfe0ceb55879c98d7069c2c788af90b76b27d7 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Wed, 10 Jul 2024 10:45:55 -0500 Subject: [PATCH] Prevent paywall PurchaseHandler from being cleared on rerender (#4035) ### Motivation Fixes #4034 `PurchaseHandler` was being cleared/reset when a paywall was being rendered (ex: if an overlay was applied to it) ### Description - Restores `PurchaseHandler` back to being a `@StateObject` from #3187 - `checkForConfigurationConsistency()` has a few changes - Takes a `PurchaseHandler` as a parameter so it doesn't try to access the `StateObject` - This was fixes the purple Xcode warning of being access wrongly by it being a `StateObject` again - Removes `mutating` keyword and returns an `NSError?` which it now sets in the constructor ### Demo #### Using RevenueCat https://github.com/RevenueCat/purchases-ios/assets/401294/7e6430ca-6243-49ad-b7c0-8a90d90894f8 #### Using MyApp https://github.com/RevenueCat/purchases-ios/assets/401294/829f12c3-a11a-4952-883c-7ae7addd9c1d --- .../Data/PaywallViewConfiguration.swift | 4 +- RevenueCatUI/PaywallView.swift | 43 ++++++++----------- RevenueCatUI/View+PresentPaywall.swift | 3 +- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/RevenueCatUI/Data/PaywallViewConfiguration.swift b/RevenueCatUI/Data/PaywallViewConfiguration.swift index b859144590..283228998d 100644 --- a/RevenueCatUI/Data/PaywallViewConfiguration.swift +++ b/RevenueCatUI/Data/PaywallViewConfiguration.swift @@ -20,7 +20,7 @@ struct PaywallViewConfiguration { var fonts: PaywallFontProvider var displayCloseButton: Bool var introEligibility: TrialOrIntroEligibilityChecker? - var purchaseHandler: PurchaseHandler + var purchaseHandler: PurchaseHandler? init( content: Content, @@ -29,7 +29,7 @@ struct PaywallViewConfiguration { fonts: PaywallFontProvider = DefaultPaywallFontProvider(), displayCloseButton: Bool = false, introEligibility: TrialOrIntroEligibilityChecker? = nil, - purchaseHandler: PurchaseHandler + purchaseHandler: PurchaseHandler? = nil ) { self.content = content self.customerInfo = customerInfo diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 1a3741df0a..a1ae444a5d 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -33,14 +33,7 @@ public struct PaywallView: View { @Environment(\.locale) private var locale - // Do NOT reference this object directly, always use `purchaseHandler`. - // `doNotAccessPurchaseHandler` is here to own the `PurchaseHandler` when - // it is owned by this view. When it is owned by an external object, this - // references a "dummy" purchase handler. @StateObject - private var doNotAccessPurchaseHandler: PurchaseHandler - - @ObservedObject private var purchaseHandler: PurchaseHandler @StateObject @@ -110,16 +103,9 @@ public struct PaywallView: View { } // @PublicForExternalTesting - init(configuration: PaywallViewConfiguration, paywallViewOwnsPurchaseHandler: Bool = true) { - if paywallViewOwnsPurchaseHandler { - self._doNotAccessPurchaseHandler = .init(wrappedValue: configuration.purchaseHandler) - } else { - // this is unused and is only present to fulfill the need to have an object assigned - // to a @StateObject - self._doNotAccessPurchaseHandler = .init(wrappedValue: PurchaseHandler.default()) - } - - self.purchaseHandler = configuration.purchaseHandler + init(configuration: PaywallViewConfiguration) { + let purchaseHandler = configuration.purchaseHandler ?? .default() + self._purchaseHandler = .init(wrappedValue: purchaseHandler) self._introEligibility = .init(wrappedValue: configuration.introEligibility ?? .default()) @@ -135,31 +121,36 @@ public struct PaywallView: View { self.fonts = configuration.fonts self.displayCloseButton = configuration.displayCloseButton - checkForConfigurationConsistency() + self.initializationError = Self.checkForConfigurationConsistency(purchaseHandler: purchaseHandler) } - private mutating func checkForConfigurationConsistency() { - switch self.purchaseHandler.purchasesAreCompletedBy { + private static func checkForConfigurationConsistency(purchaseHandler: PurchaseHandler) -> NSError? { + switch purchaseHandler.purchasesAreCompletedBy { case .myApp: - if self.purchaseHandler.performPurchase == nil || self.purchaseHandler.performRestore == nil { + if purchaseHandler.performPurchase == nil || purchaseHandler.performRestore == nil { let missingBlocks: String - if self.purchaseHandler.performPurchase == nil && self.purchaseHandler.performRestore == nil { + if purchaseHandler.performPurchase == nil && purchaseHandler.performRestore == nil { missingBlocks = "performPurchase and performRestore are" - } else if self.purchaseHandler.performPurchase == nil { + } else if purchaseHandler.performPurchase == nil { missingBlocks = "performPurchase is" } else { missingBlocks = "performRestore is" } - let error = PaywallError.performPurchaseAndRestoreHandlersNotDefined(missingBlocks: missingBlocks) - self.initializationError = error as NSError + let error = PaywallError.performPurchaseAndRestoreHandlersNotDefined( + missingBlocks: missingBlocks + ) as NSError Logger.error(error) + + return error } case .revenueCat: - if self.purchaseHandler.performPurchase != nil || self.purchaseHandler.performRestore != nil { + if purchaseHandler.performPurchase != nil || purchaseHandler.performRestore != nil { Logger.warning(PaywallError.purchaseAndRestoreDefinedForRevenueCat) } } + + return nil } // swiftlint:disable:next missing_docs diff --git a/RevenueCatUI/View+PresentPaywall.swift b/RevenueCatUI/View+PresentPaywall.swift index f0df999dca..c140674b47 100644 --- a/RevenueCatUI/View+PresentPaywall.swift +++ b/RevenueCatUI/View+PresentPaywall.swift @@ -493,8 +493,7 @@ private struct PresentingPaywallModifier: ViewModifier { displayCloseButton: true, introEligibility: self.introEligibility, purchaseHandler: self.purchaseHandler - ), - paywallViewOwnsPurchaseHandler: false + ) ) .onPurchaseStarted { self.purchaseStarted?($0)