* Prevent duplicate post receipt requests caused by timestamp-only attribute changes in the `consentStatus` SubscriberAttribute
* unit tests
* More tests
* Simplified MockDateProvider
* Add unit test covering edge case
* Fix lint
* Introduce PurchaseParams to allow passing extra configuration info when making a purchase
* simplify naming
* fallback to old method when flag is not enabled
* Add $(inherited) to the test targets so they pick up compiler flags from Local.xcconfig
* Include product/package inside PurchaseParams
* fix tests
* add compiler flag check
* update purchase tester
* fix docs
* add tests
* Update Sources/Networking/Operations/PostReceiptDataOperation.swift
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
---------
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
When compiling with Xcode 16, we started throwing a bunch of new warnings. Here's a list and explanation of each:
- The most common one was needing to explicitly restate the @unchecked Sendable conformance from a parent class.
- There were a couple switches where a new entry was added in tvOS but only for iOS 18, and needed to add it to make it exhaustive.
- There we a couple of places where we were adding a protocol conformance to a class from a different module e.g. extension DispatchTimeInterval: Comparable. This is considered unsafe now because the owners of the module could add the conformance later. Solved by removing the conformance which wasn't being used widely in the codebase, only in tests.
- Finally there was a class where we were capturing SKRequest and SKProductsRequest instances in a sendable block, but only to either log something, or use them as dictionary key, so I just made the classes Sendable (with the new @retroactice attribute because they belong to a different module)
### Motivation
Get more context with targeting information
### Description
- Parse targeting information into `Offerings`
- Add targeting context into `Offerings.current`
- Add targeting context into `getCurrentOffering(forPlacement: String)`
- Post targeting context in post receipts
---------
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
### Motivation
New function to get `Offering` by a placement identifier
### Description
New `currentOffering(forPlacement: String): Offering?` method on
`Offerings`
- Added demo button for this in purchase tester app
```swift
// Example usage
if let offering = offerings.currentOffering(forPlacement: "onboarding") {
showPaywall(offering)
}
```
#### Logic flow
- If a placement identifier explicitly exists within the
`placements.offeringIdsByPlacement` dictionary, the method attempts to
fetch the corresponding `Offering` without resorting to a fallback
- Example: when there is a `nil` / null value for a placement, we don't
want to use the fallback
- If the placement identifier does not exist, the method then attempts
to fetch a fallback offering defined by `placements.fallbackOfferingId`
#### New placements object in offerings response
```js
{
// other offerings stuff
"placements": {
"fallback_offering_id": "offering_a", // can be null
"offering_ids_by_placement": {
"onboarding_start": null,
"onboarding_end": "offering_b",
}
}
}
```
---------
Co-authored-by: Guido Torres <guido@revenuecat.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
I've moved `PaywallViewMode` from `RevenueCatUI` to `RevenueCat`. This
uses the same approach as #2645. This method is `public` so it can be
used from `RevenueCatUI`, but the documentation stays its purpose.
The behavior is covered with integration tests and a snapshot test.
Depends on https://github.com/RevenueCat/khepri/pull/6364.
This adds tests and a fix for a scenario discovered by @aboedo and
@mcastany.
### Timeline:
- Make a purchase of product_1
- Close app
- Renewals happen in the background
- Subscription expires
- Open app, renewals are in the queue but not processed yet
- Start a purchase of `product_1` (`SKPaymentQueue.add`)
- StoreKit queue shows renewal from step 3 as finished, it's expired
- SDK sees finished transaction in the queue for `product_1`
- SDK sees completion block for `product_1`, considers it a successful
`.purchase`
- SDK posts receipt, `initiation_source: .purchase` 💥
- Backend replies with 422 because StoreKit hasn't processed the
transaction from 6
### Fix:
The fix looks at the date when the completion block was added (the time
`.purchase()` is called).
If the transaction was created _before_ this, we know it didn't come
from the actual purchase, and therefore it should be identified as
`.queue`.
### 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
### 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`
Cherry-picked #2549 post refactors (#2540 and #2542). This becomes a lot
simpler since #2542, thanks to the fact that we can put this new
parameter in `PurchasedTransactionData`.
### Changes:
- Added `aadAttributionToken` to `PurchasedTransactionData`
- Exposed `AttributionPoster.adServicesTokenToPostIfNeeded`
- Added snapshot test to verify it's sent
- Added `PurchasesOrchestrator` tests (SK1/SK2) for sending the
attribution token
- Added missing `PurchasesOrchestrator` tests for sending attributes
- Added log when marking `AdServices` token as synced
- Exposed `Purchases.attribution` for custom entitlement computation
framework (and added to API tester)
- Exposed `Purchases.enableAdServicesAttributionTokenCollection` for
custom entitlement computation framework (and added to API tester)
Follow up to #2540.
This was necessary to ensure that we can implement #2533 without
reference cycles.
This was the hierarchy before this change:

The red arrows are what this PR removes.
## Changes:
- `TransactionPoster` no longer has references to `IdentityManager` and
`Attribution`
- `markSyncedIfNeeded` is no longer in `TransactionPoster`, moved back
to `PurchasesOrchestrator`
- Changed `TransactionPoster`'s callback to return `Result<CustomerInfo,
BackendError>`, providing greater flexibility for users to be able to
read the specific errors.
- Extracted `PurchasedTransactionData` to be reduce the number of
parameters passed around. This removed the need for 2
`swiftlint:disable:next function_parameter_count`. This is the majority
of this diff. It was helpful to combine in this PR because I needed to
add another parameter to the flow: `unsyncedAttributes`, since
`TransactionPoster` could no longer read these directly.
### 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.
Should fix#2198 and [SDKONCALL-200]. This is a big performance
optimization in time and memory, as we no longer need to convert the
entire receipt to `base64` and concatenate it to the string, which is
what was likely causing #2198.
This now uses `Data.hashString`, which avoids any data copies and
efficiently creates a hash with the receipt content.
Apart from the new tests for this property,
`BackendPostReceiptDataTests.testDoesntCacheForDifferentReceipts`
ensures that different receipts have different cache keys, and therefore
this new implementation still avoids collisions (other than the
extremely unlikely hash collisions).
[SDKONCALL-200]:
https://revenuecats.atlassian.net/browse/SDKONCALL-200?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
### Changes:
- Created `CacheableNetworkOperationFactory` to abstract operation and
cache key creation
- Instead of always creating an operation, we create a factory first,
which pre-computes the cache key
- This cache key is used to determine if it's a cache hit, as well as
for the subsequently created operation
I'm still investigating a leak (#2105), and I suspect it's coming from
us creating and storing completion blocks, and those staying in memory
beyond the lifetime of the operation.
This refactor and performance improvement helps us avoid get more things
in memory than we need to.
See #2138 for how this is tested.
This was fine when we only had `debug` logs, but since #2080 we need to
check for `verbose` too.
Thanks to #2102 we can just use `<=`, because `LogLevel` conforms to
`Comparable`.
### Motivation:
Initially I wanted to make `SubscriberAttribute.asDictionary` work using `Codable`. However, this isn't the right refactor because we need the actual raw types to be encoded inside `UserDefaults`.
At least this refactor provides several improvements:
### Changes:
- `SubscriberAttribute` now has value semantics
- ⚠️ `SubscriberAttribute.isSynced` used to be mutable, with reference semantics, which was dangerous
- `SubscriberAttribute: Equatable` implementation is now automatic
- Cleaned up `SubscriberAttribute.asDictionary` implementation, extracting keys into an `enum`
- Moved `Dictionary` -> `SubscriberAttribute` creation to the type itself
- ⚠️ `SubscriberAttribute.init(dictionary:)` returns an `Optional` instead of using force-unwrap, avoiding potential crashes
- Added more serialization / deserialization tests
- Removed `SubscriberAttributesMarshaller` and moved method into `SubscriberAttribute` extension
- Moved `SubscriberAttributeDict` into `SubscriberAttribute.Dictionary`
Extract out various dependencies that are required to instantiate the `Backend` object.
Refactor to assist with the next few PRs that extract out other pieces
See [CSDK-82]
Resolves [CSDK-155]
* Resolves [CF-443]
* Adds `configureWithConfiguration` to `RCPurchasesAPI.m`
* Create Configuration object and API to configure Purchases
* Update API doc for new Purchases API
* Add example code to Configuration documentation
* Adds deprecations for various Purchases.configure methods
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
For #695.
The biggest change is that this code at the beginning of `HTTPClient.handle` is now gone:
```swift
var statusCode: HTTPStatusCode = .networkConnectTimeoutError
var jsonObject: [String: Any]?
var httpResponse: HTTPResponse? = HTTPResponse(statusCode: statusCode, jsonObject: jsonObject)
var receivedJSONError: Error?
```
It was hard to trace which paths modified these default values. The new implementation has a mostly linear path (part of it abstracted in smaller methods now) and doesn’t have mutable values.
This is the new `handle` implementation:
```swift
func handle(urlResponse: URLResponse?,
request: Request,
urlRequest: URLRequest,
data: Data?,
error networkError: Error?) {
let (response, shouldRetry) = self.parse(
urlResponse: urlResponse,
request: request,
urlRequest: urlRequest,
data: data,
error: networkError
)
if shouldRetry {
Logger.debug(Strings.network.retrying_request(httpMethod: request.method.httpMethod,
path: request.path))
self.state.modify {
$0.queuedRequests.insert(request.retriedRequest(), at: 0)
}
} else {
request.completionHandler?(response)
}
self.beginNextRequest()
}
```
## All Changes:
- `HTTPClient.perform` no longer has a bunch of mutable variables with forking paths that make it hard to follow. Most of the response handling is now in a new `parse` method. This will simplify the task of continuing to refactor response deserialization.
- `HTTPClient`’s completion abstracts response into `HTTPResponse`
- `UserInfoAttributeParser` is now an `enum`
- `ETagManager` no longer takes an `Error`. The only thing it did with it is to return the default response.
- `HTTPResponse.jsonObject` is no longer `Optional`
- Moved `defaultJsonEncoder` / `defaultJsonDecoder` to `JSONDecoder+Extensions`. They used by `HTTPClient` and others soon.