* 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>
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'
```
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
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`
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.
### Purpose:
This is a prerequisite for #2288. The new grace period means that if we
didn't update the date from the cached responses, entitlements would
become stale and expired after 3 days.
### Changes:
- Added `HTTPResponseBody.copy(with newRequestDate:)` to be able to
modify responses from the header request date
- `CustomerInfo` implements this method to modify its request date
- Added `HTTPResponse.requestDate` to be able to keep track of the
request date from the server
- Added `VerificationResult.from(cache:response:)` to determine the most
restrictive verification result based on what's cached or checked from a
response
- Added `HTTPResponse.copy(with:)` to modify the verification result of
a response using the previous method
- `HTTPClient` now uses the most restrictive verification result
- `HTTPClient` updates request date from server responses or cached
responses (unless verification failed). This was the missing piece for
#2288.
### Tests:
- Verify that `ETagManager` does not use an ETag if verification was
previously not enabled
- Verify that `ETagManager` returns the request date from the server
when returning a cached response
- Verify that `HTTPClient` updates the request date from the server or
from a cached response
- Verify that `HTTPClient` does not update request date if verification
failed
- Tests for `HTTPResponse` request date parsing
- Test for `CustomerInfoResponseHandler` updating request date
- Tests for `CustomerInfo.copy(with newRequestDate:)`
### Other smaller changes:
- Moved `CustomerInfo.asData()` into `Encodable.asJSONEncodedData()`
- Renamed `requestTime` to `requestDate` everywhere for consistency
Fixes [CSDK-629].
Depends on #2274 and #2267.
Entitlement verification is accomplished by preventing MiTM attacks
between the SDK and the RevenueCat server.
With verification enabled, the SDK ensures that the response created by
the server was not modified by a third-party, and the entitlements
received are exactly what was sent.
Entitlements are only verified if enabled using
`Configuration.Builder.with(entitlementVerificationMode:)`, which is
disabled by default.
### Changes:
- Added new `VerificationResult`, which replaces
`HTTPResponseVerificationResult`
- Renamed `Configuration.EntitlementVerificationLevel` to
`Configuration.EntitlementVerificationMode`
- `Configuration.with(entitlementVerificationMode:)` is now `public`
- Added `CustomerInfo.entitlementVerification`
- Added `EntitlementInfos.verification` and
`EntitlementInfo.verification`
- Added tests to cover serialization of these new values
- `CustomerInfoResponseHandler` copies the `ValidationResult` to
`CustomerInfo` (all operations that fetch `CustomerInfo` use this)
- Renamed "validation" to "verification" everywhere for consistency
- Finalized documentation
### TODO:
- [x] Figure out whether to use a separate type from
`HTTPResponseValidationResult` or rename that one
- [x] Figure out final API type and property names
- [x] Add tests for `CustomerInfo.copy(with:)`
- [x] Ensure validation result is encoded and decoded (for caching)
- [x] Add missing snapshots
[CSDK-629]:
https://revenuecats.atlassian.net/browse/CSDK-629?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
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
Looking into the leak in #2135, I realized that we have an operation
which doesn't call `completion`, we would be leaking the completion
block in the cache.
This ensures that it doesn't happen in tests. In production, we won't
risk crashing.
### 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`.
Fixes [CSDK-419].
This is an improvement over the fix in #1292. In there, we avoided
appending on an existing `CustomerInfoOperation` if there were any
`PostReceiptDataOperation`s in progress, because those might come back
with outdated entitlements.
This fix removes that cache key hack, and instead *reuses* the entire
response to that `PostReceiptDataOperation` by "stealing" that request's
cache key.
This is perfectly captured by the existing test
`BackendPostReceiptDataTests.testGetsUpdatedSubscriberInfoAfterPost`,
added in #1292. Amazingly enough, that test still passes with one minor
change: the entire process requires one fewer API call 🎉🐼
[CSDK-419]:
https://revenuecats.atlassian.net/browse/CSDK-419?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
We don't actually need to share a single queue to synchronize all the callback caches.
This new implementation also avoids using the risky `DispatchQueue.sync`.
This also fixes potential issues because `cachedCallbacksByKey` was exposed, and it could have been read without synchronizing access with the queue. This is now impossible.
* [CF-106] Fetch AdServices Token (#1519)
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
* [CF-553] Post AdServices token (#1534)
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
* Update ios 14 snapshots tets
* Reworked adservices to use new Purchases.shared.attribution API
* Updated with some comments from PR
* Add iAd code back (#1739)
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
* use new mapKeys dictionary extension for device cache
* update for some pr comments
* remove purchasestests
* generate missing snapshots
* fix deprecation messages to clarify method now called on shared instance
* Fix for inserting nonstring into userdefaults
* Add tests for bug
* fix lint
* Added availablility checks for tests and removed unused varialbes
* Whoops... wrong class
Co-authored-by: beylmk <maddie@revenuecat.com>
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
### 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`