Transaction.all 或 Transaction.updates 中缺少 StoreKit 续订交易

The*_*heo 15 storekit in-app-purchase ios swift

我在 iOS 应用程序中提供订阅服务。如果应用程序在 iOS 15 或更高版本上运行,我会StoreKit 2用来处理订阅启动和续订。我的实现紧密遵循Apple 的示例代码。

我的一小部分用户 (<1%) 报告说,他们的有效订阅未被识别- 通常是在续订之后(开始新订阅似乎总是有效)。似乎没有显示任何用于续订的 StoreKit 交易。

经过一些故障排除后我发现:

  • 强制退出并重新启动应用程序永远不会有帮助。
  • 打电话从来AppStore.sync()没有帮助。
  • 重新启动设备对某些用户有帮助,但并非对所有用户都有帮助。
  • 从 App Store 删除并重新下载该应用程序始终有效。

我永远无法在我的设备上重现这个错误。

这是我的实现的要点:我有一个StoreManager类来处理与StoreKit. 初始化后,我立即迭代Transaction.all以获取用户完整的购买历史记录,并启动一个监听Transaction.updates. PurchasedItem是一个自定义结构,我用它来对有关交易的所有相关信息进行分组。购买的物品收集在字典中purchasedItems,我在字典中使用交易identifier作为键。对该字典的所有写入仅发生在绑定updatePurchasedItemFor()MainActor.

class StoreManager {
  static let shared = StoreManager()

  private var updateListenerTask: Task<Void, Error>? = nil

  init() {
    updateListenerTask = listenForTransactions()
    loadAllTransactions()
  }

  func loadAllTransactions() {
    Task { @MainActor in
      for await result in Transaction.all {
        if let transaction = try? checkVerified(result) {
          await updatePurchasedItemFor(transaction)
        }
      }
    }
  }

  func listenForTransactions() -> Task<Void, Error> {
    return Task(priority: .background) {
      // Iterate through any transactions which didn't come from a direct call to `purchase()`.
      for await result in Transaction.updates {
        do {
          let transaction = try self.checkVerified(result)
      
          // Deliver content to the user.
          await self.updatePurchasedItemFor(transaction)
      
          // Always finish a transaction.
          await transaction.finish()
      } catch {
        //StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
        Analytics.logError(error, forActivity: "Verification on transaction update")
      }
    }
  }

  private(set) var purchasedItems: [UInt64: PurchasedItem] = [:]

  @MainActor
  func updatePurchasedItemFor(_ transaction: Transaction) async {
    let item = PurchasedItem(productId: transaction.productID,
                             originalPurchaseDate: transaction.originalPurchaseDate,
                             transactionId: transaction.id,
                             originalTransactionId: transaction.originalID,
                             expirationDate: transaction.expirationDate,
                             isInTrial: transaction.offerType == .introductory)
    if transaction.revocationDate == nil {
      // If the App Store has not revoked the transaction, add it to the list of `purchasedItems`.
      purchasedItems[transaction.id] = item
    } else {
      // If the App Store has revoked this transaction, remove it from the list of `purchasedItems`.
      purchasedItems[transaction.id] = nil
    }

    NotificationCenter.default.post(name: StoreManager.purchasesDidUpdateNotification, object: self)
  }

  private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    // Check if the transaction passes StoreKit verification.
    switch result {
    case .unverified(_, let error):
      // StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
      throw error
    case .verified(let safe):
      // If the transaction is verified, unwrap and return it.
      return safe
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

为了查明用户是否订阅,我使用了这个简短的方法,在应用程序的其他地方实现:

var subscriberState: SubscriberState {
  for (_, item) in StoreManager.shared.purchasedItems {
    if let expirationDate = item.expirationDate,
       expirationDate > Date() {
      return .subscribed(expirationDate: expirationDate, isInTrial: item.isInTrial)
    }
  }
  return .notSubscribed
}
Run Code Online (Sandbox Code Playgroud)

所有这些代码对我来说看起来非常简单,并且与 Apple 的示例代码非常相似。尽管如此,某个地方还是有一个错误,我找不到它。

我可以想象这是以下三个问题之一:

  1. 我误解了 Swift Actor 和 async/await 的工作原理,并且存在竞争条件。
  2. 我误解了 StoreKit 2 事务的工作原理。例如,我目前假设订阅续订交易有其自己的 unique identifier,我可以将其用作将其收集到字典中的密钥。
  3. StoreKit 2 中实际上存在一个错误,一些事务实际上丢失了,并且该错误不在我的代码中。

为了排除 3.,我已向 Apple 提交了 TSI 请求。他们的回应本质上是:您应该使用Transaction.currentEntitlements而不是Transaction.all确定用户当前的订阅状态,但实际上这个实现也应该有效。如果没有,请提交错误。

我之所以使用它,Transaction.all是因为我需要用户的完整交易历史记录来自定义应用程序中的消息传递和特别优惠,而不仅仅是确定用户是否有有效订阅。所以我提交了一个错误,但还没有收到任何回复。

The*_*heo 7

通过分析从大量用户收集了大量低级事件后,我非常确信这是由 StoreKit 2 中的错误引起的,并且

Transaction.all无法可靠地返回所有旧的、已完成的订阅开始和续订交易。

我通过收集循环中的所有事务标识符for await result in Transaction.all,然后将这些标识符作为单个事件发送到我的分析后端来检查这一点。我可以清楚地看到,对于某些用户来说,先前存在的标识符有时会在随后启动应用程序时丢失。

不幸的是,苹果从 TSI 得到的唯一建议就是报告错误,而苹果从未对我的详细错误报告做出回应。*

作为解决方法,我现在将所有事务缓存在磁盘上,并在启动后将缓存的事务与来自Transaction.all和的所有新事务合并Transaction.updates

这工作完美 - 自从我实现以来,我没有收到客户关于无法识别的订阅的任何投诉。

* 弄清楚这一切并找到可靠的修复方法花了几个月的时间 - 我很高兴 Apple 只用了我收入的 30% 就提供了如此出色、可靠的服务,tysm。