uni-app 接入IAP支付

MuYan2022-10-11VueVueuni-app
  • 该文档主要讲解接入 IAP 支付,开发类似 游戏代币 的形式通过苹果审核

开发 APP 时为什么接入 IAP 支付

  • 苹果 APP 开发时,如果应用内有支付功能,必须接入 IAP 支付,否则无法通过上架审核。

  • 不接入 IAP 支付的,上架审核的时候必会通知 Guideline 3.1.1 - Business - Payments - In-App Purchase

  • IAP 支付苹果会收取 30%的抽成,如用户充值 10R,商家实际得到的就 7R,其中 3R 被苹果抽成。

开发流程

配置 manifest.json

  1. 在manifest.json - App模块权限选择 中勾选 payment(支付)
  2. 在 manifest.json - App SDK配置 中,勾选 苹果应用内支付(IAP)
  3. 这些配置需要打包生效,真机运行仍然是HBuilder基座的设置,测试需要使用自定义基座调试。

申请开通苹果支付

使用苹果开发者账号登录 App Store Connectopen in new window,在应用的功能选项卡页面,添加 App 内购买项目。

  • 内购项目的各信息需要填写完整,然后保存,此时内购项目的状态应该是准备提交,当提交应用通过审核后,状态则变为已批准。

  • 测试时,需要使用 测试证书 打一个自定义的 iOS 基座进行 沙箱测试,进行调试。[测试证书申请流程](/blogs/web/IOS APP zhengshu.html)

  • 正式测试的时候,需要在 正式 应用 TestFight 的选项卡添加 App Store Connect 测试用户,测试支付时可以使用此用户帐号进行 沙箱测试

  • 添加单个APP 内购购买项目的时候,必填参数 类型参考名称价格时间表App Store 本地化版本审核信息(代币界面截图,描述可不填写)

    • 类型 选择为 消耗型项目
    • 参考名称 建议使用代币名称加充值金额,例如:代币6
    • App 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
}

参考文档:苹果应用内支付open in new window

上次更新 2026/6/23 11:49:15
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8