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.
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
Added `rc_receipt_url` and `rc_receipt_file_exists`. This is useful when
debugging issues like #2558.
This goes from this:
> [Purchases] - ERROR: 💰 Product purchase for 'rc_1099_1w' failed with
error: Error Domain=RevenueCat.ErrorCode Code=9 "The receipt is
missing." UserInfo={NSLocalizedDescription=The receipt is missing.,
readable_error_code=MISSING_RECEIPT_FILE,
source_function=handlePurchasedTransaction(_:data:completion:),
source_file=RevenueCat/TransactionPoster.swift:96}
To this:
> [Purchases] - ERROR: 💰 Product purchase for 'rc_1099_1w' failed with
error: Error Domain=RevenueCat.ErrorCode Code=9 "The receipt is
missing." UserInfo={NSLocalizedDescription=The receipt is missing.,
readable_error_code=MISSING_RECEIPT_FILE,
source_function=handlePurchasedTransaction(_:data:completion:),
rc_receipt_url=file:///.../Build/Products/Debug/...app/Contents/_MASReceipt/receipt,
rc_receipt_file_exists=true,
source_file=RevenueCat/TransactionPoster.swift:96}
In this case, we can see the file does exist, so it must be empty.
### Changes:
- Created `PurchasedProductsFetcherType` to be able to mock and hold
fetchers, even on older devices
- Created `ProductEntitlementMappingFetcher` to abstract injecting
`ProductEntitlementMapping`
- Added logic to `CustomerInfoResponseHandler` to compute offline
`CustomerInfo` after a server error (see #2367). **This is used by all
`CustomerInfo` requests as well as `PostReceiptDataOperation`**.
- Wrote comprehensive `CustomerInfoResponseHandler` tests
### TODO:
- [x] Don't cache offline computed `CustomerInfo`: #2378
- [x] Integration tests: #2501.
### 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>
Fixes [CSDK-634].
## Changes:
- Created `HTTPResponseValidationResult`
- Added `HTTPResponseValidationResult` and `HTTPClient.ResponseHeaders`
to `HTTPResponse`
- Added new `ErrorCode.signatureVerificationFailed`
- Extracted `SigningType` to create `MockSigning` to test `HTTPClient`
without bothering with signatures
- Refactored `ETagManager` to use `HTTPResponse`, making it easier to
pass through response headers and verification results
- Stored `HTTPResponseValidationResult` in `ETagManager` cache
- `HTTPClient` handles signatures from response based on
`SystemInfo.responseVerificationLevel`
- Renamed `HTTPRequest.createIntegrityEnforcedRequestRequest` to
`HTTPRequest.createWithResponseVerification`
- Added lots of tests for this new behavior in `HTTPClient`
- Added lots of new tests for `ETagManager`
[CSDK-634]:
https://revenuecats.atlassian.net/browse/CSDK-634?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
See https://github.com/RevenueCat/purchases-hybrid-common/pull/278.
A valid error ended up as:
> failed: caught error: "Unknown error."
This change ensures that calling this method with an `NSError` does not
return `ErrorCode.unknownError`, and instead we extract the correct
error.
See https://github.com/RevenueCat/khepri/pull/4716
This makes that error `ErrorCode.invalidReceiptError` instead of
`.unknownBackendError`.
I've also added #2032 to improve error messages when these errors are
unknown in the future.
## Changes:
- `Error.localizedDescription` now matches the message being logged.
Improvements introduced in #1776, for example, now apply to the
description too.
- Logged `BackendError` messages now include underlying error.
## Examples:
The following errors are improved:
### `BackendError`:
- Before:
> 😿‼️ There was an unknown backend error.
- After:
> 😿‼️ There was an unknown backend error. Page not found
### API key error:
- Before:
> 😿‼️ There was a credentials issue. Check the underlying error for more
details.
- After:
> 😿‼️ There was a credentials issue. Check the underlying error for more
details. Invalid API key
### Attribute errors:
- Before:
> ["$email": "invalid"]
- After:
> One or more of the attributes sent could not be saved. ["$email":
"invalid"]
Improved these 2 logs:
> 🍎‼️ Error fetching offerings - The operation couldn’t be completed. (RevenueCat.OfferingsManager.Error error 0.)
Underlying error: The operation couldn’t be completed. (RevenueCat.BackendError error 0.)
> ⚠️ Attempt to update CustomerInfo from network failed.
The operation couldn’t be completed. (RevenueCat.BackendError error 0.)
These 2 now will display the underlying `NetworkError` instead of the opaque "(RevenueCat.BackendError error 0.)".
This is the last change from what was started with #1437. That PR introduced a few private error types, but for the errors returned in the public `APIs` we were still passing around `Error`s everywhere, which didn't provide any compile-time guarantees.
### Goals:
- Ensure that public APIs *only* return `ErrorCode`s wrapped with the additional `userInfo`, and not accidentally return other types like `SKError`, `BackendError`, `ErrorCode` directly, etc.
- Guarantee type-safety when passing errors around within our internal implementations instead of assuming certain thrown errors are of a particular type.
### Changes:
- Created `PurchasesError` for wrapping an `ErrorCode` with the additional `userInfo`.
- Changed returned errors from `Error` to `NSError`. This is a change in the API, but the new type is a more specific `Error` so it shouldn't lead to any changes in client apps.
- Because `PurchasesError` isn't implicitly convertible to `NSError`, this ensures that we don't return those values directly, and instead use `PurchasesError.asPublicError`.
- Changed `ErrorCodeConvertible` to a now more precise `PurchasesErrorConvertible`.
Thanks to this new type-safety this PR fixes at least 3 bugs where we were returning private errors instead of `ErrorCode`s.
### Testing:
Thanks to #1871 we know that the returned errors are still convertible to `ErrorCode` so users can `switch` over them.
Follow up to #1776. That PR added a lot of tests to cover error logging, and #1778 introduced a small error that wasn't covered: if attribute errors are empty, the `localizedDescription` could end up becoming `"[:]"`. This fixes that.
## Examples:
This was the underlying error returned (the output of `error.localizedDescription`):
#### Before:
> [:]
#### After:
> There was a credentials issue. Check the underlying error for more details.
And this is what was logged (notice that I also fixed `ErrorCode.invalidCredentialsError` to not log as an "Apple Error"):
#### Before:
> 🍎‼️There was a credentials issue. Check the underlying error for more details. [:]
#### After:
> 😿‼️ There was a credentials issue. Check the underlying error for more details.
This will help figure out the reason behind #1765.
We were currently ignoring the error returned by `ProductsManager`.
In the example below, we can see the underlying issue is `SKErrorDomain` code 5.
### Before:
> Error fetching offerings - The operation couldn’t be completed. (RevenueCat.OfferingsManager.Error error 1.)
### After:
> Error fetching offerings - The operation couldn’t be completed. (RevenueCat.OfferingsManager.Error error 1.)
_**Underlying error: The operation couldn’t be completed. (SKErrorDomain error 5.)**_
ERROR: 😿‼️ There is an issue with your configuration. Check the underlying error for more details. There's a problem with your configuration. None of the products registered in the RevenueCat dashboard could be fetched from App Store Connect (or the StoreKit Configuration file if one is being used).
More information: https://rev.cat/why-are-offerings-empty
### Before:
For errors with a custom `message` it turns out that we were never logging it.
This was extra confusing for `OfferingsManager.Error.configurationError`, which ended up displaying this:
> 😿‼️ There is an issue with your configuration. Check the underlying error for more details.
With no underlying details.
### After:
> 😿‼️ There is an issue with your configuration. Check the underlying error for more details.
> There's a problem with your configuration. None of the products registered in the RevenueCat dashboard could be fetched from App Store Connect (or the StoreKit Configuration file if one is being used).
> More information: https://rev.cat/why-are-offerings-empty
Fixes [CF-19].
This allows specifying `CustomerInfoManager` fetching behavior, both as a public API for users, and also internally.
Some future uses of `CustomerInfoManager` might want to ensure that we don't rely on cached data, for example.
Thanks to the `APITester` we know that the existing methods still work, this is purely an additive change.
This is the core of the change:
```swift
/// Specifies the behavior for a caching API.
@objc(RCCacheFetchPolicy)
public enum CacheFetchPolicy: Int {
/// Returns values from the cache, or throws an error if not available.
case fromCacheOnly
/// Always fetch the most up-to-date data.
case fetchCurrent
/// Returns the cached data if available and not stale, or fetches up-to-date data.
case notStaleCachedOrFetched
/// Default behavior: returns the cached data if available (even if stale), or fetches up-to-date data.
case cachedOrFetched
/// Default ``CacheFetchPolicy`` behavior.
public static let `default`: Self = .cachedOrFetched
}
```
The new `CustomerInfoManagerGetCustomerInfoTests` check all the different combinations of fetching behavior.
I've taken some of the existing tests and refactored them to be `async`, as well as added a bunch more tests.
> **Note**:
There is also one big hidden bug fix:
`Purchases.getCustomerInfo` now calls `BackendError.asPurchasesError`, so we don't end up returning an internal error type.
This was missed because we always return generic `Error` types, so we have no way to know at compile time that we only return `ErrorCode`s.
[CF-19]: https://revenuecats.atlassian.net/browse/CF-19?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
### Depends on:
- #1537
- #1540
- #1541
- #1543
- #1546
- #1547
- #1550
- #1551,
- #1565
### Changes
- Removed all custom deserialization code thanks to #1537, #1541, and #1543.
- The format of the customer info response is now very concise and only in one place (`CustomerInfoResponse`) instead of being passed through dictionaries and magic strings everywhere.
- `CustomerInfo` was combining the dictionaries inside of `"subscriptions"` and `"non_subscriptions"`, despite them having different formats. This is now made explicit through type conversions between the two, (see `CustomerInfoResponse.allTransactionsByProductId`).
- Removed all custom `CustomerInfo` errors since deserialization is automatic, and the underlying `Error` information will be provided thanks to `ErrorUtils.logDecodingError`.
- `CustomerInfo` deserialization always wraps errors into `ErrorCode.customerInfoError`, which is now also covered in a new test.
- Added a new snapshot test that covers `CustomerInfo` serialization by storing the result in a JSON file.
### Other changes:
- `CustomerInfo` equality is now automatic through the `CustomerInfoResponse` `Equatable` conformance.
- Simplified `schemaVersion` handling
- All tests still use `CustomerInfo(data: [String: Any])`, but that method is only visible for tests now, and it uses the underlying `Decodable` implementation (see `CustomerInfo+TestExtensions.swift`).
- Added extra tests for `CustomerInfoResponse` that use a fixture `CustomerInfo.json`, which is easier to maintain than having JSON in code.
- `RawDataContainer` implementation is changed in #1565.
### TODO:
- [x] Fix all tests
- [x] Update `RawValueContainer`
- [x] Consider recovering `CustomerInfoError`
- [x] Look at all previously logged errors
- [x] Finish `CustomerInfoResponseTests`
- [x] Handle all TODOs
- [x] Make `RawDataContainer` actually store all the original data: #1565
This is a special enough error that it's worth distinguishing from other `ErrorCode.networkError`s.
I see a couple benefits:
- When opening an app that has the SDK, this used to print lots of errors that just said "Error performing request". Now it's more clear what's happening
- Users can respond to failed calls to the SDK more appropriately, whereas before it might not be clear why a request failed.
Also fixed `logErrorIfNeeded` not passing the file/function/line parameters to the log functions.
Closes#695 and finishes [CF-195].
Depends on ~~#1431, #1432, #1433, #1437~~.
## Goals:
- [x] Handle all requests / response / deserialization errors in `HTTPClient`. Clients of `HTTPClient` will only have to handle the "happy" path: #1431
- [x] `HTTPClient` shouldn't return `Result.success` with failed responses, forcing clients to verify the response is actually a failure: https://github.com/RevenueCat/purchases-ios/pull/1431
- [x] Abstract error response deserialization / error creation: #1427
- [x] Abstract attribute error parsing: #1427
- [x] Move response deserialization to `HTTPClient` based on a `ResponseType: Decodable` type, so the completion block will simply return a `Result<HTTPResponse<ResponseType>, Error>`
- [x] `ETagManager` should store response `Data` instead of a deserialized `[String: Any]`
- [x] Improved error types in `HTTPClient` to fix tests: #1437
- [x] Dealt with non-backwards compatible `ETagManager: #1438
## Changes:
- Replaced `HTTPResponse`'s body from `[String: Any]` to a generic `HTTPResponseBody`.
- Created `HTTPResponseBody` to abstract `Decodable` and provide some default implementations for types like `Data,` `[String: Any]` (for backwards compatibility to types that aren't `Decodable` yet), and `Decodable` itself.
- New `HTTPResponse.Result` typealias (`Result<HTTPResponse<HTTPResponseBody>, Error>`) used everywhere. This will allow replacing `Error` with a more specific `Error` so we can forward known typed errors, and make sure that we don't end up with the wrong error type, or with a very complex error hierarchy and the details buried in `underlyingError`.
- Each layer (only a few for now) has its own error type: `NetworkError`, `BackendError`, `OfferingsManager.Error`.
- `HTTPClient` for example has to produce `NetworkError`, operations produce `BackendError`
- The parent `BackendError` can have specific errors like `.missingAppUserID`, but also be simply a child error `case networkError(NetworkError)`
- All of these conform to a `ErrorCodeConvertible`, so there is a single point of code that converts from simple and readable errors (like `BackendError.emptySubscriberAttributes`, `.unexpectedBackendResponse(.loginResponseDecoding)`) into errors with all the context using `ErrorUtils`
- Converted `DNSError` into `NetworkError.dnsError`. Its functionality remains unchanged.
- Removed `Backend.RCSuccessfullySyncedKey` and `ErrorDetails.finishableKey` in favor of tested properties on `NetworkError`
### Leftovers
There's a few things I've decided to not finish for now:
- [ ] `CustomerInfo` still does't conform to `Decodable` (manual deserialization is still supported by the current system): #1496
- `SubscriberAttributeDict` could also be `Codable`
- [x] Replace `OfferingsFactory` with `Decodable`: #1435
[CF-195]: https://revenuecats.atlassian.net/browse/CF-195?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
Our mapping between SK1/SK2 errors and `ErrorCode` was inconsistent, so that test was only passing for SK2.
This PR addresses that. It also makes use of `ErrorUtils` methods to ensure that the underlying error is added to the returned errors.