Skip to content

Commit

Permalink
Only wait for first transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Aug 11, 2023
1 parent 3d6c842 commit 75f5734
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 58 deletions.
72 changes: 38 additions & 34 deletions Sources/Identity/CustomerInfoManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,47 +321,33 @@ private extension CustomerInfoManager {
_ = Task<Void, Never> {
let transactions = await self.transactionFetcher.unfinishedVerifiedTransactions

if !transactions.isEmpty {
if let transactionToPost = transactions.first {
Logger.debug(
Strings.customerInfo.posting_transactions_in_lieu_of_fetching_customerinfo(transactions)
)

let storefront = await Storefront.currentStorefront

// Process all transactions in parallel
let results: [Result<CustomerInfo, BackendError>] = await withTaskGroup(
of: Result<CustomerInfo, BackendError>.self
) { group in
for transaction in transactions {
group.addTask {
return await self.transactionPoster.handlePurchasedTransaction(
transaction,
data: .init(appUserID: appUserID,
presentedOfferingID: nil,
unsyncedAttributes: [:],
storefront: storefront,
source: Self.sourceForUnfinishedTransaction)
)
}
}

var results: [Result<CustomerInfo, BackendError>] = []

for await result in group {
results.append(result)
}
let transactionData = PurchasedTransactionData(
appUserID: appUserID,
presentedOfferingID: nil,
unsyncedAttributes: [:],
storefront: await Storefront.currentStorefront,
source: Self.sourceForUnfinishedTransaction
)

return results
// Post everything but the first transaction in the background
// in parallel so they can be de-duped
let otherTransactionsToPostInParalel = Array(transactions.dropFirst())
Task.detached(priority: .background) {
await self.postTransactions(otherTransactionsToPostInParalel, transactionData)
}

// Any of the POST receipt operations will have posted the same receipt contents
// so the resulting `CustomerInfo` will be equivalent.
// For that reason, we can return the last known success if available,
// and otherwise the last result (an error).
let lastSuccess = results.last { $0.value != nil }
let result = lastSuccess ?? results.last!

completion(result)
// Return the result of posting the first transaction.
// The posted receipt will include the content of every other transaction
// so we don't need to wait for those.
completion(await self.transactionPoster.handlePurchasedTransaction(
transactionToPost,
data: transactionData
))
} else {
self.requestCustomerInfo(appUserID: appUserID,
isAppBackgrounded: isAppBackgrounded,
Expand All @@ -388,6 +374,24 @@ private extension CustomerInfoManager {
completion: completion)
}

/// Posts all `transactions` in parallel.
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
private func postTransactions(
_ transactions: [StoreTransaction],
_ data: PurchasedTransactionData
) async {
await withTaskGroup(of: Void.self) { group in
for transaction in transactions {
group.addTask {
_ = await self.transactionPoster.handlePurchasedTransaction(
transaction,
data: data
)
}
}
}
}

// Note: this is just a best guess.
private static let sourceForUnfinishedTransaction: PurchaseSource = .init(
isRestore: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Nimble
import StoreKit
import XCTest

// swiftlint:disable type_name file_length
// swiftlint:disable type_name

class BaseOfflineStoreKitIntegrationTests: BaseStoreKitIntegrationTests {

Expand Down
46 changes: 23 additions & 23 deletions Tests/UnitTests/Identity/CustomerInfoManagerPostReceiptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ class CustomerInfoManagerPostReceiptTests: BaseCustomerInfoManagerTests {
expect(parameters.data.source.initiationSource) == .queue
}

func testPostsAllTransactions() async throws {
func testPostsFirstTransaction() async throws {
let transactionToPost = Self.createTransaction()
let transactions = [
Self.createTransaction(),
transactionToPost,
Self.createTransaction(),
Self.createTransaction()
]
Expand All @@ -96,24 +97,25 @@ class CustomerInfoManagerPostReceiptTests: BaseCustomerInfoManagerTests {
isAppBackgrounded: false)
expect(self.mockBackend.invokedGetSubscriberData) == false
expect(self.mockTransactionPoster.invokedHandlePurchasedTransaction.value) == true
expect(self.mockTransactionPoster.invokedHandlePurchasedTransactionCount.value) == transactions.count

expect(
Set(
self.mockTransactionPoster.invokedHandlePurchasedTransactionParameterList.value
.map(\.transaction)
.compactMap { $0 as? StoreTransaction }
)
)
== Set(transactions)
// The first transaction is posted synchronously.
// The rest are posted in the background.
expect(self.mockTransactionPoster.invokedHandlePurchasedTransactionCount.value) >= 1

expect(self.mockTransactionPoster.allHandledTransactions).to(contain(transactionToPost))

self.logger.verifyMessageWasLogged(
Strings.customerInfo.posting_transactions_in_lieu_of_fetching_customerinfo(transactions),
level: .debug
)

try await asyncWait(
description: "The rest of transactions should be posted asynchronously"
) { [poster = self.mockTransactionPoster!] in
poster.allHandledTransactions == Set(transactions)
}
}

func testPostingAllTransactionsReturnsLastKnownSuccess() async throws {
func testPostingAllTransactionsReturnsFirstResult() async throws {
let otherMockCustomerInfo = try CustomerInfo(data: [
"request_date": "2024-12-21T02:40:36Z",
"subscriber": [
Expand All @@ -138,23 +140,21 @@ class CustomerInfoManagerPostReceiptTests: BaseCustomerInfoManagerTests {
.failure(.networkError(.serverDown()))
]

_ = try await self.customerInfoManager.fetchAndCacheCustomerInfo(appUserID: Self.userID,
isAppBackgrounded: false)
let result = try await self.customerInfoManager.fetchAndCacheCustomerInfo(appUserID: Self.userID,
isAppBackgrounded: false)
expect(result) === otherMockCustomerInfo

expect(self.mockBackend.invokedGetSubscriberData) == false
expect(self.mockTransactionPoster.invokedHandlePurchasedTransaction.value) == true
expect(self.mockTransactionPoster.invokedHandlePurchasedTransactionCount.value) == transactions.count
expect(
Set(
self.mockTransactionPoster.invokedHandlePurchasedTransactionParameterList.value
.map(\.transaction)
.compactMap { $0 as? StoreTransaction }
)
) == Set(transactions)

self.logger.verifyMessageWasLogged(
Strings.customerInfo.posting_transactions_in_lieu_of_fetching_customerinfo(transactions),
level: .debug
)

try await asyncWait { [poster = self.mockTransactionPoster!] in
poster.allHandledTransactions == Set(transactions)
}
}

}
Expand Down
9 changes: 9 additions & 0 deletions Tests/UnitTests/Mocks/MockTransactionPoster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ final class MockTransactionPoster: TransactionPosterType {
let invokedHandlePurchasedTransactionParameterList: Atomic<[(transaction: StoreTransactionType,
data: PurchasedTransactionData)]> = .init([])

var allHandledTransactions: Set<StoreTransaction> {
return Set(
self
.invokedHandlePurchasedTransactionParameterList.value
.map(\.transaction)
.compactMap { $0 as? StoreTransaction }
)
}

func handlePurchasedTransaction(
_ transaction: StoreTransactionType,
data: PurchasedTransactionData,
Expand Down

0 comments on commit 75f5734

Please sign in to comment.