前言
最近又重新写了很久之前写的内购,该项目中没有订阅,而另一个项目中包含了订阅和消费型的购买。重新整理了一下,项目中用的是SwiftyStoreKit。
我们先来看一下内购类型:
由于项目中有购买虚拟币和订阅,这里就选择了消费型和自动续期订阅,其他的配置网上一搜一大堆这里就省略了。。。
一.购买
1. 购买
先看一下购买方法
/**
* Purchase a product
* - Parameter productId: productId as specified in iTunes Connect
* - Parameter quantity: quantity of the product to be purchased
* - Parameter atomically: whether the product is purchased atomically (e.g. finishTransaction is called immediately)
* - Parameter applicationUsername: an opaque identifier for the user’s account on your system
* - Parameter completion: handler for result
*/
public class func purchaseProduct(_ productId: String, quantity: Int = 1, atomically: Bool = true, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, completion: @escaping (PurchaseResult) -> Void) {
sharedInstance.purchaseProduct(productId, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox, completion: completion)
}
参数1:productId,在iTunes Connect中指定的productId;
参数2: quantity,购买数量;
参数3: atomically,设置为ture,不管商品有没有验证成功,都会调用finishTransaction,项目里我设置为false,等客户端向服务端校验成功手动调用finishTransaction;
参数4:applicationUsername,系统上用户帐户的不透明标识符。看了好多文章都是传入用户id,这样就能将该订单充到固定账号上,由于我们项目中并没有暴露给我们用户id,所以我这里并没有传入任何数据;
参数5: simulatesAskToBuyInSandbox,是否是沙盒模拟支付;默认不填为false。
2.购买成功获取receipt-data
这个票据是我们支付成功后苹果返回给我们的。
参数:forceRefresh,如果为true,则刷新收据,即使收据已经存在。我这里用了false
/**
* Fetch application receipt
* - Parameter forceRefresh: If true, refreshes the receipt even if one already exists.
* - Parameter completion: handler for result
*/
public class func fetchReceipt(forceRefresh: Bool, completion: @escaping (FetchReceiptResult) -> Void) {
sharedInstance.receiptVerificator.fetchReceipt(forceRefresh: forceRefresh, completion: completion)
}
将返回的结果给服务端校验时需要将返回的data转成base64。
3.向服务端校验,验证成功调用finishTransaction
以上三个步骤就是购买的过程,接下来看一下代码
//1.购买
func pay(purchaseProductId:String,completeHandle:@escaping (Bool) -> Void) {
if !SwiftyStoreKit.canMakePayments {
print("您的手机没有打开程序内付费购买")
completeHandle(false)
return
}
SwiftyStoreKit.purchaseProduct(purchaseProductId, quantity: 1, atomically: false) { purchaseResult in
switch purchaseResult {
case .success(let purchase):
//处理交易
self.handleTransaction(transaction: purchase.transaction) { result in
completeHandle(result)
}
case .error(let error):
switch error.code {
case .unknown:
print("Unknown error. Please contact support")
case .clientInvalid:
print("Not allowed to make the payment")
case .paymentCancelled:
break
case .paymentInvalid:
print("The purchase identifier was invalid")
case .paymentNotAllowed:
print("The device is not allowed to make the payment")
case .storeProductNotAvailable:
print("The product is not available in the current storefront")
case .cloudServicePermissionDenied:
print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed:
print("Could not connect to the network")
case .cloudServiceRevoked:
print("User has revoked permission to use this cloud service")
default :
print("其他错误")
}
completeHandle(false)
}
}
}
//2.处理交易
func handleTransaction(transaction:PaymentTransaction,completeHandle:@escaping ((Bool) -> Void)) {
//获取receipt
SwiftyStoreKit.fetchReceipt(forceRefresh:false) { result in
switch result {
case .success(let receiptData):
let encryptedReceipt = receiptData.base64EncodedString(options: [])
print("获取校验字符串Fetch receipt success:\n\(encryptedReceipt)")
//3.服务端校验
if 验证成功 {
SwiftyStoreKit.finishTransaction(transaction)
completeHandle(true)
}else{
completeHandle(false)
}
case .error(let error):
print(" --- Fetch receipt failed: \(error)")
completeHandle(false)
}
}
}
当然也可以本地验证啦。
//本地校验苹果数据
func verifyReceipt(service: AppleReceiptValidator.VerifyReceiptURLType) {
let receiptValidator = AppleReceiptValidator(service: service, sharedSecret: sharedSecret)
SwiftyStoreKit.verifyReceipt(using: receiptValidator) { (result) in
switch result {
case .success (let receipt):
let status: Int = receipt["status"] as! Int
//沙盒测试
if status == 21007 {
}
print("receipt:\(receipt)")
case .error(let error):
print("error:\(error)")
}
}
}
sharedSecret获取方式,点击App 专用共享密钥,可以看到
二.监听完成支付的队列
AppDelegate添加以下代码,在启动时添加应用程序的观察者可确保在应用程序的所有启动过程中都会持续,从而允许应用程序接收所有支付队列通知,如果有任何未完成的处理,将会触发block,以便我们更新UI等。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
SwiftyStoreKit.completeTransactions(atomically: false) { [weak self] (purchases) in
guard let `self` = self else {return}
for purchase in purchases {
//未登录不处理交易
if 未登录 {
return
}
switch purchase.transaction.transactionState {
case .purchased,.restored:
print("已完成订单")
//处理订单交易
handleTransaction(transaction: purchase.transaction) { bool in }
case .failed,.purchasing,.deferred:
break
}
}
}
}
三.恢复购买
恢复购买用来让用户访问已经购买过的内容,比如,当用户换一台新手机,为了保证用户不丢失已经在旧手机上购买过的产品等。
func restorePurchases(_ completeHolder: ((Bool)->Void)? = nil) {
SwiftyStoreKit.restorePurchases(atomically: false) { results in
if results.restoreFailedPurchases.count > 0 {
print("Restore Failed: \(results.restoreFailedPurchases)")
}
else if results.restoredPurchases.count > 0 {
for purchase in results.restoredPurchases {
handleTransaction(transaction: purchase) { dialog, code in }
}
print("Restore Success: \(results.restoredPurchases)")
}
else {
print("Nothing to Restore")
}
if completeHolder != nil {
completeHolder!(true)
}
}
}
好了,以上就是购买的过程,接下来就说说丢单的问题。
四.丢单及解决思路
为什么会丢单呢? 我们向苹果服务器支付并且拿到票据,但是我们向服务端去校验的时候,网络出现问题,就会出现丢单,当然这只是其中一种网络原因造成的丢单,所以在项目里等向服务器校验成功了再调用finishTransaction。这样才会在队列里移除。
还有一个就是在支付过程中人为退出app.
1.当前客户端登录了用户A,现在支付校验(即客户端向服务器端校验)失败,这时A退出登录,换成B登录,此时我们重新启动App,可能将该给A的服务都给了B,这让A很吃亏啊。
解决方法(1):在本地存一个用户分离的表,每一个用户对应一个数组,数组中存放我们的票据,每次app启动时处理跟本用户相关的校验,但是如果真的给了B,A找了过来,也是个麻烦。
解决方法(2):每次购买成功都调用finishTransaction。依照解决方法(1)存起来,在每次购买前判断本地是否存有没有处理的订单。
2.上面的方法没有解决根本,当然了,确实没有,如果我们卸载了app重新安装了app,那么我们存在本地的表是没有用的,上述问题依旧会存在。那么还有一个方法就是将这些数据存储在钥匙串里,不清空钥匙串还是可以的。
3.关于多个苹果账号订阅一个客户端用户,后端处理比较麻烦,我们采取的措施是当有一个苹果账号为该用户订阅过了就不允许其他用户继续订阅。
当然了,丢单必不可免,我们只能尽量减少丢单,实在不行就只能找客服。