Fixed race condition finishing SK1 transactions#2148
Conversation
Fixes [CSDK-504]. `SKPaymentQueue.finishTransaction` is asynchronous, but we were assuming it wasn't. This meant that when performing multiple purchases (like consumables) in quick succession, before the first transaction was finished, the second purchase was reusing the first transaction. Example log: ``` [Purchases] - INFO: 💰 Finishing transaction ‘0’ for product ‘consumable.10_coins’ [Purchases] - INFO: 😻💰 Purchased product - ‘consumable.10_coins’ [Purchases] - INFO: 💰 Purchasing Product ‘consumable.10_coins’ from package in Offering ‘coins’ [Purchases] - DEBUG: ℹ️ StoreKit1Wrapper (0x00006000008a9530) removedTransaction: consumable.10_coins 0 1 ``` Notice how the "removed transaction" was notified _after_ we had already started the second purchase.
| // See `StoreKit1Wrapper.finishTransaction(:completion:)`. | ||
| // Technically this is a race condition, because `SKPaymentQueue.finishTransaction` is asynchronous | ||
| // In practice this method won't be used, because this class is only used in SK2 mode, | ||
| // and those transactions are finished through `SK2StoreTransaction`. |
There was a problem hiding this comment.
Should we log an error if this is reached then? Also, not sure if we should finish transaction in this case, if we consider it an error... But I guess we should propagate the error somehow in that case.
There was a problem hiding this comment.
having to have this here doesn't really say wonders about our class design, we might wanna start thinking about alternatives soon
There was a problem hiding this comment.
Yeah for sure. This is needed because we avoid using StoreKit1Wrapper when SK2 is enabled. But turned out we did need a payment queue wrapper for some things.
And the fact that finishTransaction needs a reference to some wrapper, because SK1 transactions are finished through the queue, but SK2’s are finished directly with the transaction :/
So yeah I agree this is a smell, but due to those 2 limitations I can’t think of a way to avoid it.
There was a problem hiding this comment.
I'll merge this for now but let me know if those 2 limitations gives any of you an idea of how we could best work around them and avoid this smell.
**This is an automatic release.** ### Bugfixes * `ErrorUtils.purchasesError(withUntypedError:)`: handle `PublicError`s (#2165) via NachoSoto (@NachoSoto) * Fixed race condition finishing `SK1` transactions (#2148) via NachoSoto (@NachoSoto) * `IntroEligibilityStatus`: added `CustomStringConvertible` implementation (#2182) via NachoSoto (@NachoSoto) * `BundleSandboxEnvironmentDetector`: fixed logic for `macOS` (#2176) via NachoSoto (@NachoSoto) * Fixed `AttributionFetcher.adServicesToken` hanging when called in simulator (#2157) via NachoSoto (@NachoSoto) ### Other Changes * `Purchase Tester`: added ability to purchase products directly with `StoreKit` (#2172) via NachoSoto (@NachoSoto) * `DNSChecker`: simplified `NetworkError` initialization (#2166) via NachoSoto (@NachoSoto) * `Purchases` initialization: refactor to avoid multiple concurrent instances in memory (#2180) via NachoSoto (@NachoSoto) * `Purchase Tester`: added button to clear messages on logger view (#2179) via NachoSoto (@NachoSoto) * `NetworkOperation`: added assertion to ensure that subclasses call completion (#2138) via NachoSoto (@NachoSoto) * `CacheableNetworkOperation`: avoid unnecessarily creating operations for cache hits (#2135) via NachoSoto (@NachoSoto) * `PurchaseTester`: fixed `macOS` support (#2175) via NachoSoto (@NachoSoto) * `IntroEligibilityCalculator`: added log including `AppleReceipt` (#2181) via NachoSoto (@NachoSoto) * `Purchase Tester`: fixed scene manifest (#2170) via NachoSoto (@NachoSoto) * `HTTPClientTests`: refactored to use `waitUntil` (#2168) via NachoSoto (@NachoSoto) * `Integration Tests`: split up tests in smaller files (#2158) via NachoSoto (@NachoSoto) * `StoreKitRequestFetcher`: removed unnecessary dispatch (#2152) via NachoSoto (@NachoSoto) * `Purchase Tester`: added companion `watchOS` app (#2140) via NachoSoto (@NachoSoto) * `StoreKit1Wrapper`: added warning if receiving too many updated transactions (#2117) via NachoSoto (@NachoSoto) * `StoreKitTestHelpers`: cleaned up unnecessary log (#2177) via NachoSoto (@NachoSoto) * `TrialOrIntroPriceEligibilityCheckerSK1Tests`: use `waitUntilValue` (#2173) via NachoSoto (@NachoSoto) * `DNSChecker`: added log with resolved host (#2167) via NachoSoto (@NachoSoto) * `MagicWeatherSwiftUI`: updated `README` to point to workspace (#2142) via NachoSoto (@NachoSoto) * `Purchase Tester`: fixed `.storekit` config file reference (#2171) via NachoSoto (@NachoSoto) * `Purchase Tester`: fixed error alerts (#2169) via NachoSoto (@NachoSoto) * `CI`: don't make releases until `release-checks` pass (#2162) via NachoSoto (@NachoSoto) * `Fastfile`: changed `match` to `readonly` (#2161) via NachoSoto (@NachoSoto)
Fixes CSDK-504.
SKPaymentQueue.finishTransactionis asynchronous, but we were assuming it wasn't.This meant that when performing multiple purchases (like consumables) in quick succession, before the first transaction was finished, the second purchase was reusing the first transaction.
Example log:
Notice how the "removed transaction" was notified after we had already started the second purchase.
See also #2148 for an earlier fix for finishing transactions.