* other(networking): don't switch hosts on device-connectivity errors
Add `NetworkError.isDeviceConnectivityError` (true only for device-side
URLError codes) and narrow the fallback-host retry gate to
`isTransient && !isDeviceConnectivityError`, so a device that is offline
no longer triggers pointless host switching. Host-side and ambiguous
failures stay switch-eligible.
Co-authored-by: Cursor <cursoragent@cursor.com>
* other(networking): make isDeviceConnectivityError doc less usage-specific
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(ads): move rewardverification to coresdk
* refactor(ads): move rewardverification to coresdk
* refactor(ads): move rewardverification to coresdk
* refactor(ads): remove unnecessary comment
* fix(ads): linter and behavior fixes
* fix(ads): gate reward-verification VC cache invalidation out of CustomEntitlementComputation
* refactor(ads): rename reward-verification transactionId → clientTransactionID
* fix(ads): remove duplicated SSV payload logic from AdMob adapter
* chore(ads): cover install via injectable
* chore(ads): fix linter
* refactor(ads): introduce RewardVerificationToken
* refactor(ads): add reward_verification_completed log
* Update AdapterSDKs/RevenueCatAdMob/Tests/RevenueCatAdMobTests/PresentRewardVerificationTests.swift
Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
* fix(ads): linter
* fix(ads): key reward verification poll retries on HTTP status
Retry network/offline transport errors and any HTTP 5xx during reward
verification polling, keying on the HTTP status code rather than the
mapped ErrorCode. Empty/unparseable 5xx responses are now retried, and
4xx client errors fail fast as .backendError instead of being retried to
a misleading .timeout.
* refactor(ads): drop redundant comments on poll retry classifier
* refactor(ads): read poll HTTP status via typed NSError accessor
* refactor(ads): reuse SDK retry classification for poll errors
Classify reward-verification poll retries from the structured BackendError
instead of re-deriving the HTTP status from a flattened public NSError.
Adds a reusable NetworkError.isTransient (5xx and connection-level failures
are transient, 4xx are terminal) and forwards it from BackendError; the
HTTPClient fallback-host retry check now delegates to it. The poll path keeps
the structured BackendError via an internal fetchRewardVerificationStatus,
letting the poller reuse BackendError.isTransient and dropping the bespoke
NSError.httpStatusCode accessor.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(ads): switch from experimental to internal
* docs(ads): reference BackendError in Poller doc comment
---------
Co-authored-by: Peter Porfy <peter.porfy@revenuecat.com>
Co-authored-by: Pol Miro <polmiro@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(workflows): don't restore stale workflows from disk on a 4xx
The getWorkflowsList failure branch restored the cached list + details
on any error. That's right for transient errors (5xx / offline) but
wrong for a 4xx, where the backend is authoritatively rejecting the
request (workflows disabled, unauthorized), so we shouldn't serve stale
prefetched data.
Generalize the existing offerings classification into
BackendError/NetworkError.shouldFallBackToCache (the offerings property
now delegates to it, no behavior change) and gate the workflows disk
restore on it. 4xx skips the restore; 5xx and connection failures still
restore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): fill gaps found in code review of 4xx stale-restore fix (#6948)
- testGetWorkflowsListCallsOnCompleteOnClientError: verifies onComplete()
fires on 4xx even though disk restore is skipped (callers must not block)
- testGetWorkflowsListRestoresDetailsFromDiskOnServerError: verifies that
on 5xx both list mapping AND workflow details are restored from disk, not
just the list mapping
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: drop redundant shouldFallBackToCachedOfferings alias
Per review: shouldFallBackToCachedOfferings was only a thin delegate to
shouldFallBackToCache after the prior refactor. Remove it on NetworkError
and BackendError and point callers (OfferingsManager, tests) at
shouldFallBackToCache directly. Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
* Use offerings cache on (most) network errors
* Add unit tests for `NetworkError`'s `shouldFallBackToCachedOfferings`
* Add unit tests for `shouldFallBackToCachedOfferings`
* Fix lint
* Add some more unit tests
* Add `isLoadShedderResponse` and `isFallbackURLResponse` properties to `VerifiedHTTPResponse`
Keeps the values also when returned by the ETagManager
* lint
* Add some tests for ETagManager
* Rename `isFallbackURLResponse` to `isFallbackUrlResponse` to prevent decoding issues
* Add tests for actual source of original cached response of ETagManager
* Add tests to HTTPClientTests regarding the new booleans
* lint
* Add `originalSource` to `CustomerInfo`
* Adds `isLoadedFromCache` boolean to CustomerInfo
* Add docs
* Unit tests for new customer info properties
* Make `OriginalSource.CustomerInfo` a `String` enum
* Add decoding tests for Customer Info with new original source field
* Add `.loadShedder` check for Customer Info obtained in Load Shedder integration tests
* Update fixture
* improve internal docs
* Add intermediate object `Offerings.Contents` to wrap `OfferingsResponse` + `OriginalSource` and `loadedFromCache`
* Adapt existing unit tests to changes in `Offerings` underlying objects
* Check Offerings original source in existing integration tests
* Fix StoreKit unit tests
* Fix RevenueCatUI tests
* More Offerings.Contents tests
* Fix test
* Add more tests
* Fix lint
* [skip ci] Generating new test snapshots (#5741)
* [skip ci] Generating new test snapshots (#5742)
* [skip ci] Generating new test snapshots (#5743)
* [skip ci] Generating new test snapshots (#5744)
* [skip ci] Generating new test snapshots (#5745)
* [skip ci] Generating new test snapshots (#5746)
* [skip ci] Generating new test snapshots (#5747)
* [skip ci] Generating new test snapshots (#5748)
* Use enum for `HTTPResponseSource`
* Update tests
* Adapt to changes in `VerifiedHTTPResponse`
* Lint
* Adapt to changes in `VerifiedHTTPResponse`
* update tests
* Add log when a request to the fallback URL was handled by load shedder (which should never happen)
* rename `HTTPResponseSource` to `HTTPResponseOriginalSource`
* Adapt to changes
* fix lint
* Adapt to changes
* Fix CustomerInfo's OriginalSource implementation + fix unit test
* Fix tests compilation
* Add Cached offerings integration test
* Fix RCUI compilation
* Move isFromCache boolean from `Offerings.Contents` to `Offerings`
* Expand `ForceServerErrorStrategy` functionality
* Improve log when faking error response
* Correct implementation of fake error response without making a request (ForceServerErrorStrategy)
* Update tests + more tests
* Fix compilation of `OfferingsManagerTests`
* Fix compilation of `DebugViewSwiftUITests`
* Improve tests
* Fix `OfflineWithNoMappingStoreKitIntegrationTests`
* FIx compilation
---------
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.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.
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
## 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
```
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
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.