What's new with in-app purchase
App Store & Distribution 进阶 17m

内购有什么新功能

What's new with in-app purchase

2020年6月23日

在 Apple 官方观看视频

一句话判断

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'])

最佳实践

  1. 尽快迁移到 Server Notifications V2。V1 通知缺少退款事件,而且格式不够规范。V2 使用 JWT 签名,验证更安全。迁移过程中 V1 和 V2 可以同时启用,确认 V2 稳定后再关闭 V1。
  2. 退款后不要急于惩罚。收到退款通知后撤销权益是合理的,但不要立刻封号或弹出警告。Apple 对频繁退款的用户有自己的风控机制,开发者不需要额外「惩罚」用户。
  3. 为家庭共享做好权限检查。如果你的内购支持家庭共享,在验证 receipt 时要检查 in_app_ownership_type。家庭共享用户可能没有「购买者」的全部权限(比如不能管理订阅),UI 上要做相应区分。

还有什么值得关注

  • 优惠码有有效期设置,过期后不可兑换。建议设置合理的有效期并在推广活动中明确标注。
  • Family Sharing 只支持非消耗型内购和自动续期订阅,消耗型内购(如游戏币)不支持。
  • Xcode 的 StoreKit 测试环境可以模拟退款场景,不用等真实用户退款才来调试逻辑。
WWDC 2020