iOS ApplePay 支付汇总一二

一、前端发起支付的参考例子:

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工具网

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/265140.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

建行账单导出的手工操作

文章目录 建行账单导出的手工操作概述笔记END 建行账单导出的手工操作 概述 自己的电商账单分析程序初步搞定. 支付宝/微信/京东导出的账单都是csv格式. 我开始做的时候, 建行的账单选的是xls(旧版excel)格式. 没注意看. 程序中用的excel库操作, 只能处理.xlsx格式(新版exce…

10--面向对象OOP--05

1、代码块 如果成员变量想要初始化的值不是一个硬编码的常量值&#xff0c;而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值&#xff0c;该怎么办呢&#xff1f;此时&#xff0c;可以考虑代码块&#xff08;或初始化块&#xff09;。 代码块(或…

使用xshell连接虚拟机(服务器)

作者&#xff1a;余小小 Xshell Xshell [1] 是一个强大的安全终端模拟软件&#xff0c;它支持SSH1, SSH2, 以及Microsoft Windows 平台的TELNET 协议。Xshell 通过互联网到远程主机的安全连接以及它创新性的设计和特色帮助用户在复杂的网络环境中享受他们的工作。 Xshell可以…

neuq-acm预备队训练week 8 P8794 [蓝桥杯 2022 国 A] 环境治理

题目描述 输入格式 输出格式 输出一行包含一个整数表示答案。 输入输出样例 解题思路 最短路二分 AC代码 #include<bits/stdc.h> using namespace std; long long temp,n, Q; long long f[105][105],min_f[105][105],cut[105],dis[105][105];//cut为减少多少&#x…

mapstruct个人学习记录

mapstruct核心技术学习 简介入门案例maven依赖 IDEA插件单一对象转换测试结果 mapping属性Spring注入的方式测试 集合的映射set类型的映射测试map类型的映射测试 MapMappingkeyDateFormatvalueDateFormat 枚举映射基础入门 简介 在工作中&#xff0c;我们经常要进行各种对象之…

Sql Server关于表的建立、修改、删除

表的创建&#xff1a; &#xff08;1&#xff09;在“对象资源管理器”面板中展开“数据库”节点&#xff0c;可以看到自己创建的数据库&#xff0c;比如Product。展开Product节点&#xff0c;右击“表”节点&#xff0c;在弹出的快捷菜单中选择“新建表”项&#xff0c;进入“…

JMS(Java Message Service)使用指南

介绍 JMS即Java消息服务&#xff08;Java Message Service&#xff09;应用程序接口&#xff0c;是一个Java平台中关于面向消息中间件&#xff08;MOM&#xff09;的API&#xff0c;用于在两个应用程序之间&#xff0c;或分布式系统中发送消息&#xff0c;进行异步通信。它是一…

SwinIR: Image Restoration Using Swin Transformer

SwinIR 简介 论文地址&#xff1a;SwinIR: Image Restoration Using Swin Transformer 代码&#xff1a;SwinIR ​ 本文提出了一个基于swin transformer的图像超分模型swinIR。其中SwinIR分为三部分&#xff1a;浅层特征提取、深层特征提取和高质量图像重建模块。 现阶段问…

【C++11(三)】智能指针详解--RAII思想循环引用问题

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:C从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习C   &#x1f51d;&#x1f51d; C11 1. 前言2. 为什么要有智能指针?3. RAII思想…

使用Python提取PDF文件中指定页面的内容

在日常工作和学习中&#xff0c;我们经常需要从PDF文件中提取特定页面的内容。在本篇文章中&#xff0c;我们将介绍如何使用Python编程语言和两个强大的库——pymupdf和wxPython&#xff0c;来实现这个任务。 1. 准备工作 首先&#xff0c;确保你已经安装了以下两个Python库&…

保姆级 | XSS Platform环境搭建

0x00 前言 XSS Platform 平台主要是用作验证跨站脚本攻击。该平台可以部署在本地或服务器环境中。我们可以使用 XSS Platfrom 平台搭建、学习或验证各种类型的 XSS 漏洞。 0x01 环境说明 HECS(云耀云服务器)xss platformUbuntu 22.04Nginx 1.24.0MySQL 5.6.51Pure-Ftpd 1.0.49…

LeetCode力扣每日一题(Java):35、搜索插入位置

一、题目 二、解题思路 1、我的思路&#xff08;又称&#xff1a;论API的重要性&#xff09; 读完题目之后&#xff0c;我心想这题目怎么看着这么眼熟&#xff1f;好像我之前学过的一个API呀&#xff01; 于是我回去翻了翻我之前写的博客&#xff1a;小白备战蓝桥杯&#xf…