苹果内购IAP记录-1

这段时间做了苹果内购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()
    }

 票据验证结果事例:

 

 可以根据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://github.com/duzhaoquan/DQIAPTool

相关参阅:

https://juejin.cn/post/6974733392260644895

https://juejin.cn/post/7050408490682023966

https://juejin.cn/post/7118958291446661134

 

 

posted @ 2023-01-17 16:34  不停奔跑的蜗牛  阅读(306)  评论(0编辑  收藏  举报