接入准备:
1、苹果后台开发者https://developer.apple.com/account/resources/certificates/list 需要把打包的配置文件勾上苹果支付。测试时,需要配置development版的mobileprovision和p12文件,使用自定义基座进行真机联调。
2、https://appstoreconnect.apple.com/apps/6743473361/distribution/iaps
添加内购项目。app没上架apple store前使用第3步的沙盒账号登录手机app store进行测。
注意:测试阶段,一个内购项目针对一个沙盒账号只能支付 一次。
创建项目类型,名称和产品ID–ID自定义
注意产品ID不能重复,即使产品下架了也不能使用已有的id
选择销售范围,根据app内容情况,自己选择。
价格表
本地化版本
审核信息:上传自己的商品信息。用iPhone手机截图。
除了可以忽略的都需要填。填完后选择存储。当前内购商品会变为准备提交。这个就可以测了。
注意productId就是我们自定义的 ‘AK10024’
出现元数据丢失就是因为信息填写的不完整。
app 提交审核后,内购项目会变成
在这里插入图片描述
3、在https://appstoreconnect.apple.com/access/users用户访问>沙盒测试员添加测试账号。在手机设置>App Store上登录沙盒账号。
测试邀请发出后,可能需要在苹果手机上登录testflight接受邀请。先在apple store上登录沙盒账号,然后再接受邀请。
一定要在手机上apple store上登录沙盒账号,否则调用 iap.requestProduct 或者 iap.requestOrder可能会报 “code”:-100,“message”:"Payment_appleiap:返回订单信息失败
在这里插入图片描述
(沙盒环境支付没有真的扣费,唤起支付和回调会有点慢,耐心调试)
4.开发流程
开发流程:
准备:测试阶段app不用提交审核。
1)配置好内购商品(待提交状态),
注意:测试阶段,一个内购项目针对一个沙盒账号只能支付 一次。
2)在iphone手机上用沙盒账号登录了apple store,接受了测试邀请
3)自定义基座的证书勾选了apple pay payment,同时app勾选了 Payment–Apple应用内支付
1、打开项目的manifest.json文件,在“App模块配置”项的“Payment(支付)”下勾选“Apple应用内支付”:
(真机运行时请使用自定义调试基座)
测试时,需要配置development版的mobileprovision和p12文件,使用自定义基座进行真机联调。
2、支付初始化,可以在进入页面的时候或者弹窗的时候处理,需要初始化一次才能发起支付
2.1、获取应用内支付对象
getIapChannels() {
console.log('------getIapChannels' );
let _this = this;
plus.payment.getChannels(function(channels){
for (let i in channels) {
let channel = channels[i];
// 获取 id 为 'appleiap' 的 channel
if (channel.id == 'appleiap') {
_this.iap = channel;
_this.requestIapOrder();
}
}
}, function(e){
showToasts("获取iap支付通道失败:" + e.message);
});
},
2.2、获取到支付对象后,初始化支付项目列表(官方文档说是获取订单信息,有歧义,其实就是获取申请的内购商品列表,初始化支付)
requestIapOrder() {
console.log('------requestIapOrder' );
// #ifdef APP-PLUS
let ids = [this.productid];
this.iap.requestOrder(ids, function(res) {
// console.log(res);
this.iapOrder = true;
}, function(e) {
this.vm.jumpPay = false;
showToasts("获取订单信息失败:" + e.code);
});
// #endif
},
3、发起支付,请求后端获取支付标识,username写入发起支付,用于回调的时候业务处理
goApplePay() {
console.log('------goApplePay' );
// 获取订单
let _this = this;
payApply().then(res => {
// 发起支付
plus.payment.request(this.iap, {
productid: this.productid,
username: res.data.out_trade_no,
}, function(result){
console.log(result);
_this.appleNotify(result);
}, function(e){
// console.log(e);
this.vm.jumpPay = false;
showToasts("支付失败");
});
}).catch(err => {
showToasts(err.message || '获取数据失败');
})
},
4、回调处理,这里是用的前端回调返回给服务端处理
appleNotify(notifyData) {
console.log('------appleNotify' );
// 关闭支付弹窗
this.popPayClose();
// 支付成功
appleNotify({notify: notifyData}).then(res => {
console.log(res);
if(res.data == 1) {
this.$emit('apply-pay-success');
}
}).catch(err => {
showToasts("支付失败");
});
},
5、服务端回调处理,在手机端支付完成后,会得到一个transactionReceipt,来进行二次验证是否支付成功,入参只有一个,就是receipt-data,POST方式请求
正式验证API:
https://buy.itunes.apple.com/verifyReceipt
沙盒验证API:
https://sandbox.itunes.apple.com/verifyReceipt
复制代码
public function appleNotify($notify)
{
// 校验签名
$verifyUrl = config('pay.apply_pay.verify_url');
$header = HuannaoSpider::buildHeader();
$res = HttpQuery::postRequest($verifyUrl, json_encode(['receipt-data' => $notify['transactionReceipt']]), $header);
$res = json_decode($res, true);
if (empty($res) || $res['status'] != 0) {
return 0;
}
return 1;
}
也可以参考官方代码
https://uniapp.dcloud.net.cn/api/plugins/payment.html#iap
test.vue 和 iap.js文件
test.vue
<template>
<view class="content">
<view class="uni-list">
<radio-group @change="applePriceChange">
<label class="uni-list-cell" v-for="(item, index) in productList" :key="index">
<radio :value="item.productid" :checked="item.checked" />
<view class="price">{{item.title}} {{item.price}}</view>
</label>
</radio-group>
</view>
<view class="uni-padding-wrap">
<button class="btn-pay" @click="payment" :loading="loading" :disabled="disabled">确认支付</button>
</view>
</view>
</template>
<script>
import {
Iap,
IapTransactionState
} from "./iap.js"
export default {
data() {
return {
title: "iap",
loading: false,
disabled: true,
productId: "",
productList: []
}
},
onLoad: function() {
// 创建示例
this._iap = new Iap({
products: [] // 苹果开发者中心创建的内购项目 如上面添加的productId:AK10024;['AK10024']
})
this.init();
},
onShow() {
if (this._iap.ready) {
this.restore();
}
},
onUnload() {},
methods: {
async init() {
uni.showLoading({
title: '检测支付环境...'
});
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) {
uni.showModal({
title: "init",
content: e.message,
showCancel: false
});
} finally {
uni.hideLoading();
}
if (this._iap.ready) {
this.restore();
}
},
async restore() {
// 检查上次用户已支付且未关闭的订单,可能出现原因:首次绑卡,网络中断等异常
// 在此处检查用户是否登陆
uni.showLoading({
title: '正在检测已支付且未关闭的订单...'
});
try {
// 从苹果服务器检查未关闭的订单,可选根据 username 过滤,和调用支付时透传的值一致
const transactions = await this._iap.restoreCompletedTransactions({
username: ""
});
if (!transactions.length) {
return;
}
// 开发者业务逻辑,从服务器获取当前用户未完成的订单列表,和本地的比较
// 此处省略
switch (transaction.transactionState) {
case IapTransactionState.purchased:
// 用户已付款,在此处请求开发者服务器,在服务器端请求苹果服务器验证票据
//let result = await this.validatePaymentResult();
// 验证通过,交易结束,关闭订单
// if (result) {
// await this._iap.finishTransaction(transaction);
// }
break;
case IapTransactionState.failed:
// 关闭未支付的订单
await this._iap.finishTransaction(transaction);
break;
default:
break;
}
} catch (e) {
uni.showModal({
content: e.message,
showCancel: false
});
} finally {
uni.hideLoading();
}
},
async payment() {
if (this.loading == true) {
return;
}
this.loading = true;
uni.showLoading({
title: '支付处理中...'
});
try {
// 从开发者服务器创建订单
// const orderId = await this.createOrder({
// productId: this.productId
// });
// 请求苹果支付
const transaction = await this._iap.requestPayment({
productid: this.productId,
manualFinishTransaction: true,
// username: username + orderId //根据业务需求透传参数,关联用户和订单关系
});
// 在此处请求开发者服务器,在服务器端请求苹果服务器验证票据
// await this.validatePaymentResult({
// orderId: orderId,
// username: username,
// transactionReceipt: transaction.transactionReceipt, // 不可作为订单唯一标识
// transactionIdentifier: transaction.transactionIdentifier
// });
// 验证成功后关闭订单
//await this._iap.finishTransaction(transaction);
// 支付成功
} catch (e) {
uni.showModal({
content: e.message,
showCancel: false
});
} finally {
this.loading = false;
uni.hideLoading();
}
},
createOrder({
productId
}) {
return new Promise((resolve, reject) => {})
},
validatePaymentResult(data) {
return new Promise((resolve, reject) => {});
},
applePriceChange(e) {
this.productId = e.detail.value;
}
}
}
</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 iap.js文件
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
}
5.app上架apple store 可能遇到的问题。
苹果测试员一直用自己的账号测试,但我们的内购项目还没上架成功,他的账号又不在沙盒里,然后一直说我们的支付不能用。想让他用我们的测试号,他也不同意。
网上的说法是,不要直接上架app里的支付,先上架一个版本把内购项目通过,再上架app里的支付功能。