uni-app 接入IAP支付
- 该文档主要讲解接入 IAP 支付,开发类似
游戏代币的形式通过苹果审核
开发 APP 时为什么接入 IAP 支付
苹果 APP 开发时,如果应用内有支付功能,必须接入 IAP 支付,否则无法通过上架审核。
不接入 IAP 支付的,上架审核的时候必会通知
Guideline 3.1.1 - Business - Payments - In-App PurchaseIAP 支付苹果会收取 30%的抽成,如用户充值 10R,商家实际得到的就 7R,其中 3R 被苹果抽成。
开发流程
配置 manifest.json
- 在manifest.json - App模块权限选择 中勾选 payment(支付)
- 在 manifest.json - App SDK配置 中,勾选
苹果应用内支付(IAP) - 这些配置需要打包生效,真机运行仍然是HBuilder基座的设置,测试需要使用自定义基座调试。
申请开通苹果支付
使用苹果开发者账号登录 App Store Connect,在应用的功能选项卡页面,添加 App 内购买项目。
内购项目的各信息需要填写完整,然后保存,此时内购项目的状态应该是准备提交,当提交应用通过审核后,状态则变为已批准。
测试时,需要使用
测试证书打一个自定义的 iOS 基座进行沙箱测试,进行调试。[测试证书申请流程](/blogs/web/IOS APP zhengshu.html)正式测试的时候,需要在
正式应用TestFight的选项卡添加App Store Connect测试用户,测试支付时可以使用此用户帐号进行沙箱测试。添加单个APP 内购购买项目的时候,必填参数
类型,参考名称,价格时间表,App Store 本地化版本,审核信息(代币界面截图,描述可不填写)类型选择为消耗型项目参考名称建议使用代币名称加充值金额,例如:代币6App Store 本地化版本填写的显示名称跟描述,是用户购买的时候看到的信息- 必须勾选
准许销售
填写
产品ID的时候建议根据代币与金额组合,例如:tokens_6在提交APP 进行审核的时候,需要勾选导入需要用到的
App 内购买项目项目信息
IAP示例代码
- 安装依赖
npm i BigNumber lodash -S
- 业务相关代码
<template>
<view class="content">
<view class="uni-list">
<radio-group @change="(e) => { productId = event.detail.value }">
<label
class="uni-list-cell"
v-for="(item, index) in productList"
:key="index"
>
<radio :value="item.productid" :checked="item.productid === productId" />
<view class="price">
<view>
{{ getTokens(item.price) }} 代币名称
</view>
<view>
{{ item.price }} 元
</view>
</view>
</label>
</radio-group>
</view>
<view class="uni-padding-wrap">
<button
class="btn-pay"
@click="payment"
:loading="loading"
:disabled="getDisabled"
>
确认支付
</button>
</view>
</view>
</template>
<script>
import {
Iap,
IapTransactionState
} from "@/common/iap.js"
import BigNumber from 'bignumber.js';
import {
cloneDeep,
isEqual,
sortBy
} from 'lodash';
const tokensIds = [
1,
6,
20,
45,
79,
168,
328,
648
]
const tokensPrefix = 'tokens_' // 苹果开发者平台,自定义的代币前缀(产品 ID)
export default {
data() {
return {
tokensIdsList: [],
productList: [],
loading: false,
disabled: true,
productId: "",
};
},
computed: {
getTokens() {
return (tokens) => {
return new BigNumber(tokens).multipliedBy(0.7)
}
},
getDisabled() {
return this.disabled || !this.productId
},
getUserName() {
return '当前登录用户的唯一标识'
},
},
onLoad() {
let list = cloneDeep(tokensIds)
this.productList = list.map(item => {
return {
productid: `${tokensPrefix}${item}`,
price: item
}
})
let products = list.map(item => `${tokensPrefix}${item}`)
this.tokensIdsList = products
// #ifdef APP-PLUS
this._iap = new Iap({
products
})
this.$nextTick(() => {
this.init();
})
// #endif
},
onShow() {
// #ifdef APP-PLUS
if (this._iap._ready && !this.disabled) {
this.restore();
}
// #endif
},
methods: {
async init() {
uni.showLoading({
title: '检测支付环境...'
});
try {
// 初始化,获取iap支付通道
await this._iap.init();
// 从苹果服务器获取产品列表
let list = await this._iap.getProduct();
// 校验本地产品列表是否与苹果服务器一致
if(isEqual(cloneDeep(tokensIds).sort(), list.map(item => item.price).sort())) {
// 升序排序后赋值
this.productList = sortBy(list, (item) => item.price)
}
// 填充产品列表,启用界面
this.disabled = false;
} catch (e) {
console.error(e)
} finally {
uni.hideLoading();
}
if (this._iap._ready) {
this.restore();
}
},
async restore() {
// 检查上次用户已支付且未关闭的订单,可能出现原因:首次绑卡,网络中断等异常
uni.showLoading({
title: '正在检测已支付订单...'
});
try {
// 从苹果服务器检查未关闭的订单,可选根据 username 过滤,和调用支付时透传的值一致
const transactions = await this._iap.restoreCompletedTransactions(this.getUserName)
if (!transactions.length) {
return;
}
// 筛选出未支付的订单
transactions.filter(item => item.transactionState === IapTransactionState.failed).forEach(item => {
// 关闭未支付的订单
this._iap.finishTransaction(item)
})
// 处理已支付的订单,此时用户已付款,在服务器端请求苹果服务器验证票据
let transaction = transactions.find(item => item.transactionState === IapTransactionState.purchased)
if(!transaction?.transactionReceipt){
return;
}
// 请求后端API,根据API返回的订单数组关闭订单,数组内容为:['transactionIdentifier 交易唯一标识','transactionIdentifier 交易唯一标识']
let res = await ['请求后端API']({
'receipt-data': transaction.transactionReceipt
})
// 循环关闭已支付订单
res.forEach(item => {
this._iap.finishTransaction({
transactionIdentifier: item
})
})
} catch (e) {
console.error(e)
} finally {
uni.hideLoading();
}
},
async payment() {
if (this.loading == true) {
return;
}
this.loading = true;
uni.showLoading({
title: '支付处理中...'
});
try {
// 请求苹果支付
const transaction = await this._iap.requestPayment({
productid: this.productId,
username: this.getUserName, //根据业务需求透传参数,关联用户和订单关系
manualFinishTransaction: true,
});
// 支付成功后关闭订单
if (this._iap._ready) {
this.restore();
}
} catch (e) {
console.error(e)
} finally {
this.loading = false;
uni.hideLoading();
}
}
}
};
</script>
<style>
.content {
padding: 15px;
}
button {
background-color: #007aff;
color: #ffffff;
}
.uni-list-cell {
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.price {
margin-left: 10px;
}
.btn-pay {
margin-top: 30px;
}
</style>
// uni iap
const ProviderType = {
IAP: 'iap'
}
const IapTransactionState = {
purchasing: "0", // 应用商店正在处理的交易 A transaction that is being processed by the App Store.
purchased: "1", // 成功的交易 A successfully processed transaction.
failed: "2", // 失败的交易 A failed transaction.
restored: "3", // 已经购买过该商品 A transaction that restores content previously purchased by the user.
deferred: "4" // 在队列中,但其最终状态为等待外部操作(如“请求购买”)的交易 A transaction that is in the queue, but its final status is pending external action such as Ask to Buy.
};
class Iap {
_channel = null;
_channelError = null;
_productIds = [];
_ready = false;
constructor({
products
}) {
this._productIds = products;
}
init() {
return new Promise((resolve, reject) => {
this.getChannels((channel) => {
this._ready = true;
resolve(channel);
}, (err) => {
reject(err);
})
})
}
getProduct(productIds) {
return new Promise((resolve, reject) => {
this._channel.requestProduct(productIds || this._productIds, (res) => {
resolve(res);
}, (err) => {
reject(err);
})
});
}
requestPayment(orderInfo) {
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'appleiap',
orderInfo: orderInfo,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
}
restoreCompletedTransactions(username) {
return new Promise((resolve, reject) => {
this._channel.restoreCompletedTransactions({
manualFinishTransaction: true,
username
}, (res) => {
resolve(res);
}, (err) => {
reject(err);
})
});
}
finishTransaction(transaction) {
return new Promise((resolve, reject) => {
this._channel.finishTransaction(transaction, (res) => {
resolve(res);
}, (err) => {
reject(err);
});
});
}
getChannels(success, fail) {
if (this._channel !== null) {
success(this._channel)
return
}
if (this._channelError !== null) {
fail(this._channelError)
return
}
uni.getProvider({
service: 'payment',
success: (res) => {
this._channel = res.providers.find((channel) => {
return (channel.id === 'appleiap')
})
if (this._channel) {
success(this._channel)
} else {
this._channelError = {
errMsg: 'paymentContext:fail iap service not found'
}
fail(this._channelError)
}
}
});
}
get channel() {
return this._channel;
}
}
export {
Iap,
IapTransactionState
}
参考文档:苹果应用内支付
Powered by Waline v2.15.8