Skip to content

Commit

Permalink
Paywalls with custom purchase and restore logic handlers (#3973)
Browse files Browse the repository at this point in the history
## tl;dr

PaywallView has a new constructor that takes `performPurchase` and
`performRestore` blocks, which are called to preform purchasing/restore
directly by the customer's app when the purchase/restore buttons are
tapped on the paywall. This makes it possible to use RC Paywalls when
`Purchases` has been configured `.with(purchasesAreCompletedBy:
.myApp)`.

Example usage:
```swift
PaywallView(performPurchase: {
    var userCancelled = false
    var error: Error?
    
    // use StoreKit to perform purchase

    return (userCancelled: userCancelled, error: error)
}, performRestore: {
    var success = false
    var error: Error?

    // use StoreKit to perform restore

    return (success: success, error: error)
})
```

## Description

When a `PaywallView` is constructed, a new `PurchaseHandler` is created.
The `PurchaseHandler` (an internal RevenueCatUI class) is owned by the
`PaywallView`, and it is responsible for executing new purchases and
restores.

When a `PaywallView` is constructed **without** `performPurchase` and
`performRestore` blocks, the `PaywallView` creates a `PurchaseHandler`
capable of preforming purchases using RevenueCat. When a `PaywallView`
is constructed with `performPurchase` and `performRestore` blocks, it
can also make purchases using the customer-supplied closures.

The `PurchaseHandler` is invoked when the user taps the
`PurchaseButton`, calling `purchaseHandler.purchase(package:
self.selectedPackage.content)`, which branches to either the internal or
external purchase code, as defined by `purchasesAreCompletedBy`:

```swift
@mainactor
func purchase(package: Package) async throws {
    switch self.purchases.purchasesAreCompletedBy {
    case .revenueCat:
        try await performPurchase(package: package)
    case .myApp:
        try await performExternalPurchaseLogic(package: package)
    }
}
```


Purchase and Restore blocks can also be assigned for paywall footers:

```swift
MyAppDefinedPaywall()
    .paywallFooter(myAppPurchaseLogic: MyAppPurchaseLogic(performPurchase: { packageToPurchase in
        var userCancelled = false
        var error: Error?

        // use StoreKit to perform purchase

        return (userCancelled: userCancelled, error: error)
    }, performRestore: {
        var success = false
        var error: Error?

        // use StoreKit to perform restore

        return (success: success, error: error)
    }))
 ```

and via `.presentPaywallIfNeeded`:

```swift
MyAppDefinedPaywal()
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "test",
myAppPurchaseLogic: MyAppPurchaseLogic(performPurchase: {
packageToPurchase in
        return (userCancelled: false, error: nil)
    }, performRestore: {
        return (success: true, error: nil)
    }))
```



## Notes

### Testing

A lot needs to be mocked, the purchase/restore blocks that are passed in need to be assigned directly to the `PurchaseHandler`, which is then passed in as part of a `PaywallConfiguration`, both of which are normally constructed internally in the PaywallView constructor.

```swift
    func testHandleExternalRestoreWithPurchaaseHandlers() throws {
        var completed = false
        var customRestoreCodeExecuted = false

        let purchasHandler = Self.externalPurchaseHandler { _ in
            return (userCancelled: true, error: nil)
        } performRestore: {
            customRestoreCodeExecuted = true
            return (success: true, error: nil)
        }

let config = PaywallViewConfiguration(purchaseHandler: purchasHandler)

        try PaywallView(configuration: config).addToHierarchy()

        Task {
            _ = try await purchasHandler.restorePurchases()
            completed = true
        }

        expect(completed).toEventually(beTrue())
        expect(customRestoreCodeExecuted) == true
    }
```

### Error Handling

For the external code blocks to be called, `purchasesAreCompletedBy` needs to be set to `.myApp` AND the blocks need to be defined. So we need to handle cases when these are not consistent:

1. If someone configures purchasesAreCompletedBy to `.myApp`, and then displays a PaywallView **without** purchase/restore handlers, the SDK will:
* Log an error when the PaywallView is constructed
* When the PaywallView is displayed, replace it with a big red banner when in debug mode, fatalError() in release mode.
* If you purchase directly via the purchase handler (which is an internal class but this is done for testing), it will throw.

2. If someone configures purchases to use `.revenueCat`, and then displays a PaywallView **with** purchase/restore handler, the SDK will:

* Log a warning when the PaywallView is constructed

Rationale:
In case 1, there could be no (acceptable) way to make a purchase, so this is very bad and it needs to be dealt with.

In case 2, they've over-specified the paywall, and while this might be confusing (why isn't my code being called??), it's not as problematic, and we want to make it easy for people to switch from using their purchase logic to our purchase logic.

These checks are made in PaywallView.swift, method `checkForConfigurationConsitency()`.

### UIKit

Not yet supported, will add in a follow-up PR, unlikely to be complicated.

### Paywall Footer and Paywall If Needed
The `performPurchase` and `performRestore` parameters for the paywall footer are contained in a struct rather than as loose parameters of closure types, because doing the latter would create a very poor experience with regards to code completion, where Xcode will always offer complete your code as a trailing closure, but will always use the first closure where the function signature matches.

The following illustrates the problem of using loose closure parameters:

The trailing closure that Xcode auto-creates here does _not_ get called for `performPurchase`, but rather `purchaseStarted`, because it also has a `package` as its input parameter, and comes first in the parameter list 😱.

https://github.com/RevenueCat/purchases-ios/assets/109382862/1f809e7c-b661-4844-89e3-fd846a029531

This is the problematic function signature:

```swift
public func paywallFooter(
        offering: Offering,
        condensed: Bool = false,
        fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
        purchaseStarted: PurchaseOfPackageStartedHandler? = nil,
        purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
        purchaseCancelled: PurchaseCancelledHandler? = nil,
        restoreStarted: RestoreStartedHandler? = nil,
        restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
        purchaseFailure: PurchaseFailureHandler? = nil,
        restoreFailure: PurchaseFailureHandler? = nil,
        performPurchase: PerformPurchase? = nil,
        performRestore: PerformRestore? = nil
    ) -> some View
 ```

To fix this, get rid of the two new parameters: 
```swift
        performPurchase: PerformPurchase? = nil,
        performRestore: PerformRestore? = nil
```
 
and embed them in a struct:

```swift
public struct MyAppPurchaseLogic {
    public let performPurchase: PerformPurchase
    public let performRestore: PerformRestore

    public init(performPurchase: @escaping PerformPurchase, performRestore: @escaping PerformRestore) {
        self.performPurchase = performPurchase
        self.performRestore = performRestore
    }
}
```

which we use as a last parameter:

```swift
        myAppPurchaseLogic: MyAppPurchaseLogic? = nil
```

 Now the code completes as so:


https://github.com/RevenueCat/purchases-ios/assets/109382862/332ac42d-ccba-42bc-bfc3-702d7870c99c

The `MyAppPurchaseLogic` doesn't complete in perfectly, but once you
initialize one of those it goes nicely the rest of the way, including
the error messages that instruct you on what you need to return.

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Josh Holtz <[email protected]>
Co-authored-by: Cesar de la Vega <[email protected]>
Co-authored-by: RevenueCat Git Bot <[email protected]>
Co-authored-by: Toni Rico <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Will Taylor <[email protected]>
Co-authored-by: Andy Boedo <[email protected]>
  • Loading branch information
8 people authored Jul 5, 2024
1 parent d2da2d2 commit 41d3541
Show file tree
Hide file tree
Showing 28 changed files with 1,026 additions and 66 deletions.
28 changes: 26 additions & 2 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,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 */; };
8834AFA52C2B9375005A72FE /* PresentIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A62222C1D168B00E1A461 /* PresentIfNeededTests.swift */; };
887A5FBD2C1D036200E1A461 /* RevenueCatUIDev.h in Headers */ = {isa = PBXBuildFile; fileRef = 887A5FBC2C1D036200E1A461 /* RevenueCatUIDev.h */; settings = {ATTRIBUTES = (Public, ); }; };
887A60672C1D037000E1A461 /* PaywallError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FC72C1D037000E1A461 /* PaywallError.swift */; };
887A60682C1D037000E1A461 /* TemplateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FC82C1D037000E1A461 /* TemplateError.swift */; };
Expand Down Expand Up @@ -759,13 +760,18 @@
887A63432C1D177800E1A461 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A621F2C1D168B00E1A461 /* LocalizationTests.swift */; };
887A63442C1D177800E1A461 /* PaywallFooterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A62202C1D168B00E1A461 /* PaywallFooterTests.swift */; };
887A63452C1D177800E1A461 /* PaywallViewEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A62212C1D168B00E1A461 /* PaywallViewEventsTests.swift */; };
887A63462C1D177800E1A461 /* PresentIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A62222C1D168B00E1A461 /* PresentIfNeededTests.swift */; };
887A63472C1D177800E1A461 /* PurchaseCompletedHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A62232C1D168B00E1A461 /* PurchaseCompletedHandlerTests.swift */; };
887A634A2C1D17EF00E1A461 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 887A63492C1D17EF00E1A461 /* Nimble */; };
887A634C2C1D17EF00E1A461 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 887A634B2C1D17EF00E1A461 /* OHHTTPStubs */; };
887A634E2C1D17EF00E1A461 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 887A634D2C1D17EF00E1A461 /* OHHTTPStubsSwift */; };
887A63502C1D17EF00E1A461 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 887A634F2C1D17EF00E1A461 /* SnapshotTesting */; };
887E7B592C13CD2C002977DE /* PurchasesAreCompletedBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887E7B582C13CD2C002977DE /* PurchasesAreCompletedBy.swift */; };
88A543DF2C37A45B0039C6A5 /* TemplatePackageSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A543DE2C37A45B0039C6A5 /* TemplatePackageSetting.swift */; };
88A543E12C37A4820039C6A5 /* TemplateView+MultiTier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A543E02C37A4820039C6A5 /* TemplateView+MultiTier.swift */; };
88A543E32C37A4970039C6A5 /* Template7View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A543E22C37A4970039C6A5 /* Template7View.swift */; };
88A543E52C37A4AF0039C6A5 /* ConsistentTierContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A543E42C37A4AF0039C6A5 /* ConsistentTierContentView.swift */; };
88A543E72C37A4C40039C6A5 /* TierSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A543E62C37A4C40039C6A5 /* TierSelectorView.swift */; };
88AD4C482C24E8EA00943C3E /* ExternalPurchaseAndRestoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AD4C472C24E8EA00943C3E /* ExternalPurchaseAndRestoreTests.swift */; };
88DE93E12C211CBC0086B6D8 /* RevenueCat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DC5621624EC63420031F69B /* RevenueCat.framework */; };
88DE93E22C211CBC0086B6D8 /* RevenueCat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2DC5621624EC63420031F69B /* RevenueCat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */; };
Expand Down Expand Up @@ -1699,6 +1705,12 @@
887A63212C1D174200E1A461 /* RevenueCatUITestsDev.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RevenueCatUITestsDev.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
887A63482C1D17A200E1A461 /* RevenueCatUIDev.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = RevenueCatUIDev.xctestplan; path = RevenueCatUI/RevenueCatUIDev.xctestplan; sourceTree = SOURCE_ROOT; };
887E7B582C13CD2C002977DE /* PurchasesAreCompletedBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesAreCompletedBy.swift; sourceTree = "<group>"; };
88A543DE2C37A45B0039C6A5 /* TemplatePackageSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TemplatePackageSetting.swift; path = RevenueCatUI/TemplatePackageSetting.swift; sourceTree = SOURCE_ROOT; };
88A543E02C37A4820039C6A5 /* TemplateView+MultiTier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TemplateView+MultiTier.swift"; sourceTree = "<group>"; };
88A543E22C37A4970039C6A5 /* Template7View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Template7View.swift; sourceTree = "<group>"; };
88A543E42C37A4AF0039C6A5 /* ConsistentTierContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsistentTierContentView.swift; sourceTree = "<group>"; };
88A543E62C37A4C40039C6A5 /* TierSelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TierSelectorView.swift; sourceTree = "<group>"; };
88AD4C472C24E8EA00943C3E /* ExternalPurchaseAndRestoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExternalPurchaseAndRestoreTests.swift; path = Templates/ExternalPurchaseAndRestoreTests.swift; sourceTree = "<group>"; };
9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogIntent.swift; sourceTree = "<group>"; };
9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigureStrings.swift; sourceTree = "<group>"; };
9A65E03A25918B0900DE00B0 /* CustomerInfoStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerInfoStrings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3196,6 +3208,7 @@
887A5FE32C1D037000E1A461 /* Package+VariableDataProvider.swift */,
887A5FE42C1D037000E1A461 /* PaywallData+Default.swift */,
887A5FE52C1D037000E1A461 /* PreviewHelpers.swift */,
88A543E02C37A4820039C6A5 /* TemplateView+MultiTier.swift */,
887A5FE62C1D037000E1A461 /* VersionDetector.swift */,
);
path = Helpers;
Expand All @@ -3205,6 +3218,7 @@
isa = PBXGroup;
children = (
887A5FE82C1D037000E1A461 /* ConsistentPackageContentView.swift */,
88A543E42C37A4AF0039C6A5 /* ConsistentTierContentView.swift */,
887A5FE92C1D037000E1A461 /* FitToAspectRatio.swift */,
887A5FEA2C1D037000E1A461 /* FooterHidingModifier.swift */,
887A5FEB2C1D037000E1A461 /* ViewExtensions.swift */,
Expand Down Expand Up @@ -3290,6 +3304,7 @@
887A604B2C1D037000E1A461 /* Template3View.swift */,
887A604C2C1D037000E1A461 /* Template4View.swift */,
887A604D2C1D037000E1A461 /* Template5View.swift */,
88A543E22C37A4970039C6A5 /* Template7View.swift */,
887A604E2C1D037000E1A461 /* TemplateViewType.swift */,
);
path = Templates;
Expand All @@ -3307,6 +3322,7 @@
887A605F2C1D037000E1A461 /* Views */ = {
isa = PBXGroup;
children = (
88A543DE2C37A45B0039C6A5 /* TemplatePackageSetting.swift */,
887A60532C1D037000E1A461 /* AsyncButton.swift */,
887A60542C1D037000E1A461 /* DebugErrorView.swift */,
887A60552C1D037000E1A461 /* ErrorDisplay.swift */,
Expand All @@ -3319,6 +3335,7 @@
887A605C2C1D037000E1A461 /* PurchaseButton.swift */,
887A605D2C1D037000E1A461 /* RemoteImage.swift */,
887A605E2C1D037000E1A461 /* TemplateBackgroundImageView.swift */,
88A543E62C37A4C40039C6A5 /* TierSelectorView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -3466,6 +3483,7 @@
887A62212C1D168B00E1A461 /* PaywallViewEventsTests.swift */,
887A62222C1D168B00E1A461 /* PresentIfNeededTests.swift */,
887A62232C1D168B00E1A461 /* PurchaseCompletedHandlerTests.swift */,
88AD4C472C24E8EA00943C3E /* ExternalPurchaseAndRestoreTests.swift */,
887A63482C1D17A200E1A461 /* RevenueCatUIDev.xctestplan */,
);
name = RevenueCatUITests;
Expand Down Expand Up @@ -5008,6 +5026,8 @@
887A60C62C1D037000E1A461 /* LoadingPaywallView.swift in Sources */,
887A60C72C1D037000E1A461 /* PackageButtonStyle.swift in Sources */,
887A60C52C1D037000E1A461 /* IntroEligibilityStateView.swift in Sources */,
88A543E72C37A4C40039C6A5 /* TierSelectorView.swift in Sources */,
88A543E12C37A4820039C6A5 /* TemplateView+MultiTier.swift in Sources */,
887A60B72C1D037000E1A461 /* WatchTemplateView.swift in Sources */,
887A60722C1D037000E1A461 /* PaywallViewMode+Extensions.swift in Sources */,
887A60C32C1D037000E1A461 /* FooterView.swift in Sources */,
Expand All @@ -5026,6 +5046,7 @@
887A60892C1D037000E1A461 /* PaywallPurchasesType.swift in Sources */,
887A60C22C1D037000E1A461 /* ErrorDisplay.swift in Sources */,
887A60692C1D037000E1A461 /* IntroEligibilityViewModel.swift in Sources */,
88A543E32C37A4970039C6A5 /* Template7View.swift in Sources */,
887A60CB2C1D037000E1A461 /* TemplateBackgroundImageView.swift in Sources */,
887A60BD2C1D037000E1A461 /* TemplateViewType.swift in Sources */,
887A606D2C1D037000E1A461 /* Localization.swift in Sources */,
Expand All @@ -5047,6 +5068,7 @@
887A607C2C1D037000E1A461 /* ColorInformation+MultiScheme.swift in Sources */,
887A60782C1D037000E1A461 /* TestData.swift in Sources */,
887A60672C1D037000E1A461 /* PaywallError.swift in Sources */,
88A543E52C37A4AF0039C6A5 /* ConsistentTierContentView.swift in Sources */,
887A606E2C1D037000E1A461 /* LocalizedAlertError.swift in Sources */,
887A60802C1D037000E1A461 /* Package+VariableDataProvider.swift in Sources */,
887A60CE2C1D037000E1A461 /* View+PresentPaywall.swift in Sources */,
Expand All @@ -5064,6 +5086,7 @@
887A60C42C1D037000E1A461 /* IconView.swift in Sources */,
887A60732C1D037000E1A461 /* ProcessedLocalizedConfiguration.swift in Sources */,
887A606F2C1D037000E1A461 /* PaywallData+Validation.swift in Sources */,
88A543DF2C37A45B0039C6A5 /* TemplatePackageSetting.swift in Sources */,
887A60762C1D037000E1A461 /* TemplateViewConfiguration+Extensions.swift in Sources */,
887A607A2C1D037000E1A461 /* Variables.swift in Sources */,
);
Expand All @@ -5073,6 +5096,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8834AFA52C2B9375005A72FE /* PresentIfNeededTests.swift in Sources */,
887A632B2C1D177800E1A461 /* LocalizedAlertErrorTests.swift in Sources */,
887A632C2C1D177800E1A461 /* PackageVariablesTests.swift in Sources */,
887A632D2C1D177800E1A461 /* PaywallDataValidationTests.swift in Sources */,
Expand All @@ -5091,6 +5115,7 @@
887A633A2C1D177800E1A461 /* PaywallViewDynamicTypeTests.swift in Sources */,
887A633B2C1D177800E1A461 /* PaywallViewLocalizationTests.swift in Sources */,
887A633C2C1D177800E1A461 /* Template1ViewTests.swift in Sources */,
88AD4C482C24E8EA00943C3E /* ExternalPurchaseAndRestoreTests.swift in Sources */,
887A633D2C1D177800E1A461 /* Template2ViewTests.swift in Sources */,
887A633E2C1D177800E1A461 /* Template3ViewTests.swift in Sources */,
887A633F2C1D177800E1A461 /* Template4ViewTests.swift in Sources */,
Expand All @@ -5100,7 +5125,6 @@
887A63432C1D177800E1A461 /* LocalizationTests.swift in Sources */,
887A63442C1D177800E1A461 /* PaywallFooterTests.swift in Sources */,
887A63452C1D177800E1A461 /* PaywallViewEventsTests.swift in Sources */,
887A63462C1D177800E1A461 /* PresentIfNeededTests.swift in Sources */,
887A63472C1D177800E1A461 /* PurchaseCompletedHandlerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
20 changes: 18 additions & 2 deletions RevenueCatUI/Data/Errors/PaywallError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,25 @@ enum PaywallError: Error {
/// The selected offering was not found.
case offeringNotFound(identifier: String)

/// The PaywallView must be initialized with ``performPurchase`` and ``performRestore``
/// when ``purchasesAreCompletedBy`` is ``.myApp``
case performPurchaseAndRestoreHandlersNotDefined(missingBlocks: String)

/// The PaywallView need not be initizlied with performPurchase and performRestore
/// when ``purchasesAreCompletedBy`` is ``.revenueCat``
case purchaseAndRestoreDefinedForRevenueCat

}

extension PaywallError: CustomNSError {
extension PaywallError: CustomNSError, CustomStringConvertible {

var errorUserInfo: [String: Any] {
return [
NSLocalizedDescriptionKey: self.description
]
}

private var description: String {
var description: String {
switch self {
case .purchasesNotConfigured:
return "Purchases instance has not been configured yet."
Expand All @@ -45,6 +53,14 @@ extension PaywallError: CustomNSError {

case let .offeringNotFound(identifier):
return "The RevenueCat dashboard does not have an offering with identifier '\(identifier)'."
case .performPurchaseAndRestoreHandlersNotDefined:
return "PaywallView has not been correctly initialized. purchasesAreCompletedBy is set to .myApp, and so " +
"the PaywallView must be initialized with a PerformPurchase and PerformRestore handler."
case .purchaseAndRestoreDefinedForRevenueCat:
return "RevenueCat is configured with purchasesAreCompletedBy set to .revenueCat, but " +
"the Paywall has purchase/restore blocks defined. These will NOT be executed. " +
"Please set purchasesAreCompletedBy to .myApp if you wish to run these blocks " +
"instead of RevenueCat's purchase/restore code."
}
}

Expand Down
10 changes: 6 additions & 4 deletions RevenueCatUI/Data/PaywallViewConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct PaywallViewConfiguration {
var fonts: PaywallFontProvider
var displayCloseButton: Bool
var introEligibility: TrialOrIntroEligibilityChecker?
var purchaseHandler: PurchaseHandler?
var purchaseHandler: PurchaseHandler

init(
content: Content,
Expand All @@ -29,7 +29,7 @@ struct PaywallViewConfiguration {
fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
displayCloseButton: Bool = false,
introEligibility: TrialOrIntroEligibilityChecker? = nil,
purchaseHandler: PurchaseHandler? = nil
purchaseHandler: PurchaseHandler
) {
self.content = content
self.customerInfo = customerInfo
Expand Down Expand Up @@ -70,16 +70,18 @@ extension PaywallViewConfiguration {
fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
displayCloseButton: Bool = false,
introEligibility: TrialOrIntroEligibilityChecker? = nil,
purchaseHandler: PurchaseHandler? = nil
purchaseHandler: PurchaseHandler = PurchaseHandler.default()
) {
let handler = purchaseHandler

self.init(
content: .optionalOffering(offering),
customerInfo: customerInfo,
mode: mode,
fonts: fonts,
displayCloseButton: displayCloseButton,
introEligibility: introEligibility,
purchaseHandler: purchaseHandler
purchaseHandler: handler
)
}

Expand Down
21 changes: 21 additions & 0 deletions RevenueCatUI/Data/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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

}

extension Strings: CustomStringConvertible {
Expand Down Expand Up @@ -96,6 +101,22 @@ 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 your app. " +
"No StoreKit purchasing logic will be performed by RevenueCat. " +
"You must have initialized your `PaywallView` appropriately."

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 your app. " +
"No StoreKit restore purchases logic will be performed by RevenueCat. " +
"You must have initialized your `PaywallView` appropriately."
}
}

Expand Down
15 changes: 15 additions & 0 deletions RevenueCatUI/Helpers/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 41d3541

Please sign in to comment.