如何在 React Native 项目中集成 K-net Payment?
结论
React Native 通常没有一个通用的 KNET SDK 可以直接完成支付。建议采用以下流程:
- React Native 向业务后端发起创建支付订单的请求。
- 后端调用签约银行或支付服务商提供的 KNET 接口。
- App 打开服务商返回的托管支付页面。
- 支付完成后,支付网关通知业务后端。
- App 向后端查询最终支付状态。
KNET 的接口、认证字段和 SDK 由签约银行或支付服务商决定。只有服务商明确提供 React Native SDK 时,才应按照其官方文档接入。其他支付网关的参数和示例不能直接套用到 KNET。
推荐的集成结构
React Native App
│
│ 1. 创建支付订单
▼
业务后端
│
│ 2. 携带商户凭据请求 KNET 支付
▼
KNET / 支付服务商
│
│ 3. 返回托管支付页面 URL
▼
React Native App 打开支付页面
│
│ 4. 用户完成支付
▼
支付网关回调业务后端
│
│ 5. 验签并更新订单状态
▼
React Native App 查询最终状态
商户密钥、网关密码和签名证书等敏感信息必须保存在后端,不能写入 React Native 代码,也不能打包进 App。
开始集成前需要获取的信息
接入前,先向签约银行或支付服务商确认:
- 测试环境和生产环境的 API 地址
- 商户编号、终端编号等身份信息
- 认证方式,例如密钥、证书或请求签名
- 创建支付请求的字段定义
- 支付成功、失败和取消时的返回地址要求
- 服务端回调机制和验签规则
- 查询交易状态的接口
- 是否允许使用外部浏览器或 WebView
- 是否提供 iOS、Android 或 React Native SDK
如果服务商只提供原生 iOS 和 Android SDK,可以分别编写原生模块,再通过 React Native Native Modules 或 Turbo Native Modules 封装。若使用的是托管支付页面,通常不需要自行封装完整的原生 SDK。
第一步:由后端创建支付订单
下面是一个抽象的 Express 示例。代码中的字段名称和签名算法仅用于说明整体结构,必须替换为服务商文档规定的真实内容。
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/payments/knet', async (req, res) => {
const { orderId } = req.body;
// 应从数据库重新读取订单,不能信任客户端传入的金额。
const order = await findOrderById(orderId);
if (!order || order.status !== 'pending') {
return res.status(400).json({ message: 'Invalid order' });
}
const gatewayPayload = {
merchantOrderId: order.id,
amount: order.amount,
currency: 'KWD',
successUrl: 'https://api.example.com/payments/knet/return',
failureUrl: 'https://api.example.com/payments/knet/return',
callbackUrl: 'https://api.example.com/payments/knet/callback',
};
// 实际 URL、认证头和参数以支付服务商文档为准。
const gatewayResponse = await fetch(process.env.KNET_PAYMENT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.KNET_API_SECRET}`,
},
body: JSON.stringify(gatewayPayload),
});
if (!gatewayResponse.ok) {
return res.status(502).json({ message: 'Unable to initialize payment' });
}
const result = await gatewayResponse.json();
await saveGatewayTransaction({
orderId: order.id,
transactionId: result.transactionId,
status: 'initiated',
});
res.json({
paymentUrl: result.paymentUrl,
transactionId: result.transactionId,
});
});
订单金额应由后端根据订单数据计算。即使客户端提交了金额,也不能直接将其转发给支付网关。
第二步:在 React Native 中打开支付页面
如果服务商允许使用系统浏览器,可以调用 React Native 的 Linking:
import React, { useState } from 'react';
import {
Alert,
Button,
Linking,
View,
} from 'react-native';
export function KnetPaymentButton({ orderId }: { orderId: string }) {
const [loading, setLoading] = useState(false);
const startPayment = async () => {
try {
setLoading(true);
const response = await fetch(
'https://api.example.com/api/payments/knet',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ orderId }),
},
);
if (!response.ok) {
throw new Error('Unable to initialize payment');
}
const { paymentUrl } = await response.json();
const supported = await Linking.canOpenURL(paymentUrl);
if (!supported) {
throw new Error('Payment URL cannot be opened');
}
await Linking.openURL(paymentUrl);
} catch (error) {
Alert.alert('Payment error', 'Unable to start the payment process.');
} finally {
setLoading(false);
}
};
return (
<View>
<Button
title={loading ? 'Loading…' : 'Pay with KNET'}
disabled={loading}
onPress={startPayment}
/>
</View>
);
}
如果需要在 App 内显示支付页面,可以考虑使用 react-native-webview:
npm install react-native-webview
import React from 'react';
import { WebView } from 'react-native-webview';
export function KnetPaymentScreen({
paymentUrl,
}: {
paymentUrl: string;
}) {
return (
<WebView
source={{ uri: paymentUrl }}
javaScriptEnabled
domStorageEnabled
onNavigationStateChange={(navigation) => {
if (navigation.url.startsWith('https://api.example.com/payments/knet/return')) {
// 关闭支付页面,然后向后端查询订单状态。
}
}}
/>
);
}
能否使用 WebView,要以银行或支付服务商的要求为准。部分支付流程会涉及页面跳转、弹窗、Cookie 或唤起银行 App,此时系统浏览器或原生浏览器组件通常有更好的兼容性。
第三步:处理回调并确认支付结果
支付返回页只能说明用户的浏览器发生了跳转,不能单独证明支付成功。后端必须验证服务端回调,并在必要时主动调用交易查询接口。
app.post('/api/payments/knet/callback', async (req, res) => {
const callback = req.body;
// verifyKnetCallback 必须按照服务商提供的验签规范实现。
const valid = verifyKnetCallback(callback);
if (!valid) {
return res.status(400).send('Invalid signature');
}
const transaction = await findTransaction(callback.transactionId);
if (!transaction) {
return res.status(404).send('Transaction not found');
}
// 建议再调用网关查询接口确认交易状态。
const verifiedStatus = await queryKnetTransaction(
callback.transactionId,
);
if (verifiedStatus === 'paid') {
await markOrderAsPaid(transaction.orderId);
} else if (verifiedStatus === 'failed') {
await markOrderAsFailed(transaction.orderId);
}
return res.status(200).send('OK');
});
回调处理必须具备幂等性。同一笔交易可能收到多次通知,后端不能因此重复扣减库存、重复发货或重复记录付款。
第四步:App 查询最终状态
用户返回 App 后,应通过订单编号向业务后端查询支付状态:
async function getPaymentStatus(orderId: string) {
const response = await fetch(
`https://api.example.com/api/orders/${encodeURIComponent(orderId)}/payment-status`,
);
if (!response.ok) {
throw new Error('Unable to query payment status');
}
return response.json();
}
app.get('/api/orders/:orderId/payment-status', async (req, res) => {
const order = await findOrderById(req.params.orderId);
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.json({
orderId: order.id,
status: order.paymentStatus,
});
});
支付状态可以设为 pending、paid、failed、cancelled 或 expired。页面跳转、Deep Link 和客户端参数只能用于改善交互,最终状态仍须以后端验证结果为准。
使用 Deep Link 返回 App
如果支付服务商允许自定义返回地址,可以配置 Universal Links、App Links 或自定义 URL Scheme,例如:
myapp://payment/result?orderId=ORDER_ID
监听返回链接的示例:
import { useEffect } from 'react';
import { Linking } from 'react-native';
export function usePaymentReturn(
onReturn: (orderId: string) => void,
) {
useEffect(() => {
const handleUrl = ({ url }: { url: string }) => {
const parsed = new URL(url);
if (
parsed.protocol === 'myapp:' &&
parsed.hostname === 'payment' &&
parsed.pathname === '/result'
) {
const orderId = parsed.searchParams.get('orderId');
if (orderId) {
onReturn(orderId);
}
}
};
const subscription = Linking.addEventListener('url', handleUrl);
Linking.getInitialURL().then((url) => {
if (url) {
handleUrl({ url });
}
});
return () => subscription.remove();
}, [onReturn]);
}
不能信任 Deep Link 中携带的 success=true、金额或交易状态。收到返回链接后,仍要调用后端接口进行确认。
关键注意事项
- 不要在 React Native 中保存商户密码、API Secret、私钥或签名证书。
- 不要让客户端决定订单金额、币种或商户订单号。
- 使用 HTTPS,并验证支付网关返回的签名。
- 为每笔请求设置唯一订单号,避免重复支付。
- 回调接口需要接受重复通知,并保证业务处理具备幂等性。
- 对成功、失败、取消、超时和状态未知等情况都要进行处理。
- App 在支付期间可能被关闭,不能只依赖页面状态或内存数据。
- 上线前需要测试网络中断、重复点击、回调延迟、用户关闭页面,以及支付成功但未返回 App 等场景。
- 日志中不要记录银行卡信息、完整认证数据或其他敏感支付信息。
- 只有后端查询结果或通过验签的服务端通知,才能作为发货、充值或将订单更新为已支付的依据。
如果还没有确定具体的 KNET 收单银行或支付服务商,应先完成商户签约并取得对应的集成文档。在缺少这些信息的情况下,可以先搭建上述通用流程,但无法准确确定请求字段、签名算法、支付 URL 和回调参数。
备注:内容仅供参考。
