Skip to content

Commit

Permalink
StoreKit 2: don't finish transactions in observer mode
Browse files Browse the repository at this point in the history
### Changes:
- Greatly increased test coverage for `StoreKit2TransactionListener`
- Extracted `AsyncSequence.extractValues`
- Created `MockStoreTransaction` to be able to mock and verify `finish()` calls
- Simplified `StoreKit2TransactionListener`, moved `finish` call to `PurchasesOrchestrator`
- Follow up to #1965, also finish SK2 transactions from `StoreKit.Transaction.updates`
  • Loading branch information
NachoSoto committed Nov 14, 2022
1 parent b3b444d commit 607eaa9
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 34 deletions.
20 changes: 20 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@
57554CC1282AE1E3009A7E58 /* TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57554CC0282AE1E3009A7E58 /* TestCase.swift */; };
57554CC2282AE1E3009A7E58 /* TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57554CC0282AE1E3009A7E58 /* TestCase.swift */; };
575A17AB2773A59300AA6F22 /* CurrentTestCaseTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */; };
575A8EE12922C56300936709 /* AsyncTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A8EE02922C56300936709 /* AsyncTestHelpers.swift */; };
575A8EE22922C56300936709 /* AsyncTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A8EE02922C56300936709 /* AsyncTestHelpers.swift */; };
575A8EE32922C5E100936709 /* AsyncTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A8EE02922C56300936709 /* AsyncTestHelpers.swift */; };
575A8EE52922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A8EE42922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift */; };
575A8EE62922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A8EE42922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift */; };
5766AA3E283C750300FA6091 /* Operators+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA3D283C750300FA6091 /* Operators+Extensions.swift */; };
5766AA42283C768600FA6091 /* OperatorExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA41283C768600FA6091 /* OperatorExtensionsTests.swift */; };
5766AA56283D4C5400FA6091 /* IgnoreHashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5766AA55283D4C5400FA6091 /* IgnoreHashable.swift */; };
Expand Down Expand Up @@ -358,6 +363,8 @@
57FDAABA284937A0009A48F1 /* SandboxEnvironmentDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FDAAB9284937A0009A48F1 /* SandboxEnvironmentDetector.swift */; };
57FDAABE28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FDAABD28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift */; };
57FDAAC028493C13009A48F1 /* MockSandboxEnvironmentDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FDAABF28493C13009A48F1 /* MockSandboxEnvironmentDetector.swift */; };
57FFD2512922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; };
57FFD2522922DBED00A9A878 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; };
6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; };
805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; };
80E80EF226970E04008F245A /* ReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */; };
Expand Down Expand Up @@ -770,6 +777,8 @@
57554C87282AC293009A7E58 /* PurchaseOwnershipTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseOwnershipTypeTests.swift; sourceTree = "<group>"; };
57554CC0282AE1E3009A7E58 /* TestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCase.swift; sourceTree = "<group>"; };
575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTestCaseTracker.swift; sourceTree = "<group>"; };
575A8EE02922C56300936709 /* AsyncTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTestHelpers.swift; sourceTree = "<group>"; };
575A8EE42922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2TransactionListenerDelegate.swift; sourceTree = "<group>"; };
5766AA3D283C750300FA6091 /* Operators+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Operators+Extensions.swift"; sourceTree = "<group>"; };
5766AA41283C768600FA6091 /* OperatorExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorExtensionsTests.swift; sourceTree = "<group>"; };
5766AA55283D4C5400FA6091 /* IgnoreHashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoreHashable.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -871,6 +880,7 @@
57FDAAB9284937A0009A48F1 /* SandboxEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxEnvironmentDetector.swift; sourceTree = "<group>"; };
57FDAABD28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxEnvironmentDetectorTests.swift; sourceTree = "<group>"; };
57FDAABF28493C13009A48F1 /* MockSandboxEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSandboxEnvironmentDetector.swift; sourceTree = "<group>"; };
57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = "<group>"; };
80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = "<group>"; };
84C3F1AC1D7E1E64341D3936 /* Pods_RevenueCat_PurchasesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RevenueCat_PurchasesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogIntent.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1423,6 +1433,8 @@
37E357D16038F07915D7825D /* MockUserDefaults.swift */,
2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */,
5793397128E77A6E00C1232C /* MockPaymentQueue.swift */,
575A8EE42922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift */,
57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -1709,6 +1721,7 @@
57057FF728B0048900995F21 /* TestLogHandler.swift */,
5705800228B0085200995F21 /* Box.swift */,
57E4A52028BD8F610095C793 /* ErrorMatcher.swift */,
575A8EE02922C56300936709 /* AsyncTestHelpers.swift */,
);
path = TestHelpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -2325,6 +2338,7 @@
3543914426F911F300E669DF /* MockCustomerInfoManager.swift in Sources */,
5738F46E278CAC520096D623 /* StoreTransactionTests.swift in Sources */,
3543914226F911F300E669DF /* MockSK1Product.swift in Sources */,
57FFD2522922DBED00A9A878 /* MockStoreTransaction.swift in Sources */,
3543913826F90FE100E669DF /* MockIntroEligibilityCalculator.swift in Sources */,
3543914126F911CC00E669DF /* MockDeviceCache.swift in Sources */,
3543913C26F9101600E669DF /* MockOperationDispatcher.swift in Sources */,
Expand All @@ -2350,6 +2364,7 @@
F5847431278D00C1001B1CE6 /* MockDNSChecker.swift in Sources */,
B3CAFF1F285CEAAA0048A994 /* MockOfferingsAPI.swift in Sources */,
F55FFA632763F60700995146 /* TransactionsManagerTests.swift in Sources */,
575A8EE62922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift in Sources */,
2D222BAB27FB7008003D5F37 /* LocalReceiptParserStoreKitTests.swift in Sources */,
B31C8BEF285BDD76001017B7 /* MockIdentityAPI.swift in Sources */,
571E7AD4279F2D0C003B3606 /* StoreKitTestHelpers.swift in Sources */,
Expand All @@ -2375,6 +2390,7 @@
576C8AB927D2996C0058FA6E /* CurrentTestCaseTracker.swift in Sources */,
B3BE0264275942D500915B4C /* AvailabilityChecks.swift in Sources */,
2D90F8B326FD2082009B9142 /* MockProductsManager.swift in Sources */,
575A8EE32922C5E100936709 /* AsyncTestHelpers.swift in Sources */,
57DE80BF2807705F008D6C6F /* XCTestCase+Extensions.swift in Sources */,
2D90F8CA26FD257A009B9142 /* MockStoreKit2TransactionListener.swift in Sources */,
35B745A82711001A00458D46 /* MockManageSubscriptionsHelper.swift in Sources */,
Expand Down Expand Up @@ -2647,6 +2663,7 @@
5774F9C12805EA3000997128 /* BaseHTTPResponseTest.swift in Sources */,
351B51B526D450E800BD2BD7 /* ProductsFetcherSK1Tests.swift in Sources */,
2DDF41CC24F6F4C3005BC22D /* AppleReceiptBuilderTests.swift in Sources */,
575A8EE52922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift in Sources */,
2DDF41CD24F6F4C3005BC22D /* ASN1ContainerBuilderTests.swift in Sources */,
573A10D52800A7C800F976E5 /* SKErrorTests.swift in Sources */,
B31C8BEC285BD58F001017B7 /* MockIdentityAPI.swift in Sources */,
Expand All @@ -2673,6 +2690,7 @@
351B51BA26D450E800BD2BD7 /* ProductRequestDataExtensions.swift in Sources */,
574A2F3F282D75E300150D40 /* OfferingsDecodingTests.swift in Sources */,
35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift in Sources */,
57FFD2512922DBED00A9A878 /* MockStoreTransaction.swift in Sources */,
F591492826B9956C00D32E58 /* MockTransaction.swift in Sources */,
5796A39427D6BD6900653165 /* BackendGetOfferingsTests.swift in Sources */,
5705800328B0085200995F21 /* Box.swift in Sources */,
Expand Down Expand Up @@ -2750,6 +2768,7 @@
F5847430278D00C0001B1CE6 /* MockDNSChecker.swift in Sources */,
5722482727C2BD3200C524A7 /* LockTests.swift in Sources */,
351B514526D449E600BD2BD7 /* MockAttributionTypeFactory.swift in Sources */,
575A8EE12922C56300936709 /* AsyncTestHelpers.swift in Sources */,
572247F727BF1ADF00C524A7 /* ArrayExtensionsTests.swift in Sources */,
F55FFA5A27634C3F00995146 /* MockTransactionsManager.swift in Sources */,
F575858F26C0893600C12B97 /* MockOfferingsManager.swift in Sources */,
Expand All @@ -2770,6 +2789,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
575A8EE22922C56300936709 /* AsyncTestHelpers.swift in Sources */,
2DA85A8B26DEA7DD00F1136D /* MockProductsRequestFactory.swift in Sources */,
2D3BFAD326DEA47100370B11 /* MockSKProductDiscount.swift in Sources */,
57DE80BE28077010008D6C6F /* XCTestCase+Extensions.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Logging/Strings/PurchaseStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ enum PurchaseStrings {

case cannot_purchase_product_appstore_configuration_error
case entitlements_revoked_syncing_purchases(productIdentifiers: [String])
case finishing_transaction(StoreTransaction)
case finishing_transaction(StoreTransactionType)
case purchasing_with_observer_mode_and_finish_transactions_false_warning
case paymentqueue_removedtransaction(transaction: SKPaymentTransaction)
case paymentqueue_revoked_entitlements_for_product_identifiers(productIdentifiers: [String])
Expand Down
13 changes: 11 additions & 2 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,16 @@ private extension PurchasesOrchestrator {
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
extension PurchasesOrchestrator: StoreKit2TransactionListenerDelegate {

func transactionsUpdated() async throws {
func storeKit2TransactionListener(
_ listener: StoreKit2TransactionListener,
updatedTransaction transaction: StoreTransactionType
) async throws {
await Async.call { completion in
self.finishTransactionIfNeeded(transaction) { @MainActor in
completion(())
}
}

// Need to restore if using observer mode (which is inverse of finishTransactions)
let isRestore = !self.systemInfo.finishTransactions

Expand Down Expand Up @@ -1059,7 +1068,7 @@ private extension PurchasesOrchestrator {
}

func finishTransactionIfNeeded(
_ transaction: StoreTransaction,
_ transaction: StoreTransactionType,
completion: @escaping @Sendable @MainActor () -> Void
) {
@Sendable
Expand Down
16 changes: 9 additions & 7 deletions Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import StoreKit
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
protocol StoreKit2TransactionListenerDelegate: AnyObject {

func transactionsUpdated() async throws
func storeKit2TransactionListener(
_ listener: StoreKit2TransactionListener,
updatedTransaction transaction: StoreTransactionType
) async throws

}

Expand Down Expand Up @@ -98,13 +101,12 @@ private extension StoreKit2TransactionListener {
error: verificationError
)

case .verified(let verifiedTransaction):
if fromTransactionUpdate { // Otherwise transaction will be finished by `PurchasesOrchestrator`
await verifiedTransaction.finish()
}

case let .verified(verifiedTransaction):
if fromTransactionUpdate, let delegate = self.delegate {
_ = try await delegate.transactionsUpdated()
try await delegate.storeKit2TransactionListener(
self,
updatedTransaction: StoreTransaction(sk2Transaction: verifiedTransaction)
)
}

return verifiedTransaction
Expand Down
10 changes: 0 additions & 10 deletions Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -636,13 +636,3 @@ private extension StoreKit1IntegrationTests {
}

}

private extension AsyncSequence {

func extractValues() async rethrows -> [Element] {
return try await self.reduce(into: [Element]()) {
$0 += [$1]
}
}

}
40 changes: 28 additions & 12 deletions Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -510,29 +510,45 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase {
func testStoreKit2TransactionListenerDelegate() async throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

customerInfoManager.stubbedCachedCustomerInfoResult = mockCustomerInfo
backend.stubbedPostReceiptResult = .success(mockCustomerInfo)
self.setUpStoreKit2Listener()

try await self.orchestrator.transactionsUpdated()
self.customerInfoManager.stubbedCachedCustomerInfoResult = self.mockCustomerInfo
self.backend.stubbedPostReceiptResult = .success(self.mockCustomerInfo)

expect(self.backend.invokedPostReceiptData).to(beTrue())
expect(self.backend.invokedPostReceiptDataParameters?.isRestore).to(beFalse())
let transaction = MockStoreTransaction()

try await self.orchestrator.storeKit2TransactionListener(
self.mockStoreKit2TransactionListener!,
updatedTransaction: transaction
)

expect(transaction.finishInvoked) == true
expect(self.backend.invokedPostReceiptData) == true
expect(self.backend.invokedPostReceiptDataParameters?.isRestore) == false
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testStoreKit2TransactionListenerDelegateWithObserverMode() async throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

try setUpSystemInfo(finishTransactions: false)
setUpOrchestrator()
try self.setUpSystemInfo(finishTransactions: false, storeKit2Setting: .enabledForCompatibleDevices)

backend.stubbedPostReceiptResult = .success(mockCustomerInfo)
customerInfoManager.stubbedCachedCustomerInfoResult = mockCustomerInfo
self.setUpOrchestrator()
self.setUpStoreKit2Listener()

self.backend.stubbedPostReceiptResult = .success(self.mockCustomerInfo)
self.customerInfoManager.stubbedCachedCustomerInfoResult = self.mockCustomerInfo

try await self.orchestrator.transactionsUpdated()
let transaction = MockStoreTransaction()

try await self.orchestrator.storeKit2TransactionListener(
self.mockStoreKit2TransactionListener!,
updatedTransaction: transaction
)

expect(self.backend.invokedPostReceiptData).to(beTrue())
expect(self.backend.invokedPostReceiptDataParameters?.isRestore).to(beTrue())
expect(transaction.finishInvoked) == false
expect(self.backend.invokedPostReceiptData) == true
expect(self.backend.invokedPostReceiptDataParameters?.isRestore) == true
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
Expand Down
Loading

0 comments on commit 607eaa9

Please sign in to comment.