如果大家的 App 有使用 IAP 功能,那么可能会遇到用户反馈苹果充值成功,但是服务没有到账的情况,用户一般会提供这样的苹果收据:
用户反馈时提供的苹果收据中,有一个字段中 ORDER ID
,苹果叫 Invoice order ID
(发票订单号),与我们开发者从 App 内获取到的 receipt
收据解析后,并没有 ORDER ID 字段!!!所以,我们无法定位和联系这个用户提供的发票与我们后台的订单号,从而无法给用户正常补发服务,开发者也是很无奈!
而今年,这个问题苹果终于提供解决方案啦!是不是很开心!点一个赞吧~
大家都知道,手机游戏的收入重要来源就是虚拟物品
购买,而 iOS 需要通过 App Store 必须使用苹果的 In-App Purchase
(应用内购买,下文统一使用IAP
表示内购功能。)功能。而 37手游 是三七互娱旗下独立子公司,作为国内顶尖的手游发行平台,累计运营超过2000款游戏,所以对于 37手游 来说,IAP 的重要性不言而喻!
去年的 WWDC20,苹果推出 IAP退款通知
时,在 What’s new with in-app purchase - WWDC 2020 解读时,小编在 疑问解答 时给出了2个大胆推测:
1、 苹果后台能否查看到退款的订单详情?
答:暂无。(估计明年 WWDC2021 会有啦?)
2、 消耗型、非消耗型、非续期订阅能不能在沙盒环境测试退款?
答:暂时不能。(估计未来会有?等更新吧....)
今年的 WWDC21 大会开始后,小编第一时间就关注 IAP 相关的 Sessions 会议,大喜!今年的 IAP 功能更加开放和透明,去年大家的2个疑问,今年都给解决了!以下就是苹果今年关于 IAP 的三步曲:
因为以上三个 Session 内容上是相互之间紧密相连,密不可分,所以小编接下来就在本文将这三步曲混合来解读,主要分成三部分:
StoreKit 2 主要更新
StoreKit 2 for Swift only
!没错!仅适用于 Swift !StoreKit 2
利用 Swift的最新特性,包括 Swift并发 等新语言接口,简化在App中获取产品信息、商品产品、处理交易以及管理对内容和订阅的访问。并且,StoreKit 2 只支持 iOS 15+ 。
还在维护 Objective-C 代码的朋友们,是不是瞬间哭晕在洗手间!与新特性无缘,所以现在就是开始学习 Swift 的最佳时刻了,再不学 Swift 开发,连 iOS 开发都不能愉快进行啊~
我们开发者要怎么选择呢?苹果在选择文档在给出了答案:
苹果现在把原来的 StoreKit v1 定义为 Original API for In-App Purchase
,StoreKit v2 定义为 In-App Purchase
。(小编注:目前来说,使用 v1 和 v2 版本都可以实现完整的 IAP 购买流程,区别就是 v2 必须使用 Swift 开发,同时提供更加强大的 APIs。)
很好理解,因为 StoreKit v2 目前是重新设计实现,所以部分 v1 提供的 IAP API 在 v2 版本还没有提供相应的 API,所以还需要使用 v1 版本。
如果您的应用程序依赖于以下任何功能,您可能需要使用原始的应用程序内购买API:
小编注解:
StoreKit 2
提供了以上更新的类(方法)来轻松访问 IAP 接口,可以理解为增强的版本,详细下文会讲解。
Product 类增加了品项的类型:
public static var consumable: Product.ProductType
public static var nonConsumable: Product.ProductType
public static var nonRenewable: Product.ProductType
public static var autoRenewable: Product.ProductType
同时也扩展订阅类型的信息。订阅类型的品项,包含 isEligibleForIntroOffer
,这个字段的作用是判断,用户是否有资格使用优惠价格进行订阅。借助 StoreKit 2,我们以后就可以更轻松地确定客户是否符合您的推介促销优惠的条件。关于订阅类型的复杂度这里就不展开了,大多数同学可能也接触不多,详细可查看自动续期订阅 。
另外,StoreKit 2 向前兼容原来的 Product,添加称为 BackingValue
的包装类型来实现这一点,用于与 App Store 通信的数据类型。详见文档:BackingValue
除了原有的请示品项信息外,购买时,增加了一些可选参数 Purchase opthons
。
除了购买数据、促销优惠 外,最重要的是新字段:App account token
!
App account token
App account token
使用 UUID
格式这个 App account token
是给开发者将用户的 ID 绑定到交易(Transcation)中,也就是把苹果的交易订单数据与用户信息进行映射,可以起到防止充值掉单的问题啊~
示例代码:
let uuid = Product.PurchaseOption.appAccountToken(UUID.init(uuidString: "uid")!)
//Begin a purchase.
let result = try await product.purchase(options: [uuid])
小编注:UUID
是苹果定义的接口 UUID().uuidString
获取,格式如:4713AE2D-11A5-40EA-B836-CBCD1EC96A76
。如果需要关联 用户ID 和开发者订单号,需要开发者自动映射,或者服务器端生成返回等。
签名的交易(Transcation)信息:
这里插入一下 Manage in-app purchases on your server 里讲解使用 JWS 数据格式的原因:
小编注:JSON Web Token(JWT)是一个规范,这个规范允许我们使用JWT在两个组织之间传递安全可靠的信息。JWT并不等于JWS(JSON Web Signature),JWS只是JWT的一种实现,除了JWS外,JWE(JSON Web Encryption)也是JWT的一种实现。详细查看 JWS (RFC 7515)
JWS的主要目的是保证了数据在传输过程中不被修改,验证数据的完整性。但由于仅采用Base64对消息内容编码,因此不保证数据的不可泄露性。所以不适合用于传输敏感数据。引用
这里简单的说一下,拿到的 JWS 格式的 transaction info 格式:
Base64() + "." + Base64(payload) + "." + sign( Base64(header) + "." + Base64(payload) )
这个 header 与 payload 通过 header 中声明的 alg 加密方式,使用密钥 secret 进行加密,生成签名。然后逆向构造过程,decode出 JWT 的三个部分:
验证相关的文档和流程可查看 App Store Server API,这里就不展开了。
视频中 展示了一个完整的 Demo 示例,这里就不展开了。可以自动 下载 Dmeo 查看。
提供了三个新的交易(Transcation)相关的 API:
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension Product {
/// The most recent transaction for the product, or `nil` if the user has never purchased this product.
public var latestTransaction: VerificationResult? { get async }
/// The transaction that entitles the user to this product, or `nil` if the user is not currently entitled to
/// this product.
public var currentEntitlement: VerificationResult? { get async }
}
反正就是很强大的接口:
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension Transaction {
/// A sequence of every transaction for this user and app.
public static var all: Transaction.TransactionSequence { get }
/// Returns all transactions for products the user is currently entitled to
///
/// i.e. all currently-subscribed transactions, and all purchased (and not refunded) non-consumables
public static var currentEntitlements: Transaction.TransactionSequence { get }
/// Get the transaction that entitles the user to a product.
/// - Parameter productID: Identifies the product to check entitlements for.
/// - Returns: A transaction if the user is entitled to the product, or `nil` if they are not.
public static func currentEntitlement(for productID: String) async -> VerificationResult?
/// The user's latest transaction for a product.
/// - Parameter productID: Identifies the product to check entitlements for.
/// - Returns: A verified transaction, or `nil` if the user has never purchased this product.
public static func latest(for productID: String) async -> VerificationResult?
}
Current entitlements
这个目前是,方便开发者直接通过接口就能读取当前用户可用的订阅品项和非消耗品项,不用开发者做硬编码写死 productID 请求苹果查询,直接一个接口搞定!特别是对个人开发者来说,确定是很方便,不用搭服务器。
查询同一个用户在不同的设备上的交易订单,假设用户在 A 设备购买了一笔交易订单,那么在用户的 B 设备上,可以实时查到这个购买的交易订单。苹果工程师说,一般系统会自动刷新,逼不得已不需要使用同步接口刷新。
一般情况下,第一次打开 App 时,开发者就可以通过 StoreKit 2 提供的接口在后台实时帮用户恢复购买记录。对于非消耗品项,用户在一个新设备时,可能需要提供给用户恢复购买记录的 UI 入口。而对于订阅类型,比如某个视频网站的月卡,虽然都是登陆一个苹果账号,但是购买时,是绑定到视频网络的用户的,不是绑定到苹果账号下,所以,订阅类型可能就无法直接恢复啊。
所有的交易都可以用在所有的 StoreKit 接口;使用 StoreKit v1 的购买记录,在 v2 的接口也可以获取到;使用 v2 进行的购买可在统一收据中获得。
订阅类型项目的状态,比如获取最新的交易、获取更新订阅的状态,获取更新订阅的信息等。
其中获取更新订阅的信息,可以获取更新的状态、品项 id、如果过期的话,可以知道过期的原因。(比如用户取消、扣费失败、订阅正常过期等。),获取的所有数据都是 JWS 格式验证。
最后,是签名校验,上面已经提到,这里就不在展开。
苹果工程师建议,因为校验是用到 bundle ID (应用包名),所以建议是写死硬编码,不要读取 info.plist 文件配置。然后按规则格式进行验证 payload 是否被篡改。
StoreKit v2 提供了验证 JWS 格式的 API,开发者可以直接调用,不需要自行解析。
StoreKit v2 总结来说,强大的新 IAP 接口,新的 JWS 交易信息格式,交易详细内容和历史接口,额外的订阅类型信息。总之,牛逼~
订阅者如何在我的应用内管理他们的订阅?
提供了新的 API,可以直接在开发者 App 中显示用户当前的订阅品项界面,不用在跳转到 App Store 。
接口如上,调用后,打开的界面如下:
可以在开发者 App 中取消订阅、升级或降级订阅等级等。
客户如何在我的应用内申请退款?
提供新的 Request refund API,允许用户在开发者的 App 中直接进行退款申请。
用户进行申请退款后,App 可以收到通知、另外苹果服务器也会通知开发者服务器(下文会有说),退款测试在沙盒环境下,可以进行测试啦!
接口如上,调用后,打开的界面如下:
用户退款的流程界面(这个是系统的界面),所以可能对用户是很方便啦,对开发者来说,可能就需要在考虑一下?
接下来,我们说说,苹果服务器 API 接口的更新。
如图,苹果服务器、用户设备、开发者服务器,三者之间的交互越来越多,随着苹果的迭代和开放,三者如今已经成循环~
构建开发者的服务器:
接下来,将会从以上几个方面展开说:
Receipt 收据验证方式:
/verifyReceipt
接口验证收据旧的 receipts 收据内容如上图。
新的 JWS 格式的交易格式内容,如上图。对比 receipts 收据,可以知道有那些变化:
旧的有 GMT(格林威治标准时间)、PST(太平洋标准时间)、Unix timestamp(Unix 时间戳),新的格式,只保留了 Unix 时间戳
,并且字段做了更新。
内购的类型,也有返回了。
这个就是上面提到的关系的用户信息的 UUID。这里苹果用 appAccountToken
字段。
这个是用户退款时间和退款原因的字段。从之前的 cancellation_date
改成现在的 revocationData
。
最后是促销优惠的类型。
验证签名信息,这里就不多说了,上文已经说过了。
使用 APIs 检查状态
新提供了2个接口:
先来看看订阅品项状态查询 API。需要参数只有一个:originalTransactionId
,这个大家很熟悉了,就不展开了。详细文档:Get All Subscription Statuses
接口请求和返回的数据格式示意如上。
lastTransactions
是最后的订阅状态,1是有效,2是过期,3是账号扣费重试,4是账号宽限期(这个是开发者设置,比如到期扣费失败时,可以给用户延期多长时间。),5是已经撤销。
对 signedTransactionId
进行 JWS 解码后的内容,就是单个更新订阅类型的数据内容。
获取用户的交易历史记录,包括他们在你的 App 中的所有应用内购买。 也是只需要参数一个:originalTransactionId
,注意,只需要是用户的任意一个交易的 originalTransactionId 就可以啦。这个大家一看就明白了,就不展开了。详细文档:Get Transaction History
需要注意的是,这个返回的数据有一个字段 hasMore
为 ture,表示有更新的历史订单有更新,默认是 20 条。目前开发者不能控制这个条数。
App Store Server 接口标准:
所有的 App Store Server API 接口都必须使用 JWT 认证,关于验证规则和流程,请查看文档:Generating Tokens for API Requests
在苹果后台生成私钥的示例,详见文档:Creating API Keys to Use With the App Store Server API
验证可查看文档:Generating Tokens for API Requests
所以,如果需要使用 App Store Server API 查询订阅品项状态或用户的历史订单,关键要点:
originalTransactionId
)一个字段走天下!给我们点个赞吧~
)通过通知跟踪状态!
苹果服务器的通知更新,苹果说很好,开发者可以接受通知、更新的状态也及时?不需要开发者主动请求询问!行吧,你说的都对~
苹果服务器通知 v2 版本的更新,这里就不展开了。好像没有什么好说的,跟上文的差不多。
主要变动是,通知的类型,有一部分是删除了,也新增了一些通知类型。
小编注:变动的原因,有很多方面,主要是苹果的自动订阅类型品项,越来越复杂了,所以有一些字段意义已经不大,另外,苹果新推出的家庭共享功能,主账号可以授权家庭子账号或者撤销授权。所以
CANCEL
取消类型就不明确意义了,所以更新为REVOKE
撤销,他的含义和作用更多,可以是用户申请退款或者授权取消,都是撤销的一个。
当然,这里变动这么多,苹果不可能在原来的接口直接改啊!所以是有 v1 和 v2 接口,开发者可以设置,下文会提到,这些先略过。
另外,订阅的类型下,还有子类型。这个也好理解。比如 SUBSCRIBED
订阅,可以是首次订阅的状态,也可以是重新订阅的状态,都是订阅。
然后就展开了通知主类型,有那些子类型。如下这些,就不展开了,大家看看图片就好:
最后,关于订阅,有非常多的状态,所以,订阅品项的通知的复杂度就不言而喻!
总结来说,App Store server notifications V2 提供了多达20多种通知类型!子类型提供了更精细的通知类型!
最后就是通知返回的内容,多了一个 subtype
子类型,还有对应的 version
为 2 表示 App Store server notifications V2 版本。
新的购买流程处理
最后,总结一下,提供了服务端新查询接口后,对开发者服务器有那些变动和更新注意事项等。
对于首次订阅的购买,流程上的变化是,开发者 App 与开发者服务器完成订阅流程后,苹果服务器也会发送通知 SUBSCRIBED + INITAL_BUY
,然后开发者服务器可以随时通过接口 inApps/v1/subscriptions
随时查询用户订阅项目的状态,不用等苹果服务器的通知也可以啦!避免了开发者处于被动的情况,更好的实时获取。
订阅更新,也是开发者服务器随时通过接口 inApps/v1/subscriptions
随时查询用户订阅项目的状态。是不是爽歪歪啦~ 点个赞吧~
订阅类型的账单宽限期和计费重试,也是同样的道理,苹果服务器会发通知 DID_FAIL_TO_RENEW
、DID_RECOVER
给开发者服务器,开发者服务器随时通过接口 inApps/v1/subscriptions
随时查询用户订阅项目的状态,然后对 App 里用户实时操作和限制等。
首次消耗型购买,还是一样。不同的时,开发者可以用 receipt 收据或者使用 StoreKit v2 新的 signed transactiond 来验证订单啊。
而用户退款,也出现了新的时代!除了苹果服务器的通知退款 REFUND
后,开发者现在可以主动通过 inApps/v1/history
接口,查询用户的所有交易订单,来确认订单的状态是不是退款(撤销)。
如果是订阅类型的退款,开发者服务器就通过接口 inApps/v1/subscriptions
随时查询用户订阅项目的最新状态。
迁移到 JWS 格式交易验证
对于 StoreKit v2 新的接口,苹果已经弃用了 receip 收据验证,所以,对于开发者来说,应该怎么迁移到新的 JWS 格式验证呢?所以,苹果给出了方案:
如果开发者需要兼容 StoreKit v1 版本,那么还可以使用 receipt 收据通过苹果接口 /verifyReceipt
验证收据,收据中是包含 originalTransactionId
的,所以,可以开发者可以通过 inApps/v1/history
接口,随时了解交易的状态。
订阅类型,就通过 inApps/v1/subscriptions
接口查询。
管理家庭共享
目前苹果对 非消耗型
和 自动订阅
类型品项是支持 家庭共享
(family sharing),另外,苹果会返回一个字段 inAppOwnershipType
表示当前用户是否为购买品项的主用户。
家庭共享更新了新的通知,增加了4个类型的通知。
终于来到最后的一节啦!也是很重要的内容,关于用户服务支持和用户申请退款的处理。
以前,用户关于内购有问题时,只能自己解决,并且是通过电话、邮件、苹果支持 App、网站、论坛等方式,对用户非常的不友好!
一般用户遇到问题的情场有那些呢?
如何识别该客户进行的应用内购买?
这个就是前言提到的用户收到苹果的收据发票时,无法与开发者的订单匹配的问题!现有有新的 API 来解决了:
这个新的接口,可以让用户提供的发票上的 ORDER ID
查到对应的 transaction 交易信息。
小编注:目前2021-06-17,在 苹果接口文档 还没有看到此接口,不清楚是没有更新,不是说在开发中…
然后,整个流程,主是这样,用户客诉,提供订单 ID,查询到状态,为用户进行补单或者支持服务。
返回的数据格式都是一样的,这里也不展开了。
最后,开发者服务器记得保存对应的用户订单 ID,做好映射?
我如何查找该客户过去的退款?
同样的,苹果提供了查询所有内购订单的接口,但是不可能让开发者查一次,然后在判断那些是退款订单吧!所以,苹果提供了另一个接口:
这个接口也是一样,通过用户的任一个 originalTransactionId
可以查到这个用户的所有退款记录订单。
小编注:目前2021-06-17,在 苹果接口文档 还没有看到此接口,不清楚是没有更新,不是说在开发中…
返回的格式也是一样。
用户退款,是有一个单独的 refundDate
字段,如果有内容时间,就表示是退款啊。
我如何补偿订阅者的服务问题?
主要的问题是,比如开发者服务器宕机了,导致用户无法使用 App 服务,这时候开发者可以想补偿用户,所以开发者可以提供一个内购对兑码(所有的内购类型都可以),在苹果后台那里生成。然后让用户在 App Store 进行兑换,也可以在 App 里通过 presentCodeRedemptionSheet()
接口调用,弹出系统的兑换界面:
用户通过对兑码进行获取补偿。
小编注:针对游戏来说,这个对兑码不太适用,因为游戏里有用户账号、游戏区服、角色账号等,对兑的内容,无法自动分配给某个账号某个角色。另外,国内好像都没有这样的补偿习惯?
我如何安抚客户中断或取消的活动?
如果开发者服务器宕机,或者活动取消,这时候可能想安抚用户,然后想补偿用户一些福利,苹果提供了一个新接口:
这个接口的作用:开发者一年有2次机会给订阅内购用户每次加90天免费补偿。也就是有自动订阅类型的 App,可以开发者主动在服务器给用户补偿(免费延长)用户的订单时间,每次最多是90天。
小编注:目前2021-06-17,在 苹果接口文档 还没有看到此接口,不清楚是没有更新,不是说在开发中…
需要在接口给出延长的天数,还有原因代码。
开发者服务中断或宕机,导致用户无法使用服务,开发者主动给用户进行补偿。流程图已经很清晰的表达了,这里就不解析了。
开发者主动取消了活动,给用户发补偿。
退款通知
最后,是关于退款通知,在去年 WWDC20 苹果推出的退款通知开发者的流程:
那么,现在有没有什么好的最佳实践呢?
苹果深入解决了退款通知的流程,就是开发者收到退款通知时,这个退款可能是48小时内的任意时刻。
决定要不要退款,苹果有一个“退款决策系统”(Refund decisioning system)根据用户的信息、设备信息、购买记录和退款记录等,最终决定是否同意用户退款。
而现在!苹果增加了一个新的决策影响因素:Developer signals
(开发者信号),这个是什么?就是开发者,可以在用户申请退款时,可以把用户的一些信息给到苹果,协助决策系统来决定。
当用户申请退款时,苹果通知(CONSUMPTION_REQUEST
)开发者服务器,开发者可在12小时内,提供用户的信息(比如游戏金币是否已消费、用户充值过多少钱、退款过多少钱等),最后苹果收到这些信息,协助“退款决策系统” 来决定是否允许用户退款!详细见文档:[Send Consumption Information(https://developer.apple.com/documentation/appstoreserverapi/send_consumption_information)
需要提供给苹果的参数,详细可查看文档:ConsumptionRequest
需要注意:customerConsented
字段,表示用户是否同意提供消费数据。所以,这个用户的信息,是要求用户允许共享才行!(赶紧加到用户协议里?)
新增的退款通知类型有2个,一个是请求开发者提供决策信息,另一个是退款拒绝的通知!(当收到用户申请退款被拒绝后,开发者可以考虑做一些安抚用户的操作?)
最后,整个流程图如上!
这个接口是可以测试的,配合上文中提到的,在 App 里提供让用户退款界面和接口时,当发起退款时,这个测试也会通过苹果服务器通知到开发者服务器。另外,今年新增了设置单独的沙盒环境通知URL!(下文会讲到啊~ 点个赞吧~)
客服支持和共享共赢
不管是内购退款,还是内购补偿,其实目的都是为了用户!
总结:
沙盒测试环境
最后就是沙盒环境的更新!真最后一节啦!给个点赞吧~
App Store server notifications 新增沙盒环境的回调 URL 配置!
App Store server notifications 新增沙盒环境的回调 URL 配置!
App Store server notifications 新增沙盒环境的回调 URL 配置!
这个测试以后就方便啦~
App Store server notifications 设置 URL 支持 V1 或者 V2 配置,因为 V2 变动比较大,所以就增加了一个新的版本,不知道明年会不会有 V3? -.-
沙盒测试:
清历史购买记录:
改帐号所在地区:
测试订阅过期时间更多选择:
最后!最后!到了总结的时候啦,此时应该有掌声!点个赞~
目前在 Human Interface Guidelines 人机设计文档看到苹果关于在 App 中给用户提供退款功能的设计,目前的情况来看,应该不是强制要求所有 App 必须在 App 中提供这个退款功能。所以,也是需要开发者进行思考~ 退款和内购,本质是什么?
其实,对于一般的用户,开发者是不会限制用户退款,正常的退款,但总是存在不合理的情况,有恶意退款的,一般游戏的坏帐率有 5%以上,在去年苹果提供退款通知开发者之前,甚至有 20% 以上。举例来说,一亿的5%就是五百万。
那开发者应该怎么考虑内购和退款的问题呢?
苹果有给出一些提示:
所以,做好一个产品,提高用户的满意度,用户满意了,是不是更多愿意使用开发者提供的服务,从而正向循环~
那要不要 App 里提供退款功能呢?
这是一个值得所有开发者思考和探索的问题~
欢迎大家一起在评论区交流~
欢迎关注我们“37手游iOS技术运营团队”,了解更多 iOS 和 Apple 的资讯~
37手游iOS技术运营团队:分享技术动态、实践和思考!热爱,开放,严谨,担当~