Skip to content

iOS 接入(Swift) ​

这套接入适合谁 ​

原生 iOS 应用(Swift / SwiftUI 或 UIKit)需要在自己的界面里展示应付金额与收款信息, 并在买家付款后刷新订单状态。示例用 URLSession + Codable,不引第三方库。

前置准备 ​

  • 已拿到 API KEY(见立即接入)。
  • 已配置收款地址并设为启用。
  • 有一个你自己的服务端。API KEY 不能放进 App。

KEY 不能打进 IPA

iOS 包体可以被反编译,Info.plist 与二进制里的字符串都不安全。App 里的下单请求发往你自己的服务端。

安装 ​

无需额外依赖,系统自带的 URLSession 即可;界面按需用 SwiftUI。

最小可跑示例 ​

1. 你的服务端下单 ​

以 Swift 服务端风格(Vapor / 或任意后端)为代表,核心是"带 KEY 调开放接口":

swift
import Foundation

func createOrder(orderNo: String, amount: String) async throws -> Data {
    var req = URLRequest(url: URL(string: "\(ProcessInfo.processInfo.environment["API_BASE"]!)/api/pay/open/createOrder")!)
    req.httpMethod = "POST"
    req.setValue(ProcessInfo.processInfo.environment["PAY_API_KEY"], forHTTPHeaderField: "X-Pay-Api-Key")
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.httpBody = try JSONSerialization.data(withJSONObject: [
        "merchantOrderNo": orderNo,      // 幂等键
        "chainId": 2,
        "tokenId": 1,
        "amount": amount,                // 代币单位小数字符串
        "subject": "订单 \(orderNo)",
        "notifyUrl": "https://your-shop.com/pay/notify",
    ])
    let (data, _) = try await URLSession.shared.data(for: req)
    return data
}

2. App 侧:只打你自己的后端 ​

swift
struct CheckoutRequest: Codable { let orderNo: String; let amount: String }

struct CheckoutResponse: Codable {
    let code: Int
    let orderNo: String?
    let payAddress: String?
    let payAmountDisplay: String?
    let payAmountMinimal: String?
    let chainKey: String?
    let msg: String?
}

struct StatusResponse: Codable {
    let code: Int
    let status: Int          // 1..6,只认 3/4 为已付款
    let statusText: String?
    let txHash: String?
}

enum ShopAPI {
    static func checkout(_ body: CheckoutRequest) async throws -> CheckoutResponse {
        var req = URLRequest(url: URL(string: "https://your-shop.com/api/checkout")!)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.httpBody = try JSONEncoder().encode(body)   // KEY 不在这里
        let (data, _) = try await URLSession.shared.data(for: req)
        return try JSONDecoder().decode(CheckoutResponse.self, from: data)
    }

    static func status(orderNo: String) async throws -> StatusResponse {
        let url = URL(string: "https://your-shop.com/api/order-status?orderNo=\(orderNo)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(StatusResponse.self, from: data)
    }
}
swift
@MainActor
final class CheckoutViewModel: ObservableObject {
    enum State { case loading, ready(CheckoutResponse), pending(CheckoutResponse),
                 paid(CheckoutResponse), error(String) }

    @Published var state: State = .loading

    func start(orderNo: String, amount: String) async {
        do {
            let o = try await ShopAPI.checkout(.init(orderNo: orderNo, amount: amount))
            guard o.code == 0 else { state = .error(o.msg ?? "下单失败"); return }
            state = .ready(o)

            while true {
                try await Task.sleep(for: .seconds(3))
                let s = try await ShopAPI.status(orderNo: o.orderNo ?? orderNo)
                switch s.status {
                case 1: state = .ready(o)          // 等待付款
                case 2: state = .pending(o)        // 已提交,等确认
                case 3, 4: state = .paid(o); return
                case 5: state = .error("订单已过期"); return
                case 6: state = .error("待人工认领,请联系客服"); return
                default: break
                }
            }
        } catch {
            state = .error(error.localizedDescription)
        }
    }
}

也可以直接打开平台收银台:{cashierBaseUrl}/pay/cashier/m/{merchantNo}, 两个值从商户侧 GET /api/pay/getMyProfile 取,用 UIApplication.shared.open 打开。

回调处理 ​

回调由你的服务端接收。Swift 服务端验签示例:

swift
import CryptoKit

func verify(rawBody: String, ts: String, sig: String, apiKey: String) -> Bool {
    let key = SymmetricKey(data: Data(apiKey.utf8))
    var mac = HMAC<SHA256>.init(key: key)
    mac.update(data: Data(ts.utf8))        // 时间戳 + 原始正文,字符串拼接
    mac.update(data: Data(rawBody.utf8))
    let want = mac.finalize().map { String(format: "%02x", $0) }.joined()
    // 常数时间比较
    return constantTimeEqual(want, sig)
}

处理要点:

  • 幂等:回调会重试,用 merchantOrderNo 去重,别重复发货。
  • 快返回:约 10 秒超时,耗时逻辑放后台任务。
  • 以查询为准:关键动作前用查询订单复核。

唤起钱包与 App 间跳转 ​

  • iOS 上多数加密钱包支持通用链接(Universal Link)或自定义 scheme,UIApplication.shared.open 即可尝试唤起。
  • 在 Info.plist 的 LSApplicationQueriesSchemes 里声明你要探测的 scheme,才能用 canOpenURL 判断是否已安装。
  • 同时展示二维码作为兜底:没装钱包的用户可以用另一台设备扫码。

常见问题 ​

问题说明
KEY 能放 Info.plist / Keychain 吗不能。App 端不持有 KEY,下单走后端
后台被系统挂起导致轮询中断进入后台暂停轮询,scenePhase 回到前台时恢复并立即查一次
能不能用 WebView 嵌收银页可以,但直接用系统浏览器打开平台收银台更省事
怎么判断已付款只认状态 3 / 4,不要用数值大小比较
重复请求会不会多开单不会,同一个 merchantOrderNo 幂等复用

相关 ​

面向全球中小商家的 Web3 支付基础设施