/** * WechatPayment Component * ───────────────────────────────────────────────────────────────────────────── * Handles WeChat Pay Native (QR code) for PC and H5 for mobile browsers. * * Flow: * 1. User enters CNY amount * 2. Component calls payment.createWechatOrder mutation * 3. PC (NATIVE): displays QR code for user to scan with WeChat * Mobile (H5): redirects to WeChat H5 payment page * 4. Polls payment.queryWechatOrder every 5 seconds * 5. On success (dbStatus === "distributed"), shows success message * * Note: JSAPI pay (inside WeChat browser) requires openid — not implemented here. * For WeChat browser users, the H5 pay type is used as fallback. */ import { useState, useEffect } from "react"; import { trpc } from "@/lib/trpc"; import { toast } from "sonner"; const XIC_PRICE_USD = 0.02; const CNY_USD_RATE = 0.138; function calcXicFromCny(cny: number): number { const usd = cny * CNY_USD_RATE; return Math.floor(usd / XIC_PRICE_USD); } // Simple QR code display using a public QR API (no external dependency needed) // In production, use a proper QR library like qrcode.react function QRCodeDisplay({ url, size = 200 }: { url: string; size?: number }) { // Use Google Charts QR API as fallback (works without npm package) // TODO: Replace with qrcode.react for production (npm install qrcode.react) const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(url)}&bgcolor=0a0a0f&color=f0b429&margin=10`; return (
WeChat Pay QR Code { // Fallback: show text URL if image fails (e.target as HTMLImageElement).style.display = "none"; }} />

Scan with WeChat to pay

); } interface WechatPaymentProps { xicReceiveAddress: string; onSuccess?: (xicAmount: number, orderId: string) => void; } export default function WechatPayment({ xicReceiveAddress, onSuccess }: WechatPaymentProps) { const [cnyInput, setCnyInput] = useState("100"); const [orderId, setOrderId] = useState(null); const [qrCodeUrl, setQrCodeUrl] = useState(null); const [h5Url, setH5Url] = useState(null); const [paymentStatus, setPaymentStatus] = useState<"idle" | "waiting" | "success" | "failed">("idle"); const cnyAmount = parseFloat(cnyInput) || 0; const xicAmount = calcXicFromCny(cnyAmount); const fenAmount = Math.round(cnyAmount * 100); // convert to fen (integer) const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); const isWechat = /MicroMessenger/i.test(navigator.userAgent); const payType = isWechat ? "JSAPI" : isMobile ? "H5" : "NATIVE"; const createOrder = trpc.payment.createWechatOrder.useMutation({ onSuccess: (data) => { setOrderId(data.orderId); setPaymentStatus("waiting"); if (data.qrCodeUrl) { setQrCodeUrl(data.qrCodeUrl); } else if (data.h5Url) { setH5Url(data.h5Url); window.location.href = data.h5Url; } else if (data.jsapiParams) { // JSAPI: call WeixinJSBridge (inside WeChat browser) invokeWechatJsapi(data.jsapiParams); } }, onError: (err) => { toast.error(`Failed to create WeChat Pay order: ${err.message}`); }, }); const queryOrder = trpc.payment.queryWechatOrder.useQuery( { orderId: orderId! }, { enabled: !!orderId && paymentStatus === "waiting", refetchInterval: 5000, refetchIntervalInBackground: true, } ); useEffect(() => { if (!queryOrder.data) return; const { dbStatus, xicAmount: dbXicAmount } = queryOrder.data as any; if (dbStatus === "distributed" || dbStatus === "paid") { setPaymentStatus("success"); onSuccess?.(parseFloat(dbXicAmount || "0"), orderId!); toast.success(`Payment confirmed! ${parseFloat(dbXicAmount || "0").toLocaleString()} XIC tokens will be distributed.`); } else if (dbStatus === "failed" || dbStatus === "expired") { setPaymentStatus("failed"); toast.error("Payment failed or expired. Please try again."); } }, [queryOrder.data]); function invokeWechatJsapi(params: any) { const wx = (window as any).WeixinJSBridge; if (!wx) { toast.error("WeChat browser not detected. Please open this page in WeChat."); return; } wx.invoke("getBrandWCPayRequest", { appId: params.appId, timeStamp: params.timeStamp, nonceStr: params.nonceStr, package: params.package, signType: params.signType, paySign: params.paySign, }, (res: any) => { if (res.err_msg === "get_brand_wcpay_request:ok") { setPaymentStatus("success"); toast.success("Payment successful!"); } else if (res.err_msg === "get_brand_wcpay_request:cancel") { toast.info("Payment cancelled."); setPaymentStatus("idle"); } else { toast.error("Payment failed. Please try again."); setPaymentStatus("failed"); } }); } const handlePay = () => { if (!xicReceiveAddress || xicReceiveAddress.length < 10) { toast.error("Please enter your XIC receive address first."); return; } if (cnyAmount < 0.01) { toast.error("Minimum payment is ¥0.01 CNY."); return; } createOrder.mutate({ totalFen: fenAmount, xicReceiveAddress, payType, }); }; const handleReset = () => { setOrderId(null); setQrCodeUrl(null); setH5Url(null); setPaymentStatus("idle"); setCnyInput("100"); }; // ── Success State ────────────────────────────────────────────────────────── if (paymentStatus === "success") { return (
🎉

Payment Successful!

{xicAmount.toLocaleString()} {" "} XIC tokens are being distributed to your address.

); } // ── QR Code Waiting State ────────────────────────────────────────────────── if (paymentStatus === "waiting" && qrCodeUrl) { return (

Scan QR Code with WeChat

Amount: ¥{cnyAmount.toFixed(2)} CNY

You will receive: {xicAmount.toLocaleString()} XIC

Checking payment status every 5 seconds...

Order ID: {orderId}

); } // ── H5 Waiting State ─────────────────────────────────────────────────────── if (paymentStatus === "waiting" && !qrCodeUrl) { return (

Redirected to WeChat Pay

Complete the payment in WeChat, then return here.

Order ID: {orderId}

{h5Url && ( )}
); } // ── Input State ──────────────────────────────────────────────────────────── return (
{/* Amount Input */}
¥ setCnyInput(e.target.value)} min="0.01" step="1" className="w-full pl-8 pr-4 py-3 rounded-xl text-white font-mono focus:outline-none" style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(7,193,96,0.3)", fontSize: "1rem" }} placeholder="100" />
{[100, 500, 1000, 5000].map((amt) => ( ))}
{/* Conversion Preview */} {cnyAmount > 0 && (
CNY Amount ¥{cnyAmount.toFixed(2)}
≈ USD ${(cnyAmount * CNY_USD_RATE).toFixed(2)}
XIC Tokens {xicAmount.toLocaleString()} XIC
)} {/* Pay Button */} {/* Info */}

• PC: Scan QR code with WeChat app

• Mobile: Redirects to WeChat H5 payment

• XIC tokens distributed within 1–5 minutes after confirmation

); }