/** * Credit Payment Form Component * * Uses Stripe Elements for secure card collection when purchasing * communication credits. */ import React, { useState, useEffect } from 'react'; import { loadStripe } from '@stripe/stripe-js'; import { Elements, PaymentElement, useStripe, useElements, } from '@stripe/react-stripe-js'; import { CreditCard, Loader2, X, CheckCircle, AlertCircle } from 'lucide-react'; import { useCreatePaymentIntent, useConfirmPayment } from '../hooks/useCommunicationCredits'; // Initialize Stripe const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ''); interface PaymentFormProps { amountCents: number; onSuccess: () => void; onCancel: () => void; savePaymentMethod?: boolean; } const PaymentFormInner: React.FC = ({ amountCents, onSuccess, onCancel, savePaymentMethod = false, }) => { const stripe = useStripe(); const elements = useElements(); const [isProcessing, setIsProcessing] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [isComplete, setIsComplete] = useState(false); const [isElementReady, setIsElementReady] = useState(false); const confirmPayment = useConfirmPayment(); const formatCurrency = (cents: number) => `$${(cents / 100).toFixed(2)}`; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe || !elements) { return; } setIsProcessing(true); setErrorMessage(null); try { // Confirm the payment with Stripe const { error, paymentIntent } = await stripe.confirmPayment({ elements, confirmParams: { return_url: window.location.href, }, redirect: 'if_required', }); if (error) { setErrorMessage(error.message || 'Payment failed. Please try again.'); setIsProcessing(false); return; } if (paymentIntent && paymentIntent.status === 'succeeded') { // Confirm the payment on the backend await confirmPayment.mutateAsync({ payment_intent_id: paymentIntent.id, save_payment_method: savePaymentMethod, }); setIsComplete(true); setTimeout(() => { onSuccess(); }, 1500); } } catch (err: any) { setErrorMessage(err.message || 'An unexpected error occurred.'); setIsProcessing(false); } }; if (isComplete) { return (

Payment Successful!

{formatCurrency(amountCents)} has been added to your credits.

); } return (
Amount {formatCurrency(amountCents)}
{!isElementReady && (
Loading payment form...
)}
setIsElementReady(true)} options={{ layout: 'tabs', }} />
{errorMessage && (

{errorMessage}

)}

Your payment is securely processed by Stripe

); }; interface CreditPaymentModalProps { isOpen: boolean; onClose: () => void; onSuccess: () => void; amountCents: number; onAmountChange?: (cents: number) => void; savePaymentMethod?: boolean; skipAmountSelection?: boolean; } export const CreditPaymentModal: React.FC = ({ isOpen, onClose, onSuccess, amountCents, onAmountChange, savePaymentMethod = false, skipAmountSelection = false, }) => { const [clientSecret, setClientSecret] = useState(null); const [isLoadingIntent, setIsLoadingIntent] = useState(false); const [error, setError] = useState(null); const [showPaymentForm, setShowPaymentForm] = useState(false); const [autoInitialized, setAutoInitialized] = useState(false); const createPaymentIntent = useCreatePaymentIntent(); const formatCurrency = (cents: number) => `$${(cents / 100).toFixed(2)}`; useEffect(() => { if (!isOpen) { setClientSecret(null); setShowPaymentForm(false); setError(null); setAutoInitialized(false); } }, [isOpen]); // Auto-initialize payment when skipping amount selection useEffect(() => { if (isOpen && skipAmountSelection && !autoInitialized && !isLoadingIntent && !clientSecret) { setAutoInitialized(true); handleContinueToPayment(); } }, [isOpen, skipAmountSelection, autoInitialized, isLoadingIntent, clientSecret]); const handleContinueToPayment = async () => { setIsLoadingIntent(true); setError(null); try { const result = await createPaymentIntent.mutateAsync(amountCents); setClientSecret(result.client_secret); setShowPaymentForm(true); } catch (err: any) { setError(err.response?.data?.error || 'Failed to initialize payment. Please try again.'); } finally { setIsLoadingIntent(false); } }; if (!isOpen) return null; return (

{skipAmountSelection ? 'Complete Payment' : 'Add Credits'}

{skipAmountSelection ? `Loading ${formatCurrency(amountCents)} to your balance` : 'Choose an amount to add to your balance' }

{/* Loading state when auto-initializing */} {skipAmountSelection && isLoadingIntent && !clientSecret ? (

Setting up payment...

) : skipAmountSelection && error && !clientSecret ? (

{error}

) : !showPaymentForm && !skipAmountSelection ? (
{[1000, 2500, 5000].map((amount) => ( ))}
$ { const val = e.target.value.replace(/[^0-9]/g, ''); onAmountChange?.(Math.max(5, parseInt(val) || 5) * 100); }} onKeyDown={(e) => { if (!/[0-9]/.test(e.key) && !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) { e.preventDefault(); } }} className="w-full pl-8 pr-12 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" /> .00
{error && (

{error}

)}
) : clientSecret ? ( { setShowPaymentForm(false); setClientSecret(null); }} savePaymentMethod={savePaymentMethod} /> ) : null}
); }; export default CreditPaymentModal;