• 作者:老汪软件技巧
  • 发表时间:2023-12-27 18:00
  • 浏览量:

前言

在写这篇记录的时候,关于ios的支付已经对接的差不多了,下一步就是测试好了直接发版,总共花了好几周的时间,从0到1对于首次做ios支付来说,确实很多坑。

其实业务层面很简单,甚至比安卓支付还简单,因为支付的整体流程那边已经提供好了,甚至可以直接套模板。主要坑在于不了解ios内购这套东西,及其细节处理。

该APP使用的是uv-ui组件库,uv-ui 破釜沉舟之兼容vue3+2、nvue、app、h5、小程序等多端基于uni-app和.x的生态框架,支持单独导入,开箱即用,利剑出击。

准备工作

最重要的就是准备证书、描述文件等环节。现在开发ios没有这两样东西,不能真机运行。还需要区分测试证书和正式证书,本地真机运行只能使用测试证书,正式证书只能打包上传后台到中下载测试或提App Store审核(即时到了这步,内购也只能使用沙箱环境支付,非正式支付。只有App Store审核通过后才能走真实支付,这里建议开发灰度测试功能,后续会详细讲解)。

注册账号及创建APP等准备工作都是产品去做的,所以对此流程可能会有遗漏,所以只记录大概我所了解。

创建你的 Apple ID:创建一个App应用: 创建证书、描述证书等(, & ):Apple后台、申请证书引导 最终需要的东西: 首次运行需要进行基座签名 如果有ios内购项目,添加内购项目: 添加沙盒账号 真机运行 完成上面的所有准备,就可以直接使用运行到苹果手机上面总之,在使用运行到真机的时候,提示缺什么,我们就需要按照上面的方法准备什么 正式开发 业务开发 使用app-nvue技术开发ios,90%的代码与安卓都是通用的,毕竟多平台跨端开发,只是有些兼容性问题,需要单独处理而已,具体问题具体分析。具体开发的内容就不做详细的介绍,接下来把ios内购买项目做简单的记录。 ios内购开发 参考文档:之苹果应用内支付,在开发之前一定要对该文档进行通读和了解,很多开发代码合流程都在这里面。后端需要准备两个接口: 前端开发步骤:

充值页面混入pay.ios.js:

import { Iap, IapTransactionState } from "@/common/js/iap.js"
export default {
	data() {
		return {
			title: "iap",
			loadingIOS: false,
			disabled: true,
			productId: "",
			productList: [],
			isError: false
		}
	},
	methods: {
		async payInitIOS() {
			uni.showLoading({
				mask: true,
				title: '苹果验证中,请稍等'
			});
			this.isError = false;
			// 创建实例
			this._iap = new Iap({
				products: [this.productId] // 苹果开发者中心创建
			})
			try {
				// 初始化,获取iap支付通道
				await this._iap.init();
				// 从苹果服务器获取产品列表
				this.productList = await this._iap.getProduct();
				this.productList[0].checked = true;
				this.productId = this.productList[0].productid;
				// 填充产品列表,启用界面
				this.disabled = false;
			} catch (e) {
				this.isError = true;
				uni.showModal({
					title: "init",
					content: e.message,
					showCancel: false
				});
			} finally {
				if (this._iap._ready && !this.isError) {
					this.restore();
				} else {
					uni.hideLoading();
				}
			}
		},
		async restore() {
			// 检查上次用户已支付且未关闭的订单,可能出现原因:首次绑卡,网络中断等异常
			// 在此处检查用户是否登陆
			// uni.showLoading({
			// 	mask: true,
			// 	title: '苹果验证中,请稍等'
			// });
			try {
				// 从苹果服务器检查未关闭的订单,可选根据 username 过滤,和调用支付时透传的值一致
				const transactions = await this._iap.restoreCompletedTransactions({
					username: ''
				});
				if (!transactions.length) {
					return;
				}
				// 开发者业务逻辑,从服务器获取当前用户未完成的订单列表,和本地的比较
				// 此处省略
				for (let i = 0; i < transactions.length; i++) {
					const transaction = transactions[i];
					switch (transaction.transactionState) {
						case IapTransactionState.purchased:
							this.isError = true;
							// 用户已付款,在此处请求开发者服务器,在服务器端请求苹果服务器验证票据
							uni.showLoading({
								mask: true,
								title: '您有一笔订单正在处理中...'
							})
							const order_sn = transaction.payment.username || uni.getStorageSync('IOSPAYORDERID');
							if(!order_sn) {
								this.isError = false;
								return await this._iap.finishTransaction(transaction);
							}
							let result = await this.validatePaymentResult({
								product_id: transaction.payment.productid,
								order_sn: order_sn,
								receipt: transaction.transactionReceipt, // 不可作为订单唯一标识
								transactionIdentifier: transaction.transactionIdentifier
							}, 0);
							// 验证通过,交易结束,关闭订单
							if (result) {
								await this._iap.finishTransaction(transaction);
							}
							break;
						case IapTransactionState.failed:
							this.isError = false;
							// 关闭未支付的订单
							await this._iap.finishTransaction(transaction);
							break;
						default:
							break;
					}
				}
			} catch (e) {
				// 为了兼容高版本机型在取消订单时候出现的错误,重启后不存在
				if(e.code == -100 && e.errMsg.indexOf("本地没有响应要移除的事务")>-1){
					this.isError = false;
					return;
				}
				this.isError = true;
				uni.showModal({
					title: `restore${e.errCode}`,
					content: e.message,
					showCancel: false
				});
			} finally {
				if (!this.isError) {
					this.paymentIOS();
				} else {
					uni.hideLoading();
				}
			}
		},
		async paymentIOS() {
			if (this.loadingIOS == true) {
				return;
			}
			this.loadingIOS = true;
			uni.showLoading({
				mask: true,
				title: '支付处理中...'
			});
			try {
				// 从开发者服务器创建订单
				const orderId = await this.createOrder({
					productId: this.productId
				});
				// orderId存在本地,防止丢失
				uni.setStorageSync('IOSPAYORDERID', orderId);
				// 请求苹果支付
				const transaction = await this._iap.requestPayment({
					productid: this.productId,
					username: orderId,
					manualFinishTransaction: true,
					quantity: 1
				});
				// 在此处请求开发者服务器,在服务器端请求苹果服务器验证票据
				await this.validatePaymentResult({
					product_id: this.productId,
					order_sn: transaction.payment.username || orderId,
					receipt: transaction.transactionReceipt, // 不可作为订单唯一标识
					transactionIdentifier: transaction.transactionIdentifier
				});
				// 验证成功后关闭订单
				await this._iap.finishTransaction(transaction);
				// 支付成功
				this.paySccuess();
			} catch (e) {
				uni.$uv.toast('支付取消或失败');
			} finally {
				this.loadingIOS = false;
				uni.hideLoading();
			}
		},
		createOrder({ productId }) {
			return new Promise((resolve, reject) => {
				this.getOrderInfo({ product_id: productId }).then(res => {
					resolve(res.order_no);
				})
			})
		},
		/**
		 * 充值,e.code = 201 或 then返回均代表 处理成功
		 * @param {Object} data 订单数据
		 */
		validatePaymentResult(data, type = 1) {
			return new Promise((resolve, reject) => {
				const fn = (loading = 1) => {
					this.validatePayment(data, loading).then(res => {
						// 处理成功
						uni.hideLoading();
						if (type == 0) {
							this.successTip();
						}
						resolve(true);
					}).catch(e => {
						if (e.code == 201) { //处理成功-订单已更新
							this.successTip();
							uni.hideLoading();
							resolve(true);
						} else {
							setTimeout(() => {
								fn(0);
							}, 3000)
						}
					})
				}
				fn(type == 0 ? 0 : 1);
			});
		},
		applePriceChange(e) {
			this.productId = e.detail.value;
		},
		successTip() {
			uni.showModal({
				title: '温馨提示',
				content: '您的待处理订单已经处理成功,充值金额已到您的账户余额中,请注查收!',
				showCancel: false,
				confirmText: '我知道了'
			});
		}
	}
}

测试支付 其实ios内购在整个业务逻辑并不复杂,不需要像其他支付进行轮询监听等逻辑,ios内部已经做好了这些事情。开始测试就需要对ios这个后台有所了解,我也是第一次接触,所以更多的时间是摸索后台怎么设置,我就讲讲我到底经历了哪些问题: 开发过程中遇到的坑

首次开发ios及其内购买项目,遇到坑是正常的,感谢这次机会,至少让我得到了成长,接下来就讲讲整个ios开发遇到了哪些坑:

坑一:本地没有响应要移除的事务

如果输入沙箱账号和密码支付后未完成后续验证,杀掉APP进程,重启APP进行补单。这时候肯定会检测到未支付的订单,就需要手动关闭订单this._iap.。但是某些苹果机型一直反馈错误信息:.:本地没有响应要移除的事务,

原因分析:在6s机型没有这问题,在7等机型会有这个问题,导致支付流程不能往下执行

解决方案:捕捉到此错误,然后就当正确的逻辑处理,在上述完整示例代码中也有体现

catch (e) {
	// 为了兼容高版本机型在取消订单时候出现的错误,重启后不存在
	if(e.code == -100 && e.errMsg.indexOf("本地没有响应要移除的事务")>-1){
		this.isError = false;
		return;
	}

坑二:不能真实支付

本地只能沙箱账号进行支付测试,怎么办?

解决方案:根据uni官方的回复,灰度测试,设置几个固定账号进行上线后测试,其他账号暂不支持支付。官方回答:#!

坑三:丢单+补单

输入密码支付过程中,杀掉进程,会造成丢单情况

原因分析:由于网络或者用户主动关闭APP等情况,支付流程断掉,如果根据进行订单关联,可能有些机型在补单的时候丢失该值,最终导致丢单,这在ios是正常情况

解决方案:

根据业务需求,配合本地缓存将订单记录,在补单的时候好做对应可以不使用订单号,据说ios没得订单号的概念,直接后端进行验证,这种方案我们没试过这里有个keep客户端开发也遇到丢单的情况,经过多次测试修改,最终的流程和我们的处理方案一致,这个很有参考意义:根治顽疾:Keep客户端 In-App 掉单踩坑指南 坑四:打包上传

打包上传到 Store,每次上传都得高于上一次,可以不变

上传到 Store的工具推荐(必须mac):通过 App 上传 App 的二进制文件

坑五:打开APP苹果手机发烧严重

同一套代码,在安卓机没问题。但是在ios发现发烧很严重,打开APP就开始发烧。

原因分析:1. 开始以为是本地基座的问题,其实仔细想想不会是这个问题,不会这么拉胯;2. 经过代码排查,发现是因为image标签使用了@load,我们APP中恰好有很多图片展示,这应该是ios这边的机制比较耗CPU,导致发热严重。

解决方案:去掉image上的@load,取消图片加载效果,只做图片失败效果

坑六:打包提示:打包时未添加OAuth模块

原因分析:代码中使用了uni.相关,但是ios并未涉及相关模块,所以在ios端屏蔽掉就OK了。

解决方案:参考文档:

坑七:审核多次驳回 后续补充