Uniapp 苹果应用内支付

接入准备:

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里的支付功能。

//微信充值 //支付接口测试 function balance(url, data) { uni.request({ url: cfg.originUrl + '/wx/mp/js_sig.do', data: { route: url }, method: 'GET', success: (res) => { jweixin.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来 appId: res.data.appId, // 必填,公众号的唯一标识 timestamp: res.data.timestamp, // 必填,生成签名的时间戳 nonceStr: res.data.nonceStr, // 必填,生成签名的随机串 signature: res.data.signature, // 必填,签名 jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表 }); jweixin.ready(function() { uni.request({ url: cfg.originUrl + '/wx/recharge/pay.do', method: 'POST', header: { 'Content-type': "application/x-www-form-urlencoded", }, data: JSON.stringify(data), success: function(res) { alert("下单成功"); alert(JSON.stringify(res)); alert(res.data.order_id); all.globalData.orderId = res.data.order_id; uni.setStorageSync('orderId', res.data.order_id); jweixin.chooseWXPay({ timestamp: res.data.payParams.timeStamp, // 支付签名时间戳 nonceStr: res.data.payParams.nonceStr, // 支付签名随机串 package: res.data.payParams.package, // 接口返回的prepay_id参数 signType: res.data.payParams.signType, // 签名方式 paySign: res.data.payParams.paySign, // 支付签名 success: function(e) { alert("支付成功"); alert(JSON.stringify(e)); // 支付成功后的回调函数 } }); } }) }); jweixin.error(function(res) { // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 console.log("验证失败!") }); } }) }
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值