How to properly handle StoreKitError or PurchaseError from product.purchase()

Hello!

We are implementing consumable IAP for iOS using StoreKit 2 together with our own server backend. Our happy path looks like this: 1. Call /prepare on our server • We only allow one purchase at a time, so this fails if a pending transaction already exists. 2. Call /purchase via StoreKit 3. If successful, call /complete on our server 4. Call .finish() on the Transaction

The Problem

Some users report being charged by Apple but not receiving coins. I suspect this happens in rare cases where the purchase flow throws an exception: • Every time we throw, we immediately cancel the transaction on our server. • However, some errors may actually be temporary. StoreKit seems to recover by later sending an update through Transactions.updates. • When that happens, since the transaction was already canceled on our server, we cannot match it. As a result, we just call .finish() without granting consumables.

Additional Observations

From user logs, the issue tends to appear when the following errors are reported: • リクエストを完了できません。 • Triggered by: StoreKit.notEntitled • Triggered by: StoreKit.unknown • ヘルパーアプリケーションと通信できませんでした。

I was not able to reproduce the last one, even when testing all possible StoreKit configuration errors. Some sources suggest this may be a rare case where the app cannot communicate with the App Store itself.

Additionally, affected users often seem to be using non-standard payment methods (PayPay, gift cards, carrier billing, etc.) rather than credit cards.

My Question

What is the best practice for handling StoreKitError and PurchaseError? Specifically: • Should some of these errors be treated as temporary instead of final? • How should we ensure users always receive their consumables in such cases?

Code

do {
  let result = try await wrapper.product.purchase()
  switch result {
  case .success(let result):
    let transaction = try StoreKitVerifier.checkVerified(result)
    let iOSPurchase = IOSPurchase(transaction: transaction, jws: result.jwsRepresentation)
    return .Success(purchase: iOSPurchase)
  case .userCancelled:
    return .Canceled()
  case .pending:
    return .Pending()
  @unknown default:
    return .Error(
      message: "Purchase result doesn't match with anything. This must not be executed")
  }
} catch {
  return .Error(message: error.localizedDescription)
}
How to properly handle StoreKitError or PurchaseError from product.purchase()
 
 
Q