如何将 Payment Gateway 集成到 NativeScript App
结论
不要在 NativeScript 前端直接集成 Payment Gateway,也不要把”是否有开源 JavaScript API”作为首要选型标准。
更合适的结构通常是:
- NativeScript App 请求业务后端创建订单。
- 后端调用 Payment Gateway 的服务端 API。
- App 通过支付服务商提供的原生 SDK、Hosted Checkout 或安全的浏览器跳转完成付款。
- Payment Gateway 使用 Webhook 将最终支付结果通知后端。
- App 从后端查询订单状态。
JavaScript 库能够运行,不代表它能安全、完整地用于 NativeScript。NativeScript 既不是普通浏览器,也不具备完整的 Node.js 环境。一个库能否使用,要看它依赖哪些运行时 API,以及支付服务商是否支持 Android、iOS 和移动端支付流程。
如果 App 销售应用内使用的数字内容或数字服务,还应先确认 Apple App Store 和 Google Play 的应用内购买规则。这类商品未必允许使用第三方 Payment Gateway。普通网关支付通常更适合实体商品和线下服务,但仍要遵守应用商店的现行规则及所在地区的要求。
推荐的集成架构
可以采用下面的支付流程:
NativeScript App
|
| 1. 创建订单
v
业务后端
|
| 2. 使用服务端密钥调用网关
v
Payment Gateway / IPG
|
| 3. 返回 checkout URL、session ID 或 client secret
v
NativeScript App
|
| 4. Hosted Checkout / 原生 SDK / 系统浏览器
v
用户完成支付
|
| 5. Webhook 通知
v
业务后端更新订单状态
|
| 6. App 查询结果
v
显示支付成功或失败
支付金额、货币、商品信息和订单状态必须由后端控制。不要信任 App 上传的金额,也不能因为客户端跳转到了”成功页面”,就将订单标记为已付款。
编写代码前需要确认的信息
商品类型
先确认销售的是哪类商品或服务:
- 实体商品
- 线下服务
- 应用外消费的服务
- 数字内容
- 会员、订阅或应用内功能
如果销售数字商品、订阅或应用内功能,应优先研究 Apple In-App Purchase 和 Google Play Billing,而不是直接选择 TeleCash 一类普通 Payment Gateway。
目标市场和付款方式
“Supported payment methods”并不是由 JavaScript 库决定的。实际可用的付款方式取决于服务提供商、商户账户、签约地区、币种和风险审核,可能包括:
- Credit/Debit Card
- Apple Pay
- Google Pay
- PayPal
- SEPA Direct Debit
- Bank Transfer
- 本地钱包或本地银行支付
- 分期付款
- Recurring Payment
即使 Gateway 的文档列出了某种付款方式,也不代表你的商户账户会自动获得相应能力。
服务提供商的移动端能力
选型时需要检查:
- 是否提供 Android SDK
- 是否提供 iOS SDK
- 是否支持 Hosted Checkout
- 是否支持系统浏览器和 Deep Link 回跳
- 是否支持 3-D Secure 2
- 是否提供 Webhook
- 是否提供 Sandbox/Test Environment
- 是否支持 Refund、Partial Refund 和取消授权
- 是否支持 Tokenization
- 是否支持目标国家、币种和结算账户
- 如何划分 PCI DSS 责任范围
- 是否有正式维护的文档和支持渠道
如果现有网站已经能够收款,应先了解网站目前使用的 Gateway。复用同一个商户账户和后端订单系统,通常比为 App 重新选择一套网关更合理。
通用 JavaScript API 未必适用于 NativeScript
NativeScript 可以使用 JavaScript 或 TypeScript 编写业务逻辑,也能访问 Android 和 iOS 原生 API,但它不是浏览器环境。
JavaScript 库可能依赖以下能力:
windowdocument- DOM
<script>标签- 浏览器 Cookie
localStorage- iframe
- Node.js 的
fs、http、crypto或Buffer - 浏览器专用的重定向和弹窗机制
NativeScript 可能不提供这些能力,或者其行为与浏览器不同。因此,不能只因为一个库使用 JavaScript 编写,就认定它兼容 NativeScript。
这类库大致可以分成三类。
纯 JavaScript 库
如果一个库只负责数据转换、签名格式处理或普通业务计算,不依赖 DOM、Node.js 内置模块或特定浏览器 API,它可能可以使用,但仍需实际测试。
浏览器端支付库
依赖 DOM、iframe 或服务商网页脚本的库,通常不能直接放入 NativeScript 页面运行。此类方案一般应改用 Hosted Checkout,并通过系统浏览器或服务商认可的 WebView 流程打开。
不要自行认定 WebView 一定受支持。由于 3-D Secure、Cookie、安全验证或第三方钱包兼容性等原因,有些服务商会明确要求使用系统浏览器或原生 SDK。
Node.js 服务端 SDK
这类 SDK 应运行在业务后端,而不是 NativeScript App 中。即使使用 polyfill 后能够勉强打包,也不能将 Secret API Key 放入客户端。
App 安装包中的密钥可以被提取,因此不要把以下内容放入 App:
Secret API Key
Merchant Secret
Webhook Secret
Private Signing Key
网关管理账号密码
三种常见接入方式
Hosted Checkout
对于刚开始接触支付集成的项目,Hosted Checkout 通常是最合适的方式。
后端创建支付会话后,将网关提供的支付页面 URL 返回给 App。App 使用系统浏览器打开该页面,付款完成后再通过 Deep Link 或 Universal Link/App Link 返回。
它有以下优点:
- App 不直接处理银行卡数据
- PCI DSS 范围通常较小
- 3-D Secure 等验证流程由服务商处理
- 支付页面可以统一呈现多种付款方式
- Android 与 iOS 的实现差异较少
回跳只能用于改善用户体验,不能作为最终的支付凭证。最终状态仍应以经过验证的 Webhook 或服务端查询结果为准。
原生 Android/iOS SDK
如果服务商提供 Android 和 iOS SDK,却没有 NativeScript 插件,可以采用以下方式:
- 寻找仍在维护的 NativeScript 社区插件
- 自行封装 NativeScript 插件
- 在 NativeScript 中直接调用对外暴露的原生 API
原生 SDK 可以提供更好的原生体验,但 Android 和 iOS 需要分别处理,包括回调、生命周期、Deep Link、Gradle/CocoaPods 依赖及 SDK 升级。
自建银行卡输入界面
除非服务商提供合规的 Hosted Fields、Tokenization 组件或原生安全控件,否则不建议自行采集银行卡号、CVV 等敏感信息。
直接处理银行卡数据会明显扩大安全和 PCI DSS 合规范围,仅使用 HTTPS 无法解决这些问题。
一个简化的实现示例
下面的代码只用于说明推荐的职责划分。gateway.example、字段名和返回结构都是占位示例,实际代码必须遵循所选服务商的 API 文档。
NativeScript 创建支付订单
import { ApplicationSettings, Utils } from "@nativescript/core";
interface CheckoutResponse {
orderId: string;
checkoutUrl: string;
}
export async function startCheckout(productId: string): Promise<void> {
const response = await fetch("https://api.example.com/orders/checkout", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${ApplicationSettings.getString("accessToken")}`
},
body: JSON.stringify({
productId,
quantity: 1
})
});
if (!response.ok) {
throw new Error(`Unable to create checkout: ${response.status}`);
}
const checkout = await response.json() as CheckoutResponse;
// 示例使用系统浏览器。实际项目还需要处理 Deep Link 回跳。
await Utils.openUrl(checkout.checkoutUrl);
}
这里有意只发送 productId 和数量,不发送可被视为可信的金额。后端应根据数据库中的商品价格计算应付金额。
后端创建支付会话
import express from "express";
import crypto from "node:crypto";
const app = express();
app.use(express.json());
app.post("/orders/checkout", requireUser, async (req, res) => {
const product = await products.findById(req.body.productId);
if (!product || !product.active) {
return res.status(404).json({ error: "Product not found" });
}
const order = await orders.create({
userId: req.user.id,
productId: product.id,
quantity: 1,
amount: product.price,
currency: product.currency,
status: "pending"
});
const gatewayResponse = await fetch(
"https://gateway.example/v1/checkout-sessions",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GATEWAY_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": order.id
},
body: JSON.stringify({
amount: order.amount,
currency: order.currency,
merchantReference: order.id,
successUrl: `https://example.com/payment/return?order=${order.id}`,
cancelUrl: `https://example.com/payment/cancel?order=${order.id}`
})
}
);
if (!gatewayResponse.ok) {
await orders.markPaymentCreationFailed(order.id);
return res.status(502).json({ error: "Payment provider rejected request" });
}
const session = await gatewayResponse.json();
await orders.attachGatewaySession(order.id, session.id);
res.json({
orderId: order.id,
checkoutUrl: session.checkoutUrl
});
});
生产环境还需要处理超时、重试、日志脱敏、幂等冲突和 Gateway 返回的具体错误码。
Webhook 更新订单
app.post(
"/webhooks/payment",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.get("X-Gateway-Signature");
if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (await webhookEvents.exists(event.id)) {
return res.sendStatus(200);
}
await database.transaction(async (tx) => {
await webhookEvents.record(event.id, tx);
if (event.type === "payment.succeeded") {
await orders.markPaid(
event.data.merchantReference,
event.data.paymentId,
tx
);
}
if (event.type === "payment.failed") {
await orders.markFailed(
event.data.merchantReference,
event.data.reason,
tx
);
}
});
res.sendStatus(200);
}
);
verifyWebhook 的算法、签名 Header 和原始请求体要求,必须严格按照服务商文档实现,不能自行设计。Webhook 还必须具备幂等性,因为同一个事件可能被重复投递。
Payment Gateway 与 IPG 的区别
Payment Gateway 是一个通用术语,指商户系统与支付处理体系之间的接入服务。它通常负责安全传输支付信息、发起授权、处理验证,并将支付结果返回给商户。
IPG 常见的完整写法是 Internet Payment Gateway,侧重互联网或电子商务支付。在许多商业文档中,IPG 和 Payment Gateway 指的是同一类产品,只是命名范围不同。
IPG 并不是一种含义全球统一的技术标准,具体含义应以服务商合同和文档中的定义为准。
Payment Gateway 也不一定是最终完成资金清算的一方。一次银行卡支付还可能涉及:
- Merchant
- Payment Gateway
- Payment Service Provider
- Acquirer
- Card Network
- Issuer
- Payment Processor
有些服务商会打包提供上述多种能力,因此产品页面使用的名称未必严格一致。
“开源 API”不是合适的判断标准
API 是服务提供商公开的调用接口,SDK 或客户端库才可能是开源软件。即使 SDK 开源,实际交易仍依赖服务商的服务器、商户合同和密钥,不会因此成为免费、独立的支付系统。
评估集成方案时,更需要关注:
- 是否提供正式、稳定的 API
- SDK 是否持续维护
- 是否符合当前 Android 和 iOS 的要求
- 是否提供测试环境
- 是否支持 Webhook 和幂等机制
- 是否支持所需的付款方式
- 合规责任是否划分清楚
- 费率、结算周期和退款流程是否合适
- 出现争议、拒付或故障时能否获得支持
社区开源插件可以减少封装工作,但不能成为支付系统的信任边界。使用前应检查插件最近一次维护的时间、依赖版本、Issue、敏感数据处理方式,以及 Android 和 iOS 是否都得到了实际支持。
关于 TeleCash
不能只根据”文档是否完善”或”SDK 是否开源”,判断 TeleCash 的文档或 API 是否适合使用。还需要向服务商或现有网站的维护方确认:
- 现有网站使用的是哪个具体产品和接口版本
- 当前商户合同是否允许 Mobile App 交易
- 支持哪些国家、币种和付款方式
- 是否提供 Hosted Checkout 或移动 SDK
- 是否支持 3-D Secure 2
- 是否允许在 WebView 中运行
- App 回跳应如何配置
- Webhook 如何验签
- 如何申请测试账号和完成认证流程
- 是否支持 Tokenization、退款和周期性付款
如果网站已经通过 TeleCash 收款,通常更适合让 NativeScript App 复用网站的支付后端,而不是将 TeleCash 接口重新直接移植到 App。
上线前不能忽略的问题
支付功能至少需要处理以下情况:
- 用户重复点击付款按钮
- 网络超时,但支付实际已经成功
- App 在付款过程中被关闭
- Deep Link 未能成功返回 App
- Webhook 重复、延迟或乱序到达
- 客户端篡改支付金额或币种
- 付款成功后业务发货失败
- 退款、部分退款和拒付
- 订单过期
- 3-D Secure 验证被取消或失败
- 日志意外记录卡号、Token 或 Secret
- 测试环境与生产环境的密钥被混用
订单状态也不应只有 success 和 failed。通常至少需要区分:
created
pending
authorized
paid
failed
cancelled
expired
partially_refunded
refunded
chargeback
具体状态仍要根据业务需求和 Gateway 的状态模型确定。
实际开始时,不必继续寻找”适用于所有 IPG 的 NativeScript 开源 API”。应先确认商品类型、应用商店规则、现有网站使用的支付提供商和所需付款方式。确定服务商后,再从其 Hosted Checkout、Android SDK、iOS SDK 和服务端 API 中选择合适的接入方式。
备注:内容仅供参考。
