本文作者:0linatan0
IAP 次要阐明
内购我的项目
开发者接入 IAP 时,须要依照苹果提供的标准,依据 App 提供商品的性能和类型来抉择不同的内购我的项目类型,进行创立商品。相当于在咱们业务服务端有一份商品列表,苹果 AppStoreConnect 也有一份商品列表与之对应。目前 IAP 中内购我的项目分为四类:
-
Consumable products(消耗型商品)
- 比方:Look 直播中的音符
- 同一个 AppleID 能够购买屡次,即买即用
-
Non-consumable products(非消耗型商品)
- 比方:解锁 App 中性能关卡
- 同一个 AppleID 只能购买一次,再次购买会提醒 ” 已购买 ”, 永恒无效
-
Auto-renewable subscriptions(主动续期订阅)
- 比方:云音乐中黑胶会员间断包月
- 同一 Apple ID 在购买时会查看是否购买过,如果购买过并且还在续期权限中,零碎会提醒已购买而无奈再购买;如果购买过之后勾销过,则能够再次购买
-
Non-renewable subscriptions(非续期订阅)
- 比方: 月度 / 季度 / 年度 会员
- 同一 Apple ID 能够购买屡次,能够再次购买,权利受期限限度
创立治理 IAP 商品
抉择商品类型后,AppStore Connect 中创立商品,以消耗型商品创立为例,须要提供如下信息:
-
product identifier : 标识商品的 ID
- 在此利用下是惟一的,只有创立过即便删除也会存在
-
price:依据苹果提供的价格等级,不能随便填写金额
- 会呈现同一等级对应不同国家的 AppleID 账号价格换算差别大
-
商品形容
- 反对多种语言,会依据 AppleID 所在地区展现
- 截图 & 操作门路【送审须要】
具体操作手册参见 Create in-app purchases
我的项目实现 IAP 购买
开发者须要接入零碎库 StoreKit,苹果在 WWDC21 推出新的 StoreKit2 反对购买,但其须要 iOS15 及以上才反对,目前咱们我的项目中还是应用老的 StoreKit。
对于 IAP 购买领取的过程是苹果零碎解决,只是在交易实现之后,更新本地的交易票据信息并回调 App(票据能够了解为蕴含交易领取相干信息的加密数据),而对于这份数据是可能会反复或者伪造;须要对其进行验证,苹果提供两种形式:本地验证和服务端验证;个别出于安全性和性能思考会选用服务端验证。服务端会拿着这份票据再去申请苹果服务端,获取交易领取的详细信息,依据信息判断解决履约状况。
流程图
整体流程构造如下图:
主动订阅类型的商品因为波及到下个周期代扣履约的状况,会多一些解决,一是服务端能够通过 App Store Server Notifications 接管订阅续期的状况;二是 App 在启动时收到苹果对于续期胜利的票据更新回调。
主体逻辑
-
通过 ProductId 申请获取具体的商品信息
SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:self.productIdentifier]]; request.delegate = self; .... [request start];
- (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
- 领取实现后,StoreKit 解决领取,返回此次交易信息
// 须要监听 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 商品和订单号的数据,当收到回调时,依据交易中 ProductId 获取对应的订单号,一并带到服务端申请验证
- 如果因为某些起因未获取到订单号,服务端能够依据票据交易信息在订单零碎中向前回溯实用的订单进行履约
-
Apple 已扣款,但 App 中权利未到账
- 网络抖动、客户端票据失落无奈向服务端发动申请验证等状况都有可能导致该问题
-
优化形式:
- 客户端获取到小票交易信息存储本地,如果验证未实现,定时向服务端发动验证
- 提供用户手动发动验证入口,刷新本地小票数据,向服务端发动验证
- 欠缺每个阶段的日志,便于追溯交易行为
-
主动续费下个周期代扣问题
-
有如下路径能够让服务端感知到扣费时间
- 服务端能够通过 Apple Server-To-Server Notification 接管音讯
- 客户端收到 StoreKit 扣款胜利回调,带上本地票据信息申请服务端解决
- 但因为服务端回调有时不稳固以及依赖设施开启状态,还有一种形式是服务端保留已签约用户的小票数据,在到期前通过这批旧小票向苹果服务端申请续费状态
-
NEStoreKit
针对上述提到的问题进行解决,也随同着云音乐多个产品线开发上线,接入 IAP 需要也在减少,因而咱们开发了根底库 NEStoreKit,对业务流程进行形象,不便各团队疾速接入;保障领取履约实现,欠缺交易场景,记录各个阶段交易日志,对问题无效排查。
整体构造
将 IAP 交易解决逻辑封装在外部,回调的交易信息包装成 Task,放入队列中,顺次交由 Verifier 申请服务端进行验证。
SDK 内部应用
// 配置
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;
IAP 票据后果的可靠性
-
沙盒环境权利发放的隔离
- 审核版本(TestFlight 包)App 运行的是正式环境,IAP 内购走的是沙盒环境,不须要实在领取,会导致一批没有实在领取的账号兑现线上权利;
- 须要对这部分票据验证实现的权利发放进行限度,行为可追溯;非审核期间敞开正式环境的沙盒校验
-
票据后果解析的可靠性
-
因为票据信息依赖于客户端发动申请,有概率会被混充,服务端须要校验后果合法性
- bundle_id:查看是不是自家 App 产生票据(不同的 bundle_id 下是能够创立雷同 product_id 内购我的项目,苹果验证申请只返回后果,不会做任何校验)
-
交易信息的查看
- product_id、purchase_date_ms : 和 App 端订单零碎比对 IAPProductId,下单工夫
- transaction_id、original_transaction_id : 标识交易的唯一性(非主动订阅在 restore 之后会生成新的交易,transaction_id 会更改,original_transaction_id 不变)
- web_order_line_item_id:主动订阅时才会生成,标识交易的唯一性(因为一份主动订阅,original_transaction_id 是雷同的,transaction_id 也会因为 restore 会生成不一样,避免重复使用,只能用这个)
-
-
退款问题
- 大量退款,波及到对外结算对账会比拟头疼,能够接入解决苹果提供的 App Store Server Notifications 中返回的
REFUND
类型
- 大量退款,波及到对外结算对账会比拟头疼,能够接入解决苹果提供的 App Store Server Notifications 中返回的
- 事实利用中还会遇到其余各种问题,客户端有详尽的各阶段日志,服务端保留上传的小票信息,风控解决,接入苹果查问领取相干的 API
StoreKit2
苹果在 WWDC2021 提出的针对 IAP 的全新设计,Meet StoreKit 2
-
客户端:API 是应用 Swift5.5 个性 async/await 进行开发,iOS15 及以上
-
返回的 ProductInfo 信息更全面
- productType,subscription,jsonRepresentation
- MakePayment 时反对传入 appAccountToken, 能够将 AppleId 和 App 中账户对应 (不会像 applicationUserName 那样容易丢了)
- 苹果主动校验 Transaction 的合法性,但对于咱们还是会须要通过服务端去校验
- 反对查看历史账单:这个和设置里看账单历史是对应的,但只能看非消耗型、订阅和主动订阅的
- 反对查看订阅信息:最近交易信息,订阅状态,主动订阅补充信息
-
-
服务端
-
基于 JWS(JSON Web Signature) 新 Server API
- LookUpOrderId API : 依据用户提供的苹果账单上的 invoice order ID
- Get Refund History: 传入用户某次的 originTransactionId 能够查问历史退款
- Get All Subscription Statuses
-
Apple server Notification V2 文档
- V1 返回是 jsonObj
- V2 返回的是用 jws 数据格式
-
Origin StoreKit vs StoreKit2
- 所有交易信息是互通的
- 原先老版本购买的,新版本能够获取
- 新版本购买的,老版本能够获取到
StoreKit2 提供的 API 应用更为简略,对于客户端来说能够用 appAccountToken 替换 applicationUserName,将 AppleId 和 App 中账户对应,不会像之前容易失落;同时服务端也能够通过这个标识将用户的消费行为发给苹果,帮助苹果解决用户对消费型商品退款的状况。目前较大问题是 iOS 版本的限度。
最初
IAP 的应用始终为开发者诟病,包含创立商品的流程繁琐,以及刚开始接入主动续费时,踩了不少坑,在和苹果开发人员交换和反馈中,苹果逐步为开发者提供了更多更全面的 API,诸如调用接口治理 IAP 商品 Create an In-App Purchase,服务端通过 App Store Server API 自主查问交易信息。作为 iOS 开发人员须要继续关注 StoreKit 的倒退,与服务端交换,不断完善交易系统的牢靠和安全性。
参考链接
- App 内购买我的项目
- StoreKit——In-App Purchase
- Validating receipts with the App Store
- App Store Receipts responseBody
- App Store Server Notifications
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.musicfe(at)corp.netease.com!