前端ApplePay支付-H5全流程实战指南

发布于:2025-07-25 ⋅ 阅读:(18) ⋅ 点赞:(0)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

近期公司开展关于苹果支付的相关业务,与之前不同的是,以前后台直接获取第三方Wallet封装好的接口获取支付地址,H5页面直接跳转使用ApplePay支付就行了,相当于第三方直接和银行达成合作;如今,需要我们和银行合作,自己去拉取ApplePay支付。没错,又是一个学习和摸索的一个阶段,哈哈哈哈,如果有不对的地方,欢迎大家指正!!

一、前期工作

  1. 注册苹果开发者账号
  2. 创建一个AppId
  3. 完成商户验证,配置商户号:银行需与 Apple 交换加密证书,用于验证支付请求的合法性和安全性,同时配置商户号Merchant Identity并绑定支付的域名
  4. 生产环境必须使用HTTPS,沙盒可以使用HTTP,但商户验证仍需HTTPS

PS:银行提供支付处理证书,使用该证书提交到Apple开发者后台用于获取商户身份证书。
具体包括:

  • 商户身份证书(Merchant Identity Certificate):由银行协助商户在 Apple 开发者后台申请,用于前端唤起 Apple Pay 时的身份验证。
  • 支付处理证书(Payment Processing Certificate):银行需支持解密 Apple Pay 生成的支付令牌(Token),通常由银行提供证书或密钥给商户的支付服务商。

二、前端后端支付流程

1.初始校验
作用:验证是否存在ApplePay内核,存在则显示ApplePay支付方式

 内核主要是唤起 Apple Pay 界面、收集用户支付信息(如银行卡、金额)、生成加密的 “支付令牌(Payment Token)” 并传递给后端。

// 检验ApplePay环境 - 针对存在applePay内核时,才显示ApplePay支付方式
async checkApplePaySupport() {
		  if (!window.ApplePaySession) {//浏览器不支持 Apple Pay
			 this.isApplePaySupported = false
			 return
		  }
		  try {
            // 检测当前设备是否支持 Apple Pay 功能
			const canMakePayments = await ApplePaySession.canMakePayments(); 
           // 用于检查设备是否支持 Apple Pay 并且 用户已添加至少一张有效支付卡。
			const canMakePaymentsWithActiveCard = await ApplePaySession.canMakePaymentsWithActiveCard(this.merchantIdentifier); 
			if (canMakePayments && canMakePaymentsWithActiveCard) {
			  this.isApplePaySupported = true
			} else if (!canMakePayments) { // 设备不支持 Apple Pay
				this.isApplePaySupported = false
				console.log('设备不支持 Apple Pay')
			} else { // 未添加支付有效卡
				this.isApplePaySupported = true
				console.log('未添加支付有效卡')
			}
		  } catch (error) {// 检查失败: ${error.message}`
			  this.isApplePaySupported = false
			  console.log(error.message)
		  }
		  // console.log('是否支持Apple',this.isApplePaySupported)
		  // // 测试支持的支付网络 - 低版本不支持-所以干脆去掉了
		  // ApplePaySession.getApplePayCapability({
		  //   merchantIdentifier: this.merchantIdentifier,
		  //   supportedNetworks: ['visa', 'masterCard', 'chinaUnionPay', 'amex', 'jcb']
		  // })
		  // .then(capabilities => console.log('支持的网络:', capabilities.supportedNetworks))
		  // .catch(error => console.error('检测失败:', error));
},
 2. 商户验证流程
  1. 前端创建会话 ApplePaySession,设置会话最长等待时长
  2. 前端触发onvalidatemerchant商户验证事件
  3. 前端将获取到的 validationURL 通过后端提供的商户验证接口传递给后端
  4. 后端使用validationURL与商户证书向 Apple 服务器请求商户信息 merchantSession
  5. 后端返回 merchantSession 给前端,确保 merchantIdentifier 与前端一致(返回的MID可能是hash字符串)
  6. 前端调用 session.completeMerchantValidation 完成商户验证
3. 支付验证流程
  1. 用户授权支付后,触发 onpaymentauthorized 事件
  2. 前端将支付令牌 paymentToken 发送到后端
  3. 后端将支付令牌转发给支付网关(如 Stripe、银联)
  4. 后端返回支付结果给前端
  5. 前端调用 session.completePayment 通知 Apple Pay 支付状态

 用户授权支付动作:指的是输入了支付密码这个动作,当初因为测试机没有卡,一直无法触发支付验证事件,找了半天原因。

// ApplePay支付
applePayClicked() {
	this.session = null
    // 预支付信息
	const paymentRequest = {
		countryCode: 'HK',// 交易的国家
		currencyCode: 'HKD',// 货币
		supportedNetworks: ['visa', 'masterCard', 'chinaUnionPay', 'amex', 'jcb'],// supportedNetworks 列出支持的卡 chinaUnionPay 银联
		merchantCapabilities: ['supports3DS'],// 支持的支付特性
		total: { 
			label: 'XXX電子支付', // 支付的标签和金额
			amount: 100.00,
			type: 'final' 
	   },
	}
	// 1.创建ApplePay支付会话
	this.session = new window.ApplePaySession(3, paymentRequest);
	// 设置会话最长等待时间
	this.session.timeoutInterval = 120; // 单位:秒
	// 2.触发商户验证事件 - 获取validationURL,传递给后台
	this.session.onvalidatemerchant = (event) => {
		this.validateMerchant(event.validationURL)
	};

	// 3.支付授权事件 - 用户授权支付后,触发 onpaymentauthorized 事件
	this.session.onpaymentauthorized = async(event) => {
		console.log('=== 支付授权 ===');
		this.info = event 
		// 3.1 获取paymentToken
		let paymentToken  = event.payment.token.paymentData; 
		this.paymentToken = JSON.stringify(paymentToken) || ''
		// 3.2 前端将支付令牌paymentToken+订单信息发送到后端 
		let initPayResult = await this.goPay();
		if(initPayResult =='fail'){ // this.goPay() 提交订单信息,接口返回错误时,关闭当前会话
			this.session.completePayment(window.ApplePaySession.STATUS_FAILURE);
			return
		}
		// 3.3 轮询订单查询接口
		try{ 
			 this.applyPayRes = searchApi({orderId:this.id});
			 let res = await this.applyPayRes
			 if(res.status===0){ 
			    // 3.4 查询到订单结果 - 需要completePayment 通知 Apple Pay 支付状态
				await this.session.completePayment(window.ApplePaySession.STATUS_SUCCESS); // 必须调用!!!
				// 跳转成功界面
				uni.navigateTo({
					url:'XXXXXURL',
				})
			 }else{
				 await this.session.completePayment(window.ApplePaySession.STATUS_FAILURE);
				 this.openPopup('該訂單查詢結果失敗')
			 }
		}catch(error){
			 console.log('支付報錯',error)
			 this.applyPayRes = null;
			 this.session.completePayment(window.ApplePaySession.STATUS_FAILURE);
		}finally{ // 轮询完成或失败时才重置实例
			this.applePayRes = null
		}
	};
	// 4.错误事件
	this.session.onerror = (error) => {
		console.error('Apple Pay 错误:', error);
		this.openPopup('支付错误: ${error.message}')
	  };
	// 5.取消事件
	this.session.oncancel = () => {
		console.log('用户取消支付');
		this.cancelPolling() // 取消輪詢 
		this.session = null;
	  };
	// 启动支付流程
	this.session.begin();  
},
// 2.1 ApplePay商户验证(调用后端 API)- 实现商户验证逻辑
validateMerchant(validationURL) {
  return new Promise((resolve, reject) => {
	uni.request({
	  url: '/XXXX/validate',
	  method: 'POST',
	  data: { validationURL },
	  success: (res) => {
		if (res.data.success) { // res.data.success =true
			let merchantSession = res.data.data
			if (this.validateMerchantSession(merchantSession)) { // 校验接口返回字段
			    // 2.3 完成商户验证
				this.session.completeMerchantValidation(merchantSession);
				resolve(merchantSession);
			} else {
				reject(new Error('商户会话数据无效'));
			}
		} else {
			reject(new Error(res.data.message));
		}
	  },
	  fail: (err) => {
		console.error('商户验证请求失败:', err);
		reject(err);
	  }
	});
  });
},
 // 2.2 验证商户会话接口返回数据
validateMerchantSession(sessionData) {
  const requiredFields = ['signature', 'merchantIdentifier', 'domainName', 'expiresAt'];
  for (const field of requiredFields) {
	if (!sessionData[field]) {
	  console.error(`缺少必需字段: ${field}`);
	  return false;
	}
  }
  // 验证域名一致性
  if (sessionData.domainName !== window.location.hostname) {
	console.error(`域名不匹配: ${sessionData.domainName} vs ${window.location.hostname}`);
	return false;
  }
  // 验证有效期
  if (sessionData.expiresAt < Date.now()) {
	console.error('商户会话已过期');
	return false;
  }
  return true;
}

三、调试与部署注意事项

1.证书配置 
  • 确保后端持有有效的 Apple Pay 商户证书和私钥
  • 证书需与商户 ID 和域名匹配
2.HTTPS要求
  • 生产环境必须使用 HTTPS
  • 沙盒环境可以使用 HTTP,但商户验证仍需 HTTPS
3.地区与货币
  • 针对香港或内地用户,对应的国家地区countryCode与货币currencyCode是不一样的,香港countryCode:'HK',currencyCode:'HKD';而澳门是'MO' 与'MOP'
  • 确保支付网关支持香港或内地地区的货币和卡组织
  • PS:一定要注意卡卡卡!!!我们做的香港业务,内地支付卡是无效的!所以需要包含对应地区的支付卡,哪怕内地卡是可以输入密码,但仍然无法获取后台所需要的paymentToken参数,甚至就跟陷入无限循环一样,不扣钱但也无法获取参数。
4.商户ID一致性
  • 确保 merchantIdentifier 与证书中的商户 ID 一致

商户Id如果不正确会导致拉取ApplePay会话后,可进行商户验证,但无法触发支付验证并且界面立刻关闭。后端在商户验证的接口中返回merchantIdentifier 是一串hash数字,经过加密的,无需前端处理,直接将获取得到的数据进行商户校验即可。

5.使用completePayment 告知ApplePay支付结果

支付查询到结果后使用completePayment告知ApplePay会话结果;如果不调用会导致会话持续处于 "处理中" 状态。

我就是没调用告知结果,付完钱一直搁那溜溜转了很久,最终界面显示取消支付,立刻关闭。

四、报错记录:

1. payment Services Exception merchantId=XXX not registered for domain=XXX 报错
  1. 商户 ID 未注册:商户 ID(XXX)可能未在支付服务提供商处完成注册或激活。
  2. 域名未配置:支付服务提供商可能未将 https://xxx.com 添加到该商户 ID 的允许域名列表中。
  3. 配置延迟:新注册的商户 ID 或域名配置可能需要时间生效(例如,需等待几分钟至几小时)。
  4. 域名不匹配:检查 URL 是否包含端口号、子域名等额外信息。
  5. 测试环境与生产环境混淆:确认使用的商户 ID 和 API 密钥对应正确的环境(测试 / 生产)。
2. ApplePay报错:must create a new ApplePaySession from a user gesture handler;
  1. 严格的用户手势要求:Apple Pay 会话必须直接在用户点击事件的处理函数中创建,不能通过 Promise、setTimeout、await 或其他异步方式延迟创建。(我就是在点击时先进行了订单查询的动作,再去创建ApplePay会话,存在异步操作,导致ApplePay会话都不创建了!!!)

  2. 避免预创建会话:不要在页面加载时或其他非交互时机创建 ApplePaySession 实例。

  3. 验证用户交互:确保你的按钮或其他交互元素确实被用户点击后才触发会话创建。

3.商户验证后会话立即关闭,提示未完成付款 
   1. 支付请求参数配置错误
  • countryCode 与 currencyCode 不匹配(如使用 HK 国家代码但货币为 CNY
  • supportedNetworks 中无用户已添加的有效卡类型
  • total 金额格式或精度不符合要求
   2. 商户验证流程问题
  • 商户证书与实际域名不匹配
  • merchantSession 中的签名或时间戳无效
  • 后端验证接口响应超时
   3. 设备或用户环境问题
  • 设备未添加有效支付卡
  • 支付卡已过期或被冻结
  • iOS 设置中的 Apple Pay 权限异常


网站公告

今日签到

点亮在社区的每一天
去签到