﻿# SDK-Apple pay

::: danger  注意：
**需申请开通** 

如需使用该功能，请提前联系您的 BD/AM 或技术支持团队申请开通。
:::

客户端SDK模式专为原生iOS应用程序设计。通过 PayerMax Apple Pay SDK，您可以在 iOS App 中提供原生的 Touch ID / Face ID 支付体验，同时无需自行处理 Apple Pay Token 的复杂解密工作。PayerMax 后端代您完成解密与收单，您只需极少量的 Swift 代码即可完成完整的支付闭环。
在开始之前，您需要已加入[Apple Developer Program](https://developer.apple.com/programs/)。

## 1. 交互流程

商户 App、商户服务端、PayerMax SDK、Apple PassKit 以及 PayerMax 后端的交互时序如下：

```mermaid
%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#e6f0ff',
    'primaryTextColor': '#333',
    'primaryBorderColor': '#5b9bd5',
    'lineColor': '#888',
    'actorMargin': 40,
    'noteBkgColor': '#0056b3',
    'noteTextColor': '#ffffff',
    'noteBorderColor': '#004a99'
  }
}}%%
sequenceDiagram
    participant App as 商户App
    participant MerchantServer as 商户服务端
    participant PMSDK as PayerMax SDK
    participant PassKit
    participant PMBackend as PayerMax后端

    %% 流程步骤
    App->>MerchantServer: 请求创建订单
    MerchantServer->>PMBackend: 调用 /applyApplePaySession(金额/币种/国家)
    PMBackend-->>MerchantServer: amount / currency / country / merchantId / networks / orderToken
    MerchantServer-->>App: 返回订单参数(amount/currency/country/merchantId/networks/orderToken)
    App->>PMSDK: present(PKPaymentRequest)
    PMSDK->>PassKit: 弹出 Apple Pay 面板
    PassKit-->>PMSDK: didAuthorize(PKPayment 加密 token)
    PMSDK->>PMBackend: POST /orderAndPay (含 orderToken 及加密 payload)
    PMBackend-->>PMSDK: 收单结果(成功/失败)
    PMSDK-->>App: completion(result)
```

## 2. 接入前步骤

在开始编写代码之前，您需要完成 Apple 侧的资产配置，并与 PayerMax 交换必要的证书。以下配置是启动 Apple Pay 的前提。

### 2.1 创建商户 IDs

登录 Apple Developer 网站，在 [Certificates,Identifiers & Profiles -> Identifiers -> Merchant IDs](https://developer.apple.com/account/resources/identifiers/add/merchant) 页面中注册一个新的 Merchant ID。该 ID 用于唯一标识您的商户身份，并在 Apple Pay 的加密流程中扮演关键角色。
在表单中填写描述和标识符。描述内容仅供您自己记录之用，之后可随时更改。PayerMax 建议用您的应用程序的名称作为标识符（例如，`merchant.com.&lbrace;&lbrace;YOUR_APP_NAME&rbrace;&rbrace;`）。

### 2.2 配置 Payment Processing Certificate

为您的应用创建证书，以加密支付数据。PayerMax 采用**后端自持解密**方案，即由 PayerMax 后端持有私钥并负责解密 Apple Pay Token，商户客户端无需接触任何私钥。
配置步骤如下：
1. 联系 PayerMax 技术支持，获取专属的 Certificate Signing Request（CSR）文件。
2. 在 [Apple Develope 后台](https://developer.apple.com/account/resources/certificates/add)，使用 PayerMax 提供的 CSR 文件为您的 Merchant ID 生成 Payment Processing Certificate。
3. 将生成的证书文件（`.cer`）提交给 PayerMax，由 PayerMax 后端完成导入与配置。
>注意：

>请务必使用 PayerMax 提供的 CSR 文件生成证书，而非自行生成 CSR。一个 CSR 文件只能签发一张证书。如果您更换了 Apple Merchant ID，则必须重新联系 PayerMax 技术支持获取新的 CSR 和证书。

## 3. 接口介绍

### 3.1 接口列表

| 关联步骤                   | 调用方向               | 接口类型 | 接口 PATH                                                                                                                                                           |
| -------------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 4.3 创建付款请求           | 商户服务端 -> PayerMax | 后端接口 | [/applyApplePaySession](https://docs.payermax.com/api.html?docName=New%20Version&docVer=v1.0&docLang=cn#/paths/aggregate-pay-api-gateway-applyApplePaySession/post) |
| 4.4 出示支付表单与提交付款 | SDK -> PayerMax        | 后端接口 | [/orderAndPay](https://docs.payermax.com/api.html?docName=New%20Version&docVer=v1.0&docLang=cn#/paths/aggregate-pay-api-gateway-orderAndPay/post)                   |
| 4.5 获取支付结果           | PayerMax -> 商户       | 后端接口 | [/collectResultNotifyUrl](https://docs.payermax.com/api.html?docName=New%20Version&docVer=v1.0&docLang=cn#/paths/collectResultNotifyUrl/post)                       |

### 3.2 环境信息

- **测试环境**：https:// `pay-gate-uat.payermax.com`/aggregate-pay/api/gateway/ `<接口PATH>`

- **集成环境**：https:// `pay-gate.payermax.com`/aggregate-pay/api/gateway/ `<接口PATH>`

### 3.3 请求 Header

``` json
{
  "Accept": "application/json",
  "sign": "请参考签名规则：https://docs-v2.payermax.com/202606-version/developer/config-settings.html",
  "Content-Type": "application/json"
}
```

## 4. 开始集成

### 4.1 获取 SDK

PayerMax 推荐使用现代的依赖管理工具来引入 Apple Pay SDK ，支持 Swift Package Manager (SPM) 和 CocoaPods 两种方式。

**1. Swift Package Manager (SPM)**

在 Xcode 中，选择 **File > Add Package Dependencies...**，输入以下仓库地址，然后选择最新的版本号，并将 `PayerMaxApplePay` 模块添加到您的应用程序目标（Target）中：

``` plain text
https://github.com/payermax/payermax-ios-sdk

```

**2. CocoaPods**

在您的 `Podfile` 中添加以下依赖 ，然后运行 `pod install`：

``` ruby
pod 'PayerMax/ApplePay'
```

### 4.2 集成 Xcode 与配置能力

在 Xcode 中，打开您的项目设置，选择目标（Target），然后点击 **Signing & Capabilities** 选项卡。点击左上角的 `+ Capability`，在弹出的列表中搜索并添加 **Apple Pay** 功能。

在 Apple Pay 配置区域，勾选您在第 2.1 步中创建的 Merchant ID。这一步会将 Merchant ID 写入应用程序的 entitlement 文件中，使您的应用程序具备拉起 Apple Pay 的权限。

### 4.3 初始化配置与检查可用性

在您的应用程序启动时（例如在 `AppDelegate` 中），使用您在 PayerMax 商户后台获取的 Publishable Key 完成 SDK 初始化。在向用户展示 Apple Pay 按钮之前，调用 `PMApplePayConfiguration.canMakePayments()` 验证当前设备是否支持 Apple Pay 且用户已绑卡。

``` swift
import UIKit
import PayerMaxApplePay
import PassKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        // 使用您在 PayerMax 商户后台获取的 Publishable Key 完成 SDK 初始化
        PMAPIClient.defaultPublishableKey = "your_publishable_key"

        return true
    }
}

class CheckoutViewController: UIViewController {
    let applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)

    override func viewDidLoad() {
        super.viewDidLoad()

        // 仅在设备支持 Apple Pay 且用户已绑卡时展示按钮
        if PMApplePayConfiguration.canMakePayments() {
            applePayButton.addTarget(self, action: #selector(handleApplePayButtonTapped), for: .touchUpInside)
            view.addSubview(applePayButton)
        }
    }
}
```

### 4.4 发起支付请求与处理结果

当用户点击 Apple Pay 按钮时，商户 App 先请求自己的服务端创建订单；商户服务端调用 PayerMax 的 `/applyApplePaySession` 接口，获取 `amount`、`currency`、`merchantId`、`networks` 等支付参数以及 `orderToken` 后返回给 App。

App 拿到参数后，调用 `PMApplePayConfiguration.paymentRequest` 构造 `PKPaymentRequest`，再通过 `PMApplePayContext` 展示原生 Apple Pay 面板。用户通过 Face ID 或 Touch ID 授权后，SDK 自动将加密 Token 连同 `orderToken` 上送至 PayerMax 后端的 `/orderAndPay` 接口完成收单确认，最终通过 delegate 回调将结果返回给商户 App。

**服务端**

商户服务端需提供一个接口，内部调用 PayerMax `/applyApplePaySession`，并将订单参数返回给客户端：

``` json
// POST /applyApplePaySession 请求示例
{
  "version": "1.5",
  "keyVersion": "1",
  "requestTime": "2025-05-14T16:30:27.174+08:00",
  "appId": "your_app_id",
  "merchantNo": "your_merchant_no",
  "data": {
    "outTradeNo": "your_order_id",
    "totalAmount": 50.00,
    "currency": "USD",
    "country": "US",
    "userId": "your_user_id",
    "subject": "Your Order Subject"
  }
}
```

``` json
// 响应示例
{
  "code": "APPLY_SUCCESS",
  "msg": "Success.",
  "data": {
    "amount": "50.00",
    "currency": "USD",
    "country": "US",
    "merchantId": "merchant.com.yourcompany.yourapp",
    "networks": ["visa", "masterCard", "amex"],
    "orderToken": "T2025051210335071234567"
  }
}
```

**客户端**

``` swift
@objc func handleApplePayButtonTapped() {
    // 从您的服务端获取订单参数
    // 您的服务端负责调用 PayerMax /applyApplePaySession 接口并将结果返回给 App
    fetchOrderParamsFromYourServer { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .success(let orderParams):
            // orderParams 包含服务端返回的 amount、currency、country、merchantId、networks、orderToken
            let paymentRequest = PMApplePayConfiguration.paymentRequest(
                withMerchantIdentifier: orderParams.merchantId, // 来自服务端
                country: orderParams.country,                   // 来自服务端
                currency: orderParams.currency                  // 来自服务端
            )

            // 配置付款请求上的摘要行
            // 最后一行应代表您的公司；它将以"Pay"一词开头（即"Pay Your Company $50"）
            // 金额来自服务端，不得由客户端自行决定
            paymentRequest.paymentSummaryItems = [
                PKPaymentSummaryItem(
                    label: "Your Company Name",
                    amount: NSDecimalNumber(string: orderParams.amount) // 来自服务端
                )
            ]

            // 保存 orderToken 供后续 /orderAndPay 使用
            self.orderToken = orderParams.orderToken

            // 初始化 PMApplePayContext 并展示 Apple Pay 面板
            // 注意：present 必须由用户手势直接触发，不能在异步操作后延迟调用
            if let applePayContext = PMApplePayContext(
                paymentRequest: paymentRequest,
                delegate: self
            ) {
                applePayContext.presentApplePay(on: self)
            } else {
                print("初始化 Apple Pay 失败，请检查 Merchant ID 配置")
            }

        case .failure(let error):
            print("获取订单参数失败: \(error.localizedDescription)")
        }
    }
}

// MARK: - PMApplePayContextDelegate

extension CheckoutViewController: PMApplePayContextDelegate {

    // 用户授权后，SDK 回调此方法，您需要返回服务端的 orderToken 用于后续 /orderAndPay
    func applePayContext(
        _ context: PMApplePayContext,
        didCreatePaymentMethod paymentMethod: PMPaymentMethod,
        paymentInformation: PKPayment
    ) async throws -> String {
        // 返回创建订单时服务端下发的 orderToken
        // SDK 将携带此 token 及加密 payload 自动调用 PayerMax /orderAndPay
        return self.orderToken
    }

    // 支付完成后，SDK 回调此方法，status 为最终支付结果
    func applePayContext(
        _ context: PMApplePayContext,
        didCompleteWith status: PMApplePayContext.PaymentStatus,
        error: Error?
    ) {
        switch status {
        case .success:
            // 支付成功，展示订单确认页面
            print("支付成功")
        case .error:
            // 支付失败，展示错误提示
            print("支付失败: \(error?.localizedDescription ?? "")")
        case .userCancellation:
            // 用户主动取消
            break
        @unknown default:
            break
        }
    }
}
```

> **说明：**金额应始终由服务端决定，客户端仅负责展示。请勿在客户端硬编码金额，以防止恶意篡改。`/orderAndPay` 由 SDK 在用户授权后自动调用，商户无需手动处理。

### 4.5 获取支付结果

`PMApplePayContextDelegate` 的 `didCompleteWith` 回调中的 `.success` 状态表明 PayerMax 后端已完成收单确认。除此之外，PayerMax 还会通过异步 Webhook 通知向您的服务端推送最终的支付结果。
建议您同时监听 Webhook 通知，而不是仅依赖客户端回调，以确保在用户关闭应用程序等异常场景下也能可靠地获取支付状态。详情请查看[支付结果-支付结果通知](https://docs.payermax.com/api.html?docName=New%20Version&docVer=v1.0&docLang=cn#/paths/collectResultNotifyUrl/post)。

## 5. 测试与上线

### 5.1 沙盒测试

Apple Pay 的测试需要使用专门的沙盒环境。您无法将普通测试卡添加到真机的 Apple 钱包中，需要按照以下步骤进行配置：
1. 在 [Apple Developer 网站](https://developer.apple.com/apple-pay/sandbox-testing/)创建一个沙盒测试账号（Sandbox Tester）。
2. 在测试 iPhone 或 iPad 上，前往 **设置 > App Store**，使用沙盒测试账号登录。
3. 前往 **设置 > 钱包与 Apple Pay**，使用 Apple 提供的测试卡号添加一张测试卡。
4. 在 SDK 初始化时，使用测试环境的 Publishable Key：

``` swift
// 测试环境初始化
PMAPIClient.defaultPublishableKey = "your_test_publishable_key"
```

> 注意：在测试环境中，`127.0.0.1`、局域网 IP、`localhost` 都无法拉起 Apple Pay，需要放在带有 SSL 证书的 HTTPS 域名下。

### 5.2 切换生产环境

测试完成后，将 Publishable Key 替换为生产环境的值即可完成上线切换：

``` swift
// 生产环境初始化
PMAPIClient.defaultPublishableKey = "your_live_publishable_key"
```

## 6. 常见问题排查

如果在集成过程中遇到问题，请优先参考下表中的常见原因与解决方案。

| 错误现象                    | 可能原因                    | 解决方案                                                                                                                                             |
| --------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **ApplePay面板无法弹出**    | MerchantID配置不匹配        | 检查Xcode的ApplePayCapability中勾选的MerchantID，是否与服务端 `/applyApplePaySession` 返回的 `merchantId` 完全一致。                                 |
| **ApplePay面板无法弹出**    | 设备未绑卡或不支持Apple Pay | 确认 `PMApplePayConfiguration.canMakePayments()` 返回 `true`。 若返回 `false`，请检查测试设备是否已在钱包中添加了沙盒测试卡。                        |
| **后端返回Token解密失败**   | 使用了错误的CSR生成证书     | 确保您在Apple Developer后台使用的是PayerMax提供的CSR文件，而非自行生成的CSR。如有疑问，请联系PayerMax技术支持重新获取CSR。                           |
| **Xcode中ApplePay配置失效** | Xcode缓存了旧的证书信息     | 在Xcode中关闭并重新打开ApplePay Capability（先取消勾选MerchantID，保存后再重新勾选），以强制刷新 `entitlement` 配置。                                |
| **支付金额显示异常**        | 客户端金额与服务端不一致    | 请确保 `paymentSummaryItems` 中的金额与服务端 `/applyApplePaySession` 接口返回的订单金额严格一致，否则可能导致支付失败或风控拦截。                   |
| **网络重试导致重复扣款**    | 未正确处理幂等性            | SDK内部已将Apple返回的 `transactionIdentifier` 作为幂等键（Idempotency-Key）传递给PayerMax后端。请确认您的服务端已正确处理该幂等键，以防止重复扣款。 |
