### 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
This is a simple example, but on a larger JSON it becomes easier for us
to debug decoding the entire JSON rather than seeing only the part that
failed:
```
[codable] DEBUG: ℹ️ Encountered error when decoding JSON: {"e": "e3"}
[codable] ERROR: 😿‼️ Couldn't decode data from json, it was corrupted: Context(codingPath: [CodingKeys(stringValue: "e", intValue: nil)], debugDescription: "Cannot initialize E from invalid String value e3", underlyingError: nil)
```
### 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
We were escaping users for every (hopefully) request. This refactor
moves that responsibility to the path creation in `HTTPRequest.Path` to
ensure it's done consistently.
Additionally, this is required so that `SigningTests` produce a
consistent path (see #2746).
### 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`
### New format:
- 32 bytes: intermediate public key
- 4 bytes: Expiration (in days since epoch)
- 64 bytes: intermediate public key signature, signed with the root
private key
- 16 bytes: salt
- 64 bytes: payload signature:
- salt
- nonce (if present)
- request time (as int string)
- etag (if present)
- payload
This also adds support for optional nonces for "static" signatures,
which is required for #2667.
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>