PAYATHON 2026

如何在 React-Native App 中添加类似 Starbucks 的移动支付功能?

支付老李

结论

技术上可行,但不是安装一个 React Native library 就能解决,也不建议将用户充值款直接结算到普通私人银行账户。

这类功能与 Starbucks 的储值模式相似,本质是“信用卡充值 + 闭环储值账户 + 商户消费”。React Native 负责支付界面和支付服务调用,难点则在支付通道、服务端账本、资金托管、退款与拒付处理,以及当地针对储值、预付卡和资金传输业务的监管要求。

没有公司时,部分支付服务商可能允许个体经营者或自然人以商户身份开户。不过,能否接受“用户充值并长期保留余额”的业务,仍取决于所在国家、具体模式和支付服务商的审核结果。不要先接入普通信用卡收款,再把充值伪装成商品销售,否则支付机构可能冻结资金、关闭账户或要求补充资质。

先确认业务是否属于储值

下面这种模式通常属于闭环储值:

  1. 用户用信用卡充值 100 元;
  2. App 显示余额 100 元;
  3. 余额只能购买你自己提供的商品或服务;
  4. 余额不能转给其他用户,通常也不能提现。

如果余额支持转账、提现或跨商户使用,业务性质就更接近电子钱包或资金传输服务,监管和合规要求会高很多,通常不适合由个人开发者直接经营。

还需要根据用户购买的内容区分支付方式:

  • 购买线下商品或现实世界服务,例如咖啡、配送、预约服务,通常使用信用卡支付通道。
  • 购买 App 内的数字内容、会员权益或虚拟功能,可能需要使用 Apple In-App Purchase 或 Google Play Billing。
  • 将信用卡资金兑换成可购买数字内容的 App 余额,也不一定能够绕过应用商店针对数字商品的支付规则。

最终是否允许,需要结合应用提供的商品类型、发布地区,以及提交审核时有效的 Apple、Google 政策来判断。

推荐的实现路径

1. 选择支持这种模式的支付服务商

联系支付服务商时,应说明完整的资金流,不要只问“能否收信用卡”:

用户使用信用卡为闭环账户充值,余额只能购买本商户提供的商品或服务,不可转账、不可提现。

需要确认这些问题:

  • 是否接受个人或个体经营者开户;
  • 是否允许 stored value、wallet top-up 或 prepaid balance;
  • 资金可以结算到哪种银行账户;
  • 是否要求成立公司或提供经营登记;
  • 是否提供身份验证、3D Secure、退款和拒付接口;
  • 是否需要由持牌合作方托管用户资金。

支付服务商支持信用卡收款,不代表它也支持储值充值。是否能够上线,应以其合规团队针对具体业务给出的书面审核结果为准。

2. 建立服务端,不要从 App 直接处理资金

基本的安全流程如下:

React Native App
        │
        │ 请求创建充值订单
        ▼
你的服务端 ────── 创建支付请求 ──────► 支付服务商
        ▲                                  │
        │                                  │ 返回客户端支付参数
        └──────────────────────────────────┘
        │
        │ App 使用官方 SDK 完成支付确认
        ▼
支付服务商 Webhook ──► 你的服务端 ──► 更新余额账本

服务端只能根据验证通过的 Webhook 更新余额。不能因为客户端显示“支付成功”就增加余额,因为客户端响应可能被伪造或重复提交,网络异常也可能造成状态不一致。

3. 用账本记录余额变化

不要只在用户表中保存一个可以直接修改的 balance 字段。更稳妥的做法是为每次余额变化保留不可随意覆盖的账本记录:

wallet_transactions
- id
- user_id
- type              // topup, purchase, refund, chargeback, adjustment
- amount_minor      // 最小货币单位,例如分
- currency
- status            // pending, succeeded, failed, reversed
- provider_event_id
- created_at

例如,人民币 100 元应以整数 10000 保存,避免浮点数误差:

const amountMinor = 10000; // CNY 100.00

支付成功、退款和拒付都要分别生成账本记录,以便保留完整的审计轨迹。

React Native 示例

下面的代码只展示通用结构。接口名称均为示例,不表示任何特定支付服务商一定允许储值业务。实际项目应使用所选服务商的官方 React Native SDK,并由服务端创建支付请求。

import React, { useState } from 'react';
import {Alert, Button, View} from 'react-native';

export function TopUpButton() {
  const [loading, setLoading] = useState(false);

  const topUp = async () => {
    if (loading) return;

    setLoading(true);

    try {
      const response = await fetch('https://api.example.com/wallet/topups', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: 'Bearer USER_ACCESS_TOKEN',
          'Idempotency-Key': crypto.randomUUID(),
        },
        body: JSON.stringify({
          amount: 10000,
          currency: 'CNY',
        }),
      });

      if (!response.ok) {
        throw new Error('Unable to create top-up');
      }

      const payment = await response.json();

      // 使用支付服务商的官方 SDK 确认付款。
      // 不要在这里直接增加本地或服务端余额。
      await confirmPaymentWithProvider(payment.clientSecret);

      Alert.alert(
        '支付处理中',
        '到账结果将由服务端确认,请稍后刷新余额。',
      );
    } catch (error) {
      Alert.alert('充值失败', '请检查支付状态后重试。');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View>
      <Button
        title={loading ? '处理中…' : '充值 ¥100'}
        disabled={loading}
        onPress={topUp}
      />
    </View>
  );
}

async function confirmPaymentWithProvider(clientSecret: string) {
  // 调用所选支付服务商的 React Native SDK。
  // 具体实现取决于支付服务商。
  void clientSecret;
}

服务端创建充值订单时,需要验证金额、币种和用户身份:

app.post('/wallet/topups', requireUser, async (req, res) => {
  const amount = Number(req.body.amount);
  const currency = String(req.body.currency);

  if (!Number.isInteger(amount) || amount < 100 || amount > 50000) {
    return res.status(400).json({error: 'Invalid amount'});
  }

  if (currency !== 'CNY') {
    return res.status(400).json({error: 'Unsupported currency'});
  }

  const topup = await createPendingTopup({
    userId: req.user.id,
    amount,
    currency,
    idempotencyKey: req.get('Idempotency-Key'),
  });

  const payment = await paymentProvider.createPayment({
    amount,
    currency,
    metadata: {
      topupId: topup.id,
      userId: req.user.id,
    },
  });

  res.json({
    topupId: topup.id,
    clientSecret: payment.clientSecret,
  });
});

Webhook 必须验证签名,并确保同一个事件只处理一次:

app.post('/payments/webhook', rawBodyMiddleware, async (req, res) => {
  let event;

  try {
    event = paymentProvider.verifyWebhook(
      req.body,
      req.headers['provider-signature'],
    );
  } catch {
    return res.status(400).send('Invalid signature');
  }

  await database.transaction(async (tx) => {
    const alreadyProcessed = await tx.webhookEvents.exists(event.id);

    if (alreadyProcessed) return;

    await tx.webhookEvents.insert({
      providerEventId: event.id,
    });

    if (event.type === 'payment.succeeded') {
      await tx.wallet.creditFromTopup({
        topupId: event.metadata.topupId,
        providerEventId: event.id,
      });
    }

    if (
      event.type === 'payment.refunded' ||
      event.type === 'payment.chargeback'
    ) {
      await tx.wallet.reverseTopup({
        topupId: event.metadata.topupId,
        providerEventId: event.id,
      });
    }
  });

  res.sendStatus(200);
});

其中的 paymentProvider、事件名称和字段都是抽象示例,需要替换为实际服务商提供的 API。

私人银行账户的问题

没有公司不代表完全无法收款,但普通个人银行账户通常不适合作为结算账户。实际运营中可能遇到以下问题:

  • 支付服务商要求提供商户注册信息或经营证明;
  • 银行账户姓名必须与商户主体一致;
  • 银行可能限制个人账户持续接收经营款项;
  • 用户充值款可能属于负债或代管资金,不能直接视为已经取得的营业收入;
  • 经营者需要处理税务申报、未消费余额、退款和消费者保护;
  • 交易量增加后,支付服务商可能重新审核账户并暂缓结算。

更现实的做法是先注册当地允许的个体经营主体或公司账户,再申请明确支持此类业务的商户支付产品。如果业务支持提现、用户间转账或跨商户支付,应优先寻找持牌钱包、支付机构或 Banking-as-a-Service 合作方。

必须处理的安全与风控

上线前至少要处理这些事项:

  • 不在 App 或自己的服务器中保存原始卡号、CVV;
  • 使用支付服务商提供的 SDK 或托管支付页面采集卡信息;
  • 使用 HTTPS,并妥善保存服务端密钥;
  • 为充值接口设置单笔、单日和账户累计限额;
  • 使用 Idempotency-Key 防止重复充值;
  • 验证 Webhook 签名,并记录已经处理的事件 ID;
  • 处理退款、拒付、盗刷和负余额;
  • 对异常设备、短期大量充值和多卡充值进行风控;
  • 明确余额有效期、退款条件,以及账户关闭后的余额处理方式;
  • 根据当地要求完成 KYC、AML、隐私和税务合规。

信用卡充值很容易出现“盗刷充值后立即消费”的情况。即使商品已经交付,持卡人仍可能发起拒付。因此,余额到账并不表示这笔资金已经没有风险。

React Native library 能解决什么

React Native 支付库通常可以处理以下客户端功能:

  • 展示银行卡输入组件;
  • 调用原生支付 SDK;
  • Tokenization;
  • 3D Secure 身份验证;
  • Apple Pay 或 Google Pay 的客户端交互;
  • 向服务端传递支付确认结果。

它不能代替你完成商户开户、资金结算、储值许可、服务端账本或应用商店合规。选库时应优先使用支付服务商维护的官方 SDK。如果服务商没有 React Native SDK,可以使用其托管结账页面,也可以编写原生桥接模块,但服务端和合规审核仍不可缺少。

正确的顺序是先确认支付服务商是否接受你的业务模式和经营主体,再选择相应的 React Native SDK。找到一个信用卡组件,并不表示这种资金模式能够合法、稳定地上线。

备注:内容仅供参考。