开发者接入 IAP 时,需要按照苹果提供的规范,根据 App 提供商品的功能和类型来选择不同的内购项目类型,进行创建商品。相当于在我们业务服务端有一份商品列表,苹果 AppStoreConnect 也有一份商品列表与之对应。目前 IAP 中内购项目分为四类:
选择商品类型后,AppStore Connect 中创建商品,以消耗型商品创建为例,需要提供如下信息:
具体操作手册参见Create in-app purchases
开发者需要接入系统库 StoreKit,苹果在 WWDC21 推出新的 StoreKit2 支持购买,但其需要 iOS15 及以上才支持,目前我们项目中还是使用老的 StoreKit 。
对于 IAP 购买支付的过程是苹果系统处理,只是在交易完成之后,更新本地的交易票据信息并回调 App (票据可以理解为包含交易支付相关信息的加密数据),而对于这份数据是可能会重复或者伪造;需要对其进行验证,苹果提供两种方式:本地验证和服务端验证;一般出于安全性和功能考虑会选用服务端验证。服务端会拿着这份票据再去请求苹果服务端,获取交易支付的详细信息,根据信息判断处理履约情况。
整体流程结构如下图:
自动订阅类型的商品因为涉及到下个周期代扣履约的情况,会多一些处理,一是服务端可以通过 App Store Server Notifications接收订阅续期的情况;二是 App 在启动时收到苹果关于续期成功的票据更新回调。
SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:self.productIdentifier]];
request.delegate = self;
....
[request start];
//SKRequestDelegate callback
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{....}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{....}
IAP Product 是在 AppStoreConnect 中配置,是与我们的App对应。特别需要注意的是在测试包App被重签名时,将会获取不到对应的 IAP 商品信息。
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:self.product];
payment.quantity = MAX(_quantity,1);
payment.applicationUsername = self.userIdentifier;
[[SKPaymentQueue defaultQueue] addPayment:payment];
IAP 支持批量购买,但支持的最大数量是 10 ,具体说明参见 SKMutablePayment——quantity
//需要监听Payment Queue,建议是在didFinishLaunchingWithOptions:时就增加监听
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
//处理回调事件
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
//购买完成...
break;
case SKPaymentTransactionStateFailed:
//交易失败...
break;
case SKPaymentTransactionStateRestored:
//恢复交易...
break;
case SKPaymentTransactionStatePurchasing:
//交易正在进行..
break;
default:
break;
}
}
}
//获取小票
NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
//请求服务端验证
....
//交易完成
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
沙盒环境: https://sandbox.itunes.apple.com/verifyReceipt
正式环境: https://buy.itunes.apple.com/verifyReceipt
沙盒环境不需要真实购买,在 AppStoreConnect 创建沙盒测试账号,可以模拟支付。
正式环境是针对 AppStore 上架的应用内购买,如果将沙盒环境小票发送到正式环境验证,会收到 21007 的 Status Code
{
"receipt-data":"xxxxx", //客户端本地的小票数据
"password":"xxxxxx" //可选,自动订阅设置时在 AppStoreConnect 生成的密钥(无自动订阅时不需要)
}
可以看到验证请求接口没有过多限制,只要是真实的小票数据,就可以通过验证接口请求返回结果,这也对服务端对票据结果的真实可靠性需要做完备的校验
//消费型商品购买验证结果
{
"receipt": {
"receipt_type": "Production", //交易产生的环境
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "xxxxxxx", //小票归属的 App bundleId
"application_version": "0",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2023-02-22 11:02:52 Etc/GMT",
"receipt_creation_date_ms": "1677063772000", //生成小票的时间戳
"receipt_creation_date_pst": "2023-02-22 03:02:52 America/Los_Angeles",
"request_date": "2023-02-24 04:20:38 Etc/GMT",
"request_date_ms": "1677212438488",
"request_date_pst": "2023-02-23 20:20:38 America/Los_Angeles",
"original_purchase_date": "2022-12-16 05:46:18 Etc/GMT",
"original_purchase_date_ms": "1671169578000",
"original_purchase_date_pst": "2022-12-15 21:46:18 America/Los_Angeles",
"original_application_version": "0",
"in_app": [ //所有交易小票信息
{
"quantity": "1",
"product_id": "xxxxxxxxx.xxxx.xxxx", //交易商品的标识符
"transaction_id": "470001434498518", //每次交易发生产的唯一标识符
"original_transaction_id": "470001434498518",//原始购买的交易标识符,自动续费下次代扣发生交易,改址不变
"purchase_date": "2023-02-22 11:02:52 Etc/GMT",
"purchase_date_ms": "1677063772000", //购买时间戳
"purchase_date_pst": "2023-02-22 03:02:52 America/Los_Angeles",
"original_purchase_date": "2023-02-22 11:02:52 Etc/GMT",
"original_purchase_date_ms": "1677063772000",
"original_purchase_date_pst": "2023-02-22 03:02:52 America/Los_Angeles",
"is_trial_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
},
"environment": "Production", //票据产生环境,Sandbox/Production
"status": 0 //标识票据是否合法
}
//自动订阅商品购买验证结果
{
"status": 0,
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "xxxxxx",
"application_version": "0",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2019-05-15 12:00:08 Etc/GMT",
"receipt_creation_date_ms": "1557921608000",
"receipt_creation_date_pst": "2019-05-15 05:00:08 America/Los_Angeles",
"request_date": "2019-06-03 08:47:04 Etc/GMT",
"request_date_ms": "1559551624568",
"request_date_pst": "2019-06-03 01:47:04 America/Los_Angeles",
"original_purchase_date": "2018-08-26 03:28:11 Etc/GMT",
"original_purchase_date_ms": "1535254091000",
"original_purchase_date_pst": "2018-08-25 20:28:11 America/Los_Angeles",
"original_application_version": "0",
"in_app": [{
"quantity": "1",
"product_id": "xxxxxxxxxxx",
"transaction_id": "370000374840125",
"original_transaction_id": "370000374840125",
"purchase_date": "2019-05-15 11:59:38 Etc/GMT",
"purchase_date_ms": "1557921578000",
"purchase_date_pst": "2019-05-15 04:59:38 America/Los_Angeles",
"original_purchase_date": "2019-05-15 11:59:40 Etc/GMT",
"original_purchase_date_ms": "1557921580000",
"original_purchase_date_pst": "2019-05-15 04:59:40 America/Los_Angeles",
"expires_date": "2019-06-15 11:59:38 Etc/GMT",
"expires_date_ms": "1560599978000",
"expires_date_pst": "2019-06-15 04:59:38 America/Los_Angeles",
"web_order_line_item_id": "370000115213929",
"is_trial_period": "false",
"is_in_intro_offer_period": "true"
}]
},
"latest_receipt_info": [{ //除已完成的消费型商品以外的所有交易信息
"quantity": "1",
"product_id": "xxxxxxxxx.xxxx.xxxx",
"transaction_id": "370000374840125",
"original_transaction_id": "370000374840125",
"purchase_date": "2019-05-15 11:59:38 Etc/GMT",
"purchase_date_ms": "1557921578000",
"purchase_date_pst": "2019-05-15 04:59:38 America/Los_Angeles",
"original_purchase_date": "2019-05-15 11:59:40 Etc/GMT",
"original_purchase_date_ms": "1557921580000",
"original_purchase_date_pst": "2019-05-15 04:59:40 America/Los_Angeles",
"expires_date": "2019-06-15 11:59:38 Etc/GMT",
"expires_date_ms": "1560599978000",
"expires_date_pst": "2019-06-15 04:59:38 America/Los_Angeles",
"web_order_line_item_id": "370000115213929",
"is_trial_period": "false",
"is_in_intro_offer_period": "true"
}],
"latest_receipt": "xxxxxxxxxxx latest_receipt_info xxxxxxxxxxxxx", //只包含自动续费相关票据
"pending_renewal_info": [{ //自动续费具体状态和内容
"auto_renew_product_id": "xxxxxxxxx.xxxx.xxxx",
"original_transaction_id": "370000374840125",
"product_id": "xxxxxxxxx.xxxx.xxxx",
"auto_renew_status": "1"
}]
}
所有字段的含义可以参见App Store Receipts responseBody
可以看到返回结果中包含交易的详细信息,但没有和我们 App 内部相关的,需要服务端解析这些信息处理,将权益发放给用户,因此也会产生较多的问题
从上述流程中发现,IAP 商品交易支付是在系统内部流转,对于 App 只有发起和交易结果回调的感知,而最终交易结果需要依托客户端向服务端发起票据验证请求,获取到结果再和自身服务做匹配履约;服务端无法主动向苹果请求订单结果。
因此在实际应用场景中会遇到各种问题:
针对上述提到的问题进行解决,也伴随着云音乐多个产品线开发上线,接入 IAP 需求也在增加,因此我们开发了基础库 NEStoreKit,对业务流程进行抽象,方便各团队快速接入;保障支付履约完成,完善交易场景,记录各个阶段交易日志,对问题有效排查。
将 IAP 交易处理逻辑封装在内部,回调的交易信息包装成 Task,放入队列中,依次交由 Verifier 请求服务端进行验证。
//配置
NEStoreConfig *storeConfig = [NEStoreConfig new];
storeConfig.verifyRequestUrl = xxxx
//重试验证回调处理
storeConfig.silentVerifyCompletionBlock = ^(NEStorePaymentResult *paymentResult) {
};
//取消购买回调
storeConfig.cancelPaymentBlock = ^(NEStorePaymentResult *paymentResult, SKPaymentTransaction *transaction) {
//...
};
[[NEStoreManager defaultManager] setConfig:storeConfig];
//发起购买调用
- (void)makePayment:(NSString *)productIdentifier
quantity:(NSInteger)quantity
userIdentifier:(nullable NSString *)userIdentifier
userInfo:(nullable NSDictionary *)userInfo
success:(nullable NEPaymentCompletionBlock)success
failure:(nullable NEPaymentCompletionBlock)failure;
REFUND
类型苹果在WWDC2021提出的针对IAP的全新设计,Meet StoreKit 2
StoreKit2 提供的 API 使用更为简单,对于客户端来说可以用 appAccountToken 替换 applicationUserName ,将 AppleId 和 App 中账户对应,不会像之前容易丢失;同时服务端也可以通过这个标识将用户的消费行为发给苹果,协助苹果处理用户对消费型商品退款的情况。目前较大问题是iOS版本的限制。
IAP的使用一直为开发者诟病,包括创建商品的流程繁琐,以及刚开始接入自动续费时,踩了不少坑,在和苹果开发人员交流和反馈中,苹果逐渐为开发者提供了更多更全面的API,诸如调用接口管理 IAP 商品Create an In-App Purchase,服务端通过App Store Server API自主查询交易信息。作为iOS开发人员需要持续关注 StoreKit 的发展,与服务端交流,不断完善交易系统的可靠和安全性。
本文发布自网易云音乐技术团队,文章未经授权禁止任何形式的转载。我们常年招收各类技术岗位,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!