我在 iOS 应用程序中提供订阅服务。如果应用程序在 iOS 15 或更高版本上运行,我使用
StoreKit 2
来处理订阅启动和续订。我的实现紧密遵循 Apple 的示例代码。
我的用户中的一小部分(<1%) report that 他们的活动订阅无法被识别 - 通常在续订之后(开始新的订阅似乎总是有效)。看起来好像没有 StoreKit 交易显示用于续订。
经过一些故障排除后我发现:
AppStore.sync()
没有任何帮助。我永远无法在我的设备上重现此错误。
这是我的实现要点:我有一个
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的示例代码非常相似。不过,某个地方有一个错误,我找不到它。
我可以想象这是以下三个问题之一:
identifier
,我可以将其用作将其收集到字典中的密钥。为了排除 3.,我已向 Apple 提交了 TSI 请求。他们的回应本质上是:您应该使用
Transaction.currentEntitlements
而不是 Transaction.all
来确定用户当前的订阅状态,但实际上这个实现也应该有效。如果没有,请提交错误。
我使用
Transaction.all
是因为我需要用户的完整交易历史记录来自定义应用程序中的消息传递和特别优惠,而不仅仅是确定用户是否有有效订阅。所以我提交了一个错误,但还没有收到任何回复。
通过分析从大量用户收集大量低级事件后,我非常有信心这是由 StoreKit 2 中的错误引起的,并且
Transaction.all
无法可靠地返回所有旧的、已完成的订阅开始和续订交易。我通过收集
for await result in Transaction.all
循环中的所有交易标识符来检查这一点,然后将这些标识符作为单个事件发送到我的分析后端。我可以清楚地看到,对于某些用户来说,先前存在的标识符有时会在随后启动应用程序时丢失。
不幸的是,苹果从 TSI 得到的唯一建议就是报告错误,而苹果从未回复我的详细错误报告。*
作为解决方法,我现在将所有事务缓存在磁盘上,并在启动后将缓存的事务与来自
Transaction.all
和 Transaction.updates
的所有新事务合并。
这工作完美 - 自从我实现以来,我没有收到客户关于无法识别的订阅的任何投诉。
* 弄清楚这一切并找到可靠的修复方法花了几个月的时间 - 我很高兴 Apple 只用了我收入的 30% 就提供了如此出色、可靠的服务,tysm。
我也有同样的问题。一旦您购买了订阅,它就可以正常工作,但是当用户计划降级订阅时,currentEntitlements 将仅列出有效的订阅,该订阅可能尚未过期。因此,用户看不到更新的订阅期。
我猜您的用户也有同样的问题。当前订阅期到期后,更改后的订阅将自动出现。
我找到了一些建议,但无法编码。 autoRenewalPreference 应该显示下一个即将推出的订阅产品的更新后的productid,因此是降级的产品,但无法弄清楚如何从代码中获取此值。