* Added an internal API for overriding the API base URL
* Updated and added tests for API baseURL configuration option
* Revert change that removed serverHostURL from HTTPRequestPath since it broke other types implementing this protocol
* swiftlint
* Removed the apiBaseURL property from Purchases since it's only used for internal testing anyways
* Remove unused @_spi(Internal)
* Updated comment on api base URL vs proxy URL
* Fixed linting issue
* change fallback URL logic to support a different path from the main server
* Adapt unit tests to changes in fallback urls logic
* lint
* Add unit test for fallback urls
* fix switched logic
* Add new public APIs to RevenueCatUI to set the preferred local for the UI components
* fix compliation of RevenueCatUI with new APIs
* add API tests
* implement preferred locale in paywalls
* complete implementation
* adapt unit tests
* adapt paywalls tester
* Add new tests to check preferred locale override
* fix lint
* discard all changes in RevenueCatUI (for separate PR)
* remove trailing comma
* fix tests
* Fix tests
* fix paywalls tester compilation
* remove spoiler from future PR
* Refactor `PreferredLocalesProvider` (#5299)
* refactor: reduce `PreferredLocalesProviderType` into `PreferredLocalesProvider` class
* make `preferredLocaleOverride` private in `PreferredLocalesProvider`
* make `SystemInfo`'s `preferredLocalesProvider` private
* rename `PreferredLocalesProvider`'s mock init parameters
* fix compilation issue
* feat: implement fallback host in all URL requests
* cleanup fallback host API implementation
* Adapt existing tests to changes in `HTTPRequestPath`
* fix implementation
* add unit tests for fallback API host
* add extra unit test
* fix tests
* fix lint
* implement new `host` parameter in `http_request_performed` diagnostics event
* add unit tests for new `host` diagnostics parameter
* change the logic from fallback hosts to fallback paths
* fix test
* fix fallback endpoint for get product entitlement mapping
* fix backend integration tests not building after changes
* rewrite fallback host functionality knowing that only some specific requests support a fallback host
For those requests that support a fallback host, the path, headers and response structure remain the same
* fix text name
* fix unit test
* remove extra comma
* fix unit test
* feat: implement fallback host in all URL requests
* cleanup fallback host API implementation
* Adapt existing tests to changes in `HTTPRequestPath`
* fix implementation
* add unit tests for fallback API host
* add extra unit test
* fix tests
* fix lint
* change the logic from fallback hosts to fallback paths
* fix test
* fix fallback endpoint for get product entitlement mapping
* fix backend integration tests not building after changes
* rewrite fallback host functionality knowing that only some specific requests support a fallback host
For those requests that support a fallback host, the path, headers and response structure remain the same
* fix text name
* Add X-Is-Backgrounded header
* Add unit test for presence of new header
* [skip ci] Generating new test snapshots (#4939)
* [skip ci] Generating new test snapshots (#4940)
* [skip ci] Generating new test snapshots (#4941)
* [skip ci] Generating new test snapshots (#4942)
* [skip ci] Generating new test snapshots (#4943)
* [skip ci] Generating new test snapshots (#4944)
* [skip ci] Generating new test snapshots (#4945)
* Lock isAppBackgrounded state to false during tests
* one more update
* Revert "Lock isAppBackgrounded state to false during tests"
This reverts commit 6a915a756a.
* Lock isAppBackgrounded state to false during tests
* Update tests
* Fix iOS 15 snapshot tests
* Remove unused isApplicationBackgrounded async method
---------
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
* 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`
<!-- 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.
### 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`
### 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.
### 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>
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.
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'
```
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.
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
### 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
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.
```
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
### 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`
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.
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.
## 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
```
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
```
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.
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)
### 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>