美文网首页
Swift苹果内购IAP(余额充值场景)

Swift苹果内购IAP(余额充值场景)

作者: 雨打芭蕉落 | 来源:发表于2019-05-23 20:18 被阅读0次

    前言

    老夫接到一个项目,需要苹果内购做充值,经过一周多的努力把核心代码完成,希望对大家有所帮助。

    效果图

    废话不多说,直接上图

    余额充值页面

    流程说明

    • 正常充值流程
      1、【可选】App询问服务端是否可以请求充值ACG_PAY_50(50元)
      2、若允许,App向苹果发起支付(50元)
      3、苹果支付成功,返回凭证(payload)到App
      4.1、App使用payload请求本接口,
      4.2、服务端使用payload到苹果拿到下面Json数据
      4.3、校验Json数据中transaction_id是否已经使用
      4.4、若未使用,则给用户充值(50元),生成充值记录
      4.5、处理结束,返回成功(code=success)
      5、App把收到凭证从本地清除,完成支付,刷新余额

    • 异常情况:
      1、若充值过程(第3步)退出App,可充值成功,余额未给用户增加,再次打开App完成后续步骤即可
      2、若校验过程(第4步)网络断开,则可能校验成功,余额增加,App未收到校验信息,再次打开App仍然会发布充值,服务端判断transaction_id为重复,忽略即可。
      3、校验返回码每次都不成功时,App不会清除凭证,会造成每次打开App都校验,该商品也无法再次购买成功(系统提示:已经支付,可以恢复购买)。

    核心代码,稍作修改即可复用

    • IAPHelper.swift
    //
    //  IAPHelper.swift
    //  IAP
    //
    //  Created by lin bo on 2019/5/13.
    //  Copyright © 2019 appTech. All rights reserved.
    //
    
    import UIKit
    import StoreKit
    
    /// 商品列表
    enum ACG_PAY_ID: String {
        
        case pay50 = "ACG_PAY_50"
        case pay98 = "ACG_PAY_98"
        case pay148 = "ACG_PAY_148"
        case pay198 = "ACG_PAY_198"
        case pay248 = "ACG_PAY_248"
        case pay298 = "ACG_PAY_298"
    
        func price() -> Int {
            
            switch self {
                
            case .pay50: return 50
            case .pay98: return 98
            case .pay148: return 148
            case .pay198: return 198
            case .pay248: return 248
            case .pay298: return 298
            }
        }
    }
    
    /// 回调状态
    enum IAPProgress: Int {
        
        /// 初始状态
        case none
        /// 开始
        case started
        /// 购买中
        case purchasing
        
        /// 支付成功
        case purchased
        /// 失败
        case payFailed
        /// 重复购买
        case payRestored
        /// 状态未确认
        case payDeferred
        /// 其他
        
        case payOther
        /// 开始后端校验
        case checking
        /// 后端校验成功
        case checkedSuccess
        /// 后端校验失败
        case checkedFailed
    
    }
    
    enum IAPPayCheck {
        
        case busy /// 有支付正在进行
        case notInit /// 未初始化
        case initFailed /// 初始化失败
        case notFound /// 没有找到该商品,中断
        case systemFailed /// 系统检测失败
        case ok /// 可以进行
    
    }
    
    class IAPHelper: ATBaseHelper {
        
        static let shared = IAPHelper()
        
        /// 检测初始化回调
        fileprivate var checkBlock: ((_ b: IAPPayCheck) -> ())?
        /// 支付过程回调
        var resultBlock: ((_ type: IAPProgress, _ pID: ACG_PAY_ID?) -> ())?
        
        /// 是否正在支付
        fileprivate var isBusy: Bool {
            get {
                switch progress {
                case .none:
                    return false
                default:
                    return true
                }
            }
        }
       
        /// 购买的状态
        fileprivate var progress: IAPProgress = .none {
            didSet {
                /// 状态改变回调
                if let block = resultBlock {
                    block(progress, currentPID)
                }
            }
        }
        
        /// 当前付费的ID
        fileprivate var currentPID: ACG_PAY_ID?
        /// 商品列表
        fileprivate var productList: [SKProduct]?
    
        /// 初始化配置,请求商品
        func config() {
            
            SKPaymentQueue.default().add(self)
            requestAllProduct()
        }
        
        /// 初始化,请求商品列表
        func initPayments(_ block: @escaping ((_ b: IAPPayCheck) -> ())) {
            
            let c = checkPayments()
            
            if c == .notInit {
                
                requestAllProduct()
                checkBlock = block
    
            }else {
                
                block(c)
            }
        }
        
        /// 检测支付环境,非.ok不允许充值
        func checkPayments() -> IAPPayCheck {
            
            guard isBusy == false else {
                return .busy
            }
            
            guard let plist = productList, !plist.isEmpty else {
                return .notInit
            }
            
            guard SKPaymentQueue.canMakePayments() else {
                return .systemFailed
            }
            
            return .ok
        }
        
        /// 请求商品列表
        private func requestAllProduct() {
            
            let set: Set<String> = [ACG_PAY_ID.pay50.rawValue,
                            ACG_PAY_ID.pay98.rawValue,
                            ACG_PAY_ID.pay148.rawValue,
                            ACG_PAY_ID.pay198.rawValue,
                            ACG_PAY_ID.pay248.rawValue,
                            ACG_PAY_ID.pay298.rawValue]
            
            let request = SKProductsRequest(productIdentifiers: set)
            request.delegate = self
            request.start()
        }
        
        /// 支付商品
        @discardableResult
        func pay(pID: ACG_PAY_ID) -> IAPPayCheck {
            
            let c = checkPayments()
            
            if c == .ok {
                
                guard let plist = productList, !plist.isEmpty else {
                    return .notInit
                }
    
                let pdts = plist.filter {
                    return $0.productIdentifier == pID.rawValue
                }
                
                guard let product = pdts.first else {
                    return .notFound
                }
                
                currentPID = pID
                requestProduct(pdt: product)
            }
            
            return c
        }
        
        /// 请求充值
        fileprivate func requestProduct(pdt: SKProduct) {
            
            progress = .started
    
            let pay: SKMutablePayment = SKMutablePayment(product: pdt)
            SKPaymentQueue.default().add(pay)
        }
        
        /// 重置
        fileprivate func payFinish() {
            
            currentPID = nil
            progress = .none
        }
        
        /// 充值完成后给后台校验
        func completeTransaction(_ checkList: [SKPaymentTransaction]) {
            
            if resultBlock == nil {
                showAlert("充值校验中...")
            }
            ALog("充值校验中...")
            progress = .checking
            
            guard let rURL = Bundle.main.appStoreReceiptURL, let data = try? Data(contentsOf: rURL) else {
                ALog("appStoreReceiptURL error")
                
                progress = .checkedFailed
                payFinish()
                return
            }
            
            let str = data.base64EncodedString()
            print(str)
            
            OrderServer.shared.requestCheckIAP(str) { [weak self] (code, msg, result) in
                
                guard let helper = self else {
                    return
                }
                
                if result { // 成功则删除
                    checkList.forEach({ (transaction) in
                        SKPaymentQueue.default().finishTransaction(transaction)
                    })
                }
                
                if helper.resultBlock == nil {
                    showAlert(result ? "校验成功" : "校验失败")
                }
                ALog(result ? "充值成功" : "充值失败")
                
                helper.progress = result ? .checkedSuccess : .checkedFailed
                helper.payFinish()
            }
        }
    }
    
    // MARK: - SKProductsRequestDelegate
    extension IAPHelper: SKProductsRequestDelegate {
        
        func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
            ALog("---IAP---")
    
            if currentPID == nil {
                // 列表赋值
                productList = response.products
            }
        }
        
        func requestDidFinish(_ request: SKRequest) {
            ALog("---IAP---")
    
            if currentPID == nil {
                
                if let block = checkBlock {
                    
                    if let pList = productList, !pList.isEmpty {
                        block(.ok)
                    }else {
                        block(.initFailed)
                    }
                    checkBlock = nil
                }
            }
        }
        
        func request(_ request: SKRequest, didFailWithError error: Error) {
            ALog("---IAP---")
    
            if currentPID == nil {
                
                if let block = checkBlock {
                    block(.initFailed)
                    checkBlock = nil
                }
            }
        }
    }
    
    // MARK: - SKPaymentTransactionObserver
    extension IAPHelper: SKPaymentTransactionObserver {
        
        func paymentQueue(_ queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) {
            ALog("---IAP---")
        }
        
        func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
            ALog("---IAP---")
        }
        
        func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
            ALog("---IAP---")
    
            var checkList: [SKPaymentTransaction] = []
            var type: IAPProgress = progress
    
            for transaction in transactions {
                
                ALog("支付结果: \(transaction.description)")
    
                let pid = transaction.payment.productIdentifier
                switch transaction.transactionState {
                    
                case .purchasing:
                    
                    ALog("支付中:\(pid)")
                    type = .purchasing
    
                case .purchased:
                    
                    checkList.append(transaction)
                    ALog("支付成功:\(pid)")
                    type = .purchased
    
                case .failed:
                    
                    ALog("支付失败:\(pid)")
                    type = .payFailed
                    SKPaymentQueue.default().finishTransaction(transaction)
    
                case .restored:
                    
                    checkList.append(transaction)
                    ALog("支付已购买过:\(pid)")
                    type = .payRestored
    
                case .deferred:
                    
                    ALog("支付不确认:\(pid)")
                    type = .payDeferred
                    SKPaymentQueue.default().finishTransaction(transaction)
    
                @unknown default:
                    
                    ALog("支付未知状态:\(pid)")
                    type = .payOther
                    SKPaymentQueue.default().finishTransaction(transaction)
                }
            }
            
            progress = type
            
            if !checkList.isEmpty {
                // 有内购已经完成
                completeTransaction(checkList)
                
            }else if type == .purchasing {
                // 正常情况:内购正在支付
                // 特殊情况:若该商品已购买,未执行finishTransaction,系统会提示(免费恢复项目),回调中断
                // 解决方法:在应用开启的时候捕捉到restored状态的商品,提交后台校验后执行finishTransaction
    
            }else { // 其他状态
                
                payFinish()
            }
        }
    
        func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
            ALog("---IAP---")
        }
        
        func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
            ALog("---IAP---")
        }
        
        func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
            ALog("---IAP---")
            return true
        }
    }
    
    
    • ViewController调用
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            // 回调支持过程,处理HUD显隐和用户提示
            IAPHelper.shared.resultBlock = { [weak self] (result, pID) in
                
                guard let vc = self else {
                    return
                }
                
                switch result {
    
                case .none:
                    break
                    
                case .started:
                    vc.updateHUD(true, text: "支付中")
                    
                case .purchasing:
                    break
                    
                case .purchased:
                    break
                    
                case .payFailed:
                    vc.updateHUD(false, text: "支付取消")
    
                case .payRestored:
                    vc.updateHUD(false)
    
                case .payDeferred:
                    vc.updateHUD(false)
    
                case .payOther:
                    vc.updateHUD(false)
    
                case .checking:
                    vc.updateHUD(true, text: "充值中")
                    
                case .checkedSuccess:
                    vc.updateHUD(false, text: "充值成功")
                    vc.updateData()
                    
                case .checkedFailed:
                    vc.updateHUD(false, text: "充值失败,请检测网络")
                    vc.updateData()
                }
            }
        }
        
        override func viewDidDisappear(_ animated: Bool) {
            
            IAPHelper.shared.resultBlock = nil
        }
    
        @IBAction func payAction(_ sender: Any) {
            
            for bt in sumBtns {
                if bt.isSelected == true {
     
                    switch bt.tag {
                    case 1:
                        pay(id: .pay50)
                    case 2:
                        pay(id: .pay98)
                    case 3:
                        pay(id: .pay148)
                    case 4:
                        pay(id: .pay198)
                    case 5:
                        pay(id: .pay248)
                    case 6:
                        pay(id: .pay298)
                    default:
                        break
                    }
                    break
                }
            }
        }
    
        func pay(id: ACG_PAY_ID) {
            
            // 请求支付
            let p = IAPHelper.shared.pay(pID: id)
            
            switch p {
                
            case .ok:
                break
                
            case .notInit:
                IAPHelper.shared.initPayments { (c) in
                    
                    if c == .ok {
                        
                        if IAPHelper.shared.pay(pID: id) != .ok {
                            showAlert("暂时无法支付,请稍后再试")
                        }
                        
                    }else {
                        showAlert("暂时无法支付,请稍后再试")
                    }
                }
                break
    
            default:
                showAlert("暂时无法支付,请稍后再试")
                break
            }
        }
    
    
    • AppDelegate需要初始化一下
    IAPHelper.shared.config()
    

    附件:

    • 支付成功后的交易凭证数据很大,后端接收这个数据,跟苹果校验后,判断用户即可给该用户充值。
    
    
            guard let rURL = Bundle.main.appStoreReceiptURL, let data = try? Data(contentsOf: rURL) else {
                ALog("appStoreReceiptURL error")
            }
            
            let str = data.base64EncodedString()
            print(str)
    
    
    
    • 打印出来是这样的(很长)
    
    MIIVKgYJKoZIhvcNAQcCoIIVGzCCFRcCAQExCzAJBgUrDgMCGgUAMIIEywYJKoZIhvcNAQcBoIIEvASCBLgxggS0MAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQECAQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADALAgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQEAgIAiTANAgENAgEBBAUCAwHViDANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAyNTIwGAIBBAIBAgQQEXxl0gRk5NBqMO8/VFkmNzAZA... ...
    
    
    • 苹果返回Json

    1、收到这个数据表示,里面的项目肯定付费成功
    2、通过transaction_id判断是否重复校验
    3、通过product_id * quantity 判断支付金额
    4、通过environment判断所在环境

    注意:用户信息通过判断App登录用户,透传App用户信息和App订单信息都是不可靠的。

    
    {
      "environment": "Sandbox",
      "receipt": {
        "in_app": [{
            "transaction_id": "1000000529594470",
            "original_purchase_date": "2019-05-21 08:25:55 Etc/GMT",
            "quantity": "1",
            "original_transaction_id": "1000000529594470",
            "purchase_date_pst": "2019-05-21 01:25:55 America/Los_Angeles",
            "original_purchase_date_ms": "1558427155000",
            "purchase_date_ms": "1558427155000",
            "product_id": "ACG_PAY_50",
            "original_purchase_date_pst": "2019-05-21 01:25:55 America/Los_Angeles",
            "is_trial_period": "false",
            "purchase_date": "2019-05-21 08:25:55 Etc/GMT"
          },
          {
            "transaction_id": "1000000529074541",
            "original_purchase_date": "2019-05-20 02:29:04 Etc/GMT",
            "quantity": "1",
            "original_transaction_id": "1000000529074541",
            "purchase_date_pst": "2019-05-19 19:29:04 America/Los_Angeles",
            "original_purchase_date_ms": "1558319344000",
            "purchase_date_ms": "1558319344000",
            "product_id": "ACG_PAY_98",
            "original_purchase_date_pst": "2019-05-19 19:29:04 America/Los_Angeles",
            "is_trial_period": "false",
            "purchase_date": "2019-05-20 02:29:04 Etc/GMT"
          }
        ],
        "adam_id": 0,
        "receipt_creation_date": "2019-05-21 08:51:54 Etc/GMT",
        "original_application_version": "1.0",
        "app_item_id": 0,
        "original_purchase_date_ms": "1375340400000",
        "request_date_ms": "1558430410539",
        "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
        "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
        "receipt_creation_date_pst": "2019-05-21 01:51:54 America/Los_Angeles",
        "receipt_type": "ProductionSandbox",
        "bundle_id": "com.xxx.acg",
        "receipt_creation_date_ms": "1558428714000",
        "request_date": "2019-05-21 09:20:10 Etc/GMT",
        "version_external_identifier": 0,
        "request_date_pst": "2019-05-21 02:20:10 America/Los_Angeles",
        "download_id": 0,
        "application_version": "1"
      },
      "status": 0
    }
    
    

    后端实现参考文章

    1、流程写得很详细,php源码也有:
    http://www.cnblogs.com/wangboy91/p/7162335.html
    2、Java端的支持:
    https://blog.csdn.net/jianzhonghao/article/details/79343887

    相关文章

      网友评论

          本文标题:Swift苹果内购IAP(余额充值场景)

          本文链接:https://www.haomeiwen.com/subject/bcfizqtx.html