Files
purchases-ios/Sources/Networking/Caching/CallbackCache.swift
T
5f9143563c Improve accuracy of transactions origin Part 8: sync cached local transaction metadata (#6073)
* Small cleanup

* add `LocalTransactionDetailsStorage`

* Add models for local transaction metadata caching. Reset PurchasesOrchestrator

* Complete `Codable`, lossless implementation of `LocalTransactionMetadata`

* Rename `LocalTransactionMetadataCache` to `LocalTransactionMetadataStore`

* Comment

* lint

* Remove unnecessary import

* Remove unused method

* Replace `Storefront` with `storeCountry` in both `ProductRequestData` and `PurchasedTransactionData`

This removes the need of a `StorefrontEncodedWrapper` object

* Disambiguate `asDictionary()` method

Now that `SubscriberAttribute` is `Codable`, `Encodable`'s `asDictionary()` and `SubscriberAttribute`'s `asDictionary()` were clashing

* Undo `Codable` conformance of `PresentedOfferingContext` and `TargetingContext`

* lint

* `LocalTransactionMetadata` now refers to a single transaction. `LocalTransactionMetadataStore` now stores one file per transaction and does not use an in-memory cache

* Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review

* Revert "Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review"

This reverts commit d958601711.

* Small code cleanup

* Remove `EncodedAppleReceipt` from `LocalTransactionMetadata`

* Undo `Codable` conformance of `EncodedAppleReceipt`

Not needed for now

* Remove `self.appTransactionJWS` from `LocalTransactionMetadata`

* Adapt to API changes in `SynchronizedLargeItemCache`

* Plug in `LocalTransactionMetadataStore` and store + get stored `LocalTransactionMetadata`

* Removed unused parameter

* Remove `schemaVersion` property from `LocalTransactionMetadata`

* Add `MockLocalTransactionMetadataStore` for tests

* Remove `appUserID` from `PurchasedTransactionData`

* Update tests

* Add `transaction_id` field to POST receipt

* adapt tests

* Add transaction ID parameter to cache key for `PostReceiptDataOperation`'s factory

* [skip ci] Generating new test snapshots (#6024)

* [skip ci] Generating new test snapshots (#6025)

* [skip ci] Generating new test snapshots (#6027)

* [skip ci] Generating new test snapshots (#6026)

* [skip ci] Generating new test snapshots (#6028)

* [skip ci] Generating new test snapshots (#6029)

* [skip ci] Generating new test snapshots (#6033)

* [skip ci] Generating new test snapshots (#6032)

* Fix lint

* Only send transaction ID when there's associated metadata

* Fix tests compilation

* Add unit test

* Add tests

* Add tests for `LocalTransactionMetadataStore`

* Have `PurchasesOrchestrator` post transaction with syncing purchases in SK2 go through `TransactionPoster` to include the `LocalTransactionMetadata` logic there

* Correct values for `purchaseCompletedBy` field in `PostReceiptDataOperation`

* Fix finish transaction logic

* Add more tests and fix test compilation

* More tests

* [skip ci] Generating new test snapshots (#6057)

* [skip ci] Generating new test snapshots (#6050)

* [skip ci] Generating new test snapshots (#6051)

* [skip ci] Generating new test snapshots (#6052)

* [skip ci] Generating new test snapshots (#6053)

* [skip ci] Generating new test snapshots (#6054)

* [skip ci] Generating new test snapshots (#6055)

* [skip ci] Generating new test snapshots (#6056)

* Fix lint

* Remove TODO

* Remove redundancy

* Fix tests and add test

* [skip ci] Generating new test snapshots (#6059)

* [skip ci] Generating new test snapshots (#6060)

* [skip ci] Generating new test snapshots (#6061)

* [skip ci] Generating new test snapshots (#6062)

* [skip ci] Generating new test snapshots (#6063)

* [skip ci] Generating new test snapshots (#6064)

* [skip ci] Generating new test snapshots (#6068)

* [skip ci] Generating new test snapshots (#6069)

* Add `transactionId` to `LocalTransactionMetadata`

* Add `getAllStoredMetadata()` method to `LocalTransactionMetadataStore`

* Cleanup stored metadata file if it fails to be read or decode

* Fix lint

* Simplify parameters in `handlePostReceiptResult` method in `PurchasesOrchestrator`

The same subscriber attributes were already included in the `PurchasedTransactionData` object

* Fix recovering `PresentedOfferingContext` from `LocalTransactionMetadata`

* Add `EncodedAppleReceipt` to `LocalTransactionMetadata`

* Take `source: PurchaseSource` out from `PurchasedTransactionData`

* Rename `PurchaseSource` to `PostReceiptSource` and `ProductRequestData.InitiationSource` to `PostReceiptSource.InitiationSource`

* Pass receipt as parameter directly

* Fix lint

* Remove unused variable

* Add internal interface to sync cached local transaction metadata

* Adapt unit tests

* Trigger cached transaction metadata sync on `applicationWillEnterForeground`

* fix test with previous value in snapshot

* Fix compilation of tests

* Introduce `sdk_originated` parameter in POST /receipt

* Adapt tests to new `sdkOriginated` property

* TEMP: Remove sdkOriginated field (to split into 2 PRs)

* Revert "TEMP: Remove sdkOriginated field (to split into 2 PRs)"

This reverts commit 6d361fde30.

* Remove sdkOriginated field as we won't be adding it for now

This reverts commit e4989a3d33.

* Store transaction metadata when offering context or paywall is present

* Fix PaywallEvent initialization in tests

* Use variable

* Cache CustomerInfo when receiving responses from cached transaction metadata syncs

* Add more tests

* Fix tests

* Move syncing of cached local metadata to `applicationDidBecomeActive`, which does happen on cold app launches

* Prevent concurrent cached transaction metadata syncs

* Add tests for applicationDidBecomeActive syncing cached transaction metadata

* Small improvements

* Perform sync in `applicationWillEnterForeground` + at `configure` time

* Fix tests after merge from main

* Fix and improved tests

* Fix tests

* test improvements

* Use `transactionId` in `PostData` as cacheKey only if it `containsAttributionData`

This means that POST /receipt for renewals are deduplicated, while purchases with attribution data are guaranteed to always be sent (regardless of whether there's an in-flight POST /receipt request for a renewal of that purchase)

* Adapt tests to new behavior + new tests

* Update Sources/Networking/Operations/PostReceiptDataOperation.swift

Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>

* Do not clear `LocalTransactionMetadata` when receiving offline-computed CustomerInfo

* Add tests

* Fix test + add test

* Fix test

* Fix lint

* Fix test compilation

* Improve comment

* Fix codable `CodingKeys` to not use snake case in rawValues As the `JSONEncoder.default` uses `convertToSnakeCase` `keyEncodingStrategy`

This allows decoding using the `JSONDecoder.default (which uses `convertFromSnakeCase` `keyDecodingStrategy` to correctly decode the JSONs resulting from encoding with `JSONEncoder.default`.

* Add tests

* Improve comment

* Add `Codable` idempotency tests for `EncodedAppleReceipt`

* Add tests for encoding/decoding `LocalTransactionMetadata`

* Have `LocalTransactionMetadataStore` use a `.persistence` `DirectoryType` instead of a `.cache` one

* Add E2E tests for `LocalTransactionMetadataStore`

* Fix test

* Add some logs and reduce some flakiness of tests

* Remove test (will be added in a future PR after a small rewrite)

* Reduce test flakiness

* Change directory name

* Perform cached transaction metadata sync without delay on tests

* Remove unnecessary helper in tests

* Fix deadlock

* Inject `applicationSupportDirectory` to `LocalTransactionMetadataStore` for more reliable tests

* Fix lint

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
2026-01-26 19:59:45 +01:00

76 lines
2.8 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// CallbackCache.swift
//
// Created by Joshua Liebowitz on 11/18/21.
import Foundation
/**
Generic callback cache whose primary usage is to help ensure API calls in flight are not duplicated.
Users of this class will store a completion block for any Cacheable API call that is running. If the same request is
made while a request is in-flight, the completion block will be added to the list and the API call will not be
performed. Once the first API call has finished, the user is required to call `performOnAllItemsAndRemoveFromCache`.
This way the results from the initial API call will be surfaced to the waiting completion blocks from the duplicate
API calls that were not sent. After being called these blocks are removed from the cache.
*/
final class CallbackCache<T> where T: CacheKeyProviding {
private let _cachedCallbacksByKey: Atomic<[String: [T]]> = .init([:])
var cachedCallbacksByKey: [String: [T]] { return self._cachedCallbacksByKey.value }
func add(_ callback: T) -> CallbackCacheStatus {
return self._cachedCallbacksByKey.modify { cachedCallbacksByKey in
var values = cachedCallbacksByKey[callback.cacheKey] ?? []
let cacheStatus: CallbackCacheStatus = !values.isEmpty ?
.addedToExistingInFlightList :
.firstCallbackAddedToList
values.append(callback)
cachedCallbacksByKey[callback.cacheKey] = values
return cacheStatus
}
}
func performOnAllItemsAndRemoveFromCache(withCacheable cacheable: CacheKeyProviding, _ block: (T) -> Void) {
// Remove items from cache while holding the lock, then invoke callbacks AFTER releasing it.
// This prevents deadlock when callbacks synchronously add new items to the cache.
let items: [T]? = self._cachedCallbacksByKey.modify { cachedCallbacksByKey in
return cachedCallbacksByKey.removeValue(forKey: cacheable.cacheKey)
}
items?.forEach(block)
}
deinit {
#if DEBUG
if ProcessInfo.isRunningRevenueCatTests {
precondition(
self.cachedCallbacksByKey.isEmpty,
"\(type(of: self)) was deallocated with callbacks still stored."
)
}
#endif
}
}
extension CallbackCache: Sendable where T: Sendable {}
/**
For use with `CallbackCache`. We store a list of callback objects in the cache and the key used for the list of
callbacks is provided by an object that conforms to `CacheKeyProviding`.
*/
protocol CacheKeyProviding {
var cacheKey: String { get }
}