Open jamesrb1 opened 2 weeks ago
the attention to detail in code completion is 🥇
it'd be amazing to be able to resolve the inconsistent configuration at an API level, but I have a hard time coming up with great ways to do it.
One thought would be to wrap the configuration methods from RevenueCat in RevenueCatUI so that we could also add one that has a purchaseHandler (or force you to set up a purchaseHandler when you set purchasesAreFinishedBy .myapp).
Another would be to use Swift macros to check that the methods are called consistently. I think that's probably a lot of work, but not impossible if understand it correctly. Not something we need to take care of right away, though
I believe I've addressed all the comments, save for a couple that have been left as to-dos for future PRs:
Investigate if we can call custom view modifier closures from within the modified view without using Preferences, so that we can run the closures before the triggering call returns.\
\
For example, in the code below, .onPurchaseCompleted {
may be called after await Self.purchaseHandler.purchase(package:
returns:
func testOnPurchaseCompleted() async throws {
var customerInfo: CustomerInfo?
try PaywallView(
offering: Self.offering.withLocalImages,
customerInfo: TestData.customerInfo,
introEligibility: .producing(eligibility: .eligible),
purchaseHandler: Self.purchaseHandler
)
.onPurchaseCompleted {
customerInfo = $0
}
.addToHierarchy()
_ = try await Self.purchaseHandler.purchase(package: Self.package)
expect(customerInfo).to(be(TestData.customerInfo))
}
tl;dr
PaywallView has a new constructor that takes
performPurchase
andperformRestore
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 whenPurchases
has been configured.with(purchasesAreCompletedBy: .myApp)
.Example usage:
Description
When a
PaywallView
is constructed, a newPurchaseHandler
is created. ThePurchaseHandler
(an internal RevenueCatUI class) is owned by thePaywallView
, and it is responsible for executing new purchases and restores.When a
PaywallView
is constructed withoutperformPurchase
andperformRestore
blocks, thePaywallView
creates aPurchaseHandler
capable of preforming purchases using RevenueCat. When aPaywallView
is constructed withperformPurchase
andperformRestore
blocks, it can also make purchases using the customer-supplied closures.The
PurchaseHandler
is invoked when the user taps thePurchaseButton
, callingpurchaseHandler.purchase(package: self.selectedPackage.content)
, which branches to either the internal or external purchase code, as defined bypurchasesAreCompletedBy
:Purchase and Restore blocks can also be assigned for paywall footers:
and via
.presentPaywallIfNeeded
: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 aPaywallConfiguration
, both of which are normally constructed internally in the PaywallView constructor.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:If someone configures purchasesAreCompletedBy to
.myApp
, and then displays a PaywallView without purchase/restore handlers, the SDK will:If someone configures purchases to use
.revenueCat
, and then displays a PaywallView with purchase/restore handler, the SDK will: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
andperformRestore
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 ratherpurchaseStarted
, because it also has apackage
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:
To fix this, get rid of the two new parameters:
and embed them in a struct:
which we use as a last parameter:
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.