RevenueCat 报 RECEIPT_ALREADY_IN_USE 前如何阻止扣款?
结论
在 Apple 扣款前,无法通过 RevenueCat 可靠判断当前 Apple Account 的订阅是否属于另一个 App User ID。
RevenueCat 只有在收到并验证新交易的收据后,才会发现归属冲突并返回 RECEIPT_ALREADY_IN_USE。此时 StoreKit 已经完成或接受交易,RevenueCat 无法撤销 Apple 的扣款流程,也没有提供用于购买前检查 Apple Account 是否已被其他 App User ID 使用的 API。
实际可行的办法是,在购买前要求用户恢复或同步购买,尽早发现冲突并关闭应用内购买入口,同时提示用户登录原账号。这能降低误购概率,但无法绝对避免扣款。
为什么错误发生在付款之后
这次操作涉及 StoreKit 和 RevenueCat 两个独立系统,处理顺序如下:
- StoreKit 使用 Apple Account X 发起购买。
- Apple 完成交易并生成收据。
- RevenueCat SDK 将收据发送给 RevenueCat。
- RevenueCat 验证收据,发现收据原本属于 User A。
- 由于项目使用
Keep with original App User ID,RevenueCat 不会转移收据,而是返回RECEIPT_ALREADY_IN_USE。
RevenueCat 必须拿到收据后才能识别其原始归属。因此,这个错误的含义是“交易无法归属给当前 App User ID”,而不是“Apple 没有完成购买”。
如果 monthly 和 yearly 位于同一个 subscription group,Apple 还可能将第二次操作视为升级、降级或套餐切换。实际扣费时间取决于 App Store 的订阅规则。sandbox 中的续订周期和状态变化也不能完全代表生产环境。
推荐处理流程
1. 购买前确认 RevenueCat 登录身份
用户登录业务账号后,应先调用 RevenueCat 的登录接口,确认 SDK 当前使用的是稳定且唯一的 App User ID。不要继续使用上一位用户的身份,也不要意外创建匿名 ID。
Purchases.shared.logIn("user-b-id") { customerInfo, created, error in
if let error {
// 登录 RevenueCat 身份失败,不开放购买入口
return
}
// 身份确认成功后,再执行订阅状态检查
}
用户退出业务账号时,也需要同步处理 RevenueCat 身份。是否调用 logOut(),取决于应用是否允许匿名用户购买。如果不允许,用户退出后应直接隐藏购买入口,等新用户完成登录后再显示。
2. 在购买前让用户恢复购买
新用户登录时,如果本机可能使用过其他业务账号,可以先让用户执行“恢复购买”。恢复成功后,根据 CustomerInfo 判断当前用户是否已有权益。如果 RevenueCat 返回收据归属冲突,则阻止购买,并提示用户登录原业务账号。
func checkStoreAccountBeforePurchase(
completion: @escaping (Bool, String?) -> Void
) {
Purchases.shared.restorePurchases { customerInfo, error in
if let error = error as NSError? {
// 应根据当前 RevenueCat SDK 版本公开的错误类型或错误码判断。
// 不要只依赖可能变化的 localizedDescription 文本。
if error.localizedDescription.contains("RECEIPT_ALREADY_IN_USE") {
completion(
false,
"这个 App Store 账号的订阅已绑定到另一个应用账号,请登录原账号后继续。"
)
return
}
completion(false, "无法验证已有购买,请稍后重试。")
return
}
completion(true, nil)
}
}
这里使用字符串判断只是为了展示流程。正式代码应使用当前 RevenueCat SDK 版本公开的错误枚举或错误码,不要假定不同版本和平台使用相同的类型名称或固定常量。
恢复购买可能触发 Apple 身份验证,因此更适合由用户主动点击“恢复购买”,或者在购买前通过说明清楚的确认页面执行,不应在后台频繁调用。
3. 预检查通过后再显示购买确认
func startYearlyPurchase(package: Package) {
checkStoreAccountBeforePurchase { allowed, message in
guard allowed else {
showAlert(message ?? "当前无法购买。")
return
}
Purchases.shared.purchase(package: package) {
transaction,
customerInfo,
error,
userCancelled in
if userCancelled {
return
}
if let error {
// 即使完成过预检查,仍必须处理 RECEIPT_ALREADY_IN_USE。
// 检查与实际购买之间存在状态变化窗口。
handlePurchaseError(error)
return
}
if customerInfo?.entitlements["premium"]?.isActive == true {
unlockPremium()
}
}
}
}
不要仅凭 StoreKit 返回“购买成功”就开通权益。最终应检查 RevenueCat 返回的 CustomerInfo,或者由后端通过 RevenueCat webhook / API 确认 entitlement 已归属当前 App User ID。
预检查为什么仍不能完全避免扣款
即使在购买前执行恢复,仍有以下限制:
- 应用无法直接获取或比较用户的 Apple Account 标识。
- 用户可能更换 App Store 账号,也可能在其他设备上改变订阅状态。
- 恢复完成与实际购买之间存在竞态窗口。
- 用户可以直接在 App Store 的订阅管理页面操作订阅,绕过应用内购买入口。
- StoreKit 是否立即扣费、延期生效或按比例结算,由 Apple 决定。
- 首次购买前,本地收据信息可能不足以识别历史归属。
所以,购买前恢复只能作为防止误购的措施,不能充当事务级强制校验。
更稳妥的产品设计
如果业务要求“一个 Apple 订阅永久属于最初绑定的业务账号”,建议同时采取以下措施:
- 在支付页明确说明订阅会与业务账号绑定,退出账号不会自动转移订阅。
- 检测到本机更换业务账号时,优先显示“恢复购买”或“登录原账号”,暂时隐藏购买按钮。
- 针对
RECEIPT_ALREADY_IN_USE显示专门提示,不要将其当作普通支付失败处理。 - 提供找回原业务账号的流程。
- 服务端保存 RevenueCat webhook 提供的订阅归属信息,供客服排查。不要尝试保存或推断用户的 Apple Account。
- 如果业务允许换绑,应重新评估 RevenueCat 的 restore behavior,而不是继续使用
Keep with original App User ID。修改该策略会影响订阅归属和安全模型,因此需要先验证现有用户的迁移规则。
已经发生扣款时
RevenueCat 无法直接撤销 App Store 交易或办理退款。应先确认这笔交易是立即扣费、订阅升级,还是从下一周期开始生效的套餐变更。如果确实需要退款,应引导用户通过 Apple 的退款渠道申请,再由客服结合 RevenueCat 事件记录核对交易状态。
应用可以在购买前增加恢复检查并限制购买入口,但无法保证在 RevenueCat 验证收据前拦截 Apple 的交易。
备注:内容仅供参考。
