一、前端发起支付的参考例子:
import UIKit
import MMKV
import StoreKit
import Alamofire
import SVProgressHUD/// ApplePay 工具类
class ApplePayUtils : NSObject {static let shareInstance = ApplePayUtils.init()/// 丢单存储集Key,验证成功从集合中移除let loseBillKey = "ApplePayUtils.lose.bill"/// 支付渠道let applePayChannel = "apple_app"/// 当前查询的产品Idprivate lazy var selectProductID:String? = nil/// 对应后台订单号private lazy var outTradeNo:String? = nilprivate lazy var checkCount:Int = 0/// 请求售卖商品回调private lazy var finishBlock:CMRequest.SuccessBlock? = nil/// 支付购买回调private lazy var buyFinishBlock:((Bool,Any?)->Void)? = nil//MARK: 添加/移除监听/// 添加监听func applePayAddObserver(){SKPaymentQueue.default().add(ApplePayUtils.shareInstance)}/// 移除监听func applePayRemoveObserver(){SKPaymentQueue.default().remove(ApplePayUtils.shareInstance)}
}//MARK: -
extension ApplePayUtils {//MARK: 恢复购买/// 主要针对非消耗品func applePayReplyToBuy(){SKPaymentQueue.default().restoreCompletedTransactions()}//MARK: 请求所有内购商品/// 根据ApplePay的产品Id获取售卖的商品集合/// - Parameters:/// - _pId: 产品Id/// - _fb: (_ responseData:Any?) -> (Void)func applePayGetGoodIdsFor(ProductId _pId:String,andFinishBlock _fb:@escaping CMRequest.SuccessBlock,withLoading _loading:Bool = false) {if _loading {SVProgressHUD.show(withStatus: "拉取商品中...")}self.selectProductID = _pIdself.finishBlock = nilself.finishBlock = _fb//1、设置产品Id集let _setProductIdentifiers:Set<String> = [_pId]//2、根据产品Id集请求所有可卖商品集(此前需要调用 applePayAddObserver,添加监听)let sKProductsRequest = SKProductsRequest.init(productIdentifiers: _setProductIdentifiers)sKProductsRequest.delegate = ApplePayUtils.shareInstance//2.1 开始请求sKProductsRequest.start()}//MARK: 购买某件商品/// 购买某件商品(此前需要调用 applePayAddObserver,添加监听)/// - Parameter _p: SKProduct/// - Parameter _otn: String 支付订单号(丢/漏单,后台校验需要此参数)func applePayBuyGoodsFor(Product _p:SKProduct,andOutTradeNo _otn:String,andBuyFinishBlock _fb:((Bool,Any)->Void)? = nil,withLoading _loading:Bool = false) {self.outTradeNo = _otnself.buyFinishBlock = nilself.buyFinishBlock = _fbif _loading {UIApplication.shared.isNetworkActivityIndicatorVisible = trueSVProgressHUD.show(withStatus: "商品购买中...")}//1、创建票据let payment = SKPayment.init(product: _p)//2、将票据加入到交易队列SKPaymentQueue.default().add(payment)}//MARK: 购买成功/// 购买成功/// - Parameter _pt: <#_pt description#>private func applePayPurchaseSucceedsFor(PaymentTransaction _pt:SKPaymentTransaction,andTransactionId _tId:String?) {let productIdentifier = _pt.payment.productIdentifier
#if DEBUGNSLog("applePayPurchaseSucceedsFor-{productIdentifier:%@,transactionId:%@}", productIdentifier,_tId ?? "--")
#endifif #available(iOS 7.0, *) {//验证凭据,获取到苹果返回的交易凭据//appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址if let _url = Bundle.main.appStoreReceiptURL {URLSession.shared.dataTask(with: _url) { (_data:Data?, _:URLResponse?, _error:Error?) inif _data != nil {let transactionReceiptString = _data!.base64EncodedString(options: .init(rawValue: 0))// 保存本地,丢单后重新验证的处理self.saveOrderReceiptWith(ProductIdentifier: productIdentifier,andReceipt: transactionReceiptString,andTransactionId: _tId ?? "--")// 回调,去服务端校验是否真正的支付成功了self.buyFinishBlock?(true,["productIdentifier":productIdentifier,"receipt":transactionReceiptString,"transactionId":_tId ?? "--"])}else{
#if DEBUGNSLog("applePayPurchaseSucceedsFor-{_error:%@}", _error?.localizedDescription ?? "--")
#endif}}.resume()}}else{print("applePayPurchaseSucceedsFor-{info:iOS 7.0 及以下,不处理}")}}//MARK: 校验支付成功结果/// 检验是否真的成功了(里面已做了二次校验,外面无需处理)/// - Parameter _base64: String 苹果支付返回的 交易凭据(receipt)/// - Parameter _pId: String 内购的产品编号/// - Parameter _otn: String 具体的支付订单号/// - Parameter _sandBox: Bool true 沙盒(0) false AppleStore(1)/// - Parameter _tId: String 交易成功编号/// - Parameter _crblock: ((Any)->Void)? 校验结果回调/// - Parameter _loading: Bool true 显示加载框func checkAppStorePayResultWith(Receipt _base64:String,andProductIdentifier _pId:String,andOutTradeNo _otn:String,andisSandbox _sandBox:Bool,andTranscationId _tId:String,andCheckResultBlock _crblock:((Bool,String,Any?)->Void)? = nil,withLoading _loading:Bool = false) {/*** 生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明* 注意:* 0 代表沙盒 1代表 正式的内购* 自己测试的时候使用的是沙盒购买(测试环境)* App Store审核的时候也使用的是沙盒购买(测试环境)* 上线以后就不是用的沙盒购买了(正式环境)* 所以此时应该先验证正式环境,在验证测试环境* 正式环境验证成功,说明是线上用户在使用* 正式环境验证不成功返回21007,说明是自己测试或者审核人员在测试* 苹果AppStore线上的购买凭证地址是: https://buy.itunes.apple.com/verifyReceipt* 测试地址是:https://sandbox.itunes.apple.com/verifyReceipt*//*** 验证购买,避免越狱软件模拟苹果请求达到非法购买问题票据的校验是保证内购安全完成的非常关键的一步,一般有三种方式:* 1、服务器验证,获取票据信息后上传至信任的服务器,由服务器完成与App Store的验证(提倡使用此方法,比较安全* 2、本地票据校验* 3、本地App Store请求验证** 内购验证凭据返回结果状态码说明:* 0 通过校验:{"status":0,"in_app": [...]}* 其他未通过:{"status":21007}* 21000 App Store无法读取你提供的JSON数据* 21002 收据数据不符合格式* 21003 收据无法被验证* 21004 你提供的共享密钥和账户的共享密钥不一致* 21005 收据服务器当前不可用* 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中* 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证* 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证*/if _loading {SVProgressHUD.show(withStatus: "正在确认支付结果中...")}var prgam = ["sandbox":_sandBox ? "0":"1","receipt":_base64,"orderNo":_otn,"transactionId":_tId]//防止死循环self.checkCount = 0self.beginCheckFor(Parameters: prgam,andCheckResultBlock: {[weak self] (_r:Bool,_msg:String, _data:Any?) inguard let self = self else { return }if _r == true {_crblock?(true,_msg,_data)//移除丢单集里面的对应信息self.removeReceiptWith(Orderno: _otn)}else if let _apple_status = (_data as? [String:Any])?["status"] as? Int {if self.checkCount <= 1 {if _apple_status == 21007 || _apple_status == 21008 {prgam["sandbox"] = _apple_status == 21007 ? "0":"1"#if DEBUGNSLog("重新发往,%@认证:%@",_apple_status == 21007 ? "沙盒":"Apple Store" ,prgam)
#endif//二次认证self.beginCheckFor(Parameters: prgam,andCheckResultBlock: { (_r:Bool,_msg:String, _data:Any?) in_crblock?(_r,_msg,_data)},withLoading: false)}else{_crblock?(false,_msg,_data)}}else{_crblock?(false,_msg,_data)}}else{_crblock?(false,_msg,_data)}},withLoading: _loading)}private func beginCheckFor(Parameters _p:[String:String],andCheckResultBlock _crblock:((Bool,String,Any?)->Void)? = nil,withLoading _loading:Bool = false) {self.checkCount += 1let _b = Utils.shareInstance().getJsonDataFor(Any: _p)let _strUrl = UrlSetting.shareInstance.applePayCheckResult()var headers:HTTPHeaders = ["Accept":"application/json","Content-Type":"application/json;charset=UTF-8"]if let userLoginToken: String = MMKV.default()?.string(forKey: Key.shareInstance.userLoginToken),userLoginToken != "" {headers.add(HTTPHeader.init(name: UrlSetting.shareInstance.K_USER_TOKEN, value: userLoginToken))}// 国内访问苹果服务器比较慢,timeoutInterval 需要长一点CMRequest.shareInstance().postRequestWithBodyFor(strUrl: _strUrl,andHeaders: headers,andBody: _b,andFinishBack: { responseData inSVProgressHUD.dismiss()if let _dicResult = Utils.shareInstance().getDicDataFor(Data: responseData) {
#if DEBUGNSLog("beginCheckFor-{_responseData:%@}", _dicResult)
#endifvar code:Int? = _dicResult[UrlSetting.shareInstance.K_API_RESULT_CODE] as? Intif code == nil, let strTemp = _dicResult[UrlSetting.shareInstance.K_API_RESULT_CODE] as? String {code = NSNumber.init(pointer:strTemp).intValue}else if code == nil,let _code = _dicResult[UrlSetting.shareInstance.K_API_RESULT_CODE2] as? Int {code = _code}let _apple_status = (_dicResult[UrlSetting.shareInstance.K_API_RESULT_DATA] as? [String:Any])?["status"] as? Intif (_apple_status == 0 || _apple_status == 10) && (UrlSetting.shareInstance.apiIsOk(rs: code) || code == 200) {_crblock?(true,_dicResult[UrlSetting.shareInstance.K_API_RESULT_MESSAGE] as? String ?? "购买成功",_dicResult[UrlSetting.shareInstance.K_API_RESULT_DATA])}else{//根据apple 校验失败状态,再次发起校验var _msg = _dicResult[UrlSetting.shareInstance.K_API_RESULT_MESSAGE] as? String ?? "校验失败"if _apple_status != 0 && _apple_status != 10 {_msg = "校验失败"}_crblock?(false,_msg,_dicResult[UrlSetting.shareInstance.K_API_RESULT_DATA])}}else{_crblock?(false,String(describing: responseData),nil)print("beginCheckFor-数据解析类型转换失败!详见:\(String(describing: responseData))")}},withisLoading: _loading,withTimeoutInterval: 10)}}//MARK: - 丢单处理
/*** 由于IAP服务器无法保证质量, 或者自己服务器验证凭证出现问题时, 可能会出现丢单(用户付费成功, 但是凭证无法成功向自己服务器验证)的情况*/
extension ApplePayUtils {/// 保存到本地/// - Parameters:/// - _pId: String 产品编号/// - _r: String base64private func saveOrderReceiptWith(ProductIdentifier _pId:String,andReceipt _r:String,andTransactionId _tId:String) {DispatchQueue.main.async {var dicData = [String:[String]]()if let _dataTemp = MMKV.default()?.data(forKey: self.loseBillKey),let _dicTemp = Utils.shareInstance().getDicDataFor(Data: _dataTemp) as? [String:[String]] {dicData = _dicTemp}if let _oId = self.outTradeNo,_oId != "" {/*** {订单号(后台返回的):[支付凭据,产品编号,交易成功编号]}*/dicData[_oId] = [_r,_pId,_tId]if let _data = Utils.shareInstance().getJsonDataFor(Any: dicData) {MMKV.default()?.set(_data, forKey: self.loseBillKey)}}}}/// 验证成功,从集合中移除/// - Parameter _oId: String 订单号private func removeReceiptWith(Orderno _oId:String) {var dicData = [String:[String]]()if let _dataTemp = MMKV.default()?.data(forKey: loseBillKey),let _dicTemp = Utils.shareInstance().getDicDataFor(Data: _dataTemp) as? [String:[String]] {dicData = _dicTemp}dicData[_oId] = nilif dicData.count <= 0 {//没有丢单了MMKV.default()?.removeValue(forKey: loseBillKey)}else {if let _data = Utils.shareInstance().getJsonDataFor(Any: dicData) {MMKV.default()?.set(_data, forKey: loseBillKey)}}}/// 启动App发起丢/漏单校验处理public func loseUpdateStatusForLaunch(){if let _dataTemp = MMKV.default()?.data(forKey: self.loseBillKey),let _dicTemp = Utils.shareInstance().getDicDataFor(Data: _dataTemp) as? [String:[String]],_dicTemp.keys.count > 0 {if let _outTradeNo = _dicTemp.keys.first,let _arrTemp = _dicTemp[_outTradeNo],_arrTemp.count > 2 {self.checkAppStorePayResultWith(Receipt: _arrTemp[0],andProductIdentifier: _arrTemp[1],andOutTradeNo: _outTradeNo,andisSandbox: true,andTranscationId: _arrTemp[2]) { (_cr:Bool, _msg:String ,_cdata:Any) in
#if DEBUGNSLog("loseUpdateStatusForLaunch-校验状态:%@,详见:{outTradeNo:%@,ProductIdentifier:%@,Receipt:%@,msg:%@}", _cr == true ? "成功":"失败",_outTradeNo,_arrTemp[1],_arrTemp[0],_msg)
#endif}}}}
}//MARK: - SKProductsRequestDelegate,SKPaymentTransactionObserver
extension ApplePayUtils : SKProductsRequestDelegate,SKPaymentTransactionObserver {/// 请求内购商品监听/// - Parameters:/// - request: <#request description#>/// - response: <#response description#>func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
#if DEBUGNSLog("productsRequest-可卖商品的信息:{%lD => %@,selectProductID => %@}", response.products.count,response.products,self.selectProductID ?? "--")
#endifSVProgressHUD.dismiss()if response.products.count <= 0 {print("没有可卖商品")self.finishBlock?(nil)}else{//过滤出对应产品Id 下的所有商品let _arrResult:[SKProduct]? = response.products.filter { $0.productIdentifier == self.selectProductID ?? "" }self.finishBlock?(_arrResult)}}/// 购买支付监听/// - Parameters:/// - queue: SKPaymentQueue/// - transactions: <#transactions description#>func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
#if DEBUGNSLog("paymentQueue-购买支付监听:{transactions:%@,queue:%@}", transactions,queue)
#endiflet _tempBlock = {UIApplication.shared.isNetworkActivityIndicatorVisible = falseSVProgressHUD.dismiss()}/**SKPaymentTransactionStatePurchasing, 正在购买SKPaymentTransactionStatePurchased, 已经购买SKPaymentTransactionStateFailed, 购买失败SKPaymentTransactionStateRestored, 回复购买中SKPaymentTransactionStateDeferred 交易还在队列里面,但最终状态还没有决定*/for transaction in transactions {switch transaction.transactionState {case .purchasing:
#if DEBUGNSLog("正在购买:{payment:%@,transactionIdentifier:%@}", transaction.payment,transaction.transactionIdentifier ?? "--")
#endifbreakcase .purchased:
#if DEBUGNSLog("购买成功:{payment:%@,transactionIdentifier:%@}", transaction.payment,transaction.transactionIdentifier ?? "--")
#endif// 购买后告诉交易队列,把这个成功的交易移除掉queue.finishTransaction(transaction)_tempBlock()//验证是否真成功self.applePayPurchaseSucceedsFor(PaymentTransaction: transaction,andTransactionId: transaction.transactionIdentifier)breakcase .failed:
#if DEBUGNSLog("购买失败:{payment:%@,transactionIdentifier:%@,error:%@}", transaction.payment,transaction.transactionIdentifier ?? "--",transaction.error?.localizedDescription ?? "--")
#endif// 购买失败也要把这个交易移除掉queue.finishTransaction(transaction)_tempBlock()self.buyFinishBlock?(false,transaction.error?.localizedDescription ?? "购买失败")breakcase .restored:
#if DEBUGNSLog("回复购买中,也叫做已经购买:{payment:%@,transactionIdentifier:%@}", transaction.payment,transaction.transactionIdentifier ?? "--")
#endif// 回复购买中也要把这个交易移除掉queue.finishTransaction(transaction)_tempBlock()self.buyFinishBlock?(false,transaction.error?.localizedDescription ?? "当前商品已经购买")breakcase .deferred:
#if DEBUGNSLog("交易还在队列里面,但最终状态还没有决定:{payment:%@,transactionIdentifier:%@}", transaction.payment,transaction.transactionIdentifier ?? "--")
#endifbreak@unknown default:
#if DEBUGNSLog("未知购买状态类型:{payment:%@,transactionIdentifier:%@,error:%@}", transaction.payment,transaction.transactionIdentifier ?? "--",transaction.error?.localizedDescription ?? "")
#endiffatalError()break}}}}
二、服务端监听状态处理(这里是问题最大的环节)
1、 支付凭证(receipt) 校验
https://sandbox.itunes.apple.com/verifyReceipt
2、支付成功、退款等服务端到服务端状态监听
https://api.storekit.itunes.apple.com/inApps/v1/notifications/test
https://api.storekit-sandbox.itunes.apple.com/inApps/v1/notifications/test
*注意:
测试退款在沙盒环境无法测试,应为沙盒购买没有记录,正式退款是在对应的购买记录里面发起退款
后面经查找资料:可以使用 StorekitTest 发起退款测试
其他相关参考文件如下:
GitHub - apple/app-store-server-library-java
V1版本苹果通知_app store 服务器通知-CSDN博客
WWDC22 - In App Purchase 更新总结 - 知乎
app-store - App Store 服务器到服务器通知的目的? - IT工具网