PAYATHON 2026

如何在 iOS 应用中集成 `gate2play`?

支付老李

结论

目前没有足够明确且可验证的 gate2play iOS 原生 SDK 集成资料,因此无法像常见支付 SDK 那样直接接入。较稳妥的做法是由服务端与 gate2play 通信并创建支付会话,iOS 端负责打开托管支付页面。支付完成后,再由服务端通过查询或回调确认结果。

具体的接口地址、签名算法和字段名称,必须以商户后台提供的文档为准,不能照搬其他支付平台的参数。

推荐的集成流程

支付流程可以这样设计:

  1. iOS 应用向自己的服务端提交订单。
  2. 服务端保存订单,并调用 gate2play 的支付接口。
  3. gate2play 返回支付页面 URL 或功能相同的会话信息。
  4. iOS 使用 ASWebAuthenticationSession 或 WKWebView 打开支付页面。
  5. 支付结束后,页面跳转到预先配置的回跳地址。
  6. gate2play 向服务端发送支付通知,或者由服务端主动查询订单状态。
  7. 服务端验证签名、金额、币种和订单号,然后更新订单。
  8. iOS 从自己的服务端获取最终支付状态。

客户端收到回跳 URL 并不代表支付成功。用户可能中途关闭页面,也可能伪造 URL,服务端收到支付通知的时间也可能晚于页面回跳。

服务端应负责的工作

密钥和签名逻辑必须保存在服务端。下面的代码只用于说明接口结构,其中的 URL、字段和签名方法都是占位设计,需要替换为 gate2play 实际提供的定义。

POST /api/payments
Content-Type: application/json

{
  "orderId": "ORDER-10001"
}

服务端处理逻辑可以抽象为:

app.post("/api/payments", async (req, res) => {
  const order = await findOrder(req.body.orderId);

  // 参数名称仅为示意,必须以 gate2play 商户文档为准。
  const paymentRequest = {
    merchantOrderId: order.id,
    amount: order.amount,
    currency: order.currency,
    returnUrl: "myapp://payment-result",
    notificationUrl: "https://example.com/api/payments/callback"
  };

  // 签名算法也必须来自 gate2play 的正式接入资料。
  const signature = createSignature(paymentRequest, process.env.GATE2PLAY_SECRET);

  const response = await callGate2Play({
    ...paymentRequest,
    signature
  });

  res.json({
    paymentUrl: response.paymentUrl
  });
});

处理支付回调时,至少要完成以下检查:

app.post("/api/payments/callback", async (req, res) => {
  if (!verifyGate2PlaySignature(req.body)) {
    return res.sendStatus(400);
  }

  const order = await findOrder(req.body.merchantOrderId);

  if (
    order.amount !== req.body.amount ||
    order.currency !== req.body.currency
  ) {
    return res.sendStatus(400);
  }

  // 回调可能重复发送,更新操作应当具备幂等性。
  if (req.body.status === "PAID") {
    await markOrderAsPaid(order.id, req.body.transactionId);
  }

  res.sendStatus(200);
});

PAID、transactionId 等名称仅用于展示结构,不代表 gate2play 的真实字段。

iOS 打开支付页面

如果支付流程支持外部认证会话,可以优先使用 ASWebAuthenticationSession:

import AuthenticationServices

final class PaymentCoordinator: NSObject {
    private var session: ASWebAuthenticationSession?

    func startPayment(paymentURL: URL) {
        session = ASWebAuthenticationSession(
            url: paymentURL,
            callbackURLScheme: "myapp"
        ) { callbackURL, error in
            if let error = error {
                print("Payment page closed or failed: \(error)")
                return
            }

            guard let callbackURL else {
                return
            }

            print("Returned from payment page: \(callbackURL)")

            // 回跳不代表支付已经成功。
            // 此处应请求自己的服务端查询订单状态。
            self.refreshPaymentStatus()
        }

        session?.presentationContextProvider = self
        session?.prefersEphemeralWebBrowserSession = false
        session?.start()
    }

    private func refreshPaymentStatus() {
        // 调用自己的订单状态接口。
    }
}

extension PaymentCoordinator: ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(
        for session: ASWebAuthenticationSession
    ) -> ASPresentationAnchor {
        guard let scene = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene })
            .first,
              let window = scene.windows.first(where: { $0.isKeyWindow }) else {
            return ASPresentationAnchor()
        }

        return window
    }
}

应用还要注册相应的 URL Scheme,例如 myapp。如果使用 Universal Links,则需要配置 Associated Domains,并把回跳地址改成 HTTPS 链接。

查询最终支付状态

返回应用后,iOS 应向自己的服务端查询订单状态,不能直接信任回跳参数:

struct PaymentStatus: Decodable {
    let status: String
}

func fetchPaymentStatus(orderId: String) async throws -> PaymentStatus {
    var components = URLComponents(
        string: "https://example.com/api/payments/status"
    )!

    components.queryItems = [
        URLQueryItem(name: "orderId", value: orderId)
    ]

    let (data, response) = try await URLSession.shared.data(
        from: components.url!
    )

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }

    return try JSONDecoder().decode(PaymentStatus.self, from: data)
}

支付状态最好使用 pending、paid、failed、cancelled 等含义明确的值。客户端收到回跳后,如果订单仍处于 pending 状态,可以在短时间内轮询,但要限制轮询次数和持续时间。

如果必须使用 WKWebView

只有当 gate2play 明确允许使用嵌入式 WebView,并确认支付页面涉及的银行验证、Cookie、弹窗和跳转都能正常工作时,才适合使用 WKWebView。部分 3-D Secure 或第三方认证页面会限制嵌入式浏览器,遇到这种情况应改用系统浏览器或 ASWebAuthenticationSession。

import WebKit

final class PaymentViewController: UIViewController, WKNavigationDelegate {
    private let webView = WKWebView()

    override func viewDidLoad() {
        super.viewDidLoad()

        webView.navigationDelegate = self
        webView.frame = view.bounds
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(webView)
    }

    func loadPaymentPage(_ url: URL) {
        webView.load(URLRequest(url: url))
    }

    func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
    ) {
        if navigationAction.request.url?.scheme == "myapp" {
            decisionHandler(.cancel)

            // 关闭支付页面,并向自己的服务端查询订单状态。
            dismiss(animated: true)
            return
        }

        decisionHandler(.allow)
    }
}

接入前需要向 gate2play 确认

由于公开的 iOS 接入资料不足,正式开发前应向 gate2play 的技术支持或商户对接人员索取以下信息:

  • 当前有效的 API 文档和测试环境地址
  • 商户号、密钥及签名算法
  • 创建支付会话的字段定义
  • 支付页面是否支持移动端和嵌入式 WebView
  • 回跳地址与服务端通知地址的配置方法
  • 3-D Secure 流程及浏览器限制
  • 支付状态枚举、错误码和重试规则
  • 回调验签、重复通知及主动查询接口说明
  • App Store 应用内购买政策方面的适用范围

如果对方只能提供网页支付接口,可以采用“服务端创建支付 + iOS 打开托管页面”的方式。如果既没有原生 SDK,也没有稳定的托管支付页、回调和验签文档,就不适合接入生产环境。

注意事项

不要在 iOS 应用中保存商户密钥,也不要由客户端生成支付签名。订单金额应由服务端根据商品和订单重新计算,不能直接采用客户端传入的金额。

如果用户购买的是应用内使用的数字内容或数字服务,还要确认是否必须使用 Apple 的 In-App Purchase。实体商品、线下服务等场景通常适用不同规则,具体应根据当前业务类型和 Apple 最新审核政策判断。

备注:内容仅供参考。