## 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] <support@github.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
Co-authored-by: Cesar de la Vega <cesarvegaro@gmail.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Will Taylor <wtaylor151@gmail.com>
Co-authored-by: Andy Boedo <andresboedo@gmail.com>
This PR adds:
* `purchasesAreCompletedBy`
which replaces and deprecates public instances of:
* `observerMode`
* `finishTransactions`
Internal references to the above will be removed in a subsequent PR.
---------
Co-authored-by: Cesar de la Vega <cesarvegaro@gmail.com>
Create `DiagnosticsTracker` and adds it to the `Purchases`
initialization. I don't expose the `diagnosticsEnabled` yet
---------
Co-authored-by: Toni Rico <antonio.rico.diez@revenuecat.com>
I tried conditionally compiling the `availability` attribute, and
marking it as `unavailable` in `RELEASE` builds, but the compiler is
smart enough to ignore the method and choose the disfavored overload in
that case.
I also tried conditionally compiling the return type in the same way, to
cause a compilation error in `RELEASE` builds, but the compiler is again
smart enough to choose the disfavored overload.
### Description
This removes the extra APIs that were added to support showing store
messages in ObjC. We will add these in PHC. If there is a demand for it
we can always add it later.
---------
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
### Description
StoreKit displays different types of in-app messages on app launch, but
it provides an API to disable that and show these messages at a
different time. This PR wraps those APIs to make them easier to use
through our SDK.
#### New APIs
- Add new configuration parameter to defer StoreKit's automatic in-app
messages
- Add new method in `Purchases` to display a list of specific message
types
- New `StoreKitMessageType` enum mapping to the types used by StoreKit
---------
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Since #3032 developers using observer mode need to configure the SDK
with the correct StoreKit 2 setting.
A future API will allow users to sync transactions made with StoreKit2.
### Checklist
- [X] If applicable, unit tests
- [X] If applicable, create follow-up issues for `purchases-android` and
hybrids
### Motivation
Hardcoding the user id is not desirable in almost any scenario, so we
want to warn the developer when that happens.
### Description
Following what we did for `Purchases.logIn` in
https://github.com/RevenueCat/purchases-ios/pull/1958, detect when
`Builder.with(appUserId:)` is called with a static string and log a
warning.
---------
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
This isn't ready quite ready to be released yet. With this change,
`main` can be released once again.
I've added a comment to all the changes so we can easily find them when
ready to be made `public` again.
Depends on #2326 and #2328.
This allows us to keep all the code, except users won't be able to
configure the SDK with this option.
Note that `Signing.ResponseVerificationMode.enforced` _is_ available, so
we can still test all that behavior, including from integration tests.
There's been a lot of customer confusion with the use SK2 parameter.
In order to reduce confusion, and to improve the overall experience,
we're moving into a more opinionated approach.
Currently, we're defaulting to SK1-only.
Once we get diagnostics data in, we'll be able to get more visibility
into real-world, production performance of StoreKit 1 vs StoreKit 2 on a
per-API basis. Using this data, we will implement remote configuration
to ensure that the best implementation is utilized for each specific
functionality, optimizing for maximum stability and performance.
Until then, we can save our customers from confusion and headache by
going back to the simplest approach.
### What this looks like at the callsite
<img width="1152" alt="Screenshot 2023-02-17 at 2 38 02 PM"
src="https://user-images.githubusercontent.com/3922667/219732743-a974a641-f716-49e9-ad66-d6e3d77328eb.png">
### What the relevant DocC page looks like
<img width="1028" alt="Screenshot 2023-02-17 at 2 28 20 PM"
src="https://user-images.githubusercontent.com/3922667/219732706-fd48dd6d-3071-48b9-83e7-cb138a443155.png">
---------
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Fixes [CSDK-629].
Depends on #2274 and #2267.
Entitlement verification is accomplished by preventing MiTM attacks
between the SDK and the RevenueCat server.
With verification enabled, the SDK ensures that the response created by
the server was not modified by a third-party, and the entitlements
received are exactly what was sent.
Entitlements are only verified if enabled using
`Configuration.Builder.with(entitlementVerificationMode:)`, which is
disabled by default.
### Changes:
- Added new `VerificationResult`, which replaces
`HTTPResponseVerificationResult`
- Renamed `Configuration.EntitlementVerificationLevel` to
`Configuration.EntitlementVerificationMode`
- `Configuration.with(entitlementVerificationMode:)` is now `public`
- Added `CustomerInfo.entitlementVerification`
- Added `EntitlementInfos.verification` and
`EntitlementInfo.verification`
- Added tests to cover serialization of these new values
- `CustomerInfoResponseHandler` copies the `ValidationResult` to
`CustomerInfo` (all operations that fetch `CustomerInfo` use this)
- Renamed "validation" to "verification" everywhere for consistency
- Finalized documentation
### TODO:
- [x] Figure out whether to use a separate type from
`HTTPResponseValidationResult` or rename that one
- [x] Figure out final API type and property names
- [x] Add tests for `CustomerInfo.copy(with:)`
- [x] Ensure validation result is encoded and decoded (for caching)
- [x] Add missing snapshots
[CSDK-629]:
https://revenuecats.atlassian.net/browse/CSDK-629?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
Fixes [CSDK-632].
### Changes:
- Created `Signing` to encapsulate new signing logic and types
- Added abstractions like `Signing.Certificate` and `Signing.PublicKey`
over `Security` C types
- Created `Configuration.EntitlementVerificationLevel` and
`Signing.ResponseVerificationLevel` (the logic for the name difference
is that publicly this is used to validate entitlements, but the internal
implementation works for any response).
- Created `Signing.loadPublicKey`
- Added tests for loading public key embedded in SDK
- Added `internal` (for now)
`Configuration.with(entitlementVerificationLevel:)`
- Added `SystemInfo.responseVerificationLevel` with the loaded key, if
enabled
### For a future PR:
- Make the `Configuration.with(entitlementVerificationLevel:)` `public`
- Make `Configuration.EntitlementVerificationLevel` `public`
- Add those 2 to `APITesters`
- Improve documentation by adding a link to the docs
- Verify certificate issuer in test
[CSDK-632]:
https://revenuecats.atlassian.net/browse/CSDK-632?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
See #1893.
We’ve notified Apple of this issue through a radar, but it appears that
it's still not fixed. Using `TimingUtil` introduced in #2059 this adds a
log when the requests take too long.
The docs had this:
> Pass `nil` or an empty string if you want ``Purchases`` to generate
this for you.
But `nil` wasn't a valid input.
I noticed [this reported
issue](https://github.com/RevenueCat/purchases-ios/issues/2107#issuecomment-1332007011)
where the user was doing this:
```swift
.with(appUserID: userUID ?? "")
```
Which technically would have the same semantics, since we allow empty
and convert it to anonymous, but now this is more clear and it matches
the docstring.
The main change is `StoreKit2Setting.default` is now
`enabledForCompatibleDevices`, but I've also made sure that all places
in the codebase are derived from this value.
* internal protection level is not accessible to PurchasesHybridCommon through our Purchases extension.
Need to expose it publicly.
* Update Configuration.swift
* Update API testers
* Resolves [CF-443]
* Adds `configureWithConfiguration` to `RCPurchasesAPI.m`
* Create Configuration object and API to configure Purchases
* Update API doc for new Purchases API
* Add example code to Configuration documentation
* Adds deprecations for various Purchases.configure methods