前言
Hello!又是很长时间没有写博客了,因为最近又开始从事新项目,也是第一次接触关于uniapp开发原生IOS应用的项目,在这里做一些关于我在项目中使用苹果内购支付所实现的方式以及要注意的事项,希望能给正在做uniapp开发ios应用需要使用苹果内购支付的小伙伴一些帮助!
问题
为什么开发ios应用需要使用苹果内购支付?
原因在于,苹果要求所有开发者在上架Appstore中的应用,如果应用中出现了虚拟商品
的购买,必须使用苹果内购支付,并且绝对不能出现其他支付方式,例如微信、支付宝等支付方面的sdk,当然,如果你不怕被苹果下架的风险,你可以尝试使用webview跳转的方式,但是如果你的代码中使用了其他支付方式的sdk或者代码,是很大可能无法通过苹果严格的审核的。
ios内购为什么要专门拿出来说,对比其他支付方式有什么区别吗?
首先,他与微信、支付宝等都属于支付渠道的一种,本质上没有区别,但是由于苹果服务器的原因,导致一些非常特殊的问题,例如:回调时间长甚至没有回调、掉单、回调异常等情况,这种情况对比其他支付方式真的很鸡肋,特别是在uniapp的开发环境下,居然没有超时的回调,简直是大坑,不过这个在后面我会提到解决方案。
ios内购,事务
苹果支付走的是事务列表,每生成一笔订单就会走一笔订单,如果已经完成的订单需要使用苹果提供的关闭订单的api来进行关闭订单,否则会出现回调有误的情况。
实现步骤
Unipay(不常用)
由于我使用的是uniapp开发原生应用,本身uniapp对于支付方式就有专门的封装,如果你没有后端,那你可以尝试使用uniPay,下面是文档的链接
Unipay官方文档
基本用法(常用)
使用uniapp的uni.requestPayment
来实现是比较常用的方式,下面是支付的文档,不过看看就好,还是有挺多坑的,具体的支付流程可以参考一下官方文档,不过逻辑还有代码的正确性需要自己考量,下面我会介绍我的方式
苹果支付
获取iap通道
获取iap通道是判断当前设备是否支持苹果内购支付的必要条件,所以一定要先判断是否含有iap支付通道,如果含有支付通道,才可以走支付逻辑,否则直接return
即可,不需要任何逻辑。
export function Init() {
return new Promise((resolve, reject) => {
//使用uni.getProvider来获取通道
uni.getProvider({
service: 'payment',
success: (res) => {
let iapChannel = res.providers.find((channel) => {
return (channel.id === 'appleiap')
})
//成功之后会返回通道
resolve(iapChannel)
},
})
})
}
返回示例
如果你获取到的iap通道为null,那么你可以直接return,因为当前环境是不支持苹果内购支付的,也就不用走其他逻辑了。
获取已完成但未支付的订单
由于苹果服务器的原因,导致某些情况会出现回调时间长甚至没有回调的情况,因此,这一步必须要做,因为如果不做这一步操作,会导致下一次的支付回调了上一次的事务这种异常情况。
其中,获取订单和关闭订单是一起操作的,所以我把他们整合在了一起。
获取订单
export function restore(iapChannel) {
console.log("获取苹果服务器已支付且未关闭的交易列表")
return new Promise((resolve, reject) => {
iapChannel.restoreCompletedTransactions({
manualFinishTransaction: true,
username: ''
}, (res) => {
resolve(res)
}, (err) => {
reject(err);
})
});
}
关闭订单
export function finishTransaction(transaction, iapChannel) {
console.log("关闭订单")
return new Promise((resolve, reject) => {
iapChannel.finishTransaction(transaction, (res) => {
console.log("关闭订单成功", res)
resolve(res);
}, (err) => {
reject(err);
});
});
}
整合:
export function getReview(iapChannel, token, dev) {
//请求是否有已完成未关闭的订单
restore(iapChannel).then(res => {
//如果有并且状态为已支付则请求关闭并回调给后端
console.log(res)
if (res.length > 0) {
//轮询关闭订单
res.map(item => {
finishTransaction(item, iapChannel)
//如果状态为已完成的状态
if (item.transactionState == '1') {
//后端逻辑,此处省略,通常是完成上报凭证的操作,来完成补单
//请求后端接口上传支付凭证
submitMisson(PayBack, productId, iapChannel).then(res => {
uni.showToast({
icon: 'none',
title: '上一笔订单已支付成功,请稍后留意余额'
})
console.log(res)
})
}
})
}
})
}
注意事项
这里可以选择在合适的时机进行调用,可以选择静默处理,因为在支付的过程中,是不会允许移除事务的,所以如果调用获取订单的回调时间长,也可以不用处理,但一定要做这一步操作。
请求苹果档位列表
这一步一定要做,否则无法拉起内购支付,目的就是判断当前的内购档位信息是否有配置在苹果后台中。
/**
* 调用ID为“appleiap”的PaymentChannel对象的requestOrder方法,像Appstore请求有效的商品详情。
* 注意:IAP支付必须在调用payment.request方法之前,调用requestOrder方法,否则调用payment.request将会报错。
*/
export function requestOrder(iapChannel, productIds) {
uni.showLoading({
title: '初始化中~',
mask: true
})
return new Promise((resolve, reject) => {
iapChannel.requestOrder(productIds, (orderList) => { //必须调用此方法才能进行 iap 支付
console.log('requestOrder success: ' + JSON.stringify(orderList));
resolve(orderList)
uni.hideLoading()
}, (e) => {
console.log('requestOrder failed: ' + JSON.stringify(e));
uni.hideLoading()
uni.showToast({
icon: 'none',
title: '当前环境不支持内购支付'
})
reject(e)
});
})
}
拉起支付
这里建议将manualFinishTransaction设置为true,手动关闭订单,否则自动关闭订单可能出现订单关闭失效的情况。
uni.requestPayment({
provider: 'appleiap',
orderInfo: {
manualFinishTransaction: true, //true为手动关闭订单,false为自动关闭订单
username: res.data.osn, //透传参数
productid: productId, //档位id
},
success: (e) => {
// e 类型为 Transaction, 详见下面的描述
//后端逻辑省略
轮询订单情况
}
})
踩过的坑
回调时间长,导致掉单
如果你的应用有客服反馈的功能,那么可以申请客服反馈查询后端订单情况,进行补单的操作。
如果没有,那么你就只能手动补单,一般来说,补单需要提供订单号和票据信息。
但是由于用户手动关闭应用,导致订单号丢失,票据信息和订单号对应起来,因此我们要做一个手动队列的处理。
解决方案:在用户下单时候,将订单号和档位id关联起来做一个队列
也就是key:档位id,value: 订单号数组
原因是用户可以关闭应用之后,重新点击支付,生成了一笔新的订单号,但是回调是上一笔的票据,因此需要做一个订单号数组。
每次支付的时候获取缓存中的队列数据,如果该档位存在订单号,说明上一笔订单并没有上报成功,因此取队列中的第一个订单号作为上报订单,上报成功之后将这笔订单移除,这样就不会影响用户的正常支付,获取到上一笔订单的回调问题,影响页面逻辑。
例如:支付成功跳转成功落地页,但是回到的信息是上一笔订单这种现象。
主动关闭订单
由于上一步操作虽然正常上报,但是并没有将已完成的订单移除,所以我们还需要做一个队列,用来移除已完成的订单。
上报成功之后,将票据和osn作为队列,放入缓存中,这一步其实是为了判断订单是否已经关闭。
由于苹果服务器的原因,很可能你主动调用关闭订单,没有立即关闭,所以你需要在进入应用的时候重新主动关闭。