### Changes:
- `InternalAPI` is now `Sendable` to deal with new compilation error
- `PaywallEventStoreType` is now `Sendable`
- Extracted `SK2BeginRefundRequestHelperType` to make
`SK2BeginRefundRequestHelper` `Sendable`
- Updated `MockSK2BeginRefundRequestHelper` to be thread-safe and
`Sendable`
- Fixed compilation error in `DebugContentViews`
- Fixed compilation error in `DebugViewModel`
- Fixed compilation error in `StoreMessagesHelper`
### Context:
`#if os(iOS)` no longer evaluates to `true` when compiling for
`visionOS`
Which means we need `|| os(xrOS)`.
But to support older Xcode we need `#if swift(>=5.9) && os(xrOS)`.
But, fun fact, short-circuiting doesn’t work in `Xcode 13`.
And we still need that to run iOS <15 tests (`FB12223404` /
`FB12805891`)
💩
This works around that by defining our own `VISION_OS` flag.
### Changes:
- Added `VISION_OS` compile time flag to determine SDK
- Replaced `#if os(xrOS)` with `#if VISION_OS`
- Added flag to `Package.swift` (thanks @neonichu for helping with this)
Currently when launching an app in SK1 mode, if there are multiple
transactions in the queue, we handle them all and begin operations in
parallel. If the cache keys are the same, those end up being a combined
POST receipt request.
However with `StoreKit 2`, because we were waiting to handle each one of
the `StoreKit.Transaction.updates` using `await`, we ended up posting
them all sequentially, instead of relying on `CallbackCache`.
This also adds a lot more coverage for `StoreKit2TransactionListener` to
verify this behavior.
This is a preparatory refactor for an upcoming fix.
This improves the thread-safety of this class, and also refactors its
mock to use a `protocol` instead.
I realized we currently don't log anything, and we go straight to
posting the receipt. It would be useful for debugging if we know why
receipts are being posted.
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.
This extracts the logic from `PurchasedProductsFetcher` and adds tests.
It will be used in #2536.
Also exposed and combined the duplicated `underlyingTransaction`
implementations.
Fixes SDK-3084

## Changes:
- Added new `ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION` compile time flag
- Hidden `configureInCustomEntitlementsModeWithApiKey` behind
`ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION`
- Added new `RevenueCat_CustomEntitlementComputation` SPM target and
library
- Added CircleCI job for `swift build --target
RevenueCat_CustomEntitlementComputation`
- Added new `SPMCustomEntitlementComputationInstallation` using the new
`configureInCustomEntitlementsComputationMode`
- Added CircleCI job for `SPMCustomEntitlementComputationInstallation`
- Added `FrameworkDisambiguation.swift` to avoid using `RevenueCat.`
disambiguation for files that
- Changed API testers to only use the new methods if
`ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION` is defined
- Updated `DangerousSettings` to not expose
`customEntitlementComputation` publicly. The only way to use that now
with `public` API is through
`Purchases.configureInCustomEntitlementsModeWithApiKey`
- Updated sample app to use new library
### Motivation
I am seeing errors in my logs that are not very helpful:
`2023-02-14 04:20:35.356 Nihongo[96412:1082124] [Purchases] - DEBUG: ℹ️
Store products request failed! Error: The operation couldn’t be
completed. (RevenueCat.ProductsFetcherSK2.Error error 0.)`
It looks like there is an error being thrown by
`StoreKit.Product.products(for: identifiers)`, but it is wrapped in
`productsRequestError` and so no information from the actual underlying
error is being printed to the logs.
### Description
I tried to follow the pattern I saw in other parts of the framework
(specifically, `PurchasesDiagnostics.swift`) to update the localized
description of the error to print the localized description of the
underlying error.
I don't think there's any unit testing needed for this change, since it
should just update the debug logging, but let me know if you disagree.
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
Similarly to #2262, test execution ordering was leading to [flaky code
coverage
results](https://app.codecov.io/gh/RevenueCat/purchases-ios/pull/2262)
with this.

Because we didn't have any test coverage, only if some other test
changed the `Storefront`, this test was calling the delegate code.
This adds explicit test coverage using a `MockAsyncSequence`.
### Other changes:
- Added `StorefrontType` to `StoreKit2StorefrontListenerDelegate` method
- `Storefront` initializers are no longer `Optional` `init?`
- Simplified `StoreKit2StorefrontListenerTests` by moving
`AvailabilityChecks` to `setUp`
Follow up to #1907.
This logic is now implemented and shared in `CachingProductsManager`.
I've decided to keep the logic in `ProductsFetcherSK1` because it's
intertwined with the retry mechanism, and it wasn't trivial to remove
caching while leaving that logic.
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 #1583. Subtask of #1382.
### Description
This PR aims to invalidate and re-fetch if appropriate the stuff in the cache when the Storefront changes. I've used both `OfferingsManager` and `ProductsManager` to manage the logic of invalidate and re-fetch the data.
Changes:
- Add `invalidateAndReFetchCachedOfferingsIfAppropiate` method to `OfferingsManager` class.
- Rename `clearCachedProducts` method to `invalidateAndReFetchCachedProductsIfAppropiate` in `ProductsManager` class.
- Add `OfferingsManager` as a dependency of `PurchasesOrchestrator`.
- Clear cache methods returns the identifiers of the elements that have been removed or an empty list if there was no stuff in the cache.
- Add tests to cover some scenarios.
Fixes [CSDK-122].
Before this change, transactions were finished separately for SK1 and SK2. Now this is combined through a common method in `StoreTransactionType`.
Follow up to #1557. Subtask of #1382
### Description
This PR aims to clear the stuff in the cache when the Storefront changes.
Changes:
- Add `clearCachedOfferings` method to `DeviceCache`.
- Add `clearCache` method to `ProductFetcherSK1`.
- Make `internal` the `clearCache` method in `ProductsFetcherSK2`.
- Handle the **Storefront** changes in the `PurchasesOrchestator`.
- Add tests to cover some scenarios.
Fixes [SDKONCALL-19].
SK2 purchases were always calling `syncPurchases`, which called `Backend.post(receiptData:...)` with no `productData`, which meant that after every SK2 purchase we never posted the price.
This refactor makes a lot of methods in `PurchasesOrchestrator` use `StoreTransaction` so they can be used during SK2 purchases as well. Specifically, `handlePurchasedTransaction` is now called after both SK1 and SK2 purchases.
### Other changes:
- `StoreKit2TransactionListenerDelegate` no longer needs to return `CustomerInfo`
- `StoreKit2TransactionListenerDelegate.handle(purchaseResult:)` no longer notifies the delegate (only done by `StoreKit.Transaction.updates`), which was leading to duplicate `syncPurchases` calls.
- If the purchase is cancelled, `syncPurchases` isn't called. Instead, the `CustomerInfo` is fetched from `CustomerInfoManager`
[SDKONCALL-19]: https://revenuecats.atlassian.net/browse/SDKONCALL-19?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
* Listen changes via `paymentQueueDidChangeStorefront` method
* Create `StoreKit2StorefrontListener.swift` to listen to Storefront changes for SK2
* Update `PurchasesOrchestrator` to listen to the Storefront changes
* Include `StoreKit2StorefrontListenerTests` in StoreKitTests
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
* Remove force unwrap
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
* Add required spaces
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
* Remove useless cancel method when the listener is released
* Notify Storefront changes in the main thread
* Add cancel task when the Listener is released
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
`StoreKit2TransactionListener` was triggering an asynchronous call to `syncPurchases` during transaction verification, and at the end of every purchase, another `syncPurchases` call was being made.
This refactor combines both calls into one by making `finish(transaction:)` wait for that call to finish.