苹果内购(IAP)从入门到精通(4)

您所在的位置:网站首页 苹果手机订阅已过期为什么还收钱 苹果内购(IAP)从入门到精通(4)

苹果内购(IAP)从入门到精通(4)

2023-12-27 22:05| 来源: 网络整理| 查看: 265

该系列的其他文章:

【1】苹果内购(IAP)从入门到精通(1)-内购商品类型与配置

【2】苹果内购(IAP)从入门到精通(2)-银行卡与税务信息配置

【3】苹果内购(IAP)从入门到精通(3)- 商品充值流程(非订阅型)

【5】苹果内购(IAP)从入门到精通(5)- 掉单处理、防hook以及一些坑

【6】苹果内购(IAP)从入门到精通(6)- 实际业务结合&线上异常情况处理

1. 充值流程(自动订阅) 1.1. 商品购买

等同于消耗型商品的购买。无非也是添加支付队列监听,初始化SKPayment并添加到支付队列中,然后付款,回调Purchased状态。在这里不再赘述。详细看上一篇文章:苹果内购(IAP)从入门到精通(3)- 商品充值流程(非订阅型)。主要的区别是在后面。

1.2. 票据校验

请求苹果票据校验时,请求参数需要传一个新的参数,叫“共享秘钥”。这个在苹果后台配置商品ID的地方生成,如下所示:

image.png 自动订阅商品的票据,与消耗型商品的票据有很多不同点。

{ environment = Sandbox; "latest_receipt" = "很长一串,是请求时的加密票据"; "latest_receipt_info" = ( { "expires_date" = "2019-09-18 06:44:15 Etc/GMT"; "expires_date_ms" = 1568789055000; "expires_date_pst" = "2019-09-17 23:44:15 America/Los_Angeles"; "is_in_intro_offer_period" = false; "is_trial_period" = true; "original_purchase_date" = "2019-09-18 06:41:16 Etc/GMT"; "original_purchase_date_ms" = 1568788876000; "original_purchase_date_pst" = "2019-09-17 23:41:16 America/Los_Angeles"; "original_transaction_id" = 1000000569412514; "product_id" = "com.auto.pay6"; "purchase_date" = "2019-09-18 06:41:15 Etc/GMT"; "purchase_date_ms" = 1568788875000; "purchase_date_pst" = "2019-09-17 23:41:15 America/Los_Angeles"; quantity = 1; "subscription_group_identifier" = 20548697; "transaction_id" = 1000000569412514; "web_order_line_item_id" = 1000000046978708; } ); "pending_renewal_info" = ( { "auto_renew_product_id" = "com.auto.pay6"; //自动订阅商品ID "auto_renew_status" = 1; //自动订阅状态(0说明订阅已关闭) "original_transaction_id" = 1000000569412514; "product_id" = "com.auto.pay6"; } ); receipt = { "adam_id" = 0; "app_item_id" = 0; "application_version" = 1; "bundle_id" = "com.mytest.0522"; "download_id" = 0; "in_app" = ( { "expires_date" = "2019-09-18 06:44:15 Etc/GMT"; need //订阅到期时间 "expires_date_ms" = 1568789055000; //订阅到期时间戳 "expires_date_pst" = "2019-09-17 23:44:15 America/Los_Angeles"; //订阅到期时间(美国) "is_in_intro_offer_period" = false; //是否在享受优惠价格期间 "is_trial_period" = true; //是否享受免费试用 "original_purchase_date" = "2019-09-18 06:41:16 Etc/GMT"; //原始购买时间 "original_purchase_date_ms" = 1568788876000; //原始购买时间戳 "original_purchase_date_pst" = "2019-09-17 23:41:16 America/Los_Angeles"; //原始购买时间(美国) "original_transaction_id" = 1000000569412514; //原始购买票据ID "product_id" = "com.auto.pay6"; //商品ID "purchase_date" = "2019-09-18 06:41:15 Etc/GMT"; //购买时间 "purchase_date_ms" = 1568788875000; //购买时间戳 "purchase_date_pst" = "2019-09-17 23:41:15 America/Los_Angeles"; //购买时间(美国) quantity = 1; //购买商品数量 "transaction_id" = 1000000569412514; //票据ID "web_order_line_item_id" = 1000000046978708; ////跨设备购买事件(包括订阅更新事件)的唯一标识符。此值是识别订阅购买的主键 } ); "original_application_version" = "1.0"; "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT"; "original_purchase_date_ms" = 1375340400000; "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles"; "receipt_creation_date" = "2019-09-18 06:41:16 Etc/GMT"; "receipt_creation_date_ms" = 1568788876000; "receipt_creation_date_pst" = "2019-09-17 23:41:16 America/Los_Angeles"; "receipt_type" = ProductionSandbox; "request_date" = "2019-09-18 06:42:32 Etc/GMT"; "request_date_ms" = 1568788952928; "request_date_pst" = "2019-09-17 23:42:32 America/Los_Angeles"; "version_external_identifier" = 0; }; status = 0; }

在in_app内,多了以下几个字段

"expires_date" = "2019-09-18 06:44:15 Etc/GMT"; //订阅到期时间 "expires_date_ms" = 1568789055000; //订阅到期时间戳 "expires_date_pst" = "2019-09-17 23:44:15 America/Los_Angeles"; //订阅到期时间(美国) "is_in_intro_offer_period" = false; //是否在享受优惠期间 "web_order_line_item_id" = 1000000046978708; ////跨设备购买事件(包括订阅更新事件)的唯一标识符。此值是识别订阅购买的主键

排查其他商品类型的票据后,我发现,即使是“非续期订阅”商品,也没有这几个字段。所以,可以判定,拥有这几个字段,就说明这个商品是自动订阅商品。(服务端可以用这5个字段来判断,这个票据是不是自动订阅的票据)

校验票据是否合法的逻辑同理于消耗型商品,如果校验通过了。就通知后台下发道具、更新客户端UI等。

2. 订阅续期

“自动订阅”商品,顾名思义,苹果会自动扣款续费。

最常见的有两种情况自动订阅商品会失效:

(1)用户手动取消订阅。需要用户手动去“设置”里去取消订阅。取消后,从下一个订阅周期开始,将不再扣费。同时,游戏内需要处理这个逻辑,停止下个周期的订阅服务的下发。

(2)购买订阅的AppleID绑定的银行卡或者信用卡没钱了。苹果会反复尝试扣款。如果都没有扣款成功,将停止订阅。(有坑,后面会说)

除此之外,苹果都会自动扣款去续订。有两种方式去判断续订是否成功:

一、每次启动app时,客户端主动获取receipt_data,上传给服务器做票据校验,服务器检查票据内是否有新的续订交易(时间可以判断),有则下发新的订阅商品。

二、server to server的校验方式,也是 苹果推荐的校验方式 ,由苹果主动告知我们状态。需要在appstore connect后台配置订阅状态URL,具体参考苹果官方文档启用针对自动续期订阅的服务器通知,同时也需要用到共享秘钥(上面有说到)。

参考上面的官方文档可以发现,苹果提供了两个server to server的接口,V1和V2。两个版本的接口、参数、回调都不一样。业界内主要用的还是V1版本,更加稳定(听说V2有一些bug),因此我们这里介绍V1版本的:

如果这样配置了server to server的通知,后台就会收到下面的几种状态更新通知类型:

NOTIFICATION_TYPE描述INITIAL_BUY首次订阅CANCEL取消订阅DID_RENEW自动续订成功INTERACTIVE_RENEWALApp或者设置内交互式续订DID_FAIL_TO_RENEW计费问题未能续订DID_RECOVER已成功续订过去未能续订的过期订阅DID_CHANGE_RENEWAL_STATUS续订状态发生变化DID_CHANGE_RENEWAL_PREF续订降级CONSUMPTION_REQUEST发起退款申请REFUND退款成功PRICE_INCREASE_CONSENT提价状态INTERACTIVE_RENEWALApp或者设置内以交互方式续订REVOKE不能继续家庭共享

通过server to server,服务端开发可以通过状态和回调的original_transaction_id匹配的订单与用户,进行相应的逻辑。

注意:服务端最好处理CANCEL类型。因为IAP存在黑产:比如买了一年会员,然后打电话给苹果客服退款,如果服务端不处理,这一年会员是生效的。

(1)更新票据:

在续费的前10天,Apple会进行续费的前期检查,尽量确保用户能够正常扣款。如果前期检查出了问题,会提醒用户应该处理对应的问题。

在续费的前24小时,Apple会尝试扣款,Apple会尝试几次扣款,如果一直扣款失败会停止扣款,订阅被动取消。注意,如果是支付相关的问题,Apple可能会进行长达60天的尝试。可以通过收据中的is_in_billing_retry_period判断Apple是否还在尝试中。

同一个订单凭据是可以一直使用的,不管你后面续订了多少次,随便这些中的一个凭据发给苹果验证,就能得到所有的订单信息和订阅状态。服务端需要保存这笔订单的recipt_data,并在每个周期结束之前请求苹果票据校验接口,根据返回的票据信息去得到用户是否仍然续订的信息。

续订后,in_app内会多一组新的票据数据。代表新的续订。如下所示:

{ environment = Sandbox; "latest_receipt" = "==========很长串票据字符============"; "latest_receipt_info" = ( { "expires_date" = "2019-09-23 09:18:17 Etc/GMT"; "expires_date_ms" = 1569230297000; "expires_date_pst" = "2019-09-23 02:18:17 America/Los_Angeles"; "is_in_intro_offer_period" = false; "is_trial_period" = true; "original_purchase_date" = "2019-09-23 09:15:18 Etc/GMT"; "original_purchase_date_ms" = 1569230118000; "original_purchase_date_pst" = "2019-09-23 02:15:18 America/Los_Angeles"; "original_transaction_id" = 1000000571201990; "product_id" = "com.auto.pay6"; "purchase_date" = "2019-09-23 09:15:17 Etc/GMT"; "purchase_date_ms" = 1569230117000; "purchase_date_pst" = "2019-09-23 02:15:17 America/Los_Angeles"; quantity = 1; "subscription_group_identifier" = 20548697; "transaction_id" = 1000000571201990; "web_order_line_item_id" = 1000000047083065; }, { "expires_date" = "2019-09-23 09:21:17 Etc/GMT"; "expires_date_ms" = 1569230477000; "expires_date_pst" = "2019-09-23 02:21:17 America/Los_Angeles"; "is_in_intro_offer_period" = false; "is_trial_period" = false; "original_purchase_date" = "2019-09-23 09:15:18 Etc/GMT"; "original_purchase_date_ms" = 1569230118000; "original_purchase_date_pst" = "2019-09-23 02:15:18 America/Los_Angeles"; "original_transaction_id" = 1000000571201990; "product_id" = "com.auto.pay6"; "purchase_date" = "2019-09-23 09:18:17 Etc/GMT"; "purchase_date_ms" = 1569230297000; "purchase_date_pst" = "2019-09-23 02:18:17 America/Los_Angeles"; quantity = 1; "subscription_group_identifier" = 20548697; "transaction_id" = 1000000571203602; "web_order_line_item_id" = 1000000047083066; } ); "pending_renewal_info" = ( { "auto_renew_product_id" = "com.auto.pay6"; "auto_renew_status" = 1; "original_transaction_id" = 1000000571201990; "product_id" = "com.auto.pay6"; } ); receipt = { "adam_id" = 0; "app_item_id" = 0; "application_version" = 1; "bundle_id" = "com.mytest.0522"; "download_id" = 0; "in_app" = ( { "expires_date" = "2019-09-23 09:21:17 Etc/GMT"; "expires_date_ms" = 1569230477000; "expires_date_pst" = "2019-09-23 02:21:17 America/Los_Angeles"; "is_in_intro_offer_period" = false; "is_trial_period" = false; "original_purchase_date" = "2019-09-23 09:15:18 Etc/GMT"; "original_purchase_date_ms" = 1569230118000; "original_purchase_date_pst" = "2019-09-23 02:15:18 America/Los_Angeles"; "original_transaction_id" = 1000000571201990; "product_id" = "com.auto.pay6"; "purchase_date" = "2019-09-23 09:18:17 Etc/GMT"; "purchase_date_ms" = 1569230297000; "purchase_date_pst" = "2019-09-23 02:18:17 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000571203602; "web_order_line_item_id" = 1000000047083066; }, { "expires_date" = "2019-09-23 09:18:17 Etc/GMT"; "expires_date_ms" = 1569230297000; "expires_date_pst" = "2019-09-23 02:18:17 America/Los_Angeles"; "is_in_intro_offer_period" = false; "is_trial_period" = true; "original_purchase_date" = "2019-09-23 09:15:18 Etc/GMT"; "original_purchase_date_ms" = 1569230118000; "original_purchase_date_pst" = "2019-09-23 02:15:18 America/Los_Angeles"; "original_transaction_id" = 1000000571201990; "product_id" = "com.auto.pay6"; "purchase_date" = "2019-09-23 09:15:17 Etc/GMT"; "purchase_date_ms" = 1569230117000; "purchase_date_pst" = "2019-09-23 02:15:17 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000571201990; "web_order_line_item_id" = 1000000047083065; } ); "original_application_version" = "1.0"; "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT"; "original_purchase_date_ms" = 1375340400000; "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles"; "receipt_creation_date" = "2019-09-23 09:20:00 Etc/GMT"; "receipt_creation_date_ms" = 1569230400000; "receipt_creation_date_pst" = "2019-09-23 02:20:00 America/Los_Angeles"; "receipt_type" = ProductionSandbox; "request_date" = "2019-09-23 09:20:10 Etc/GMT"; "request_date_ms" = 1569230410417; "request_date_pst" = "2019-09-23 02:20:10 America/Los_Angeles"; "version_external_identifier" = 0; }; status = 0; }

我们关注in_app内的如下几个字段:

purchase_date:代表当前这笔订单的扣款时间。续订也是扣款。如果是续订,这个时间正好是上一笔扣款数据里purchase_date+订阅周期。 original_purchase_date:代表这个自动订阅商品的第一次购买的时间。服务端可以用来判断这个用户是什么时候开启订阅的。 original_transaction_id:表示第一次订阅的时候的票据ID。因为服务端肯定用自己的订单号(orderId)和这个票据ID进行的绑定。所以要想跟踪是哪笔订单的续订,就使用这个。transaction_id每次都会改变,即使是恢复订阅,transaction_id也会改变,所以不要使用这个字段。

(2)票据合法性校验

一般情况下,只要实现了server to server的通知,苹果都会在续订后告知你。但有小概率出现不告知的情况。所因此建议,服务端记录下来最后一个收据(receipt_data),在订阅过期时间expires_date前24小时,定时用最后一条收据轮询,如果用户续费未成功,检查is_in_billing_retry_period,如果这个为true,那么放到下个轮训队列里继续检查,直到is_in_billing_retry_period为false,表示Apple已经放弃了扣款。如果续订成功,服务端拿到最新的票据后,判断时间、商品ID是否合法,原始票据ID对应的订单ID是否合法。之后,给这个订单ID对应的用户进行订阅续期操作,发放相应道具或者权限。

(3)sandbox环境下的订阅周期

在沙盒环境下,测试自动续期订阅时,时限会缩短。此外,每天的订阅次数最多仅能自动续期12次(包括首次订阅)。

实际时限测试时限1 周3 分钟1个月5 分钟2 个月10 分钟3 个月15 分钟6 个月30 分钟1 年1 小时

而取消订阅,苹果是没办法模拟的。所以一般是采用新建一个新的沙盒账号去解决。

3. 恢复订阅

比如一个用户(人),他在A设备上购买了App的VIP(自动订阅商品)。他买了一台新的手机B,重新下载了这个App。但这个App的VIP是本地Keychain缓存设置。换了设备后,keychain没有了缓存,这个用户在设备B上不享有VIP。但用户付了钱,肯定需要享受服务,所以苹果提供了“restore”服务,恢复订阅的权限。这个是正常的逻辑。

但还有一种情况,是苹果允许,但却很bug的设定。订阅服务是跟AppleID绑定的。但我们每个app基本都是有自己的用户系统,这个用户系统跟AppleID无关(实际上iOS开发者也拿不到用户的AppleID)。所以,苹果要求:只要当前App是由一个已订阅过的AppleID下载的,这个App下的任何App账号,都可以享受这个订阅权限。

是不是很绕?我们举个最简单的例子(真的是最简单的):

用户A,有AppleID_A给这个APP下的账号A购买了会员(自动订阅);这时,用户A换了一个账号B。那么这个账号B也需要享受会员。如果没有享受,APP内可以开放一个“恢复订阅”功能,让用户A可以操作给账号B恢复订阅服务。

因为苹果这个不讲道理的要求,实际上的场景要复杂很多。AB用户、AB设备、AB苹果ID、AB账号、AB角色......我们不在这个地方过分展开说,具体场景需要结合自己的APP进行调试。

恢复流程如下所示。

开启恢复订阅:

[[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; //恢复已订阅商品

点击恢复按钮后,支付队列开启监听订阅商品状态。

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (int i = 0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3