Apple Pay 集成
Apple Pay on the web 让用户可以使用 Wallet 中的卡,通过 Face ID 或 Touch ID 完成支付。通过 Onerway 集成 Apple Pay,你可以在前端完成 Apple Pay UI 与 token 获取,同时将支付处理(以及可选的商户验证)交给 Onerway。
快速开始(TL;DR)
1. 检查设备支持
前端检查设备是否支持 Apple Pay:
window.ApplePaySession+ApplePaySession.canMakePayments()- 支持则显示 Apple Pay 按钮,否则隐藏
2. 获取配置
调用 POST /v1/txn/consultPaymentMethod,筛选 paymentMethod="ApplePay",获取 countryCode 和 subCardTypes。
3. 创建支付会话
用户点击按钮时,创建支付会话:
new ApplePaySession(version, paymentRequest)→session.begin()
4. 商户验证
onvalidatemerchant 获取 event.validationURL 传给后端,后端二选一:
- Onerway 代理接口(推荐)
- 商户自有 Merchant Identity 证书
前端调用 completeMerchantValidation(merchantSession) 完成验证。
5. 支付授权
onpaymentauthorized 获取 event.payment.token 传给后端,后端二选一:
- Onerway 解密模式(推荐):
POST /v1/txn/doTransaction,传tokenInfo.provider="ApplePay",tokenId=<token> - 商户解密模式:自行解密 token,传解密后的
cardInfo(cryptogram、eci、wallet.*)
前端必须调用 completePayment(STATUS_SUCCESS|STATUS_FAILURE)
6. 最终结果
以 webhook 为准(验签 + 幂等)。
集成概述
通过 Onerway 集成 Apple Pay 包含三个主要阶段:
- 配置获取 → 查询 Onerway API 获取 Apple Pay 配置
- Apple Pay 会话 → 创建
ApplePaySession并显示支付表单 - 支付处理 → 将 Apple Pay token 提交到 Onerway 并处理结果(以及 webhook)
前置要求
在实施 Apple Pay 集成之前,请确保您具备:
- HTTPS 网站:Apple Pay on the web 要求 HTTPS
- 支持的浏览器:Safari(仅在 Apple Pay on the web 支持的平台上可用)
- Onerway 商户账户:已开通 Apple Pay
- 后端集成:服务端能够调用 Onerway API,并提供商户验证相关接口
另见:
集成流程
前端页面准备
在进入详细的集成步骤前,先完成前端页面的基础准备工作。
1. 添加 Apple Pay 按钮
按照 Apple 的品牌规范添加按钮:
<!-- Apple Pay 按钮(使用官方样式) -->
<apple-pay-button
buttonstyle="black"
type="buy"
locale="zh-CN"
id="applePayButton"
style="display: none; --apple-pay-button-width: 200px; --apple-pay-button-height: 40px;">
</apple-pay-button>2
3
4
5
6
7
8
官方演示与按钮自定义
- 交互式演示:Apple 提供了官方演示网站,您可以体验完整的 Apple Pay 支付流程。
- 按钮样式:如需完整的按钮自定义选项和属性,请参阅 Apple Pay 按钮文档。
2. 检查设备支持并显示按钮
// 检查设备支持 Apple Pay
if (window.ApplePaySession && ApplePaySession.canMakePayments()) {
document.getElementById('applePayButton').style.display = 'block'
}2
3
4
3. 创建支付会话
点击按钮时创建 ApplePaySession:
button.addEventListener('click', () => {
// 构造支付请求
const paymentRequest = {
countryCode: config.countryCode, // 来自 Onerway 配置
currencyCode: 'USD',
supportedNetworks: config.supportedNetworks, // 来自 Onerway 配置
merchantCapabilities: ['supports3DS'],
total: { label: 'Your Store', amount: '10.00' }
}
// 创建会话(版本 3+)
const session = new ApplePaySession(3, paymentRequest)
// 处理事件(详细实现见下文)
session.onvalidatemerchant = async (event) => { /* ... */ }
session.onpaymentauthorized = async (event) => { /* ... */ }
session.oncancel = () => { /* 用户取消 */ }
// 启动 Apple Pay 表单
session.begin()
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
完整可运行示例见:前端完整示例(HTML)。
1. 获取 Onerway 配置
在初始化 Apple Pay 之前,调用支付方式查询接口获取 countryCode 和 subCardTypes。
后端实现
调用支付方式查询接口,筛选 Apple Pay 配置并返回给前端:
// 商户后端 → Onerway: POST /v1/txn/consultPaymentMethod
config = consultPaymentMethod({ /* ... */ })
applePay = config.find(paymentMethod == "ApplePay")
// 返回前端
return {
countryCode: applePay.countryCode, // 国家代码
supportedNetworks: applePay.subCardTypes // 支持的卡网络
}2
3
4
5
6
7
8
9
前端使用
// 从你的后端获取配置
const config = await fetch('/api/apple-pay/config').then(r => r.json())
// 用于创建 ApplePaySession
const paymentRequest = {
countryCode: config.countryCode, // 来自 Onerway 支付方式查询
supportedNetworks: config.supportedNetworks, // 来自 Onerway 支付方式查询
currencyCode: 'USD',
merchantCapabilities: ['supports3DS'],
total: { label: 'Your Store', amount: '10.00' }
}2
3
4
5
6
7
8
9
10
11
2. 商户验证
每个 Apple Pay 会话都需要商户验证:
- 仅服务器调用:你的服务端必须向 Apple 请求 payment session,不要在客户端请求。
- 白名单:只访问 Apple 提供的 validation URL。
- 有效期:payment session 5 分钟过期,必须立即处理。
前端实现
在 ApplePaySession 的 onvalidatemerchant 事件中,获取 validationURL 并发送给后端:
session.onvalidatemerchant = async (event) => {
await handleMerchantValidation(session, event)
}2
3
async function handleMerchantValidation(session, event) {
// 1. 安全校验 validationURL
if (!event.validationURL.startsWith('https://apple-pay-gateway')) {
session.abort()
return
}
// 2. 发送到后端进行商户验证
const response = await fetch('/api/validate-merchant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
validationURL: event.validationURL,
website: window.location.hostname
})
})
// 3. 完成验证
const merchantSession = await response.json()
session.completeMerchantValidation(merchantSession)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
后端实现
// 商户后端 → Onerway: POST /txn/apiCheckApplePay
merchantSession = validateMerchant({
verifyUrl: event.validationURL, // Onerway 代理模式对应 API 参数 `verifyUrl`,商户自有账户模式对应 API 参数 `validationURL`
website: window.location.hostname, // Onerway 代理模式对应 API 参数 `website`,商户自有账户模式对应 API 参数 `initiativeContext`
/* ... */
})
// 返回前端
return merchantSession // JSON 字符串(需解析)2
3
4
5
6
7
8
9
Onerway 代理模式(推荐)
使用 Onerway 代理接口获取 merchantSession
| Parameter | Type | Length | Required | Signed | Description |
|---|---|---|---|---|---|
appId | String | 20 | Yes | Yes | Merchant application ID assigned by Onerway for website identification. |
merchantNo | String | 20 | Yes | Yes | Merchant number assigned by |
requestId | String | 64 | Yes | Yes | Unique request identifier for tracking and deduplication. |
sign | String | / | Yes | No | Digital signature string for request verification and security. Please refer to Signature for signature generation method. |
verifyUrl | String | 256 | Yes | Yes | Apple Pay merchant validation URL (from |
website | String | 128 | Yes | Yes | Merchant domain name registered with Apple Pay (maps to Apple Pay |
响应
| Name | Type | Description |
|---|---|---|
respCode | String | Response code from |
respMsg | String | Response message from |
data | Object | Response data. Refer to object data |
merchantSession 以 JSON 字符串形式返回在 data:
| Name | Type | Description |
|---|---|---|
└data | String | Apple Pay merchant session data as JSON string. This data should be passed directly to the Apple Pay session completion handler. |
最小请求/响应示例
{
"verifyUrl": "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession",
"website": "example.com",
"appId": "...",
"merchantNo": "...",
"requestId": "...",
"sign": "..."
}2
3
4
5
6
7
8
{
"respCode": "20000",
"respMsg": "Success",
"data": "{...merchantSession json string...}"
}2
3
4
5
商户自有 Merchant Identity 证书模式(直连 Apple)
使用商户自有 Merchant Identity 证书
如果自行管理 Merchant Identity 证书,后端必须用 mutual TLS(mTLS)向 Apple 请求 payment session。
请求体(Apple Pay on the web):
merchantIdentifierdisplayName(稳定的店铺名称)initiative = "web"initiativeContext = "<your fully qualified domain>"
将 merchantSession 返回给前端,并调用 completeMerchantValidation(merchantSession)。
详细说明见:Apple Pay - 请求支付会话。
3. 支付授权
前端实现
在 ApplePaySession 的 onpaymentauthorized 事件中,获取 payment token 并发送给后端处理:
session.onpaymentauthorized = async (event) => {
await handlePaymentAuthorization(session, event)
}2
3
async function handlePaymentAuthorization(session, event) {
// 1. 从 Apple 事件中获取 payment token
const paymentToken = event.payment.token
// 2. 发送到你的后端进行支付处理
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentToken })
})
const result = await response.json()
// 3. 根据结果完成支付(必须调用)
if (result.success) {
session.completePayment(ApplePaySession.STATUS_SUCCESS)
} else {
session.completePayment(ApplePaySession.STATUS_FAILURE)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
必须调用 completePayment
无论成功或失败,前端都 必须 调用 session.completePayment(),否则 Apple Pay sheet 会卡住。
后端实现
// 商户后端 → Onerway: POST /v1/txn/doTransaction
result = processPayment({
tokenInfo: {
provider: "ApplePay",
tokenId: event.payment.token // 来自前端
},
/* ... */
})
// 返回前端
if (result.respCode != "20000") {
return { status: "F" } // 请求失败
} else if (result.data.status == "S") {
return { status: "S" } // 支付成功
} else {
return { status: "P" } // 处理中(等待 webhook)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
API 详细说明
查看完整 API 参数和响应定义
请求参数
| Parameter | Type | Length | Required | Signed | Description |
|---|---|---|---|---|---|
billingInformation | String | / | Yes | Yes | Transaction billing information in JSON string format. See TransactionAddressfor complete structure. |
cardInfo | String | / | Conditional | Yes | Card information for wallet payments in JSON string format (merchant decrypt mode). See TxnCardInfoWalletfor complete structure. |
merchantCustId | String | 50 | Conditional | Yes | Unique customer identifier in merchant system. |
merchantNo | String | 20 | Yes | Yes | Merchant number assigned by |
merchantTxnId | String | 64 | Yes | Yes | Unique transaction identifier for each customer payment, generated by the merchant system. |
merchantTxnOriginalId | String | 128 | No | Yes | Master transaction ID generated by merchant for grouping related transactions. |
merchantTxnTime | String | / | No | Yes | Transaction timestamp when the merchant initiated the transaction. |
merchantTxnTimeZone | String | 64 | No | Yes | Transaction timezone offset for the merchant transaction time. |
orderAmount | String | 19 | Yes | Yes | Transaction amount in the specified currency, formatted as a decimal string. |
orderCurrency | String | 8 | Yes | Yes | |
productType | String | 16 | Yes | Yes | Payment method category that determines which payment options are available to customers. See ProductTypeEnumfor all available options. |
shippingInformation | String | / | Yes | Yes | Transaction shipping information in JSON string format. See TransactionAddressfor complete structure. |
sign | String | / | Yes | No | Digital signature string for request verification and security. Please refer to Signature for signature generation method. |
subProductType | String | 16 | Yes | Yes | Specific implementation method within the selected product type, defining how the payment is processed. See SubProductTypeEnumfor all available options. |
tokenInfo | String | / | Conditional | Yes | Wallet token information in JSON string format (Onerway decrypt mode). See WalletTokenInfofor the wallet token structure. |
txnOrderMsg | String | / | Yes | Yes | Transaction business information in JSON string format. See TxnOrderMsgfor complete structure. |
txnType | String | 16 | Yes | Yes | Transaction type that defines the payment operation to be performed. See TxnTypeEnumfor all available options and detailed usage scenarios. |
Token 解密模式(二选一)
每笔交易只选一种模式:
- Onerway 解密模式(推荐):把原始 Apple Pay wallet token 放到
tokenInfo里,由 Onerway 完成校验与解密(provider="ApplePay",tokenId=<paymentToken>)。 - 商户解密模式:你自行校验/解密 Apple Pay token,再把解密后的字段放到
cardInfo(例如:cryptogram、eci、wallet.*)。该模式 不要 传tokenInfo。
安全与合规
商户解密模式意味着你的后端会直接处理解密后的敏感数据与 3DS 相关字段。请确保满足 PCI DSS 以及内部安全要求。
商户解密模式:解密后的 token 示例
{
"applicationPrimaryAccountNumber": "4111111111111111",
"applicationExpirationDate": "2510",
"currencyCode": "840",
"transactionAmount": 1.00,
"deviceManufacturerIdentifier": "040010030273",
"paymentDataType": "3DSecure",
"onlinePaymentCryptogram": "Af9x5...base64-3ds-cryptogram...",
"eciIndicator": "05"
}2
3
4
5
6
7
8
9
10
响应
| Name | Type | Description |
|---|---|---|
respCode | String | Response code from |
respMsg | String | Response message from |
data | Object | Response data. Refer to object data |
data
| Name | Type | Description |
|---|---|---|
transactionId | String | Transaction order number created by |
responseTime | String | Interface response time Format: |
txnTime | String | Transaction completion time Format: |
txnTimeZone | String | Transaction time zone Format: |
orderAmount | String | Order amount |
orderCurrency | String | |
txnAmount | String | Order amount after conversion to settlement currency |
txnCurrency | String | |
status | String | Transaction processing result Refer to TxnStatusEnum |
redirectUrl | String | Redirection URL for 3D Secure verification |
contractId | String | Subscription contract number |
tokenId | String | Payment token |
eci | String | Electronic Commerce Indicator |
periodValue | String | Installment payment number of periods |
codeForm | String | Code form for specific payment methods See CodeFormEnum |
presentContext | String | Context information for presentation layer |
actionType | String | Action type for the transaction See ActionTypeEnum |
subscriptionManageUrl | String | Subscription management URL |
sign | String |
4. Webhook 通知
你必须把 webhook 作为最终状态来源:
- 验签
- 幂等(按
merchantTxnId去重) - 回执:只返回
transactionId
详见:交易通知
API 使用示例
{
"appId": "1727880846378401792",
"merchantNo": "800209",
"merchantTxnId": "0104b16f-cc96-44ed-94bd-e814f6b64bec",
"orderAmount": "1",
"orderCurrency": "USD",
"productType": "CARD",
"subProductType": "DIRECT",
"txnType": "SALE",
"tokenInfo": "{\"provider\":\"ApplePay\",\"tokenId\":\"{\\\"paymentData\\\":{\\\"data\\\":\\\"Zg8fgQuOqRemQ7XPmartChBCZbfaaWBEk6AVRS6bcO7Qfqmm90S90l1nnsYKqwBMmDQcpsOfFF+HHgMAGwzo1XurrbZprbZc1Quw6mNJMrLwMiUMva4hf2sNNGCp9EHeSMx+MofeeMbHjI5fladdD19/GzXVNR1OAFdfuZLB/rkTKI5cVsKFqzeshxJiUAbHQvAwkdNvyjpEuGoWhSrWpaAbX6SBB6NlcUXQ4KT46LJJ+05QiwIO+drTdP9j/0Fbl+lg2UYcQPIE/3lownbSRPOq9JMpqF4xOZmO+3gYOaME5Xhev95g8kG3Hogw6MYBpGxmRA5Btu9Cma9fNEekA+KXqS78TQT24IktEpYUW9yKO+xUXTWqg0auFwb1bKOo2D2rSIAoYSW6Vnehdo0gmLP4BApyDf5Nug1YQEaKoKk=\\\",\\\"signature\\\":\\\"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\\\",\\\"header\\\":{\\\"publicKeyHash\\\":\\\"H8goTIjHUSUL1pcwkMV8ljgmU73N51JAIvz6Ti05LlI=\\\",\\\"ephemeralPublicKey\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV5gfSdV5Hf71RaGUoqrwQitC7SE7A/uQ7biDcM41u7ksBeIEeoBDGs4koPA8vDLDn9kgpEC38aXpBqvHp4IREw==\\\",\\\"transactionId\\\":\\\"fc46411c0070e4ca0ed2de2a7a12b9796ba94f00b72c4bdf406a1deb03d7487b\\\"},\\\"version\\\":\\\"EC_v1\\\"},\\\"paymentMethod\\\":{\\\"displayName\\\":\\\"Visa 0121\\\",\\\"network\\\":\\\"Visa\\\",\\\"type\\\":\\\"credit\\\"},\\\"transactionIdentifier\\\":\\\"fc46411c0070e4ca0ed2de2a7a12b9796ba94f00b72c4bdf406a1deb03d7487b\\\"}\"}",
"shippingInformation": "{\"address\":\"72981 Alejandrin Port\",\"city\":\"Lake Judd\",\"country\":\"US\",\"email\":\"Tracy_Stanton58@gmail.com\",\"firstName\":\"Noble\",\"lastName\":\"Baumbach\",\"phone\":\"18434119887\",\"postalCode\":\"12053\",\"province\":\"CO\"}",
"txnOrderMsg": "{\"returnUrl\":\"https://docs.onerway.com/\",\"notifyUrl\":\"https://sandbox-acq.onerway.com/callback/testReceiveNotification\",\"products\":\"[{\\\"currency\\\":\\\"USD\\\",\\\"name\\\":\\\"butternut pumpkin\\\",\\\"num\\\":\\\"96\\\",\\\"price\\\":\\\"708.69\\\"}]\"}",
"sign": "8816e1f0326f2ee34f8bc909f7cd5d259aed37e59410f178d22f275f2f38fbe9"
}2
3
4
5
6
7
8
9
10
11
12
13
14
{
"appId": "1727880846378401792",
"merchantNo": "800209",
"merchantTxnId": "0104b16f-cc96-44ed-94bd-e814f6b64bec",
"orderAmount": "1",
"orderCurrency": "USD",
"productType": "CARD",
"subProductType": "DIRECT",
"txnType": "SALE",
"cardInfo": "{\"cardNumber\":\"4111111111111111\",\"month\":\"10\",\"year\":\"2025\",\"cryptogram\":\"AAAAAA...\",\"eci\":\"05\",\"wallet\":{\"type\":\"ApplePay\",\"applePay\":{\"paymentDataType\":\"3DSecure\"}}}",
"billingInformation": "{\"country\":\"US\"}",
"shippingInformation": "{\"country\":\"US\"}",
"txnOrderMsg": "{\"returnUrl\":\"https://example.com/return\",\"notifyUrl\":\"https://example.com/webhook\"}",
"sign": "..."
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"respCode": "20000",
"respMsg": "Success",
"data": {
"transactionId": "1962880981921042432",
"responseTime": "2025-09-02 22:11:13",
"txnTime": "2025-09-02 22:11:09",
"txnTimeZone": "+08:00",
"orderAmount": "1.00",
"orderCurrency": "USD",
"status": "S",
"redirectUrl": null,
"sign": "719c7533ebe9b57dfc232176091eda1242a228045ffa6481d6a3bb048ff6a20e"
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"notifyType": "TXN",
"transactionId": "1962880981921042432",
"txnType": "SALE",
"merchantNo": "800209",
"merchantTxnId": "0104b16f-cc96-44ed-94bd-e814f6b64bec",
"responseTime": "2025-09-02 22:11:13",
"txnTime": "2025-09-02 22:11:09",
"txnTimeZone": "+08:00",
"orderAmount": "1.00",
"orderCurrency": "USD",
"status": "S",
"cardBinCountry": "CA",
"reason": "{\"respCode\":\"20000\",\"respMsg\":\"Success\"}",
"paymentMethod": "VISA",
"walletTypeName": "ApplePay",
"channelRequestId": "8002091962880984022126593",
"sign": "b0cf37c8c79138a3248dc422de191416beb05d345110869045b3076819f7b2d8"
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
前端完整示例(HTML)
HTML 完整实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apple Pay 集成(示例)</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; }
#container { max-width: 520px; margin: 24px auto; }
#status { margin-top: 12px; font-size: 14px; white-space: pre-wrap; }
apple-pay-button { --apple-pay-button-width: 100%; --apple-pay-button-height: 44px; }
</style>
</head>
<body>
<div id="container">
<apple-pay-button id="applePayButton" buttonstyle="black" type="buy" locale="zh-CN" style="display:none;"></apple-pay-button>
<div id="status"></div>
</div>
<script>
// ============================================
// 1. DOM 元素和工具函数
// ============================================
const statusElement = document.getElementById('status')
const applePayButton = document.getElementById('applePayButton')
function updateStatus(message) {
statusElement.textContent = message
}
// ============================================
// 2. 后端 API 调用函数(需要商户实现)
// ============================================
/**
* 获取 Apple Pay 配置
* 后端调用:POST /v1/txn/consultPaymentMethod
* 返回:{ countryCode: string, subCardTypes: string[] }
* 示例:{ countryCode: 'US', subCardTypes: ['visa', 'masterCard', 'discover'] }
*/
async function fetchApplePayConfig() {
// TODO: 替换为实际的后端 API 调用
const response = await fetch('/api/apple-pay/config')
return response.json()
}
/**
* 商户验证
* 后端调用:POST /txn/apiCheckApplePay(Onerway 代理)或直连 Apple
* 参数:{ validationURL: string, website: string }
* 返回:merchantSession 对象
*/
async function validateMerchant(validationURL, website) {
// TODO: 替换为实际的后端 API 调用
const response = await fetch('/api/validate-merchant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ validationURL, website })
})
return response.json()
}
/**
* 处理支付
* 后端调用:POST /v1/txn/doTransaction
* 参数:{ tokenInfo: { provider: 'ApplePay', tokenId: paymentToken } }
* 返回:{ success: boolean, error?: string }
*/
async function processPayment(paymentToken) {
// TODO: 替换为实际的后端 API 调用
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentToken })
})
return response.json()
}
// ============================================
// 3. Apple Pay 事件处理函数
// ============================================
/**
* 处理商户验证
*/
async function handleMerchantValidation(session, event) {
// 安全检查:只接受 Apple 官方的验证 URL
if (!event.validationURL.startsWith('https://apple-pay-gateway')) {
updateStatus('错误:无效的验证 URL')
session.abort()
return
}
try {
updateStatus('商户验证中...')
const merchantSession = await validateMerchant(
event.validationURL,
window.location.hostname
)
session.completeMerchantValidation(merchantSession)
} catch (error) {
updateStatus('商户验证失败:' + error.message)
session.abort()
}
}
/**
* 处理支付授权
*/
async function handlePaymentAuthorization(session, event) {
try {
updateStatus('支付处理中...')
const result = await processPayment(event.payment.token)
const isSuccess = result && result.success === true
// 必须调用 completePayment,否则 Apple Pay 界面会卡住
session.completePayment(
isSuccess ? ApplePaySession.STATUS_SUCCESS : ApplePaySession.STATUS_FAILURE
)
updateStatus(isSuccess ? '支付成功' : '支付失败')
} catch (error) {
session.completePayment(ApplePaySession.STATUS_FAILURE)
updateStatus('支付失败:' + error.message)
}
}
// ============================================
// 4. 创建支付会话
// ============================================
/**
* 创建并启动 Apple Pay 支付会话
*/
function createPaymentSession(config) {
// 构建支付请求
const paymentRequest = {
countryCode: config.countryCode, // 来自 Onerway
currencyCode: 'USD',
supportedNetworks: config.subCardTypes, // 来自 Onerway,已是 Apple Pay 格式
merchantCapabilities: ['supports3DS'],
total: {
label: '示例商户',
amount: '1.00',
type: 'final'
}
}
// 创建 Apple Pay 会话(版本 3+)
const session = new ApplePaySession(3, paymentRequest)
// 注册事件处理器
session.onvalidatemerchant = (event) => handleMerchantValidation(session, event)
session.onpaymentauthorized = (event) => handlePaymentAuthorization(session, event)
session.oncancel = () => updateStatus('用户取消支付')
// 启动支付界面
session.begin()
}
// ============================================
// 5. 初始化
// ============================================
async function initializeApplePay() {
// 检查浏览器支持
if (!window.ApplePaySession) {
return updateStatus('当前浏览器不支持 Apple Pay')
}
// 检查设备支持
if (!ApplePaySession.canMakePayments()) {
return updateStatus('此设备不支持 Apple Pay')
}
try {
// 获取配置
const config = await fetchApplePayConfig()
// 显示按钮并绑定点击事件
applePayButton.style.display = 'block'
applePayButton.addEventListener('click', () => createPaymentSession(config))
updateStatus('准备就绪,点击按钮发起支付')
} catch (error) {
updateStatus('初始化失败:' + error.message)
}
}
// 页面加载完成后初始化
initializeApplePay()
</script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
错误处理
常见错误
- 按钮不显示:非 Safari / 非 HTTPS / Wallet 无可用卡 → 见 故障排查
- sheet 一闪而过:商户验证失败 → 检查域名验证文件与 validationURL allowlist
- 支付卡住:缺少
completePayment()调用
安全最佳实践
- 不要在客户端请求 Apple payment session(server-only)。
- 对 Apple validation URLs 做严格 allowlist,拒绝任何其他 URL。
- 不要记录或存储 Apple Pay payment tokens。
- webhook 必须验签,并强制幂等。
实现最佳实践
- 缓存 Onerway 配置以加快按钮响应(配置变化时刷新)。
displayName保持一致(不要本地化,不要包含订单号等动态值)。- webhook 作为最终状态来源。
集成清单
- Onerway 已开通 Apple Pay
- 域名验证文件已部署到
/.well-known/(见账号配置) - 后端配置接口返回
countryCode/subCardTypes - 已实现
onvalidatemerchant(server-only + allowlist + 5 分钟过期) - 已实现
onpaymentauthorized且始终调用completePayment() - 后端调用
doTransaction且tokenInfo结构正确(provider="ApplePay",tokenId=<paymentToken>),或cardInfo结构正确(例如:cryptogram、eci、wallet.*)。 - webhook 验签 + 幂等已实现