Commit Graph
38 Commits
Author SHA1 Message Date
Toni Rico ae7d35e569 Do not warn when using mac API keys (#4668) 2025-01-15 10:53:16 +01:00
Will Taylor c814d0e3ac support deferring win-back storekit messages 2024-10-07 09:18:44 -05:00
41d3541451 Paywalls with custom purchase and restore logic handlers (#3973)
## 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>
2024-07-05 12:05:44 -07:00
NachoSoto 2bcd0ce328 5.0.0 changes: support SK2 by default 2024-07-01 14:37:21 -05:00
James BorthwickandCesar de la Vega 9e8b40c322 finishTransactions/ObserverMode -> PurchasesAreCompletedBy (#3947)
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>
2024-06-18 10:50:04 -07:00
Toni Rico 7d570706fa Diagnostics: Make setting public (#3931)
### Description
This actually enables consumers to use diagnostics by making the setting
public

#### TODO
- [ ] Write docs
- [x] Get #3930 merged
2024-06-03 17:22:49 +02:00
Cesar de la VegaandToni Rico 74d3960262 Create DiagnosticsTracker (#3784)
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>
2024-04-12 11:47:57 +02:00
Mark Villacampa da11a79c88 Mark methods with StaticString for appUserID as deprecated (#3739)
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.
2024-02-29 19:39:43 +01:00
Toni RicoandNachoSoto f06740f87c Remove ObjC showStoreMessages API (#3269)
### 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>
2023-10-03 18:41:50 +02:00
Toni RicoandNachoSoto 773fd84a65 StoreKit In App messages support (#3252)
### 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>
2023-10-03 16:58:53 +02:00
NachoSoto 7fa0b265f3 Configuration: log warning if attempting to use observer mode with StoreKit 2 (#3066)
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.
2023-09-19 13:21:09 -07:00
NachoSoto 460f6fd6da Trusted Entitlements: improved documentation (#2782)
I've also added a link to our new documentation page:
https://rev.cat/trusted-entitlements

![Screenshot 2023-07-11 at 08 47
19](https://github.com/RevenueCat/purchases-ios/assets/685609/1f7cfdd9-7806-4f78-8630-ec860146ac73)
2023-07-11 09:27:20 -07:00
NachoSoto 1cab844f2b Trusted Entitlements: re-enable public API (#2621) 2023-07-11 07:34:39 -07:00
NachoSoto cde63f2fe7 TimingUtil: removed slow purchase logs (#2677) 2023-06-20 15:06:31 -07:00
NachoSoto 3388bc5437 Refactor: extracted all log strings (#2600)
I'm working on improving our logging setup, which will be made easier if
we don't use hardcoded strings in our logs.
2023-06-09 09:48:26 -07:00
NachoSoto 6d9594ea06 Xcode 15: fixed all documentation warnings (#2596) 2023-06-08 08:36:28 -07:00
NachoSoto 8c448378ab Configuration.Builder: fixed doc reference (#2583)
This fixes a warning on Xcode 15.
2023-06-06 07:37:46 -07:00
NachoSoto 1109b89cf3 PurchasedProductsFetcher: added log if fetching purchased products is slow (#2515) 2023-05-22 07:36:20 -07:00
Mark VillacampaandNachoSoto b8e058703d Purchases.configure: log warning if attempting to use a static appUserID (#2385)
### 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>
2023-03-28 18:11:02 +02:00
NachoSoto 4cacc9d7f7 Trusted Entitlements: changed all APIs to internal (#2350)
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.
2023-03-17 14:41:55 -07:00
NachoSoto 3fd222eaa2 Configuration.Builder.with(entitlementVerificationMode:): improved documentation (#2334)
Same as https://github.com/RevenueCat/purchases-android/pull/870

![Screenshot 2023-03-14 at 09 00
32](https://user-images.githubusercontent.com/685609/225064587-c1660514-e324-4d2a-b5d8-6b629e0d80a6.png)
2023-03-14 09:10:49 -07:00
NachoSoto 3af5fba011 Made Configuration.EntitlementVerificationMode.enforced unavailable (#2329)
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.
2023-03-13 11:42:52 -07:00
Andy BoedoandNachoSoto 9d4e398f2c Deprecate usesStoreKit2IfAvailable (#2293)
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>
2023-02-19 18:42:28 -08:00
NachoSoto 84eed758c1 Introduced Configuration.EntitlementVerificationMode and VerificationResult (#2277)
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
2023-02-17 16:49:21 -08:00
NachoSoto 61501ddc72 Signing: updated to use production public key (#2274)
Depends on #2272 and #2278.
Depends on https://github.com/RevenueCat/khepri/pull/5232.
2023-02-16 03:25:52 -08:00
NachoSoto 76dbf0037b Configuration: added (internal for now) API to load public key (#2215)
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
2023-02-08 08:53:43 -08:00
NachoSoto edf623b2ba TrialOrIntroPriceEligibilityChecker: use TimingUtil to log when it takes too long (#2238)
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.
2023-01-19 11:01:02 -08:00
NachoSoto 2cd313f355 Configuration.with(appUserID:): allow passing nil and added new tests (#2110)
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.
2022-12-05 10:16:06 -08:00
NachoSoto b91a0ecf2f StoreKit: added logs when purchasing and product requests are too slow (#2061)
Using #2059 in a few places for now.
We can add this to more places, and tweak the thresholds.

Fixes [CSDK-512].

[CSDK-512]:
https://revenuecats.atlassian.net/browse/CSDK-512?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-11-25 17:00:54 -08:00
NachoSoto 299e952883 Changed StoreKit2Setting.default back to .enabledOnlyForOptimizations (#2022)
This changes the default back to `StoreKit 1`. We decided to do this for
the following reasons:
- Purchasing with `PromotionalOffer`s does not work with StoreKit 2 due
to an Apple bug (see
https://github.com/RevenueCat/purchases-ios/pull/2020#discussion_r1012278099)
- `checkTrialOrIntroDiscountEligibility` is significantly slower with
`StoreKit 2` (#1893). We're adding optimizations to help with that
(#2007), but the underlying logic will still be slow.
- A rare race-condition where `StoreKit 2` does not have transactions
after a purchase ([TRIAGE-82]). We have some workarounds (#1945), but
it's still being investigated.

_Note: This effectively reverts 0ee540aa. That commit made it easier to
only change the default in one place which is why this PR is basically
just one line._

[TRIAGE-82]:
https://revenuecats.atlassian.net/browse/TRIAGE-82?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-11-04 12:25:41 -07:00
NachoSoto 0ee540aac4 StoreKit 2: enabled by default (#1922)
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.
2022-09-22 16:24:27 -07:00
NachoSoto 5d8c8c3bbd Configuration.with(usesStoreKit2IfAvailable:): removed "experimental" warning (#1845)
Fixes [CSDK-418].
2022-08-18 19:34:09 -07:00
Joshua Liebowitz bbaf8e2d2a Update Configuration.swift (#1760)
* internal protection level is not accessible to PurchasesHybridCommon through our Purchases extension.
Need to expose it publicly.
* Update Configuration.swift
* Update API testers
2022-07-13 10:46:38 -07:00
Joshua Liebowitz 311c35af8d Plumb platformInfo in Configuration for PHC use (#1757)
* Plumb platformInfo in Configuration for PHC use
2022-07-12 13:55:51 -07:00
NachoSoto a4bf2647f8 Purchases.configure: added overload taking a Configuration.Builder (#1720)
Fixes [CF-780].

See https://github.com/RevenueCat/purchases-ios/issues/1717#issuecomment-1160718519 for an example of a user trying to do this.
2022-06-21 12:34:11 -07:00
NachoSoto 3ee1c6c092 Validate API key (#1581)
iOS part of [CF-53].

[CF-53]: https://revenuecats.atlassian.net/browse/CF-53?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-05-20 15:09:56 -07:00
NachoSoto 588d00bdd8 Configuration.builder.with(usesStoreKit2IfAvailable:): fixed docstring (#1590) 2022-05-19 11:38:05 -07:00
Joshua Liebowitz 9bde89137c Create Configuration object and API to configure Purchases (#1556)
* 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
2022-05-12 15:43:10 -07:00