-
Notifications
You must be signed in to change notification settings - Fork 332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Paywalls
: extracted PurchaseButton
#2839
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
} | ||
|
||
Comment on lines
+135
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aboedo this looks pretty good for now :) and it gets animated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wonder if we should even consider making that variable public so that you can use it in the app |
||
} | ||
|
||
// MARK: - Extensions | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
} | ||
Comment on lines
+40
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love that even this bit is something that we're handling now |
||
|
||
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)) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ import SwiftUI | |
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) | ||
struct AsyncButton<Label>: View where Label: View { | ||
|
||
typealias Action = @Sendable () async throws -> Void | ||
typealias Action = @Sendable @MainActor () async throws -> Void | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This ensures that the action is always |
||
|
||
private let action: Action | ||
private let label: Label | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// | ||
// PurchaseButton.swift | ||
// | ||
// | ||
// Created by Nacho Soto on 7/18/23. | ||
// | ||
|
||
import RevenueCat | ||
import SwiftUI | ||
|
||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) | ||
struct PurchaseButton: View { | ||
|
||
let package: Package | ||
let purchaseHandler: PurchaseHandler | ||
let colors: PaywallData.Configuration.Colors | ||
let localization: ProcessedLocalizedConfiguration | ||
let introEligibility: IntroEligibilityStatus? | ||
let mode: PaywallViewMode | ||
|
||
@Environment(\.dismiss) | ||
private var dismiss | ||
|
||
var body: some View { | ||
self.button | ||
} | ||
|
||
private var button: some View { | ||
AsyncButton { | ||
let cancelled = try await self.purchaseHandler.purchase(package: self.package).userCancelled | ||
|
||
if !cancelled, case .fullScreen = self.mode { | ||
self.dismiss() | ||
} | ||
} label: { | ||
IntroEligibilityStateView( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels like this could be more appropriately named something like CTAText view There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it's a bad name, but it's not just for the CTA.
It's used for CTA but also for other labels. It's just a label that uses a loading intro eligibility and then decides what to display. Better name suggestions welcome. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually that's what you approved: #2837 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merging this, but let me know if you can think of a better name. |
||
textWithNoIntroOffer: self.localization.callToAction, | ||
textWithIntroOffer: self.localization.callToActionWithIntroOffer, | ||
introEligibility: self.introEligibility | ||
) | ||
.foregroundColor(self.colors.callToActionForegroundColor) | ||
.tint(self.colors.callToActionForegroundColor) | ||
.frame( | ||
maxWidth: self.mode.fullWidthButton | ||
? .infinity | ||
: nil | ||
) | ||
} | ||
.font(self.mode.buttonFont.weight(.semibold)) | ||
.tint(self.colors.callToActionBackgroundColor.gradient) | ||
.buttonBorderShape(self.mode.buttonBorderShape) | ||
.controlSize(self.mode.buttonSize) | ||
.buttonStyle(.borderedProminent) | ||
.frame(maxWidth: .infinity) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. feels somewhat confusing that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's just an implementation detail for how SwiftUI's layout works. |
||
} | ||
|
||
} | ||
|
||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) | ||
private extension PaywallViewMode { | ||
|
||
var buttonFont: Font { | ||
switch self { | ||
case .fullScreen, .card: return .title2 | ||
case .banner: return .footnote | ||
} | ||
} | ||
Comment on lines
+62
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how do you envision this scaling to different templates? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably would need to inject this font to |
||
|
||
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 | ||
} | ||
} | ||
|
||
} | ||
|
||
// MARK: - Previews | ||
|
||
#if DEBUG && canImport(SwiftUI) && canImport(UIKit) | ||
|
||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) | ||
struct PurchaseButton_Previews: PreviewProvider { | ||
|
||
private struct Preview: View { | ||
|
||
var mode: PaywallViewMode | ||
|
||
@State | ||
private var eligibility: IntroEligibilityStatus? | ||
|
||
var body: some View { | ||
PurchaseButton( | ||
package: Self.package, | ||
purchaseHandler: Self.purchaseHandler, | ||
colors: TestData.colors, | ||
localization: TestData.localization.processVariables(with: Self.package), | ||
introEligibility: self.eligibility, | ||
mode: self.mode | ||
) | ||
.task { | ||
self.eligibility = await Self.eligibilityChecker.eligibility(for: Self.package) | ||
} | ||
} | ||
|
||
private static let package: Package = TestData.packageWithIntroOffer | ||
private static let eligibilityChecker: TrialOrIntroEligibilityChecker = .producing(eligibility: .eligible) | ||
.with(delay: .seconds(0.3)) | ||
private static let purchaseHandler: PurchaseHandler = .mock() | ||
.with(delay: .seconds(0.5)) | ||
|
||
} | ||
|
||
static var previews: some View { | ||
ForEach(PaywallViewMode.allCases, id: \.self) { mode in | ||
Preview(mode: mode) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having small previewable views makes it easy to iterate on each small component. |
||
.previewLayout(.sizeThatFits) | ||
} | ||
} | ||
|
||
} | ||
|
||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small refactor because I made the
block
private
.