You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

StoreKit 2订阅续期事务在Transaction.all/updates中丢失求助

问题背景

iOS 15+环境下使用StoreKit 2实现订阅服务,新订阅开通正常,但有不到1%的用户反馈续期后有效订阅未被识别,表现为续期对应的StoreKit事务未出现在本地记录中。排查发现:强制重启应用、调用AppStore.sync()无效;重启设备仅对部分用户有效;重装应用可彻底解决问题,且无法在测试设备复现该问题。

现有核心实现逻辑:通过StoreManager统一处理StoreKit交互,初始化时遍历Transaction.all加载历史事务,同时监听Transaction.updates;用事务identifier作为键,将已购项目存储在purchasedItems字典中,所有字典写入操作通过MainActor绑定的updatePurchasedItemFor()执行;通过遍历字典判断用户订阅状态。

问题分析

结合代码与排查结果,问题大概率源于以下几点:

  1. 事务验证错误被静默忽略:loadAllTransactions中使用try?跳过验证失败的事务,若续期事务因网络、本地缓存异常等原因验证失败,会直接丢失,未加入本地存储。
  2. 订阅状态判断未兜底官方接口:仅依赖本地存储的事务列表,未使用苹果推荐的Transaction.currentEntitlements作为兜底,当本地事务不完整时无法获取正确状态。
  3. 事务存储逻辑未聚焦最新有效订阅:以事务ID为键存储所有历史事务,但判断订阅状态时遍历全部条目,若最新续期事务未被加入,可能误判为未订阅。

针对性修复方案

1. 修复事务加载的错误处理

移除try?并捕获验证错误,避免丢失事务:

func loadAllTransactions() {
  Task(priority: .userInitiated) { @MainActor in
    do {
      for await result in Transaction.all {
        let transaction = try checkVerified(result)
        await updatePurchasedItemFor(transaction)
      }
    } catch {
      Analytics.logError(error, forActivity: "加载全部事务失败")
    }
  }
}

2. 用Transaction.currentEntitlements作为订阅状态兜底

将同步计算属性改为异步方法,优先通过官方接口获取当前有效订阅,再回退到本地存储:

func fetchSubscriberState() async -> SubscriberState {
  // 优先检查官方当前权益接口
  do {
    for await result in Transaction.currentEntitlements {
      let transaction = try checkVerified(result)
      if let expirationDate = transaction.expirationDate, expirationDate > Date() {
        return .subscribed(expirationDate: expirationDate, isInTrial: transaction.offerType == .introductory)
      }
    }
  } catch {
    Analytics.logError(error, forActivity: "检查当前权益失败")
  }

  // 回退到本地存储的事务
  for (_, item) in purchasedItems {
    if let expirationDate = item.expirationDate, expirationDate > Date() {
      return .subscribed(expirationDate: expirationDate, isInTrial: item.isInTrial)
    }
  }
  return .notSubscribed
}

3. 优化事务存储,聚焦最新有效订阅

新增按原始事务ID存储最新订阅的字典,确保同一订阅仅保留最新有效事务:

class StoreManager {
  // ... 原有代码 ...
  
  private(set) var purchasedItems: [UInt64: PurchasedItem] = [:]
  // 新增:按原始事务ID存储最新的有效订阅
  private(set) var latestSubscriptionsByOriginalId: [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 {
      purchasedItems[transaction.id] = item
      
      // 更新同一原始订阅的最新条目
      if let originalId = transaction.originalID {
        let shouldUpdate = latestSubscriptionsByOriginalId[originalId].flatMap { existing in
          item.expirationDate ?? .distantFuture > existing.expirationDate ?? .distantFuture
        } ?? true
        
        if shouldUpdate {
          latestSubscriptionsByOriginalId[originalId] = item
        }
      }
    } else {
      purchasedItems[transaction.id] = nil
      
      // 若被撤销的是最新条目,重新查找同一订阅的其他有效事务
      if let originalId = transaction.originalID, 
         latestSubscriptionsByOriginalId[originalId]?.transactionId == transaction.id {
        latestSubscriptionsByOriginalId[originalId] = purchasedItems.values
          .filter { $0.originalTransactionId == originalId && ($0.expirationDate ?? .distantFuture) > Date() }
          .max { $0.expirationDate ?? .distantPast < $1.expirationDate ?? .distantPast }
      }
    }

    NotificationCenter.default.post(name: StoreManager.purchasesDidUpdateNotification, object: self)
  }
  
  // ... 原有代码 ...
}

后续判断订阅状态时,可直接遍历latestSubscriptionsByOriginalId,效率更高且更准确。

4. 增强手动同步逻辑

提供包含重新加载事务的同步方法,解决AppStore.sync()无效的问题:

func syncAndReloadPurchases() async {
  do {
    try await AppStore.sync()
    // 同步后重新加载所有事务
    await loadAllTransactions()
  } catch {
    Analytics.logError(error, forActivity: "同步并重新加载事务失败")
  }
}

5. 补充日志与用户反馈收集

在所有事务处理的错误分支添加详细日志,同时在应用内提供日志导出功能,当用户反馈问题时,收集日志定位具体故障点(如事务验证失败、未获取到续期事务等)。

关于StoreKit 2潜在bug的应对

若上述修复后仍有问题,可继续跟进苹果的bug报告,同时在应用内添加临时的订阅状态申诉入口,让用户提供相关信息协助苹果排查问题。

内容的提问来源于stack exchange,提问作者Theo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 13:36:30