Skip to content

useInvoice

提供 Telegram 原生支付功能的 Hook,让你的 Mini App 可以接收支付并处理订单。

导入

typescript
import { useInvoice } from '@xcloud/ui-telegram';

示例

API

useInvoice

typescript
const { openInvoice, status, loading, error, isAvailable } = useInvoice(options);

参数

参数类型必填描述
optionsInvoiceOptionsHook 配置选项

InvoiceOptions

属性类型描述
onSuccess() => void支付成功回调
onFailure(status: InvoiceStatus) => void支付失败回调
onCancel() => void支付取消回调

返回值

属性类型描述
openInvoice(params: InvoiceParams) => Promise<InvoiceStatus | null>打开支付发票
statusInvoiceStatus | null当前支付状态
loadingboolean是否正在处理支付
errorError | null错误信息
isAvailable() => boolean检查支付功能是否可用

InvoiceParams

属性类型必填描述
slugstring否*发票 slug(与 url 二选一)
urlstring否*发票 URL(与 slug 二选一)

InvoiceStatus

支付状态类型:

描述
'paid'支付成功
'failed'支付失败
'pending'支付待处理
'cancelled'用户取消支付

使用示例

基础支付 - 使用 Slug

使用产品 slug 发起支付:

tsx
import { useInvoice } from '@xcloud/ui-telegram';

function ProductPage({ productSlug }: { productSlug: string }) {
  const { openInvoice, loading } = useInvoice();

  const handlePurchase = async () => {
    const result = await openInvoice({ slug: productSlug });

    if (result === 'paid') {
      alert('购买成功!');
    }
  };

  return (
    <button onClick={handlePurchase} disabled={loading}>
      {loading ? '处理中...' : '立即购买'}
    </button>
  );
}

基础支付 - 使用 URL

使用发票 URL 发起支付(通常来自扫码或链接):

tsx
import { useInvoice } from '@xcloud/ui-telegram';

function PaymentPage({ invoiceUrl }: { invoiceUrl: string }) {
  const { openInvoice, loading } = useInvoice();

  const handlePay = async () => {
    const result = await openInvoice({ url: invoiceUrl });

    if (result === 'paid') {
      // 支付成功处理
      confirmOrder();
    }
  };

  return (
    <button onClick={handlePay} disabled={loading}>
      确认支付
    </button>
  );
}

使用回调处理支付结果

通过回调函数处理不同的支付状态:

tsx
import { useInvoice } from '@xcloud/ui-telegram';

function CheckoutButton() {
  const { openInvoice } = useInvoice({
    onSuccess: () => {
      console.log('支付成功!');
      showSuccessMessage();
      redirectToOrderPage();
    },
    onFailure: (status) => {
      console.error('支付失败:', status);
      showErrorMessage('支付失败,请重试');
    },
    onCancel: () => {
      console.log('用户取消了支付');
      showInfoMessage('支付已取消');
    },
  });

  const handleCheckout = async () => {
    await openInvoice({ slug: 'product_123' });
  };

  return <button onClick={handleCheckout}>结账</button>;
}

电商购物车支付

完整的购物车支付流程:

tsx
import { useInvoice } from '@xcloud/ui-telegram';
import { useState } from 'react';

function ShoppingCart({ items, totalAmount }: CartProps) {
  const [orderId, setOrderId] = useState<string | null>(null);

  const { openInvoice, loading } = useInvoice({
    onSuccess: () => {
      // 标记订单为已支付
      markOrderAsPaid(orderId);
      // 清空购物车
      clearCart();
      // 跳转到订单详情
      navigate(`/orders/${orderId}`);
    },
    onFailure: () => {
      // 取消订单
      cancelOrder(orderId);
    },
  });

  const handleCheckout = async () => {
    // 1. 创建订单
    const order = await createOrder(items);
    setOrderId(order.id);

    // 2. 生成发票
    const invoice = await generateInvoice(order.id);

    // 3. 打开支付
    await openInvoice({ slug: invoice.slug });
  };

  return (
    <div>
      <CartItems items={items} />
      <Total amount={totalAmount} />
      <button onClick={handleCheckout} disabled={loading}>
        {loading ? '处理中...' : `支付 $${totalAmount.toFixed(2)}`}
      </button>
    </div>
  );
}

订阅支付

处理订阅或会员支付:

tsx
import { useInvoice } from '@xcloud/ui-telegram';

function SubscriptionPlans() {
  const { openInvoice, loading } = useInvoice({
    onSuccess: () => {
      // 激活订阅
      activateSubscription();
      showSuccessToast('订阅已激活!');
    },
  });

  const handleSubscribe = async (plan: 'monthly' | 'yearly') => {
    // 生成订阅发票
    const invoice = await createSubscriptionInvoice(plan);

    // 打开支付
    const result = await openInvoice({ slug: invoice.slug });

    if (result === 'paid') {
      // 记录订阅开始时间
      recordSubscriptionStart(plan);
    }
  };

  return (
    <div>
      <PlanCard
        name="月度订阅"
        price={9.99}
        onSelect={() => handleSubscribe('monthly')}
        loading={loading}
      />
      <PlanCard
        name="年度订阅"
        price={99.99}
        onSelect={() => handleSubscribe('yearly')}
        loading={loading}
      />
    </div>
  );
}

虚拟商品购买

购买应用内虚拟商品:

tsx
import { useInvoice } from '@xcloud/ui-telegram';

function CoinShop() {
  const { openInvoice, loading } = useInvoice({
    onSuccess: () => {
      // 发放虚拟货币
      addCoins(selectedPackage.coins);
      showReward(`+${selectedPackage.coins} 金币`);
    },
  });

  const coinPackages = [
    { id: 1, coins: 100, price: 0.99 },
    { id: 2, coins: 500, price: 3.99 },
    { id: 3, coins: 1200, price: 8.99 },
  ];

  const handlePurchase = async (pkg: CoinPackage) => {
    const invoice = await createCoinPurchaseInvoice(pkg.id);
    await openInvoice({ slug: invoice.slug });
  };

  return (
    <div>
      {coinPackages.map((pkg) => (
        <PackageCard
          key={pkg.id}
          coins={pkg.coins}
          price={pkg.price}
          onBuy={() => handlePurchase(pkg)}
          loading={loading}
        />
      ))}
    </div>
  );
}

捐赠/打赏功能

实现创作者打赏功能:

tsx
import { useInvoice } from '@xcloud/ui-telegram';
import { useState } from 'react';

function TipCreator({ creatorId }: { creatorId: string }) {
  const [amount, setAmount] = useState(5);

  const { openInvoice, loading } = useInvoice({
    onSuccess: () => {
      showThankYouMessage();
      sendNotificationToCreator(creatorId, amount);
    },
  });

  const handleTip = async () => {
    const invoice = await createTipInvoice(creatorId, amount);
    await openInvoice({ slug: invoice.slug });
  };

  const presetAmounts = [5, 10, 20, 50];

  return (
    <div>
      <h3>打赏创作者</h3>
      <div>
        {presetAmounts.map((preset) => (
          <button
            key={preset}
            onClick={() => setAmount(preset)}
            className={amount === preset ? 'selected' : ''}
          >
            ${preset}
          </button>
        ))}
      </div>
      <button onClick={handleTip} disabled={loading}>
        {loading ? '处理中...' : `打赏 $${amount}`}
      </button>
    </div>
  );
}

处理支付错误

完整的错误处理:

tsx
import { useInvoice } from '@xcloud/ui-telegram';

function RobustCheckout() {
  const { openInvoice, loading, error } = useInvoice({
    onFailure: (status) => {
      if (status === 'failed') {
        showError('支付处理失败,请稍后重试');
      }
    },
  });

  const handlePay = async () => {
    try {
      const result = await openInvoice({ slug: 'product_123' });

      if (!result) {
        // 用户可能关闭了支付窗口
        console.log('Payment was not completed');
        return;
      }

      if (result === 'paid') {
        // 支付成功
        processOrder();
      } else if (result === 'cancelled') {
        // 用户明确取消
        showInfo('您已取消支付');
      }
    } catch (err) {
      console.error('Payment error:', err);
      showError('支付过程中出现错误');
    }
  };

  return (
    <div>
      <button onClick={handlePay} disabled={loading}>
        支付
      </button>
      {error && <ErrorMessage>{error.message}</ErrorMessage>}
    </div>
  );
}

检查可用性

在不支持的环境中提供替代方案:

tsx
import { useInvoice } from '@xcloud/ui-telegram';

function PaymentButton() {
  const { openInvoice, isAvailable } = useInvoice();

  if (!isAvailable()) {
    return (
      <div>
        <p>当前环境不支持 Telegram 支付</p>
        <button onClick={redirectToWebPayment}>
          使用网页支付
        </button>
      </div>
    );
  }

  const handlePay = async () => {
    await openInvoice({ slug: 'product_123' });
  };

  return <button onClick={handlePay}>Telegram 支付</button>;
}

与 QR 扫码集成

扫码支付流程:

tsx
import { useInvoice, useQRScanner } from '@xcloud/ui-telegram';

function QRPayment() {
  const { open: openScanner } = useQRScanner();
  const { openInvoice, loading } = useInvoice({
    onSuccess: () => {
      showSuccess('支付成功!');
    },
  });

  const handleScanAndPay = async () => {
    // 1. 扫描二维码
    const invoiceUrl = await openScanner('扫描支付二维码');

    if (!invoiceUrl) {
      return; // 用户取消扫描
    }

    // 2. 验证是否为有效的支付链接
    if (!invoiceUrl.startsWith('https://t.me/$')) {
      showError('无效的支付二维码');
      return;
    }

    // 3. 打开支付
    await openInvoice({ url: invoiceUrl });
  };

  return (
    <button onClick={handleScanAndPay} disabled={loading}>
      {loading ? '处理中...' : '扫码支付'}
    </button>
  );
}

后端集成

生成发票

在后端创建支付发票:

typescript
// 后端 API 端点
async function createInvoice(productId: string, userId: string) {
  // 1. 验证产品和用户
  const product = await getProduct(productId);
  const user = await getUser(userId);

  // 2. 创建订单
  const order = await createOrder({
    userId,
    productId,
    amount: product.price,
    status: 'pending',
  });

  // 3. 通过 Telegram Bot API 创建发票
  const invoice = await telegramBot.createInvoiceLink({
    title: product.name,
    description: product.description,
    payload: order.id, // 用于回调时识别订单
    provider_token: process.env.PAYMENT_PROVIDER_TOKEN,
    currency: 'USD',
    prices: [{ label: product.name, amount: product.price * 100 }], // 价格以分为单位
  });

  return {
    invoiceUrl: invoice.url,
    invoiceSlug: extractSlugFromUrl(invoice.url),
  };
}

处理支付回调

接收并处理支付成功的回调:

typescript
// Webhook 端点
async function handlePaymentCallback(update: TelegramUpdate) {
  if (update.pre_checkout_query) {
    // 预检查支付
    const { id, invoice_payload } = update.pre_checkout_query;
    const orderId = invoice_payload;

    // 验证订单
    const order = await getOrder(orderId);
    if (!order || order.status !== 'pending') {
      // 拒绝支付
      await telegramBot.answerPreCheckoutQuery(id, false, '订单无效');
      return;
    }

    // 允许支付
    await telegramBot.answerPreCheckoutQuery(id, true);
  }

  if (update.message?.successful_payment) {
    // 支付成功
    const { invoice_payload, total_amount, currency } =
      update.message.successful_payment;
    const orderId = invoice_payload;

    // 更新订单状态
    await updateOrder(orderId, {
      status: 'paid',
      paidAt: new Date(),
      amount: total_amount / 100, // 转换回实际金额
    });

    // 发放商品/服务
    await fulfillOrder(orderId);

    // 发送确认消息
    await telegramBot.sendMessage(
      update.message.chat.id,
      '支付成功!您的订单已处理。',
    );
  }
}

最佳实践

1. 始终验证支付结果

不要仅依赖客户端的支付状态:

tsx
const handlePay = async () => {
  const result = await openInvoice({ slug: productSlug });

  if (result === 'paid') {
    // 向后端确认支付状态
    const verified = await verifyPayment(orderId);
    if (verified) {
      // 确认支付成功
      processOrder();
    }
  }
};

2. 提供清晰的用户反馈

让用户知道支付的每一步:

tsx
const { openInvoice, loading } = useInvoice({
  onSuccess: () => {
    showSuccess('支付成功!正在处理您的订单...');
  },
  onFailure: () => {
    showError('支付失败,请重试或联系客服');
  },
  onCancel: () => {
    showInfo('您已取消支付');
  },
});

3. 处理网络错误

支付过程可能因网络问题中断:

tsx
const handlePay = async () => {
  try {
    await openInvoice({ slug: productSlug });
  } catch (error) {
    if (error.message.includes('network')) {
      showError('网络连接失败,请检查网络后重试');
    } else {
      showError('支付出现问题,请稍后重试');
    }
  }
};

4. 防止重复支付

避免用户多次点击造成重复支付:

tsx
function CheckoutButton() {
  const [isPaying, setIsPaying] = useState(false);
  const { openInvoice, loading } = useInvoice();

  const handlePay = async () => {
    if (isPaying) return;

    setIsPaying(true);
    try {
      await openInvoice({ slug: productSlug });
    } finally {
      setIsPaying(false);
    }
  };

  return (
    <button onClick={handlePay} disabled={loading || isPaying}>
      {loading || isPaying ? '处理中...' : '支付'}
    </button>
  );
}

5. 记录支付日志

便于追踪和调试:

tsx
const { openInvoice } = useInvoice({
  onSuccess: () => {
    logPaymentEvent('payment_success', { orderId, amount });
  },
  onFailure: (status) => {
    logPaymentEvent('payment_failure', { orderId, status });
  },
});

注意事项

  • 支付功能仅在 Telegram 环境中可用
  • 需要 Bot 配置支付提供商(如 Stripe、YooMoney 等)
  • 货币和金额由后端控制,前端只负责发起支付
  • 支付结果应该在后端通过 webhook 验证
  • 不要在客户端存储敏感的支付信息
  • 测试时使用 Telegram 提供的测试支付环境

支持的支付提供商

Telegram Payments 支持多个支付提供商:

  • Stripe
  • YooMoney (formerly Yandex.Money)
  • Sberbank
  • Tranzzo
  • PayMaster
  • 等等

具体可用的支付方式取决于你的 Bot 配置和用户所在地区。

相关 API

基于 MIT 许可发布