From 52fb376e07d557ea655605eeadcd71d0e1f2c196 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 21 Jul 2023 06:51:31 +0200 Subject: [PATCH] `Paywalls`: extracted `PurchaseButton` (#2839) Another small refactor, follow up to #2837. Slowly taking implementation details from `Example1Template` to make it easier to implement more templates. --- RevenueCatUI/Data/TestData.swift | 16 +- .../ColorInformation+MultiScheme.swift | 9 ++ RevenueCatUI/PaywallView.swift | 15 +- RevenueCatUI/Purchasing/PurchaseHandler.swift | 23 ++- RevenueCatUI/Templates/Example1Template.swift | 71 +-------- RevenueCatUI/Views/AsyncButton.swift | 2 +- RevenueCatUI/Views/PurchaseButton.swift | 144 ++++++++++++++++++ 7 files changed, 207 insertions(+), 73 deletions(-) create mode 100644 RevenueCatUI/Views/PurchaseButton.swift diff --git a/RevenueCatUI/Data/TestData.swift b/RevenueCatUI/Data/TestData.swift index c4ec4e6fce..92c642a12f 100644 --- a/RevenueCatUI/Data/TestData.swift +++ b/RevenueCatUI/Data/TestData.swift @@ -10,6 +10,7 @@ import RevenueCat #if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) internal enum TestData { static let productWithIntroOffer = TestStoreProduct( @@ -112,6 +113,10 @@ internal enum TestData { callToActionForeground: "#000000" ) + #if canImport(SwiftUI) && canImport(UIKit) + static let colors: PaywallData.Configuration.Colors = .combine(light: Self.lightColors, dark: Self.darkColors) + #endif + static let customerInfo: CustomerInfo = { let json = """ { @@ -145,11 +150,11 @@ internal enum TestData { return try! decoder.decode(CustomerInfo.self, from: Data(json.utf8)) }() - private static let localization: PaywallData.LocalizedConfiguration = .init( + static let localization: PaywallData.LocalizedConfiguration = .init( title: "Ignite your child's curiosity", subtitle: "Get access to all our educational content trusted by thousands of parents.", callToAction: "Purchase for {{ price }}", - callToActionWithIntroOffer: nil, + callToActionWithIntroOffer: "Purchase for {{ price_per_month }} per month", offerDetails: "{{ price_per_month }} per month", offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month" ) @@ -198,10 +203,11 @@ extension PurchaseHandler { /// Creates a copy of this `PurchaseHandler` with a delay. func with(delay: Duration) -> Self { - return .init { [purchaseBlock = self.purchaseBlock] in - try? await Task.sleep(for: delay) + return self.map { purchaseBlock in { + try? await Task.sleep(for: delay) - return try await purchaseBlock($0) + return try await purchaseBlock($0) + } } } } diff --git a/RevenueCatUI/Helpers/ColorInformation+MultiScheme.swift b/RevenueCatUI/Helpers/ColorInformation+MultiScheme.swift index 12a823d182..2df170cea7 100644 --- a/RevenueCatUI/Helpers/ColorInformation+MultiScheme.swift +++ b/RevenueCatUI/Helpers/ColorInformation+MultiScheme.swift @@ -21,6 +21,15 @@ extension PaywallData.Configuration.ColorInformation { return light } + return .combine(light: light, dark: dark) + } + +} + +extension PaywallData.Configuration.Colors { + + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + static func combine(light: Self, dark: Self) -> Self { return .init( background: .init(light: light.background, dark: dark.background), foreground: .init(light: light.foreground, dark: dark.foreground), diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 6a974a4fe8..fdfca43d18 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -100,7 +100,9 @@ private struct LoadedOfferingPaywallView: View { private let paywall: PaywallData private let mode: PaywallViewMode private let introEligibility: TrialOrIntroEligibilityChecker - private let purchaseHandler: PurchaseHandler + + @ObservedObject + private var purchaseHandler: PurchaseHandler init( offering: Offering, @@ -121,6 +123,7 @@ private struct LoadedOfferingPaywallView: View { .createView(for: self.offering, mode: self.mode) .environmentObject(self.introEligibility) .environmentObject(self.purchaseHandler) + .hidden(if: self.shouldHidePaywall) if let aspectRatio = self.mode.aspectRatio { view.aspectRatio(aspectRatio, contentMode: .fit) @@ -129,6 +132,16 @@ private struct LoadedOfferingPaywallView: View { } } + private var shouldHidePaywall: Bool { + switch self.mode { + case .fullScreen: + return false + + case .card, .banner: + return self.purchaseHandler.purchased + } + } + } // MARK: - Extensions diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index a773b0e3b9..d8aec57726 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -7,13 +7,17 @@ import RevenueCat import StoreKit +import SwiftUI @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) final class PurchaseHandler: ObservableObject { typealias PurchaseBlock = @Sendable (Package) async throws -> PurchaseResultData - let purchaseBlock: PurchaseBlock + private let purchaseBlock: PurchaseBlock + + @Published + var purchased: Bool = false convenience init(purchases: Purchases = .shared) { self.init { package in @@ -27,11 +31,24 @@ final class PurchaseHandler: ObservableObject { } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) extension PurchaseHandler { func purchase(package: Package) async throws -> PurchaseResultData { - return try await self.purchaseBlock(package) + let result = try await self.purchaseBlock(package) + + if !result.userCancelled { + withAnimation(Constants.defaultAnimation) { + self.purchased = true + } + } + + return result + } + + /// Creates a copy of this `PurchaseHandler` wrapping the purchase block + func map(_ block: @escaping (@escaping PurchaseBlock) -> PurchaseBlock) -> Self { + return .init(purchase: block(self.purchaseBlock)) } } diff --git a/RevenueCatUI/Templates/Example1Template.swift b/RevenueCatUI/Templates/Example1Template.swift index d6cc834c49..e9227666d8 100644 --- a/RevenueCatUI/Templates/Example1Template.swift +++ b/RevenueCatUI/Templates/Example1Template.swift @@ -146,35 +146,14 @@ private struct Example1TemplateContent: View { @ViewBuilder private var button: some View { - let package = self.configuration.packages.single.content - - AsyncButton { @MainActor in - let cancelled = try await self.purchaseHandler.purchase(package: package).userCancelled - - if !cancelled { - self.dismiss() - } - } label: { - IntroEligibilityStateView( - textWithNoIntroOffer: self.localization.callToAction, - textWithIntroOffer: self.localization.callToActionWithIntroOffer, - introEligibility: self.introEligibility - ) - .foregroundColor(self.configuration.colors.callToActionForegroundColor) - .tint(self.configuration.colors.callToActionForegroundColor) - .frame( - maxWidth: self.configuration.mode.fullWidthButton - ? .infinity - : nil - ) - } - .font(self.configuration.mode.buttonFont) - .fontWeight(.semibold) - .tint(self.configuration.colors.callToActionBackgroundColor.gradient) - .buttonStyle(.borderedProminent) - .buttonBorderShape(self.configuration.mode.buttonBorderShape) - .controlSize(self.configuration.mode.buttonSize) - .frame(maxWidth: .infinity) + PurchaseButton( + package: self.configuration.packages.single.content, + purchaseHandler: self.purchaseHandler, + colors: self.configuration.colors, + localization: self.localization, + introEligibility: self.introEligibility, + mode: self.configuration.mode + ) } private static let imageAspectRatio = 1.1 @@ -222,38 +201,4 @@ private extension PaywallViewMode { } } - var buttonFont: Font { - switch self { - case .fullScreen, .card: return .title2 - case .banner: return .footnote - } - } - - var fullWidthButton: Bool { - switch self { - case .fullScreen, .card: return true - case .banner: return false - } - } - - var buttonSize: ControlSize { - switch self { - case .fullScreen: return .large - case .card: return .regular - case .banner: return .small - } - } - - var buttonBorderShape: ButtonBorderShape { - switch self { - case .fullScreen: - #if os(macOS) - return .roundedRectangle - #else - return .capsule - #endif - case .card, .banner: return .roundedRectangle - } - } - } diff --git a/RevenueCatUI/Views/AsyncButton.swift b/RevenueCatUI/Views/AsyncButton.swift index 8acff9de94..55ee45c708 100644 --- a/RevenueCatUI/Views/AsyncButton.swift +++ b/RevenueCatUI/Views/AsyncButton.swift @@ -11,7 +11,7 @@ import SwiftUI @available(iOS 16.0, macOS 13.0, tvOS 16.0, *) struct AsyncButton