Skip to content

Commit

Permalink
Purchasing: fixed consumable purchases by fixing transaction-finishing (
Browse files Browse the repository at this point in the history
#1965)

Fixes #1964, [TRIAGE-134], [TRIAGE-131], and possibly [TRIAGE-82].

Depends on #1967, #1968.

### Fixes:
- For `SK2` purchases we were never finishing transactions. We are now.
- For `SK1` purchases, transactions were finished _after_ the completion
block was invoked (and tests were very lenient checking that
_eventually_ this happened). This could have lead to race conditions.
- For SK2 only (at least in `SKTestSession`s), this fixes the ability to
purchase multiple consumable purchases.

This is the log from the failing test using SK2:
<details>
  <summary>Open</summary>

```
2022-10-06 15:18:52.286813-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:6f630d50b8294288a2d0862629d766fb/offerings
2022-10-06 15:18:52.383594-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ API request completed: GET /v1/subscribers/$RCAnonymousID:6f630d50b8294288a2d0862629d766fb/offerings 200
2022-10-06 15:18:52.388247-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ No existing products cached, starting store products request for: ["com.revenuecat.weekly_1.99.3_day_intro", "consumable.10_coins", "com.revenuecat.annual_39.99.2_week_intro", "com.revenuecat.monthly_4.99.1_week_intro"]
2022-10-06 15:18:52.388382-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Found an existing request for products: ["com.revenuecat.monthly_4.99.1_week_intro", "com.revenuecat.annual_39.99.2_week_intro", "com.revenuecat.weekly_1.99.3_day_intro", "consumable.10_coins"], appending to completion
2022-10-06 15:18:52.388451-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ GetOfferingsOperation: Finished
2022-10-06 15:18:52.388560-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Serial request done: GET subscribers/$RCAnonymousID%3A6f630d50b8294288a2d0862629d766fb/offerings, 0 requests left in the queue
2022-10-06 15:18:52.420210-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: 😻 Store products request request received response
2022-10-06 15:18:52.420358-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Store products request finished
2022-10-06 15:18:52.421126-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ Vending Offerings from cache
2022-10-06 15:18:52.421353-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins'
2022-10-06 15:18:52.496438-0700 BackendIntegrationTestsHostApp[45464:7076968] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-10-06 15:18:52.510974-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/A3576DC2-355E-45BA-B32C-D2C0A3811BB4/data/Containers/Data/Application/BFD385C4-52BC-414B-9C1C-44522ED1222F/StoreKit/receipt
2022-10-06 15:18:52.511072-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-06 15:18:52.511181-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-06 15:18:52.511564-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:6f630d50b8294288a2d0862629d766fb
2022-10-06 15:18:52.515448-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started
2022-10-06 15:18:52.516403-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - INFO: ℹ️ Receipt parsed successfully
2022-10-06 15:18:52.516763-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: {
  "opaque_value" : "oX91ewAAAAA=",
  "sha1_hash" : "GsyNpUKQ89SonYyj\/jpw8\/N5nr8=",
  "bundle_id" : "com.revenuecat.StoreKitTestApp",
  "in_app_purchases" : [
    {
      "quantity" : 1,
      "product_id" : "consumable.10_coins",
      "purchase_date" : "2022-10-06T22:18:52Z",
      "transaction_id" : "0"
    }
  ],
  "application_version" : "1",
  "creation_date" : "2022-10-06T22:18:52Z",
  "expiration_date" : "4001-01-01T00:00:00Z"
}
2022-10-06 15:18:52.516825-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts
2022-10-06 15:18:52.518059-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts
2022-10-06 15:18:52.960048-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200
2022-10-06 15:18:52.964274-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished
2022-10-06 15:18:52.964666-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue
2022-10-06 15:18:52.966476-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Sending updated CustomerInfo to delegate.
2022-10-06 15:18:52.966697-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins'
2022-10-06 15:18:52.966973-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ Vending Offerings from cache
2022-10-06 15:18:52.967284-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins'
2022-10-06 15:18:52.991334-0700 BackendIntegrationTestsHostApp[45464:7076973] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-10-06 15:18:53.015046-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/A3576DC2-355E-45BA-B32C-D2C0A3811BB4/data/Containers/Data/Application/BFD385C4-52BC-414B-9C1C-44522ED1222F/StoreKit/receipt
2022-10-06 15:18:53.015174-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-06 15:18:53.015262-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-06 15:18:53.015419-0700 BackendIntegrationTestsHostApp[45464:7076903] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:6f630d50b8294288a2d0862629d766fb
2022-10-06 15:18:53.015927-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started
2022-10-06 15:18:53.016558-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - INFO: ℹ️ Receipt parsed successfully
2022-10-06 15:18:53.016727-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: {
  "opaque_value" : "LP9LZQwAAAA=",
  "sha1_hash" : "WdANBTWr7wYWh8RYEgmGebO8DR8=",
  "bundle_id" : "com.revenuecat.StoreKitTestApp",
  "in_app_purchases" : [
    {
      "quantity" : 1,
      "product_id" : "consumable.10_coins",
      "purchase_date" : "2022-10-06T22:18:52Z",
      "transaction_id" : "0"
    }
  ],
  "application_version" : "1",
  "creation_date" : "2022-10-06T22:18:53Z",
  "expiration_date" : "4001-01-01T00:00:00Z"
}
2022-10-06 15:18:53.016783-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts
2022-10-06 15:18:53.017602-0700 BackendIntegrationTestsHostApp[45464:7076966] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts
2022-10-06 15:18:53.411689-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200
2022-10-06 15:18:53.413980-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished
2022-10-06 15:18:53.414147-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue
2022-10-06 15:18:53.414927-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins'
2022-10-06 15:18:53.415505-0700 BackendIntegrationTestsHostApp[45464:7076978] [Purchases] - DEBUG: ℹ️ Vending CustomerInfo from cache.
```
</details>

With the fix, you can see transactions are now finished at the right
time:
<details>
  <summary>Open</summary>

```
2022-10-07 11:08:01.967843-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins'
2022-10-07 11:08:02.123611-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-10-07 11:08:02.138918-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/F02A9A20-949B-4C9D-A01E-AB0CC7544F96/data/Containers/Data/Application/C52A5EFD-733F-46EC-84B7-734B6AF9D28C/StoreKit/receipt
2022-10-07 11:08:02.139013-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-07 11:08:02.139095-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-07 11:08:02.139481-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:bd74b9d10f2c48eea1d741a32f5acb16
2022-10-07 11:08:02.147220-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started
2022-10-07 11:08:02.148192-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - INFO: ℹ️ Receipt parsed successfully
2022-10-07 11:08:02.148552-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: {
  "opaque_value" : "\/3nevQ0AAAA=",
  "sha1_hash" : "\/bAkxLQ6BPgUg3rR9jDR28dEfig=",
  "bundle_id" : "com.revenuecat.StoreKitTestApp",
  "in_app_purchases" : [
    {
      "quantity" : 1,
      "product_id" : "consumable.10_coins",
      "purchase_date" : "2022-10-07T18:08:02Z",
      "transaction_id" : "0"
    }
  ],
  "application_version" : "1",
  "creation_date" : "2022-10-07T18:08:02Z",
  "expiration_date" : "4001-01-01T00:00:00Z"
}
2022-10-07 11:08:02.148604-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts
2022-10-07 11:08:02.149748-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts
2022-10-07 11:08:02.733225-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200
2022-10-07 11:08:02.734687-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished
2022-10-07 11:08:02.734802-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue
2022-10-07 11:08:02.735631-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Sending updated CustomerInfo to delegate.
2022-10-07 11:08:02.735700-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - INFO: 💰 Finishing transaction '0' for product 'consumable.10_coins'
2022-10-07 11:08:02.760613-0700 BackendIntegrationTestsHostApp[45714:674592] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins'
2022-10-07 11:08:02.760828-0700 BackendIntegrationTestsHostApp[45714:674590] [Purchases] - DEBUG: ℹ️ Vending Offerings from cache
2022-10-07 11:08:02.761032-0700 BackendIntegrationTestsHostApp[45714:674590] [Purchases] - INFO: 💰 Purchasing Product 'consumable.10_coins' from package in Offering 'coins'
2022-10-07 11:08:02.820835-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-10-07 11:08:02.839184-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/F02A9A20-949B-4C9D-A01E-AB0CC7544F96/data/Containers/Data/Application/C52A5EFD-733F-46EC-84B7-734B6AF9D28C/StoreKit/receipt
2022-10-07 11:08:02.839275-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-07 11:08:02.839348-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Skipping products request because products were already cached. products: ["consumable.10_coins"]
2022-10-07 11:08:02.839476-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:bd74b9d10f2c48eea1d741a32f5acb16
2022-10-07 11:08:02.839911-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started
2022-10-07 11:08:02.840457-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - INFO: ℹ️ Receipt parsed successfully
2022-10-07 11:08:02.840603-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Posting receipt: {
  "opaque_value" : "fXr9\/QYAAAA=",
  "sha1_hash" : "zreYwGKGSaOEww1bsTRu8MylAas=",
  "bundle_id" : "com.revenuecat.StoreKitTestApp",
  "in_app_purchases" : [
    {
      "quantity" : 1,
      "product_id" : "consumable.10_coins",
      "purchase_date" : "2022-10-07T18:08:02Z",
      "transaction_id" : "1"
    }
  ],
  "application_version" : "1",
  "creation_date" : "2022-10-07T18:08:02Z",
  "expiration_date" : "4001-01-01T00:00:00Z"
}
2022-10-07 11:08:02.840662-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts
2022-10-07 11:08:02.841268-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts
2022-10-07 11:08:03.313116-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ API request completed: POST /v1/receipts 200
2022-10-07 11:08:03.316115-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished
2022-10-07 11:08:03.316342-0700 BackendIntegrationTestsHostApp[45714:674589] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue
2022-10-07 11:08:03.317444-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - DEBUG: ℹ️ Sending updated CustomerInfo to delegate.
2022-10-07 11:08:03.317527-0700 BackendIntegrationTestsHostApp[45714:674399] [Purchases] - INFO: 💰 Finishing transaction '1' for product 'consumable.10_coins'
2022-10-07 11:08:03.342767-0700 BackendIntegrationTestsHostApp[45714:674602] [Purchases] - INFO: 😻💰 Purchased product - 'consumable.10_coins'
```

</details>

[TRIAGE-134]:
https://revenuecats.atlassian.net/browse/TRIAGE-134?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[TRIAGE-131]:
https://revenuecats.atlassian.net/browse/TRIAGE-131?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[TRIAGE-82]:
https://revenuecats.atlassian.net/browse/TRIAGE-82?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
NachoSoto authored Oct 11, 2022
1 parent 36dff60 commit 70365df
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 31 deletions.
9 changes: 4 additions & 5 deletions 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(transaction: SKPaymentTransaction)
case finishing_transaction(StoreTransaction)
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 Expand Up @@ -80,10 +80,9 @@ extension PurchaseStrings: CustomStringConvertible {
return "Entitlements revoked for product " +
"identifiers: \(productIdentifiers). \nsyncing purchases"

case .finishing_transaction(let transaction):
return "Finishing transaction \(transaction.payment.productIdentifier) " +
"\(transaction.transactionIdentifier ?? "") " +
"(\(transaction.original?.transactionIdentifier ?? ""))"
case let .finishing_transaction(transaction):
return "Finishing transaction '\(transaction.transactionIdentifier)' " +
"for product '\(transaction.productIdentifier)'"

case .purchasing_with_observer_mode_and_finish_transactions_false_warning:
return "Observer mode is active (finishTransactions is set to false) and " +
Expand Down
4 changes: 2 additions & 2 deletions Sources/Misc/Deprecations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,8 @@ extension CustomerInfo {
self.quantity = 1
}

func finish(_ wrapper: PaymentQueueWrapperType) {
// Nothing to do
func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void) {
completion()
}

}
Expand Down
36 changes: 28 additions & 8 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ private extension PurchasesOrchestrator {
}
}

self.finishTransactionIfNeeded(storeTransaction)
self.finishTransactionIfNeeded(storeTransaction, completion: {})
}

func handleDeferredTransaction(_ transaction: SKPaymentTransaction) {
Expand Down Expand Up @@ -899,23 +899,30 @@ private extension PurchasesOrchestrator {
error: result.error)

let completion = self.getAndRemovePurchaseCompletedCallback(forTransaction: transaction)

let error = result.error
let finishable = error?.finishable ?? false

switch result {
case let .success(customerInfo):
self.customerInfoManager.cache(customerInfo: customerInfo, appUserID: appUserID)
completion?(transaction, customerInfo, nil, false)

self.finishTransactionIfNeeded(transaction)
self.finishTransactionIfNeeded(transaction) {
completion?(transaction, customerInfo, nil, false)
}

case let .failure(error):
let purchasesError = error.asPublicError

completion?(transaction, nil, purchasesError, false)
@MainActor
func complete() {
completion?(transaction, nil, purchasesError, false)
}

if finishable {
self.finishTransactionIfNeeded(transaction)
self.finishTransactionIfNeeded(transaction) { complete() }
} else {
complete()
}
}
}
Expand Down Expand Up @@ -1044,10 +1051,23 @@ private extension PurchasesOrchestrator {
self.offeringsManager.invalidateAndReFetchCachedOfferingsIfAppropiate(appUserID: self.appUserID)
}

func finishTransactionIfNeeded(_ transaction: StoreTransaction) {
if self.finishTransactions {
transaction.finish(self.paymentQueueWrapper.paymentQueueWrapperType)
func finishTransactionIfNeeded(
_ transaction: StoreTransaction,
completion: @escaping @Sendable @MainActor () -> Void
) {
@Sendable
func complete() {
self.operationDispatcher.dispatchOnMainActor(completion)
}

guard self.finishTransactions else {
complete()
return
}

Logger.purchase(Strings.purchase.finishing_transaction(transaction))

transaction.finish(self.paymentQueueWrapper.paymentQueueWrapperType, completion: complete)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ internal struct SK1StoreTransaction: StoreTransactionType {
let transactionIdentifier: String
let quantity: Int

func finish(_ wrapper: PaymentQueueWrapperType) {
func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void) {
wrapper.finishTransaction(self.underlyingSK1Transaction)
completion()
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ internal struct SK2StoreTransaction: StoreTransactionType {
let transactionIdentifier: String
let quantity: Int

func finish(_ wrapper: PaymentQueueWrapperType) {
_ = Task<Void, Never> {
func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void) {
Async.call(with: completion) {
await self.underlyingSK2Transaction.finish()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public typealias SK2Transaction = StoreKit.Transaction
@objc public var transactionIdentifier: String { self.transaction.transactionIdentifier }
@objc public var quantity: Int { self.transaction.quantity }

func finish(_ wrapper: PaymentQueueWrapperType) {
self.transaction.finish(wrapper)
func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void) {
self.transaction.finish(wrapper, completion: completion)
}

// swiftlint:enable missing_docs
Expand Down Expand Up @@ -87,7 +87,7 @@ internal protocol StoreTransactionType: Sendable {

/// Indicates to the App Store that the app delivered the purchased content
/// or enabled the service to finish the transaction.
func finish(_ wrapper: PaymentQueueWrapperType)
func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void)

}

Expand Down
59 changes: 59 additions & 0 deletions Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,47 @@ class StoreKit1IntegrationTests: BaseBackendIntegrationTests {
try await self.purchaseMonthlyProduct()
}

func testCanPurchaseConsumable() async throws {
let info = try await self.purchaseConsumablePackage().customerInfo

expect(info.allPurchasedProductIdentifiers).to(contain(Self.consumable10Coins))
}

func testCanPurchaseConsumableMultipleTimes() async throws {
// See https://revenuecats.atlassian.net/browse/TRIAGE-134
try XCTSkipIf(Self.storeKit2Setting == .disabled, "This test is not currently passing on SK1")

let count = 2

for _ in 0..<count {
try await self.purchaseConsumablePackage()
}

let info = try await Purchases.shared.customerInfo()
expect(info.nonSubscriptions).to(haveCount(count))
expect(info.nonSubscriptions.map(\.productIdentifier)) == [
Self.consumable10Coins,
Self.consumable10Coins
]
}

func testCanPurchaseConsumableWithMultipleUsers() async throws {
func verifyPurchase(_ info: CustomerInfo) {
expect(info.nonSubscriptions).to(haveCount(1))
expect(info.nonSubscriptions.onlyElement?.productIdentifier) == Self.consumable10Coins
}

_ = try await Purchases.shared.logIn("user_1.\(UUID().uuidString)")
let info1 = try await self.purchaseConsumablePackage().customerInfo
verifyPurchase(info1)

let user2 = try await Purchases.shared.logIn("user_1.\(UUID().uuidString)").customerInfo
expect(user2.nonSubscriptions).to(beEmpty())

let info2 = try await self.purchaseConsumablePackage().customerInfo
verifyPurchase(info2)
}

func testSubscriptionIsSandbox() async throws {
let info = try await self.purchaseMonthlyOffering().customerInfo

Expand Down Expand Up @@ -394,6 +435,7 @@ class StoreKit1IntegrationTests: BaseBackendIntegrationTests {
private extension StoreKit1IntegrationTests {

static let entitlementIdentifier = "premium"
static let consumable10Coins = "consumable.10_coins"

private var currentOffering: Offering {
get async throws {
Expand Down Expand Up @@ -466,6 +508,23 @@ private extension StoreKit1IntegrationTests {
return data
}

@discardableResult
func purchaseConsumablePackage(
file: FileString = #file,
line: UInt = #line
) async throws -> PurchaseResultData {
let offering = try await XCTAsyncUnwrap(
try await Purchases.shared.offerings().offering(identifier: "coins"),
file: file, line: line
)
let package = try XCTUnwrap(
offering.package(identifier: "10.coins"),
file: file, line: line
)

return try await Purchases.shared.purchase(package: package)
}

@discardableResult
func verifyEntitlementWentThrough(
_ customerInfo: CustomerInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
"productID" : "lifetime",
"referenceName" : "lifetime",
"type" : "NonConsumable"
},
{
"displayPrice" : "0.99",
"familyShareable" : false,
"internalID" : "67E2FE0B",
"localizations" : [
{
"description" : "10 Coins",
"displayName" : "10 Coins",
"locale" : "en_US"
}
],
"productID" : "consumable.10_coins",
"referenceName" : "10 coins",
"type" : "Consumable"
}
],
"settings" : {
Expand Down
4 changes: 3 additions & 1 deletion Tests/UnitTests/Mocks/MockStoreKit1Wrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ class MockStoreKit1Wrapper: StoreKit1Wrapper {
}

var finishCalled = false
var finishProductIdentifier: String?

override func finishTransaction(_ transaction: SKPaymentTransaction) {
finishCalled = true
self.finishCalled = true
self.finishProductIdentifier = transaction.productIdentifier
}

weak var mockDelegate: StoreKit1WrapperDelegate?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,23 @@ class PurchasesPurchasingTests: BasePurchasesTests {
}

func testFinishesTransactionsIfSentToBackendCorrectly() throws {
let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1"))
self.purchases.purchase(product: product) { (_, _, _, _) in }
var finished = true

let productID = "com.product.id1"
let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: productID))

self.purchases.purchase(product: product) { (_, _, _, _) in
// Transactions must be finished by the time the callback is invoked.
expect(self.storeKit1Wrapper.finishCalled) == true
expect(self.storeKit1Wrapper.finishProductIdentifier) == productID

finished = true
}

let transaction = MockTransaction()
transaction.mockPayment = try XCTUnwrap(self.storeKit1Wrapper.payment)
transaction.mockState = .purchasing

transaction.mockState = SKPaymentTransactionState.purchasing
self.storeKit1Wrapper.delegate?.storeKit1Wrapper(self.storeKit1Wrapper, updatedTransaction: transaction)

self.backend.postReceiptResult = .success(try CustomerInfo(data: Self.emptyCustomerInfoData))
Expand All @@ -113,7 +123,7 @@ class PurchasesPurchasingTests: BasePurchasesTests {
self.storeKit1Wrapper.delegate?.storeKit1Wrapper(self.storeKit1Wrapper, updatedTransaction: transaction)

expect(self.backend.postReceiptDataCalled) == true
expect(self.storeKit1Wrapper.finishCalled).toEventually(beTrue())
expect(finished).toEventually(beTrue())
}

func testDoesntFinishTransactionsIfFinishingDisabled() throws {
Expand Down Expand Up @@ -157,11 +167,22 @@ class PurchasesPurchasingTests: BasePurchasesTests {
}

func testAfterSendingFinishesFromBackendErrorIfAppropriate() throws {
let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "com.product.id1"))
self.purchases.purchase(product: product) { (_, _, _, _) in }
var finished = false

let productID = "com.product.id1"
let product = StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: productID))

self.purchases.purchase(product: product) { (_, _, _, _) in
// Transactions must be finished by the time the callback is invoked.
expect(self.storeKit1Wrapper.finishCalled) == true
expect(self.storeKit1Wrapper.finishProductIdentifier) == productID

finished = true
}

let transaction = MockTransaction()
transaction.mockPayment = try XCTUnwrap(self.storeKit1Wrapper.payment)
transaction.mockState = .purchased

self.backend.postReceiptResult = .failure(
.networkError(.errorResponse(
Expand All @@ -170,11 +191,10 @@ class PurchasesPurchasingTests: BasePurchasesTests {
))
)

transaction.mockState = SKPaymentTransactionState.purchased
self.storeKit1Wrapper.delegate?.storeKit1Wrapper(self.storeKit1Wrapper, updatedTransaction: transaction)

expect(self.backend.postReceiptDataCalled) == true
expect(self.storeKit1Wrapper.finishCalled).toEventually(beTrue())
expect(finished).toEventually(beTrue())
}

func testNotifiesIfTransactionFailsFromBackend() throws {
Expand Down Expand Up @@ -525,10 +545,11 @@ class PurchasesPurchasingTests: BasePurchasesTests {
receivedError = error as NSError?
secondCompletionCalled = true
}

self.performTransaction()
}

self.performTransaction()
self.performTransaction()
}

expect(secondCompletionCalled).toEventually(beTrue(), timeout: .seconds(10))
Expand Down

0 comments on commit 70365df

Please sign in to comment.