PAYATHON 2026

如何在 React Native 项目中集成 K-net Payment?

支付阿杰

结论

React Native 通常没有一个通用的 KNET SDK 可以直接完成支付。建议采用以下流程:

  1. React Native 向业务后端发起创建支付订单的请求。
  2. 后端调用签约银行或支付服务商提供的 KNET 接口。
  3. App 打开服务商返回的托管支付页面。
  4. 支付完成后,支付网关通知业务后端。
  5. 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 和客户端参数只能用于改善交互,最终状态仍须以后端验证结果为准。

如果支付服务商允许自定义返回地址,可以配置 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 和回调参数。

备注:内容仅供参考。