Implement proactive in-app purchase restore
App Store & Distribution 进阶 20m

实现主动式应用内购买恢复

Implement proactive in-app purchase restore

2022年6月6日

在 Apple 官方观看视频

一句话判断

如果你的付费应用还在让用户手动点”恢复购买”,这场 Session 会让你意识到这是多么落后的体验——StoreKit 2 已经可以做到完全无感恢复。

这场 Session 讲了什么

App Store 商务技术团队的 David Wendland 详细讲解了”主动式应用内购买恢复”的最佳实践。核心理念是:应用启动时,利用设备上已有的 StoreKit 数据,自动判断用户是新客户、现有客户还是流失客户,无需用户执行任何操作。

Session 用一个叫 Ocean Journal 的示例应用贯穿始终。在传统体验中,用户面对”购买”、“登录”和”恢复购买”三个按钮,往往不知道该选哪个。在优化后的体验中,已有订阅的用户在新设备上打开应用时,应用会自动识别其订阅状态并直接解锁所有功能。

内容覆盖了 StoreKit 2(iOS 15+)和原始 StoreKit 两种实现路径,以及三种核心客户状态的定义和应用场景。

值得深挖的点

三种核心客户状态。 第一种是”新客户”——没有任何当前或历史交易记录,适合展示默认的商品推荐页和 introductory offer。第二种是”已购买/活跃订阅”——有有效交易,应用有义务立即提供服务。第三种是”非活跃客户”——曾经购买但已过期或被撤销,适合展示 win-back offer。

计费重试与宽限期。 当自动续订失败后,Apple 会尝试恢复订阅长达 60 天。如果你在 App Store Connect 中开启了计费宽限期,订阅者在重试期间仍可使用服务。这段时间内应该展示简单的提示让用户解决付款问题,而不是粗暴地中断服务。

Original Transaction ID 的关键作用。 每笔购买和每个活跃订阅都有一个唯一且持久的原始交易 ID。你应该将这个 ID 与你系统中的用户账户关联,这样配合 App Store Server Notifications,服务端就能始终掌握最新的交易状态。

StoreKit 2 的实现更简洁。 通过 Transaction.currentEntitlements 可以直接获取当前有效的交易,Transaction.all 可以获取完整的历史记录。配合 Product.SubscriptionInfo.Status 可以精确判断订阅状态。

代码片段

import StoreKit

// StoreKit 2: 主动检查当前权益
func checkPurchaseStatus() async {
    // 检查当前有效的权益
    for await result in Transaction.currentEntitlements {
        guard case .verified(let transaction) = result else { continue }
        
        // 用户有有效权益,直接解锁功能
        unlockFeature(for: transaction.productID)
        transaction.finish()
    }
}

// 检查订阅状态,区分活跃/非活跃客户
func checkSubscriptionStatus(productID: String) async {
    let product = try await Product.products(for: [productID]).first
    guard let product = product else { return }
    
    let statuses = try await product.subscription?.status ?? []
    for status in statuses {
        switch status.state {
        case .subscribed:
            // 活跃订阅者,解锁全部功能
            break
        case .revoked, .expired:
            // 非活跃订阅者,展示 win-back offer
            presentWinBackOffer()
        default:
            break
        }
    }
}

// 获取完整交易历史用于判断新/老客户
func checkCustomerHistory() async {
    var hasHistory = false
    for await result in Transaction.currentEntitlements {
        if case .verified = result {
            hasHistory = true
            break
        }
    }
    
    if !hasHistory {
        // 新客户,展示 introductory offer
        presentNewCustomerExperience()
    }
}

最佳实践

  • 应用启动时立即检查 StoreKit 交易状态,不要等用户点击”恢复购买”
  • 用 originalTransactionId 在服务端关联用户账户,配合 Server Notifications 保持状态同步
  • 对于计费重试中的订阅者,展示温和的提示而非中断服务
  • 对非活跃订阅者展示促销优惠或 offer code 来召回
  • 如果支持多个产品或订阅组,需要分别判断每个产品的客户状态,考虑混合状态的处理
  • 考虑跨平台活动(如 Web 端购买)对客户状态的影响
  • iOS 15+ 使用 StoreKit 2,更早版本用原始 StoreKit + verifyReceipt 端点实现相同逻辑

还有什么值得关注

  • App Store Server Notifications V2 提供了更丰富的订阅状态变更事件
  • Family Sharing 场景下,交易可能因为共享权限被撤销而产生 revocation date
  • 对于支持多年期的非续期订阅,过期判断逻辑与自动续订不同,需要特别处理
  • 配套观看关于减少非自愿订阅流失的 Session,了解计费重试和宽限期的完整机制
WWDC 2022