如何在 iOS 应用中集成 `gate2play`?
结论
目前没有足够明确且可验证的 gate2play iOS 原生 SDK 集成资料,因此无法像常见支付 SDK 那样直接接入。较稳妥的做法是由服务端与 gate2play 通信并创建支付会话,iOS 端负责打开托管支付页面。支付完成后,再由服务端通过查询或回调确认结果。
具体的接口地址、签名算法和字段名称,必须以商户后台提供的文档为准,不能照搬其他支付平台的参数。
推荐的集成流程
支付流程可以这样设计:
- iOS 应用向自己的服务端提交订单。
- 服务端保存订单,并调用
gate2play的支付接口。 gate2play返回支付页面 URL 或功能相同的会话信息。- iOS 使用
ASWebAuthenticationSession或WKWebView打开支付页面。 - 支付结束后,页面跳转到预先配置的回跳地址。
gate2play向服务端发送支付通知,或者由服务端主动查询订单状态。- 服务端验证签名、金额、币种和订单号,然后更新订单。
- 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 最新审核政策判断。
备注:内容仅供参考。
