* feat: disable postAttributionData requests when in UI preview mode
* tests: add unit test to check `AttributionPoster`'s new `clearPostponedAttributionData` method
* tests: add unit tests to check that `AttributionData` is never posted in UI preview mode
* refactor: simplify implementation of avoiding `AttributionPoster` requests in UI preview mode
* Send paywall events when the app is backgrounded.
* - Add tests for syncing paywall events when backgrounding
- Send events after a successful purchase + tests
* typo
* typo
* [Diagnostics] Add apple_products_request event
* Fixes from review
* Change tests to use to(beNil())
* Make MockDiagnosticsTracker sendable to fix tests
* Add new property to track storekit error type
* Use new method to process sk errors
### Description
This adds a new API `setKochavaDeviceId` that allows to set the kochava
device ID required for the Kochava integration.
Keeping on hold until backend support is added
**This is an automatic release.**
### New Features
* Paywalls with custom purchase and restore logic handlers (#3973) via
James Borthwick (@jamesrb1)
### Bugfixes
* Prevent paywall PurchaseHandler from being cleared on rerender (#4035)
via Josh Holtz (@joshdholtz)
* Update Purchase Tester for 5.0.0 (#4015) via Will Taylor
(@fire-at-will)
### Dependency Updates
* Bump fastlane from 2.221.0 to 2.221.1 (#3977) via dependabot[bot]
(@dependabot[bot])
### Other Changes
* Bring official `xcodes` back to CI (#4029) via Cesar de la Vega
(@vegaro)
* Paywalls tester with sandbox purchases (#4024) via James Borthwick
(@jamesrb1)
* Update v5 migration guide to contain current latest version (#4019)
via Toni Rico (@tonidero)
* CI Build Docs Improvements (#4014) via Will Taylor (@fire-at-will)
* Use available resource class for backend-integration-tests-offline-job
(#4013) via Will Taylor (@fire-at-will)
* Add `X-Preferred-Locales` header (#4008) via Cesar de la Vega
(@vegaro)
---------
Co-authored-by: Toni Rico <antonio.rico.diez@revenuecat.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
We had a report of a developer that is seeing that users (who are using
iOS 18 beta) are reporting app crashes on launch with 55 unique devices
so far. They are using using iOS SDK 4.17.7.
The line that is crashing is:
```
client = clientClass.perform(#selector(FakeAfficheClient.sharedClient)).takeUnretainedValue()
```
We just couldn’t have a single variable named `adClient` without an app
getting rejected, even if it wasn’t using it at all. We built the
`AfficheClient` so prevent app rejections.
It turns out Apple has removed documentation for `AdClient`, and by
looking into
https://developer.apple.com/documentation/iad/setting-up-apple-search-ads-attribution#overview
> After February 7, 2023, all requests made to the Apple Search Ads iAd
Attribution API will return with a value of "iad-attribution" = false,
or errors. See ADClient/requestAttributionDetails(_:). Use the
AdServices framework for current attribution integration with the Apple
Search Ads Campaign Management API for devices using iOS 14.3 and later.
Attribution isn't available for downloads and redownloads from devices
using iOS 14.2 or earlier.
So this wasn't even working anyways. I removed everything related but
left the public API in place to not break anyone.
### 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
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>
Attempting to fix some test flakiness.
Not the cleanest fixes, but flaky tests are dangerous.
My methodology was to run tests over and over and fix the ones that
would occasionally fail.
### Motivation
OneSignal is migrating to a new User Centric API (v11.0). Adding a new
reserved attribute to provide the new OneSignal User Id.
### Description
* Added $onesignalUserId reserved attribute with setter methods
### Description
StoreKit displays different types of in-app messages on app launch, but
it provides an API to disable that and show these messages at a
different time. This PR wraps those APIs to make them easier to use
through our SDK.
#### New APIs
- Add new configuration parameter to defer StoreKit's automatic in-app
messages
- Add new method in `Purchases` to display a list of specific message
types
- New `StoreKitMessageType` enum mapping to the types used by StoreKit
---------
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Filed `FB12653515` because `StoreKitUnitTests` hang when beginning:
> Runner never began executing tests after launching
This at least enables iOS 17 without StoreKit tests.
I moved the `Purchases.logLevel` change to the beginning of the test
too, which makes this more obvious: every test was creating 2 instances
of `MockDeviceCache`.
This also required setting
`MockCachingTrialOrIntroPriceEligibilityChecker` to `nil` because that
was making `IdentityManager` leak, and therefore one of the 2
`MockDeviceCache`s leak!
This method was a hack to deal with apps getting `didBecomeActive`
notifications in the middle of a purchase when the popups were
displayed.
This had several problems:
- It was only being used for StoreKit 1 purchases
- It wasn't used for StoreKit 2 purchases through the SDK
- It wasn't used for purchases initiated outside the SDK (like using the
upcoming paywall screens)
- It updated the cache timestamp to "now", including offerings, despite
not actually updating data
- It was an indirect and implicit way of solving a problem instead of an
explicit one
This solves the same by observing `willEnterForeground`, which only
happens when the app actually comes back from the background, and not
when it simply loses focus.
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)
This fixes the last edge-case for offline entitlements. Solves the
following scenario:
```swift
func testPurchaseWhileServerIsDownPostsReceiptWhenForegroundingApp() async throws {
let logger = TestLogHandler()
// 1. Purchase while server is down
self.serverDown()
try await self.purchaseMonthlyProduct()
logger.verifyMessageWasNotLogged("Finishing transaction")
// 2. Server is back
self.serverUp()
// 3. Request current CustomerInfo
let info1 = try await Purchases.shared.customerInfo()
try await self.verifyEntitlementWentThrough(info1)
// 4. Ensure transaction is finished
logger.verifyMessageWasLogged("Finishing transaction", level: .info)
// 5. Restart app
Purchases.shared.invalidateCustomerInfoCache()
await self.resetSingleton()
// 6. To ensure (with a clean cache) that the receipt was posted
let info2 = try await Purchases.shared.customerInfo()
try await self.verifyEntitlementWentThrough(info2)
}
```
This race condition happened when fetching `CustomerInfo` after we had
computed an offline `CustomerInfo` with a purchase: if the server is
still not aware of the purchase, then we lose that until some arbitrary
amount of time when `StoreKit` might decide to send the pending
transactions in the queue.
For that reason, instead of waiting, we proactively check now whenever
`CustomerInfoManager` fetches new info.
The refactors #2540 and #2542 where required to break a retain cycle.
Also thanks to #2534 we know for sure that there aren't any cycles.
Note that this is an iOS 15.0+ only feature. It would be useful to
better ensure consistency in older versions, but it's not strictly
required, as those versions don't support offline entitlements either.
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.
This was necessary for a follow-up refactor to #2540. The main change is
in `Backend`, the rest are test changes that are required now that the
closure is `@Sendable`.
Necessary refactor for #2533. This allows `CustomerInfoManager` to have
an instance of `TransactionPosterType` instead of the whole
`PurchasesOrchestrator`.
Essentially this becomes the hierarchy now:

### Changes:
- Created stateless `TransactionPoster`
- Extracted `handlePurchasedTransaction` into `TransactionPoster`
- The new implementation there is _mostly_ the same, with the one
difference that it's stateless. Instead of holding
`purchaseCompleteCallbacksByProductID`, that's done by
`PurchasesOrchestrator`. The methods there all take a callback, so
they're easier to use and don't require keeping a callback around.
- Created `PurchaseSource` to abstract the combo of `isRestore` and
`InitiationSource`. This is still messy using the deprecated
`allowSharingAppStoreAccount`, but at least that's abstracted out and
the new `TransactionPoster` won't deal with that deprecated property.
- I didn't mock `TransactionPoster`, which means that all existing tests
that check `PurchasesOrchestrator` continue working the same way. This
was on purpose to keep this refactor as simple as possible.
- This supersedes #2536: the change made there was now possible here
without any integration test failures, thanks to the fact that we can
process transactions independently without messing with the
`purchaseCompleteCallbacksByProductID` state now.
### Future improvements
This is just a start, there's a lot more than can be done to simplify
the `PurchasesOrchestrator` monster. The main thing would be to remove
all the custom code for `restorePurchases`, which is largely the same as
what's in `TransactionPoster` now.
### Notes:
Just posting this here for posteriority. I had to trace the call
hierarchy for both SK1 and SK2. This "block" is what's now in
`TransactionPoster`:
