更新记录

v1.0.0(2026-04-19)

初始化


平台兼容性

uni-app(4.86)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × 4.4 13
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

passkit-apple-pay

iOS Apple Pay (PassKit) UTS 插件。非应用内购 (IAP)

插件只负责:

  1. 检测设备能力
  2. 弹起 Apple Pay 收银台
  3. 把 Face ID / Touch ID 授权后的加密 PKPaymentToken 交给你
  4. 根据你的后端校验结果,给 Apple Pay 卡片"打钩"或"×"

插件不负责

  • 扣款(必须由你后端对接支付网关完成)
  • 应用内购(虚拟商品请用 StoreKit 插件)
  • Android / Harmony(会返回 NOT_SUPPORTED

整体工作流程

┌──────────┐   ①                    ┌──────────────┐   ②弹卡片     ┌──────────┐
│ 你的 JS  │──requestPayment──→     │  本插件        │──present──→ │ Apple Pay │
│  代码    │                        │ (UTS+Swift)   │               │   卡片    │
└──────────┘                        └──────────────┘                └──────────┘
     ▲                                   │                              │
     │                                   │                              │ Face ID
     │ ⑦resolve/reject                   │ ③事件回调                   │ 通过
     │                                   │   (多次触发)                  │
     │                                   ↓                              │
     │                               ┌─────────────┐  ④didAuthorize     │
     │                               │ onEvent     │←──────────────────┘
     │                               │ 回调事件流   │
     │                               └─────────────┘
     │                                   │
     │                                   │ ⑤把 paymentData 发后端
     │                                   ↓
     │                               ┌─────────────┐
     │                               │ 你的后端    │───→ 支付网关(Stripe/银联/Adyen)
     │                               │             │←─── 扣款成功/失败
     │                               └─────────────┘
     │                                   │
     │                                   │ ⑥respondAuthorization(status)
     │                                   ↓
     │                               ┌──────────┐
     │                               │ Apple    │
     │                               │ Pay 卡片 │ 打 ✓/× 并关闭
     └───────────────────────────────┴──────────┘

关键点onEvent 回调不是一次性的。同一次支付会话中会触发 presentedshippingContactSelect (可选) → authorizedfinished 等多个事件,插件内部已用 @UTSJS.keepAlive 保活。


部署前提 Checklist

在你能真正跑通 Apple Pay 之前,以下每一项都必须完成:

① Apple Developer 后台

  1. Identifiers → App IDs:为你的 App 启用 In-App Payments capability
  2. Identifiers → Merchant IDs:新建一个 Merchant ID(格式如 merchant.com.yourco.app
  3. Provisioning Profiles:重新生成 provisioning profile,勾选上面那个 Merchant ID

② 本插件的 entitlements

编辑 utssdk/app-ios/UTS.entitlements,把 com.apple.developer.in-app-payments 数组里的占位 merchant.com.example.replace-me 改成上一步创建的真实 Merchant ID。

⚠️ 这一步不做,调用 requestPayment 时 Apple Pay 卡片会弹出但 Face ID 后显示"未完成付款"(或直接 iOS 层崩溃)。

③ 支付网关对接(后端工作)

Apple Pay 只产出加密 token,不会直接扣款。后端必须和一个支付网关对接才能把 token 换成真钱:

业务场景 推荐网关
国内 CN + chinaUnionPay 银联 Apple Pay 接入(需银联商户审核)
海外 Stripe / Adyen / Braintree / Checkout.com
多国 Stripe(简单)或 Adyen(覆盖广)

后端大致流程:

接收 JS 传来的 { paymentData, transactionIdentifier, orderNo }
   ↓
调网关 API:Stripe 叫 `payment_method_data[apple_pay]`,
             Adyen 叫 `paymentMethod.applepay.token` 等
   ↓
网关返回扣款结果 → 返回给 JS

④ 设备 & Wallet

  • 真机(模拟器不支持 Apple Pay)
  • Wallet 里已添加一张你 supportedNetworks 列表内的卡

安装

  1. 把本目录整体放入 uni_modules/passkit-apple-pay/
  2. 完成上面 Checklist 的 ① ②
  3. HBuilderX → 运行 → 真机运行 → 制作自定义调试基座

首次运行会自动链接 PassKit.framework


API 总览

import {
  canMakePayments,
  canMakePaymentsWithNetworks,
  openSetup,
  requestPayment,
  respondUpdate,
  respondAuthorization,
  dismiss
} from '@/uni_modules/passkit-apple-pay'

能力检测(同步)

canMakePayments(): boolean

设备 & 系统是否具备 Apple Pay 能力(不看 Wallet 里有没有卡)。

canMakePaymentsWithNetworks(networks, capabilities?): boolean

Wallet 里是否有可用于指定卡组织的卡。

  • networks: string[] —— 如 ["visa","masterCard","chinaUnionPay"]
  • capabilities?: string[] —— 如 ["3DS","EMV","credit","debit"]

openSetup(): void

打开 Wallet 的"添加卡"界面。当 canMakePaymentsWithNetworks 返回 false 时,可引导用户来添加卡。


支付主流程

requestPayment(request, onEvent): void

这是最核心的 API。 一次调用开启一个会话,onEvent 会被触发多次,直到收到 finished 事件会话才结束。

request 参数:

字段 类型 必填 说明
merchantIdentifier string 真实 Merchant ID,和 entitlements 里一致
countryCode string ISO 国家码,"CN"/"US"/...
currencyCode string ISO 货币码,"CNY"/"USD"/...
supportedNetworks string[] 接受的卡组织,如 ["chinaUnionPay"]
merchantCapabilities string[] 支付能力,如 ["3DS","credit","debit"]
summaryItems SummaryItem[] 金额行,最后一项自动显示为"总计"
shippingMethods ShippingMethod[] 配送方式,传了才触发 shippingMethodSelect
shippingType string "shipping" / "delivery" / "storePickup" / "servicePickup"
requiredShippingContactFields string[] 要求用户填的联系人字段,触发 shippingContactSelect
requiredBillingContactFields string[] 账单联系人
applicationData string 塞入 token 的 app 自定义数据,后端可校验(如订单号)

onEvent 回调 —— 同一会话多次触发:

event.kind 触发时机 其他字段 JS 必须响应?
shippingContactSelect 用户切配送地址 requestId, contact ✅ 3s 内调 respondUpdate(requestId, {summaryItems?, shippingMethods?, errors?})
shippingMethodSelect 用户切运送方式 requestId, method ✅ 3s 内调 respondUpdate(requestId, {summaryItems})
authorized Face ID/Touch ID 通过,拿到加密 token requestId, result ✅ 25s 内调 respondAuthorization(requestId, {status})
updateTimeout JS 超时未响应;原生已用当前 summaryItems 自动放行 requestId ❌ 已自动放行
cancelled JS 主动调了 dismiss()
failed 参数非法 / 会话冲突 / 卡片弹出失败 error
finished 卡片已关闭,会话彻底结束

插件故意不实现 paymentMethodSelect 事件 —— 该回调在卡片弹起瞬间同步触发,JS 往返延迟易导致 PassKit 断言崩溃。

authorized 事件的 result

{
  paymentData: string              // PKPaymentToken.paymentData 的 base64 —— 发给后端
  transactionIdentifier: string    // 交易 ID
  paymentMethod: {
    displayName: string            // "UnionPay 1234"
    network: string                // "chinaUnionPay"
    type: number                   // 0 unknown / 1 debit / 2 credit / 3 prepaid / 4 store / 5 eMoney
  }
  shippingContact?: { ... }        // 只有请求时指定了 requiredShippingContactFields 才有
  billingContact?:  { ... }
  shippingMethod?:  { ... }
}

响应 API

respondUpdate(requestId, update): void

响应 shippingContactSelect / shippingMethodSelect

update = {
  summaryItems?:    SummaryItem[],    // 新的金额行
  shippingMethods?: ShippingMethod[], // 新的配送方式(仅 shippingContactSelect)
  errors?: Array<{                    // 字段级错误(红色高亮)
    field:     "postalAddress" | "emailAddress" | "phoneNumber" | "name",
    subField?: string,                // postalAddress 的子字段,如 "postalCode"
    message:   string
  }>
}

respondAuthorization(requestId, result): void

响应 authorized。告诉 Apple Pay 后端扣款的结果。

result = {
  status: "success" | "failure",
  errors?: 同上                      // 失败时可给出字段级错误让用户修改地址等
}

dismiss(): void

主动关闭 Apple Pay 卡片。极少用;正常流程 Apple 自己会关。


响应超时与幂等

  • 可选事件(shipping 两个) 3 秒未响应 → 原生用当前 summaryItems 自动 complete,额外发一个 updateTimeout 事件通知 JS
  • 授权事件 25 秒未响应 → 同上机制
  • 同一 requestId 多次调 respondUpdate/respondAuthorization,只认第一次,后续返回 failed { ALREADY_RESPONDED } 但不影响已放行的会话

最简 Promise 封装(推荐)

import { canMakePayments, requestPayment, respondAuthorization } from '@/uni_modules/passkit-apple-pay'

function payWithApplePay({ orderNo, amount, merchantName = 'Your Brand' }) {
  return new Promise((resolve, reject) => {
    if (!canMakePayments()) {
      return reject({ errCode: 'NOT_SUPPORTED', errMsg: '当前设备不支持 Apple Pay' })
    }
    requestPayment({
      merchantIdentifier: 'merchant.com.yourco.app',
      countryCode: 'CN',
      currencyCode: 'CNY',
      supportedNetworks: ['chinaUnionPay'],
      merchantCapabilities: ['3DS', 'credit', 'debit'],
      summaryItems: [{ label: merchantName, amount: String(amount) }],
      applicationData: JSON.stringify({ orderNo })
    }, (event) => {
      switch (event.kind) {
        case 'authorized':
          // 交给后端调支付网关
          yourBackend.chargeApplePay({
            orderNo,
            paymentData:           event.result.paymentData,
            transactionIdentifier: event.result.transactionIdentifier
          }).then(res => {
            const ok = !!res?.success
            respondAuthorization(event.requestId, { status: ok ? 'success' : 'failure' })
            ok ? resolve(res) : reject({ errCode: 'BACKEND_REJECT', errMsg: res?.message || '支付失败' })
          }).catch(err => {
            respondAuthorization(event.requestId, { status: 'failure' })
            reject(err)
          })
          break

        case 'cancelled':
          reject({ errCode: 'USER_CANCELLED', errMsg: '用户取消支付' })
          break

        case 'failed':
          reject(event.error)
          break

        // shippingContactSelect / shippingMethodSelect / updateTimeout / finished
        // 当前配置(无 shipping 配置)不会触发或无需响应
      }
    })
  })
}

// 使用
payWithApplePay({ orderNo: 'O123', amount: '99.00' })
  .then(res => console.log('支付成功', res))
  .catch(err => console.log('支付失败', err.errCode, err.errMsg))

动态运费示例

带配送地址时,用户切换地址要重算运费:

requestPayment({
  merchantIdentifier: 'merchant.com.yourco.app',
  countryCode: 'US',
  currencyCode: 'USD',
  supportedNetworks: ['visa', 'masterCard'],
  merchantCapabilities: ['3DS'],
  summaryItems: [
    { label: '商品', amount: '9.99' },
    { label: '运费', amount: '2.00' },
    { label: 'Total', amount: '11.99' }
  ],
  shippingMethods: [
    { identifier: 'std', label: '标准', amount: '2.00', detail: '3-5 天' },
    { identifier: 'exp', label: '加急', amount: '8.00', detail: '次日达' }
  ],
  shippingType: 'shipping',
  requiredShippingContactFields: ['postalAddress', 'name', 'phoneNumber']
}, (event) => {
  if (event.kind === 'shippingContactSelect') {
    // 根据 event.contact.postalAddress 调后端算运费
    const newShipping = computeShipping(event.contact.postalAddress)
    respondUpdate(event.requestId, {
      summaryItems: [
        { label: '商品', amount: '9.99' },
        { label: '运费', amount: newShipping },
        { label: 'Total', amount: (9.99 + Number(newShipping)).toFixed(2) }
      ]
    })
  } else if (event.kind === 'shippingMethodSelect') {
    const shipping = Number(event.method.amount)
    respondUpdate(event.requestId, {
      summaryItems: [
        { label: '商品', amount: '9.99' },
        { label: '运费', amount: event.method.amount },
        { label: 'Total', amount: (9.99 + shipping).toFixed(2) }
      ]
    })
  } else if (event.kind === 'authorized') {
    // 同前面
  }
})

错误码

code 含义
NOT_SUPPORTED 非 iOS 平台
CANNOT_MAKE_PAYMENTS 系统不支持
NO_CARD_FOR_NETWORKS Wallet 没符合的卡(可用 openSetup() 引导添加)
INVALID_REQUEST request 字段非法
SESSION_ACTIVE 上一次会话还没结束
PRESENT_FAILED 卡片弹出失败
USER_CANCELLED 用户主动取消
UPDATE_TIMEOUT JS 响应超时,已自动放行
ALREADY_RESPONDED 同一 requestId 重复响应
INTERNAL_ERROR 其他

常见问题

Q: 卡片弹不出来 / 弹出后 Face ID 通过但显示"未完成付款" A: 99% 是 Merchant ID / entitlements / provisioning profile 三者没对齐。检查:

  1. utssdk/app-ios/UTS.entitlements 里的 merchant ID 是不是真实的(不是 merchant.com.example.replace-me 占位)
  2. Apple Developer 后台 App ID 启用了 In-App Payments
  3. provisioning profile 里勾了这个 Merchant ID 且已下发到手机
  4. CN + chinaUnionPay 特别注意:仅 Apple 侧开通不够,还需银联商户那边审核接入

Q: 切换地址金额不刷新 A: 确认你的 respondUpdate(requestId, ...) 里的 requestId 和事件里收到的一致;必须在 3 秒内响应,否则被原生兜底放行。

Q: paymentData 是啥格式?后端怎么处理? A: 是 PKPaymentToken.paymentData 的 base64。有两种用法:

  • 直接扔给网关:Stripe 的 payment_method_data[apple_pay]、Adyen 的 applepay.token 等,网关帮你解密
  • 自行解密(需要 Merchant Payment Processing Certificate 和私钥):按 Apple 官方文档 Payment Token Format Reference 做 ECDH + AES-GCM 解密

Q: onEvent 回调收到一次之后就不响应了? A: 检查插件版本,index.uts 必须有 @UTSJS.keepAlive 装饰器(HBuilderX 4.27+)。否则 UTS 把 JS 回调当一次性。

Q: requestPayment 调用后 App 闪退 A: 常见两个原因:

  1. entitlements 配置错误 → Apple Pay 卡片在调起瞬间触发 iOS 进程杀死
  2. JS 传给 requestPayment 的 request 字段类型错(比如 amount 写成 number 而不是 string)→ UTS-Swift 桥接失败

排查建议先拿设备的 .ips 崩溃日志(设置 → 隐私与安全性 → 分析与改进 → 分析数据)。


不支持

  • 应用内购买(IAP 请用 StoreKit 插件)
  • Android / Harmony(会返回 NOT_SUPPORTED
  • 模拟器(Apple Pay 必须真机)

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。