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

问题描述 投票:0回答:2

我在 iOS 应用程序中提供订阅服务。如果应用程序在 iOS 15 或更高版本上运行,我使用

StoreKit 2
来处理订阅启动和续订。我的实现紧密遵循 Apple 的示例代码。

我的用户中的一小部分(<1%) report that 他们的活动订阅无法被识别 - 通常在续订之后(开始新的订阅似乎总是有效)。看起来好像没有 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
    }
  }
}

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

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
}

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

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

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

为了排除 3.,我已向 Apple 提交了 TSI 请求。他们的回应本质上是:您应该使用

Transaction.currentEntitlements
而不是
Transaction.all
来确定用户当前的订阅状态,但实际上这个实现也应该有效。如果没有,请提交错误。

我使用

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

ios swift in-app-purchase storekit
2个回答
7
投票

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

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

我通过收集

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

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

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

Transaction.all
Transaction.updates
的所有新事务合并。

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

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


0
投票

我也有同样的问题。一旦您购买了订阅,它就可以正常工作,但是当用户计划降级订阅时,currentEntitlements 将仅列出有效的订阅,该订阅可能尚未过期。因此,用户看不到更新的订阅期。

我猜您的用户也有同样的问题。当前订阅期到期后,更改后的订阅将自动出现。

我找到了一些建议,但无法编码。 autoRenewalPreference 应该显示下一个即将推出的订阅产品的更新后的productid,因此是降级的产品,但无法弄清楚如何从代码中获取此值。

© www.soinside.com 2019 - 2024. All rights reserved.