/** * PaypalPayment Component * ───────────────────────────────────────────────────────────────────────────── * Handles PayPal Orders API v2 payment flow. * * Flow: * 1. User enters USD amount * 2. Component calls payment.createPaypalOrder mutation * 3. Redirects user to PayPal approval URL * 4. After approval, PayPal redirects back with ?token=PAYPAL_ORDER_ID * 5. Component calls payment.capturePaypalOrder to finalize payment * 6. Polls payment.queryPaypalOrder for status * 7. On success (dbStatus === "distributed"), shows success message * * URL Parameters handled: * ?paypalReturn=1&orderId=INTERNAL_ORDER_ID&token=PAYPAL_ORDER_ID */ import { useState, useEffect } from "react"; import { trpc } from "@/lib/trpc"; import { toast } from "sonner"; const XIC_PRICE_USD = 0.02; function calcXicFromUsd(usd: number): number { return Math.floor(usd / XIC_PRICE_USD); } interface PaypalPaymentProps { xicReceiveAddress: string; onSuccess?: (xicAmount: number, orderId: string) => void; } export default function PaypalPayment({ xicReceiveAddress, onSuccess }: PaypalPaymentProps) { const [usdInput, setUsdInput] = useState("100"); const [orderId, setOrderId] = useState(null); const [paypalOrderId, setPaypalOrderId] = useState(null); const [paymentStatus, setPaymentStatus] = useState<"idle" | "redirecting" | "capturing" | "waiting" | "success" | "failed">("idle"); const usdAmount = parseFloat(usdInput) || 0; const xicAmount = calcXicFromUsd(usdAmount); // Check URL params on mount — handle PayPal return redirect useEffect(() => { const params = new URLSearchParams(window.location.search); const isReturn = params.get("paypalReturn") === "1"; const returnOrderId = params.get("orderId"); const returnToken = params.get("token"); // PayPal order ID if (isReturn && returnOrderId && returnToken) { // Clean URL const cleanUrl = window.location.pathname; window.history.replaceState({}, "", cleanUrl); setOrderId(returnOrderId); setPaypalOrderId(returnToken); setPaymentStatus("capturing"); // Auto-capture captureOrder.mutate({ paypalOrderId: returnToken, internalOrderId: returnOrderId }); } // Check for cancel if (params.get("paypalCancel") === "1") { const cleanUrl = window.location.pathname; window.history.replaceState({}, "", cleanUrl); toast.info("PayPal payment was cancelled."); } }, []); const createOrder = trpc.payment.createPaypalOrder.useMutation({ onSuccess: (data) => { setOrderId(data.orderId); setPaypalOrderId(data.paypalOrderId || null); if (data.approveUrl) { setPaymentStatus("redirecting"); toast.info("Redirecting to PayPal..."); setTimeout(() => { window.location.href = data.approveUrl!; }, 1000); } }, onError: (err) => { toast.error(`Failed to create PayPal order: ${err.message}`); setPaymentStatus("idle"); }, }); const captureOrder = trpc.payment.capturePaypalOrder.useMutation({ onSuccess: (data) => { if (data.success) { setPaymentStatus("waiting"); toast.success("Payment captured! Distributing XIC tokens..."); } }, onError: (err) => { toast.error(`Failed to capture payment: ${err.message}`); setPaymentStatus("failed"); }, }); const queryOrder = trpc.payment.queryPaypalOrder.useQuery( { orderId: orderId! }, { enabled: !!orderId && (paymentStatus === "waiting" || paymentStatus === "capturing"), 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]); const handlePay = () => { if (!xicReceiveAddress || xicReceiveAddress.length < 10) { toast.error("Please enter your XIC receive address first."); return; } if (usdAmount < 1) { toast.error("Minimum payment is $1 USD."); return; } createOrder.mutate({ usdAmount: usdAmount.toFixed(2), xicReceiveAddress, }); }; const handleReset = () => { setOrderId(null); setPaypalOrderId(null); setPaymentStatus("idle"); setUsdInput("100"); }; // ── Success State ────────────────────────────────────────────────────────── if (paymentStatus === "success") { return (
🎉

Payment Successful!

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

); } // ── Redirecting / Capturing State ────────────────────────────────────────── if (paymentStatus === "redirecting" || paymentStatus === "capturing" || paymentStatus === "waiting") { return (
{paymentStatus === "redirecting" ? "🔄" : paymentStatus === "capturing" ? "⚡" : "⏳"}

{paymentStatus === "redirecting" && "Redirecting to PayPal..."} {paymentStatus === "capturing" && "Capturing Payment..."} {paymentStatus === "waiting" && "Processing Payment..."}

{orderId && (

Order ID: {orderId}

)} {paymentStatus === "waiting" && (

Checking payment status every 5 seconds...

)}
{paymentStatus !== "redirecting" && ( )}
); } // ── Input State ──────────────────────────────────────────────────────────── return (
{/* Amount Input */}
$ setUsdInput(e.target.value)} min="1" 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(0,112,186,0.3)", fontSize: "1rem" }} placeholder="100" />
{[50, 100, 500, 1000].map((amt) => ( ))}
{/* Conversion Preview */} {usdAmount > 0 && (
USD Amount ${usdAmount.toFixed(2)}
XIC Price ${XIC_PRICE_USD} / XIC
XIC Tokens {xicAmount.toLocaleString()} XIC
)} {/* PayPal Button */} {/* Info */}

• Supports PayPal balance, credit/debit cards via PayPal

• You will be redirected to PayPal to complete payment

• XIC tokens distributed within 1–5 minutes after payment confirmation

• PayPal buyer protection applies to this transaction

); }