内购有什么新功能
What's new with in-app purchase
2020年6月23日
一句话判断
2020 年内购的重磅更新:App Store 服务器通知 V2(含退款通知)、订阅优惠码(Promotional Offers)、家庭共享支持,以及 Xcode 内置的 StoreKit 测试——做订阅制 app 的开发者终于不用两眼一抹黑地猜用户订阅状态了。
这场 Session 讲了什么
这场 Session 涵盖了 2020 年 In-App Purchase(内购)的四大更新:
App Store Server Notifications V2。苹果的服务端通知 API 升级到了 V2 版本,新增了 REFUND 通知类型。当用户申请退款并成功时,你的服务器会收到实时通知,可以立即撤销对应的虚拟商品或服务。V2 通知使用 JWT 格式签名,安全性更高。
Subscription Offer Codes(订阅优惠码)。开发者现在可以生成优惠码,用户在 App Store 中兑换后享受折扣价。支持「首月特价」「免费试用 N 天」等优惠类型。优惠码可以通过邮件、社交媒体、线下活动等渠道分发。
Family Sharing(家庭共享)。非消耗型内购和自动续期订阅现在支持家庭共享。用户购买一次,家庭成员也能使用。需要在 App Store Connect 中为每个产品开启家庭共享选项。
Xcode StoreKit Testing。与 Session 10659 同步推出的本地测试环境。
值得深挖的点
退款通知是补齐的关键拼图
之前开发者无法实时获知退款事件。用户买了订阅、用了内容、然后申请退款,开发者往往几天后查看财务报告时才发现。有了 REFUND 通知,你可以在退款发生时立即撤销权益。Session 建议在收到退款通知后:记录退款事件 -> 撤销虚拟商品 -> 但不要立即惩罚用户(可能是误操作)。
优惠码的运营策略
优惠码分为两种:一次性码(One-Time-Use Codes,每个码只能用一次)和自定义码(Custom Codes,可以设置总使用次数上限)。一次性码适合大规模分发(比如活动推广),自定义码适合定向营销(比如给流失用户发回归优惠)。每种优惠码可以设置不同的优惠类型和持续时间。
代码片段
import StoreKit
// 处理家庭共享的内购验证
func verifyFamilySharingPurchase() {
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) else { return }
// 在服务端验证 receipt,检查是否为家庭共享购买
// App Store 的 receipt 中会包含 in_app_ownership_type 字段
// "FAMILY_SHARED" 表示这是家庭共享的购买
sendToServerForVerification(receiptData: receiptData)
}
// 客户端展示优惠码兑换入口
func presentOfferCodeRedemption() {
// iOS 14 新增的优惠码兑换界面
SKPaymentQueue.default().presentCodeRedemptionSheet()
}
# 服务端处理 App Store Server Notifications V2
# 这是 Python 示例(服务端代码)
import jwt
def handle_notification(notification_payload):
"""
处理 App Store V2 通知
notification_payload 包含 signedPayload (JWT 格式)
"""
# 解码 JWT 获取通知内容
signed_payload = notification_payload['signedPayload']
# 验证签名并解码(需要苹果的根证书)
decoded = jwt.decode(signed_payload, options={"verify_signature": True})
notification_type = decoded['notificationType']
if notification_type == 'REFUND':
# 用户退款成功
transaction_id = decoded['data']['transactionId']
original_transaction_id = decoded['data']['originalTransactionId']
# 撤销用户权益
revoke_access(original_transaction_id)
print(f"用户退款: {transaction_id}")
elif notification_type == 'DID_RENEW':
# 订阅续期成功
transaction_id = decoded['data']['transactionId']
extend_access(transaction_id)
print(f"订阅续期: {transaction_id}")
elif notification_type == 'DID_FAIL_TO_RENEW':
# 续期失败(通常是扣费失败)
# 进入宽限期,不要立即撤销
enter_grace_period(decoded['data']['originalTransactionId'])
最佳实践
- 尽快迁移到 Server Notifications V2。V1 通知缺少退款事件,而且格式不够规范。V2 使用 JWT 签名,验证更安全。迁移过程中 V1 和 V2 可以同时启用,确认 V2 稳定后再关闭 V1。
- 退款后不要急于惩罚。收到退款通知后撤销权益是合理的,但不要立刻封号或弹出警告。Apple 对频繁退款的用户有自己的风控机制,开发者不需要额外「惩罚」用户。
- 为家庭共享做好权限检查。如果你的内购支持家庭共享,在验证 receipt 时要检查
in_app_ownership_type。家庭共享用户可能没有「购买者」的全部权限(比如不能管理订阅),UI 上要做相应区分。
还有什么值得关注
- 优惠码有有效期设置,过期后不可兑换。建议设置合理的有效期并在推广活动中明确标注。
- Family Sharing 只支持非消耗型内购和自动续期订阅,消耗型内购(如游戏币)不支持。
- Xcode 的 StoreKit 测试环境可以模拟退款场景,不用等真实用户退款才来调试逻辑。