Commit Graph
61 Commits
Author SHA1 Message Date
Antonio Pallares b0292f2ee7 [Diagnostics] add missing parameter to http_request_performed event (#4857)
* refactor: create `DiagnosticsEvent.Properties` struct with all possible fields in a diagnostics event

* chore: remove unnecessary `CodingKeys` and values as `JSONEncoder.KeyEncodingStrategy.convertToSnakeCase` is used in the diagnostics requests

* tests: WIP: adapt unit tests to new `DiagnosticsEvent.Properties`

* fix: have values in Diagnostics events be snake case

* tests: adapt unit tests to Diagnostics events changes

* fix: `name` parameter in DiagnosticsEvent

* tests: fix snapshot testing of JSON to use `.iso8601` date format

* tests: have `JSONEncoder` of snapshot tests be a copy of the encoder user in the SDK

* tests: update the dates in the `CustomerInfo` snapshots for decoding tests to have ISO 8601 format

* fix: remove duplicated import

* refactor: centralize some info transformations in `DiagnosticsEvent.Properties`' init

* tests: add snapshot tests for `DiagnosticsEvents` object

* feat: add `is_retry` parameter to Diagnostics `http_request_performed` event

* tests: consider new `isRetry` parameter in existing `DiagnosticsEvent` tests

* fix: make sure that the `http_request_performed` event is always called

The previous implementation would skip sending the event in the case of getting a `networkError`
2025-03-07 11:26:32 +01:00
Antonio Pallares 25d1507411 UI Preview Mode: add extra header to network requests (#4752)
* feat: add new header to network requests when in UI preview mode

* test: add unit tests for header in network requests when in UI Preview mode
2025-01-31 12:33:19 +01:00
JayShortway 57766ce7a2 Adds X-Is-Debug-Build header (#4364) 2024-10-10 17:00:03 +02:00
Toni Rico 4503a837e6 [Diagnostics] Refactor diagnostics track methods to handle background work automatically (#4270)
* Refactor diagnostics track methods to handle background work automaticaly

* fix swiftlint
2024-09-26 16:33:28 +02:00
Toni RicoandRevenueCat Git Bot fe181bd5e9 Add kochava integration (#4274)
* Reapply "Add `$kochavaDeviceId` attribution convenience method (#4238)" (#4265)

This reverts commit f2ce4416da.

* Add userAgent tracking for attribution networks

* Fix tests

* Add X-Platform-Device header

* Rename user agent to device version

* Use correct attribution method

* [skip ci] Generating new test snapshots (#4278)

* [skip ci] Generating new test snapshots (#4279)

* [skip ci] Generating new test snapshots (#4280)

* [skip ci] Generating new test snapshots (#4281)

* [skip ci] Generating new test snapshots (#4282)

* [skip ci] Generating new test snapshots (#4283)

* Generating new test snapshots (#4284)

* Fix ttests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2024-09-25 19:59:36 +02:00
Toni Rico 597af5f215 [Diagnostics] Add backend_error_code property (#4236)
<!-- Thank you for contributing to Purchases! Before pressing the
"Create Pull Request" button, please provide the following: -->

### Checklist
- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-android` and
hybrids

### Motivation
<!-- Why is this change required? What problem does it solve? -->
<!-- Please link to issues following this format: Resolves #999999 -->

### Description
This adds the `backend_error_code` property to diagnostics
`http_request_performed` events.
2024-09-04 09:03:34 +02:00
Will Taylor 348258a41f only retry post receipt for 429 status codes 2024-07-25 16:46:31 -05:00
Will Taylor a4055375f6 retry requests with HTTP status 429 2024-07-23 12:41:16 -05:00
Cesar de la Vega 29250a4f11 Add X-Preferred-Locales header (#4008)
Added a new `X-Supported-Locales` header to be used in the new customer
center feature
2024-07-02 17:32:50 +02:00
NachoSoto 2bcd0ce328 5.0.0 changes: support SK2 by default 2024-07-01 14:37:21 -05:00
Josh Holtz c6e185ff07 Only install swiftlint on Xcode 15 jobs (#3913)
### Motivation

Better implementation of
https://github.com/RevenueCat/purchases-ios/pull/3909 and what was just
commited to `5.0-dev`

### Description

- Only install `swiftlint` on Xcode 15.3 jobs
- Also moved Ruby 3.2.2 install into `install-bundle-dependencies` which
was also done in `5.0-dev`
2024-05-18 08:21:02 -05:00
Toni Rico bb8066d9b3 Add http_request_performed diagnostics event (#3897)
### Description
This adds a new diagnostics event `http_request_performed` with some
useful information we would like to track about the http requests
performed to our servers.
2024-05-16 15:13:40 +02:00
NachoSoto 1cea9978aa Trusted Entitlements: tests for signature verification without header hash (#3505)
Follow up to #3424. This adds coverage to ensure that the backend
continues to sign correctly for old SDK versions that don't support
this.
2023-12-21 12:47:34 -08:00
NachoSoto fb76e662e8 Trusted Entitlements: add support for signing request headers (#3424)
This adds support for including arbitrary headers in the signature
verification, therefore preventing tampering of them.
2023-12-21 11:53:10 -08:00
NachoSoto e31a531ca1 HTTPClient: test all request headers (#3425)
This allows us to verify exactly what headers are included in all
requests.

It also simplifies the implementation for #3424.
2023-11-17 12:19:49 -08:00
Josh HoltzandNachoSoto 7d78633fd8 Adds X-Storefront request header for App Store Storefront (#3405)
### Motivation

Allow backend to determine Storefront of customer

### Description

- Adds `X-Storefront` as a default header
- Uses `StoreKit 1` implementation since its not `async`
- `StoreKit 2` implementation is `async` and would require a some
restructuring that we don't need right now

---------

Co-authored-by: NachoSoto <ignaciosoto90@gmail.com>
2023-11-14 14:04:07 -08:00
NachoSoto ea952f0108 ErrorUtils.purchasesError(withSKError:): handle URLErrors (#3346)
Fixes #3343.

`SKPaymentTransaction.error` docs say:
> The error property is undefined except when `transactionState` is set
to `SKPaymentTransactionState.failed`. Your application can read the
error property to determine why the transaction failed. For a list of
error constants, see `SKErrorDomain` in StoreKit Constants.

Which is why [`PurchasesOrchestrator` only considered that
domain](https://github.com/RevenueCat/purchases-ios/blob/4.28.1/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift#L792).

This expands this handling to consider any other network error,
therefore providing more precise errors.

Since offline connection error detection was only in `HTTPClient`, I
also moved that out and removed `NetworkError.offlineConnection`, so
that we detect this when converting `NetworkError.networkError` instead.
2023-10-25 20:25:09 -07:00
NachoSoto ed80980d08 HTTPClient: verbose logs for request IDs (#3320)
See https://github.com/RevenueCat/khepri/pull/6798.
This can help debug issues as it helps us cross-reference requests on
our end.

### Changes:
- Logging `X-Request-ID` and `X-Amzn-Trace-ID` headers if present (only
in verbose mode)
- Logging these even for failed requests
- Also logging `request_handled_by_load_shedder` for failed requests
- To accomplish that. `HTTPURLResponse` now conforms to our
`HTTPResponseType`, this allows us to extract headers in an uniform way

### Example:
```
[network] DEBUG: ℹ️ API request completed: GET '/v1/subscribers/$RCAnonymousID%3Ad9dc4f3b33054cf28c0f2ec9da0f48d5' (304)
Request-ID: 'be534c50-5231-403b-be2f-7c077861751d'; Amzn-Trace-ID: 'Root=1-6532c6ca-7ef80f961ec9c4145484df13'
```
2023-10-25 20:24:23 -07:00
NachoSoto 341f0d340c Integration Tests: improve flaky tests (#3163)
We get a lot of flaky failures on `OfflineStoreKitIntegrationTests`
(example:
https://app.circleci.com/pipelines/github/RevenueCat/purchases-ios/14843/workflows/a3cffa7c-da25-4811-931d-fc241befee0f/jobs/118214/tests).
I managed to reproduce locally running some of these 100+ times on a
loop until they failed.

I traced the issue to this sequence:
- SK2 purchase
- `StoreKit.Transaction.unfinished` is empty

This is likely due to a race condition. In practice, when computing
offline `CustomerInfo`, we never check it right away, since we need to
wait for the server to return a 500.

I filed a Radar for this: `FB13133387`.

To make tests more realistic, I introduced a delay in `HTTPClient`
returning fake 5xx errors. I haven't been able to reproduce this issue
again, so this should make `OfflineStoreKitIntegrationTests` more
reliable.

I've also added a new debug log that could help debug similar issues,
because `Strings.offline.computed_offline_customer_info` was not very
complete.
2023-09-07 12:25:19 -07:00
NachoSoto 75c0d03ce6 HTTPClient: also parse errors with application/json;charset=utf8 (#3041)
See #2529.
2023-08-17 09:45:22 -07:00
NachoSoto 833524314d HTTPClient: extracted HTTPRequestPath protocol (#2986)
This paves the way to add a second set of paths with a separate server
URL to be able to shed requests to offerings and product entitlement
mapping to a separate server.

### Changes:
- New `HTTPRequestPath` with the same properties as `HTTPRequest.Path` +
a new `serverHostURL`
- Moved proxy logic from `SystemInfo`/`HTTPClient` to `HTTPRequestPath`
- Simplified `HTTPRequest` implementation
2023-08-10 15:23:13 -07:00
NachoSoto f7e36fb993 Trusted Entitlements: add support for signing POST body (#2753)
### Changes:
- `HTTPClient` has a new `X-Post-Params-Hash` request header
- Replaced `HTTPRequest.Method.post`'s `Encodable` value with a more
specific `HTTPRequestBody` (like `HTTPResponseBody`)
- `HTTPRequestBody` allows overriding `contentForSignature` (it defaults
to `[]`) which then `Signing` uses for the hash
- Fixed `LogInOperation` not passing the `VerificationResult` to the
result `CustomerInfo`. This is captured in unit and integration tests
now.
- Add support for signing `POST` requests in `Path.logIn` and
`Path.postReceiptData`

Depends on https://github.com/RevenueCat/khepri/pull/6246
2023-07-13 07:45:16 -07:00
NachoSoto 48e9560c09 Trusted Entitlements: log signature errors on requests with .informational mode (#2751)
Example (the first `warn` log is `debug` and when running RC tests
only):
```
[signing] WARN: ⚠️ INVALID SIGNATURE DETECTED:
Request: GET /v1/subscribers/identify
Response: 200
Headers: ["X-Signature: signature", "X-RevenueCat-Request-Time: 1658749988564", "Content-Length: 0", "X-RevenueCat-ETag: etag"]
Body (length: 0): e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
[signing] ERROR: 😿‼️ Request to subscribers/identify failed verification. This is likely due to a malicious user intercepting and modifying requests.
```
2023-07-07 14:05:55 -07:00
NachoSoto bd3ac5faf6 Trusted Entitlements: produce verification failures for static endpoints with no signature (#2752)
Equivalent to https://github.com/RevenueCat/purchases-android/pull/1119
2023-07-06 10:08:45 -07:00
NachoSoto 95a5a97c1c Trusted Entitlements: add API key and HTTPRequest.Path to signature (#2746)
Depends on #2747.

### Changes:
- Changed `Signing` to a `final class`, so it can be instantiated with
the api key.
-  Updated all `SigningTests` to include the correct signatures
2023-07-05 16:39:04 -07:00
NachoSoto 3f538fc6f9 Trusted Entitlements: update handling of 304 responses (#2698)
### Changes:
- `ETagManager` no longer stores the verification mode
- Created new `VerifiedHTTPResponse`, which has the
`VerificationResult`, extracted from `HTTPResponse`
- Removed `VerificationResult.from(cache:response:)`
- Changed `SignatureVerificationHTTPClientTests` to test using the real
`ETagManager`, so they become more "end to end" unit tests. A lot of
them were relying on implementation details of the mock instead of the
real `ETagManager`.
- Removed several `HTTPClient` tests that were checking a behavior that
was impossible (for example, no verification result despite it being
enabled), or that checked behavior based on the cached
`VerificationResult`
2023-06-30 12:24:53 -07:00
NachoSoto 7298dfa69f ETagManager: refactored e-tag creation and tests (#2671)
We had lots of different parts of the code doing
`request.url?.absoluteString` and also using different values for
e-tags.
This refactor combines all that to use a single `eTag(for:)` method.

I did this as part of #2665, which is getting closed, but it will still
be useful if we ever decide to change the content of e-tags.
2023-06-21 11:02:45 -07:00
NachoSoto f9660fbd1f LoadShedderIntegrationTests: verify requests are actually handled by load shedder (#2663) 2023-06-19 08:16:57 -07:00
NachoSoto ed35ab2da9 HTTPClient: disable URLSession cache (#2668)
Turns out this was breaking an HTTP 304 integration test in #2659. I'm
very confused why we hadn't seen it before.

Possibly it didn't matter for normal requests, but it was breaking
signature verification because the `nonce` was different on the new
request, and therefore it wouldn't match the cached signature.
2023-06-16 10:26:39 -07:00
NachoSoto 6d1cf97a41 ErrorResponse: drastically improved error messages, no more "unknown error"s (#2660)
## Description:

We've gotten several reports with error messages that aren't very
helpful, like #2390.

Turns out that whenever the API returned a non-successful response but
without an error code, we always produced _"unknown error"_.
This improves the behavior to default to showing the status code. One
example that was very obvious is setting up the SDK with an invalid API
key.

## Examples:

### `OfferingsManager.Error`:

- Before:
```
[Purchases] - ERROR: 🍎‼️ Error fetching offerings - The operation couldn’t be completed. (RevenueCat.OfferingsManager.Error error 0.)
Underlying error: The operation couldn’t be completed. (RevenueCat.NetworkError error 6.)
```

- After:
```
[Purchases] - ERROR: 🍎‼️ Error fetching offerings - The operation couldn’t be completed. (RevenueCat.OfferingsManager.Error error 0.)
Underlying error: Unknown error. API request failed with status code 401
```

### `BackendError`:
- Before:
```
[Purchases] - ERROR: 😿‼️ Unknown error.
```

- After:
```
[Purchases] - ERROR: 😿‼️ Unknown error. API request failed with status code 401
```

### Request debug logs:
- Before:
```
[Purchases] - DEBUG: ℹ️ API request failed: GET /v1/subscribers/$RCAnonymousID:988f9b85609b4a57af255750bec1fbb2/offerings: Unknown error.
```
- After:
```
[Purchases] - DEBUG: ℹ️ API request failed: GET /v1/subscribers/$RCAnonymousID:988f9b85609b4a57af255750bec1fbb2/offerings: Unknown error. API request failed with status code 401
```

### `CustomerInfoManager`:
- Before:
```
[Purchases] - WARN: ⚠️ Attempt to update CustomerInfo from network failed.
The operation couldn’t be completed. (RevenueCat.BackendError error 0.)
Underlying error: Unknown error
```

- After:
```
[Purchases] - WARN: ⚠️ Attempt to update CustomerInfo from network failed.
The operation couldn’t be completed. (RevenueCat.BackendError error 0.)
Underlying error: Unknown error. API request failed with status code 401
```
2023-06-16 09:19:16 -07:00
NachoSoto 208bf53386 HTTPClient: improved log for failed requests (#2669) 2023-06-16 07:49:52 -07:00
NachoSoto 06b0f3d723 Add InternalDangerousSettings.forceSignatureFailures (#2635)
Similar to #2486. This will be used for SDK-3181.

```
- WARN: ⚠️ Returning fake signature verification failure for 'GET /v1/subscribers/$RCAnonymousID:fc0508bea4a74a41bfc9a69db30d925f'
- ERROR: 😿‼️ Request failed signature verification. Request to /v1/subscribers/$RCAnonymousID%3Afc0508bea4a74a41bfc9a69db30d925f failed verification
```
2023-06-13 13:43:16 -07:00
NachoSoto 2276d5c2ce AppleReceipt.debugDescription: don't pretty-print JSON (#2564) 2023-05-31 07:11:23 -07:00
NachoSoto 94537a39af HTTPClient: don't assume error responses are JSON (#2529)
When receiving 500 responses, `HTTPClient` was always calling
`ErrorResponse.from(_ data: Data)`, which tried to decode the content as
if it was JSON.
However, if the response had something completely different, it would
fail to decode.

This wasn't really a bug because `HTTPClient` was still forwarding the
right result, but now it will do so without even trying to parse it.
2023-05-24 09:51:50 -07:00
NachoSoto 50ffbfb3e9 Performance: new check to ensure serialization / deserialization is done from background thread (#2496) 2023-05-19 11:58:12 -07:00
NachoSoto e9e6d887c2 Offline Entitlements: integration tests (#2501)
## Example tests:
```swift
func testOfferingsAreCachedInMemory() async throws {
    let onlineOfferings = try await Purchases.shared.offerings()
    expect(onlineOfferings.all).toNot(beEmpty())

    self.serverDown()

    let offlineOfferings = try await Purchases.shared.offerings()
    expect(offlineOfferings) === onlineOfferings
}

func testOfferingsAreCachedOnDisk() async throws {
    let onlineOfferings = try await Purchases.shared.offerings()
    expect(onlineOfferings.all).toNot(beEmpty())

    self.serverDown()
    await self.resetPurchases()

    let offlineOfferings = try await Purchases.shared.offerings()
    expect(offlineOfferings.response) == onlineOfferings.response

    let offering = try XCTUnwrap(offlineOfferings.current)
    expect(offering.availablePackages.count) == onlineOfferings.current?.availablePackages.count
    expect(offering.monthly?.storeProduct.productIdentifier) == "com.revenuecat.monthly_4.99.1_week_intro"
}
```
2023-05-19 10:54:42 -07:00
NachoSoto b04d894ccb DangerousSettings: debug-only forceServerErrors (#2486) 2023-05-16 09:59:51 -04:00
NachoSoto d68ca889e9 HTTPClient: log actual response status code (#2487)
We were currently logging the status code retrieved from `ETagManager`,
which made it seem like etags weren't working.

Example:
> API request completed: GET /v1/subscribers/identify (200)

With this change, we now log the actual response status code so we can
verify the server is returning the correct status code:
> API request completed: GET /v1/subscribers/identify (304)
2023-05-15 19:28:25 -04:00
Andy BoedoandNachoSoto 618ff121d2 Custom Entitlements Computation mode (#2439)
### Changes:

- Added `Purchases.configureInCustomEntitlementsComputationMode`
- Added `Purchases.switchUser(to:)`
- Disabled parts of the SDK in this new mode
- New example app for testing `customEntitlementsComputation` mode

---------

Co-authored-by: NachoSoto <ignaciosoto90@gmail.com>
2023-04-25 13:21:46 -07:00
NachoSotoandAndy Boedo 8b4c2eb706 ETagManager/HTTPClient: sending new X-RC-Last-Refresh-Time header (#2373)
Fixes SDK-3024 and SDK-3026.

---------

Co-authored-by: Andy Boedo <andresboedo@gmail.com>
2023-04-14 14:32:24 -07:00
NachoSoto 06f80c30a1 HTTPClient: debug log when performing redirects (#2371)
This will be used to detect the load balancer redirecting to the load
shedder. See also #2362.
2023-03-23 14:34:14 -07:00
NachoSoto 47465b01f8 HTTPClient: new flag to force server errors (#2370)
This will help testing offline entitlements.
Note that this is only available as an `internal` API, and only on
`debug` builds.
2023-03-23 14:21:47 -07:00
NachoSoto 0475dac6a9 ETagManager: don't ignore failed etags with Signing.VerificationMode.informational (#2331)
Essentially we don't want to modify any behavior on `.informational`
mode. Everything will continue behaving normally, and simply report
verification failures.
2023-03-13 12:24:02 -07:00
NachoSoto 4341745d10 IdentityManager: clear ETagManager and DeviceCache if verification is enabled but cached CustomerInfo is not (#2330)
Android counterpart:
https://github.com/RevenueCat/purchases-android/pull/844
2023-03-13 11:57:18 -07:00
NachoSoto da9ba397a3 Update CustomerInfo.requestDate from 304 responses (#2310)
### Purpose:

This is a prerequisite for #2288. The new grace period means that if we
didn't update the date from the cached responses, entitlements would
become stale and expired after 3 days.

### Changes:
- Added `HTTPResponseBody.copy(with newRequestDate:)` to be able to
modify responses from the header request date
- `CustomerInfo` implements this method to modify its request date
- Added `HTTPResponse.requestDate` to be able to keep track of the
request date from the server
- Added `VerificationResult.from(cache:response:)` to determine the most
restrictive verification result based on what's cached or checked from a
response
- Added `HTTPResponse.copy(with:)` to modify the verification result of
a response using the previous method
- `HTTPClient` now uses the most restrictive verification result
- `HTTPClient` updates request date from server responses or cached
responses (unless verification failed). This was the missing piece for
#2288.

### Tests:
- Verify that `ETagManager` does not use an ETag if verification was
previously not enabled
- Verify that `ETagManager` returns the request date from the server
when returning a cached response
- Verify that `HTTPClient` updates the request date from the server or
from a cached response
- Verify that `HTTPClient` does not update request date if verification
failed
- Tests for `HTTPResponse` request date parsing
- Test for `CustomerInfoResponseHandler` updating request date
- Tests for `CustomerInfo.copy(with newRequestDate:)`


### Other smaller changes:
- Moved `CustomerInfo.asData()` into `Encodable.asJSONEncodedData()`
- Renamed `requestTime` to `requestDate` everywhere for consistency
2023-03-01 09:24:32 -08:00
NachoSoto b707f8abc8 Signing: added request time & eTag to signature verification (#2309)
See https://github.com/RevenueCat/khepri/pull/5300
2023-02-27 09:07:23 -08:00
NachoSoto c5f963e4fa HTTPClient: changed header search to be case-insensitive (#2308) 2023-02-22 11:30:20 -08:00
NachoSoto 457ba021d9 HTTPClient: automatically add nonce based on HTTPRequest.Path (#2286)
Instead of manually choosing when to include `nonce` in requests, this
data-driven approach ensures that all requests to endpoints that support
signature verification will make `HTTPClient` do the right thing.

See also #2277 for how `CustomerInfo` and `EntitlementInfo` will use
this.
2023-02-22 09:20:25 -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 dbe93dbbf4 PurchasesDiagnostics: added step to verify signature verification (#2267)
⚠️ 🎉 This also changes integration tests to use
`EntitlementVerificationLevel.enforced` so that integration tests fail
if signatures are invalid.

#### Depends on:
- https://github.com/RevenueCat/khepri/pull/5191
- https://github.com/RevenueCat/khepri/pull/5204
- #2214
- #2215 
- #2216
- #2272

_Marking this as `feat`ure because it contains a new error in
`PurchasesDiagnostics.Error`_
2023-02-16 10:27:04 -07:00