本文作者: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];
//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
- 付出完结后,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 的合法性,但关于咱们还是会需求经过服务端去校验
- 支撑查看前史账单:这个和设置里看账单前史是对应的,但只能看非耗费型、订阅和主动订阅的
- 支撑查看订阅信息:最近买卖信息,订阅状况,主动订阅补充信息
- 回来的ProductInfo信息更全面
- 服务端
- 基于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数据格局
- 基于JWS(JSON Web Signature)新Server API
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.music-fe(at)corp.netease.com!