StoreKit In App Purchase 内购天坑指南
因公司业务要求需要接入iOS内购,期间采坑无数,但也积累了一些经验吧.鉴于网上太多资料都已经过时,本篇文章也有可能过时,所以仅供大家参考
语言环境
Swift4.2
参考资料
最好是苹果的官方文档,另外,这有一份关于收据验证的中文文档也非常实用 -- 收据验证编程指南
内购商品种类
实现需求包括基本内购商品的购买,以及特殊的订阅商品的购买. 首先是商品的种类
根据需要选择商品类型,然后记下productId
内购代码书写
大致逻辑是iOS端发起支付, 通过添加观察者同步交易的状态. 最后用户付款成功之后验证.
验证的方式分为两种: 客户端向苹果发起验证 或者 客户端向后端发起请求,让后端去向苹果验证.
无论是哪种验证方式核心都是把存储在本地的收据发送到苹果的服务器校验
基础代码是这样的:
/// 向苹果发起支付请求
func requestIAP(productId: String) {
if SKPaymentQueue.canMakePayments() {
let set = Set([productId])
let purchaseRequest = SKProductsRequest(productIdentifiers: set)
purchaseRequest.delegate = self
purchaseRequest.start()
} else {
CustomToast.showDialog("用户禁止App内购买")
}
}
SKProductsRequestDelegate的回调中添加观察者
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard response.products.count == 1 && self.currentProductId != nil else {
dealToast(with: .productIdError)
return
}
let receiveId = response.products[0].productIdentifier
guard receiveId == currentProductId! else {
dealToast(with: .receiveIdError)
return
}
SKPaymentQueue.default().add(SKPayment(product: response.products[0]))
}
观察交易的状态
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for tran in transactions {
switch tran.transactionState {
case .purchased:
// 用户购买成功
break
case .restored:
// 用户恢复购买
break
case .failed:
if tran.error?.isCancelled {
// 用户取消
}
else {
// 其他情况失败
}
case .deferred:
// 延迟支付
break
case .purchasing:
// 正在支付
break
}
}
}
购买完成之后苹果就完成对用户的扣款,这时我们需要对用户的购买进行验证. 无论是客户端验证服务器端验证都是发送收据到苹果的服务器去验证收据的有效性. 客户端需要获取到存储在本地的收据,代码如下:
guard let receiptUrl = Bundle.main.appStoreReceiptURL, let receiveData = NSData(contentsOf: receiptUrl) else {
throw KRIAPError.authServerError
}
let receipt = String(data: receiveData.base64EncodedData(options: NSData.Base64EncodingOptions.endLineWithLineFeed), encoding: .utf8) ?? "ERROR"
苹果的服务器分为沙盒和正式,只有上线App Store(也就是从App Store里下载的)才走正式的验证,其他统统走沙盒.而且苹果审核的时候也是走的沙盒验证,这点需要注意一下,否则会因为苹果走沙盒支付不通而导致审核被拒.
验证就是发送receipt到两个苹果的URL,分别是
https://sandbox.itunes.apple.com/verifyReceipt // 沙盒
https://buy.itunes.apple.com/verifyReceipt //正式
用Postman验证收据如图,客户端和服务器同理. 其中password是订阅时需要用到的App专用共享密匙,在iTunes Connect - 内购 - App 专用共享密钥能够找到
Postman验证收据苹果那边会返回一个JSON,具体参见收据验证编程指南
开发者根据苹果返回的结果,决定下面的操作.
以后就是苹果内购的基本流程了,下面说遇到的一些坑.
内购填坑指南
为什么IAP不好用!? 首先是因为苹果的网不好,现在貌似国内正式环境还挺快的,但是沙盒依然巨慢无比!!! 而且整个IAP的设计思路其实是让用户通过苹果付款,让开发者通过苹果验证该用户是否付款以决定是否开放付费功能! 但是国内的互联网产品基本都有自己的账号, 用户付费是付费到这个产品的账号(比如微信号XXX)而不是Apple Id(xx@gmail.com)上面. 鉴于验证的耗时也一般不会每次都向苹果服务器发消息做用户是否付费的验证.再加上客户端验证本身容易被破解,所以一般而言大家都是
订阅问题
仅仅用苹果支付,验证通过之后就把用户已付费计入服务器端,下次直接通过服务器端来判断该用户是否付费! 微信和支付宝都是这样的,仅仅是一个支付工具. 但是苹果就想全都管.
明白苹果的思路之后就知道怎么在苹果思路之后做基于我们自己账号的付费了.消耗型项目其实没什么问题.主要是订阅,这个服务器端有自己的订阅时间,而苹果所给的订阅过期时间是需要验证收据才能得到的. 这导致的一个问题,我在做自动续订(连续包月)时在想怎么收到苹果的通知说用户次月续费成功了!? 其实通过在AppDelegate添加一个观察者是可以收到续订的回调的. 通过transaction.original不为空来判断是不是次月的续订,当然这样判断是有问题的. 首先是沙盒情况下回调非常不稳定,其次貌似回复购买或者忘记结束交易也会回调. 如果一回调就判定是续费成功显然是不对的
抛弃续费这个概念,我们采取的是每天服务器端验证收据以确定用户的订阅是否可用. 沙盒情况下也相对好测试. 关于怎么测自动续期可以参照这个的时间
更坑的是用户取消订阅,变更订阅类型(包月到包年,包月到单月),自己写逻辑会乱死而且跟苹果对不上,还是根据苹果返回的JSON来吧.
漏单问题
用户点了购买按钮就马上把用户的购买信息存储下来(productId,购买类型,时间戳什么的),如果用户在苹果那购买成功了但是突然断网我们没有去后端验证导致漏单. 用户重进(或者恢复购买)需要结合本地的购买信息和本地收据再次向服务器端验证,验证通过后删除本地购买信息
沙盒测试
添加的沙盒账号一定要是不存在的Apple Id
审核
- 审核是沙盒环境!
- Apple Store简介里面需要有内购信息,订阅需要告诉用户怎么取消,一些项目需要添加恢复购买
- 提交审核的时候需要把IAP项目添加进去,否则只会审APP不会审IAP
暂时写什么这么多吧,有新坑继续填,有问题可以留在评论区.