这段时间做了苹果内购IAP,做一个整理记录,主要是开发层面。
一.前期工作:在开发者账号中添加银行信息同意协议等,添加沙盒账号,添加内购商品
二.项目开发,因为项目需要支持iOS15一下的版本所以使用旧版StoreKit,新版的StoreKit2只支持iOS15以上,新的nsync同步接口。
1.获取内购商品信息,可以在自己服务器中获取商品的productId数组,根据product ID 获取价格等具体商品信息,用于显示给用户,如果商品信息不经常变化,可以把结果缓存起来,不获取商品信息也可以发起内购不影响购买
private var productFetchCallbacks = [SKProductsRequest: ([SKProduct]) -> Void]() public func fetchProductsInfo(_ productIDs: [String],completion:@escaping ([SKProduct]) -> Void) { let set = Set<String>.init(productIDs) let request = SKProductsRequest.init(productIdentifiers: set) productFetchCallbacks[request] = completion request.delegate = self request.start() } //代理回调结果,注意代理回调结果是在多线程中,根据需求是否要切换成主线程 // MARK: SKProductsRequestDelegate extension MXLiveIAPPayment : SKProductsRequestDelegate{ public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { guard let callback = productFetchCallbacks[request] else { return } productFetchCallbacks[request] = nil DispatchQueue.main.async { callback(response.products) } } public func request(_ request: SKRequest, didFailWithError error: Error) { print(error.localizedDescription) if let productsFetchRequest = request as? SKProductsRequest { guard let callback = productFetchCallbacks[productsFetchRequest] else { return } productFetchCallbacks[productsFetchRequest] = nil DispatchQueue.main.async { callback([]) } } } // public func requestDidFinish(_ request: SKRequest) { // print(request) // } }
2.设置代理发起内购,
let payment = SKMutablePayment() payment.quantity = 1 payment.applicationUsername = currentOrder?.uuid payment.productIdentifier = order.productId payment.simulatesAskToBuyInSandbox = true // test deferred SKPaymentQueue.default().add(payment) //代理方法中收到支付结果 / MARK: SKPaymentTransactionObserver //处理未完成的交易 extension MXLiveIAPPayment : SKPaymentTransactionObserver{ public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for tran in transactions { switch tran.transactionState { case .purchased://购买完成 //成功的未移出的transaction进入app会会掉,失败的不会回掉 self.delegate?.orderStatusChanged(order: currentOrder, status:.purchased) currentTransaction = tran completePay(transaction: tran) print("-------IAP pay purchased--------------") break case.purchasing://商品添加进列表 // tran.transactionIdentifier此时未nil self.delegate?.orderStatusChanged(order: currentOrder, status: .purchasing) currentTransaction = tran self.updatePurchaseStatus(status: "purchasing") print("-------IAP pay purchasing--------------") break case.restored://已经购买过该商品 self.delegate?.orderStatusChanged(order: currentOrder, status: .failed(MXLiveIAPError(reason: "product restored", code: -1))) self.updatePurchaseStatus(status: "restored") currentTransaction = tran finishCurrentOrder() print("-------IAP pay restored--------------") break case.failed://购买失败 self.delegate?.orderStatusChanged(order: currentOrder, status: .failed(tran.error ?? MXLiveIAPError(reason: "purchase failed error", code: -1))) handleFailure(tran) self.updatePurchaseStatus(status: "failed") //低版本iOS13以下添加观察者之后有可能直接走到此处失败的回调中 currentTransaction = tran finishCurrentOrder() print("-------IAP pay failed--------------") break case .deferred: //https://stackoverflow.com/questions/42152560/how-to-handle-skpaymenttransactionstatedeferred //ask permission for your parent or guardian //ask for buy,We get transaction deferred state, if user is part of Apple family sharing & family admin enabled ASK TO BUY. currentTransaction = tran currentOrder?.deferedDate = Date() currentOrder?.updateTokeyChain() self.updatePurchaseStatus(status: "deferred") self.delegate?.orderStatusChanged(order: currentOrder, status: .deferred) print("-------IAP pay deferred--------------") break @unknown default: () } } } private func handleFailure(_ transaction: SKPaymentTransaction) { guard let error = transaction.error else { return } let nsError = error as NSError guard nsError.domain == SKError.errorDomain else { return } switch nsError.code { case SKError.clientInvalid.rawValue, SKError.paymentNotAllowed.rawValue: print ("You are not allowed to make IAP payment.") case SKError.paymentCancelled.rawValue: print ( "IAP Payment has been cancelled.") case SKError.unknown.rawValue, SKError.paymentInvalid.rawValue: fallthrough default: print ("Something went wrong making IAP payment.") } }
//完成的transaction要记住调用finish接口,否则下一次支付代理回调中还会收到这条transaction
3.验证支付票据,支付票据客户端可以直接调苹果的接口验证,我们是调用后台接口让后台 去验证,这样验证通过可以直接进行后续下发商品等业务
private func verifyForApple(data:Data,transaction:SKPaymentTransaction?) { self.delegate?.orderStatusChanged(order: currentOrder, status: .receiptChecking) let base64Str = data.base64EncodedString(options: .endLineWithLineFeed) let params = NSMutableDictionary() params["receipt-data"] = base64Str let body = try? JSONSerialization.data(withJSONObject: params, options: .prettyPrinted) var request = URLRequest.init(url: URL.init(string: receiptState == 21008 ? url_receipt_itunes : url_receipt_sandbox)!, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 20) request.httpMethod = "POST" request.httpBody = body let session = URLSession.shared let task = session.dataTask(with: request) { [weak self](data, response, error) in guard let data = data, let dict = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSDictionary else{ self?.delegate?.orderStatusChanged(order: self?.currentOrder, status: .failed(MXLiveIAPError(reason: "receipt check failed", code: -1))) return } print("receipt_info:") print(dict) let status = dict["status"] as? Int switch(status){ case 0: self?.delegate?.orderStatusChanged(order: self?.currentOrder, status: .complete) break case 21007: self?.receiptState = 21007 self?.verifyForApple(data: data, transaction: transaction) break default: self?.delegate?.orderStatusChanged(order: self?.currentOrder, status: .failed(MXLiveIAPError(reason: "receipt check failed", code: -1))) break } } task.resume() }
票据验证结果事例:
{ receipt = { receipt_type = "ProductionSandbox"; app_item_id = 0; receipt_creation_date = "2022-12-08 12:36:33 Etc/GMT"; bundle_id = "com.mxplay.ios.live"; original_purchase_date = "2013-08-01 07:00:00 Etc/GMT"; in_app = ( { quantity = "1"; purchase_date_ms = "1670502431000"; transaction_id = "2000000222942381"; is_trial_period = "false"; original_transaction_id = "2000000222942381"; purchase_date = "2022-12-08 12:27:11 Etc/GMT"; product_id = "mx_dq_00001"; original_purchase_date_pst = "2022-12-08 04:27:11 America/Los_Angeles"; in_app_ownership_type = "PURCHASED"; original_purchase_date_ms = "1670502431000"; purchase_date_pst = "2022-12-08 04:27:11 America/Los_Angeles"; original_purchase_date = "2022-12-08 12:27:11 Etc/GMT"; } ); adam_id = 0; receipt_creation_date_pst = "2022-12-08 04:36:33 America/Los_Angeles"; request_date = "2022-12-08 12:36:34 Etc/GMT"; request_date_pst = "2022-12-08 04:36:34 America/Los_Angeles"; version_external_identifier = 0; request_date_ms = "1670502994515"; original_purchase_date_pst = "2013-08-01 00:00:00 America/Los_Angeles"; application_version = "202202153"; original_purchase_date_ms = "1375340400000"; receipt_creation_date_ms = "1670502993000"; original_application_version = "1.0"; download_id = 0; }; status = 0; environment = "Sandbox"; }
可以根据transaction.payment.productIdentifier去匹配自己的业务订单
苹果做了限制,如果有相同的productIdentifier的transaction没有处理完,不能发起重复支付,话句话说,transaction数组中不会同时包含两个productID相同的item,也就是如果上一个product未finish,发起新的相同productid的内购会返回失败
在客户端层面也做了限制,当前交易未处理完之前不能发起新的交易,所以基本不会出现多个truncation的情况,
多个transaction验证结果事例:
{ receipt = { receipt_type = "ProductionSandbox"; app_item_id = 0; receipt_creation_date = "2022-12-08 14:16:42 Etc/GMT"; bundle_id = "com.mxplay.ios.live"; original_purchase_date = "2013-08-01 07:00:00 Etc/GMT"; in_app = ( { quantity = "1"; purchase_date_ms = "1670508859000"; transaction_id = "2000000223045245"; is_trial_period = "false"; original_transaction_id = "2000000223045245"; purchase_date = "2022-12-08 14:14:19 Etc/GMT"; product_id = "mx_dq_00001"; original_purchase_date_pst = "2022-12-08 06:14:19 America/Los_Angeles"; in_app_ownership_type = "PURCHASED"; original_purchase_date_ms = "1670508859000"; purchase_date_pst = "2022-12-08 06:14:19 America/Los_Angeles"; original_purchase_date = "2022-12-08 14:14:19 Etc/GMT"; }, { quantity = "1"; purchase_date_ms = "1670508919000"; transaction_id = "2000000223046251"; is_trial_period = "false"; original_transaction_id = "2000000223046251"; purchase_date = "2022-12-08 14:15:19 Etc/GMT"; product_id = "mx_dq_00002"; original_purchase_date_pst = "2022-12-08 06:15:19 America/Los_Angeles"; in_app_ownership_type = "PURCHASED"; original_purchase_date_ms = "1670508919000"; purchase_date_pst = "2022-12-08 06:15:19 America/Los_Angeles"; original_purchase_date = "2022-12-08 14:15:19 Etc/GMT"; } ); adam_id = 0; receipt_creation_date_pst = "2022-12-08 06:16:42 America/Los_Angeles"; request_date = "2022-12-08 14:17:15 Etc/GMT"; request_date_pst = "2022-12-08 06:17:15 America/Los_Angeles"; version_external_identifier = 0; request_date_ms = "1670509035235"; original_purchase_date_pst = "2013-08-01 00:00:00 America/Los_Angeles"; application_version = "202202153"; original_purchase_date_ms = "1375340400000"; receipt_creation_date_ms = "1670509002000"; original_application_version = "1.0"; download_id = 0; }; status = 0; environment = "Sandbox"; }
验证的错误码如下
21000 App Store无法读取你提供的JSON数据
21002 收据数据不符合格式
21003 收据无法被验证
21004 你提供的共享密钥和账户的共享密钥不一致
21005 收据服务器当前不可用
21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
注意:
IAP审核时, 需要提供沙盒测试账号和一个APP的测试账号, 在审核过程时, 我们整个流程都已经切换为正式环境, 但审核人员仍然使用测试凭证去进行验证, 我们服务器需要在审核阶段, 对于此时凭证仍然去沙盒测试验证接口去验证才能验证通过, 否则会被拒绝通过。
在审核阶段可以修改服务端验证支付凭证的流程,先验证正式的如果失败再验证沙盒环境
相关参阅:
https://juejin.cn/post/6974733392260644895
https://juejin.cn/post/7050408490682023966
https://juejin.cn/post/7118958291446661134
标签:内购,purchase,12,08,request,苹果,2022,date,IAP From: https://www.cnblogs.com/duzhaoquan/p/17058132.html