From ee307ee91a2d321c5571ed14fbd8be31091625a6 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 10:07:14 -0600 Subject: [PATCH] [OQI-005] feat: Add wallet modals and checkout pages - Add WalletDepositModal for depositing funds to wallet - Add WalletWithdrawModal for withdrawing funds from wallet - Add CheckoutSuccess page for successful Stripe checkout - Add CheckoutCancel page for canceled checkout - Update Billing page to use new wallet modals - Add routes for /payments/success and /payments/cancel - Export new modals from payments index Co-Authored-By: Claude Opus 4.5 --- src/App.tsx | 4 + .../payments/WalletDepositModal.tsx | 239 ++++++++++++++++++ .../payments/WalletWithdrawModal.tsx | 222 ++++++++++++++++ src/components/payments/index.ts | 2 + src/modules/payments/pages/Billing.tsx | 29 ++- src/modules/payments/pages/CheckoutCancel.tsx | 89 +++++++ .../payments/pages/CheckoutSuccess.tsx | 130 ++++++++++ 7 files changed, 713 insertions(+), 2 deletions(-) create mode 100644 src/components/payments/WalletDepositModal.tsx create mode 100644 src/components/payments/WalletWithdrawModal.tsx create mode 100644 src/modules/payments/pages/CheckoutCancel.tsx create mode 100644 src/modules/payments/pages/CheckoutSuccess.tsx diff --git a/src/App.tsx b/src/App.tsx index d5680a1..c40ee35 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,6 +54,8 @@ const Quiz = lazy(() => import('./modules/education/pages/Quiz')); // Lazy load modules - Payments const Pricing = lazy(() => import('./modules/payments/pages/Pricing')); const Billing = lazy(() => import('./modules/payments/pages/Billing')); +const CheckoutSuccess = lazy(() => import('./modules/payments/pages/CheckoutSuccess')); +const CheckoutCancel = lazy(() => import('./modules/payments/pages/CheckoutCancel')); // Lazy load modules - Notifications const NotificationsPage = lazy(() => import('./modules/notifications/pages/NotificationsPage')); @@ -120,6 +122,8 @@ function App() { {/* Payments */} } /> } /> + } /> + } /> {/* Settings */} } /> diff --git a/src/components/payments/WalletDepositModal.tsx b/src/components/payments/WalletDepositModal.tsx new file mode 100644 index 0000000..25283d0 --- /dev/null +++ b/src/components/payments/WalletDepositModal.tsx @@ -0,0 +1,239 @@ +/** + * WalletDepositModal Component + * Modal for depositing funds to wallet + */ + +import React, { useState, useEffect } from 'react'; +import { X, CreditCard, Loader2, AlertCircle, CheckCircle } from 'lucide-react'; +import { usePaymentStore } from '../../stores/paymentStore'; +import { depositToWallet } from '../../services/payment.service'; + +interface WalletDepositModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; + currency?: string; +} + +const PRESET_AMOUNTS = [50, 100, 250, 500, 1000]; + +export const WalletDepositModal: React.FC = ({ + isOpen, + onClose, + onSuccess, + currency = 'USD', +}) => { + const { paymentMethods, fetchPaymentMethods, loadingPaymentMethods } = usePaymentStore(); + const [amount, setAmount] = useState('100'); + const [selectedMethod, setSelectedMethod] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + useEffect(() => { + if (isOpen) { + fetchPaymentMethods(); + setAmount('100'); + setError(null); + setSuccess(false); + } + }, [isOpen, fetchPaymentMethods]); + + useEffect(() => { + // Auto-select default payment method + if (paymentMethods.length > 0 && !selectedMethod) { + const defaultMethod = paymentMethods.find((m) => m.isDefault) || paymentMethods[0]; + setSelectedMethod(defaultMethod.id); + } + }, [paymentMethods, selectedMethod]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const numAmount = parseFloat(amount); + + if (isNaN(numAmount) || numAmount < 10) { + setError('El monto minimo es $10'); + return; + } + + if (!selectedMethod) { + setError('Selecciona un metodo de pago'); + return; + } + + setLoading(true); + setError(null); + + try { + await depositToWallet(numAmount, selectedMethod); + setSuccess(true); + setTimeout(() => { + onSuccess?.(); + onClose(); + }, 2000); + } catch (err) { + setError('Error al procesar el deposito. Intenta de nuevo.'); + console.error('Deposit error:', err); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

Depositar Fondos

+ +
+ + {success ? ( +
+
+ +
+

Deposito Exitoso

+

+ Se han agregado ${parseFloat(amount).toFixed(2)} a tu wallet +

+
+ ) : ( +
+ {/* Amount Input */} +
+ +
+ $ + setAmount(e.target.value)} + className="w-full pl-8 pr-16 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white text-lg font-medium focus:outline-none focus:border-blue-500" + placeholder="0.00" + /> + + {currency} + +
+
+ + {/* Preset Amounts */} +
+ {PRESET_AMOUNTS.map((preset) => ( + + ))} +
+ + {/* Payment Method Selection */} +
+ + {loadingPaymentMethods ? ( +
+ +
+ ) : paymentMethods.length > 0 ? ( +
+ {paymentMethods.map((method) => ( + + ))} +
+ ) : ( +
+

No tienes metodos de pago guardados

+ +
+ )} +
+ + {/* Error Message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Submit Button */} + + +

+ Los depositos se procesan de forma segura a traves de Stripe +

+
+ )} +
+
+ ); +}; + +export default WalletDepositModal; diff --git a/src/components/payments/WalletWithdrawModal.tsx b/src/components/payments/WalletWithdrawModal.tsx new file mode 100644 index 0000000..fce7b74 --- /dev/null +++ b/src/components/payments/WalletWithdrawModal.tsx @@ -0,0 +1,222 @@ +/** + * WalletWithdrawModal Component + * Modal for withdrawing funds from wallet + */ + +import React, { useState } from 'react'; +import { X, Building2, Loader2, AlertCircle, CheckCircle, AlertTriangle } from 'lucide-react'; +import { withdrawFromWallet } from '../../services/payment.service'; + +interface WalletWithdrawModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; + availableBalance: number; + currency?: string; +} + +export const WalletWithdrawModal: React.FC = ({ + isOpen, + onClose, + onSuccess, + availableBalance, + currency = 'USD', +}) => { + const [amount, setAmount] = useState(''); + const [bankAccount, setBankAccount] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const numAmount = parseFloat(amount); + + if (isNaN(numAmount) || numAmount < 10) { + setError('El monto minimo de retiro es $10'); + return; + } + + if (numAmount > availableBalance) { + setError('El monto excede tu saldo disponible'); + return; + } + + if (!bankAccount.trim()) { + setError('Ingresa el ID de tu cuenta bancaria'); + return; + } + + setLoading(true); + setError(null); + + try { + await withdrawFromWallet(numAmount, { + type: 'bank_account', + accountId: bankAccount.trim(), + }); + setSuccess(true); + setTimeout(() => { + onSuccess?.(); + onClose(); + }, 2000); + } catch (err) { + setError('Error al procesar el retiro. Verifica los datos e intenta de nuevo.'); + console.error('Withdrawal error:', err); + } finally { + setLoading(false); + } + }; + + const handleMaxAmount = () => { + setAmount(String(availableBalance)); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

Retirar Fondos

+ +
+ + {success ? ( +
+
+ +
+

Retiro Solicitado

+

+ Tu retiro de ${parseFloat(amount).toFixed(2)} esta siendo procesado +

+

+ El tiempo de procesamiento es de 1-3 dias habiles +

+
+ ) : ( +
+ {/* Available Balance */} +
+

Saldo disponible

+

+ ${availableBalance.toFixed(2)} {currency} +

+
+ + {/* Amount Input */} +
+ +
+ $ + setAmount(e.target.value)} + className="w-full pl-8 pr-20 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white text-lg font-medium focus:outline-none focus:border-blue-500" + placeholder="0.00" + /> + +
+
+ + {/* Bank Account */} +
+ +
+ + setBankAccount(e.target.value)} + className="w-full pl-10 pr-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500" + placeholder="ID de cuenta bancaria" + /> +
+

+ Puedes configurar tus cuentas bancarias en el portal de Stripe +

+
+ + {/* Warning */} +
+ +
+

Importante:

+
    +
  • Los retiros tardan 1-3 dias habiles en procesarse
  • +
  • Se aplicara una comision del 1% (min $1)
  • +
  • El monto minimo de retiro es $10
  • +
+
+
+ + {/* Error Message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Summary */} + {amount && parseFloat(amount) >= 10 && ( +
+
+ Monto + ${parseFloat(amount).toFixed(2)} +
+
+ Comision (1%) + + -${Math.max(parseFloat(amount) * 0.01, 1).toFixed(2)} + +
+
+ Recibiras + + ${(parseFloat(amount) - Math.max(parseFloat(amount) * 0.01, 1)).toFixed(2)} + +
+
+ )} + + {/* Submit Button */} + +
+ )} +
+
+ ); +}; + +export default WalletWithdrawModal; diff --git a/src/components/payments/index.ts b/src/components/payments/index.ts index e82caf3..8de556c 100644 --- a/src/components/payments/index.ts +++ b/src/components/payments/index.ts @@ -7,3 +7,5 @@ export { PricingCard } from './PricingCard'; export { SubscriptionCard } from './SubscriptionCard'; export { WalletCard } from './WalletCard'; export { UsageProgress } from './UsageProgress'; +export { WalletDepositModal } from './WalletDepositModal'; +export { WalletWithdrawModal } from './WalletWithdrawModal'; diff --git a/src/modules/payments/pages/Billing.tsx b/src/modules/payments/pages/Billing.tsx index ac2cdeb..bb49544 100644 --- a/src/modules/payments/pages/Billing.tsx +++ b/src/modules/payments/pages/Billing.tsx @@ -21,6 +21,8 @@ import { SubscriptionCard, UsageProgress, WalletCard, + WalletDepositModal, + WalletWithdrawModal, } from '../../../components/payments'; type TabType = 'overview' | 'payment-methods' | 'invoices' | 'wallet'; @@ -54,6 +56,8 @@ export default function Billing() { const [activeTab, setActiveTab] = useState('overview'); const [showCancelConfirm, setShowCancelConfirm] = useState(false); + const [showDepositModal, setShowDepositModal] = useState(false); + const [showWithdrawModal, setShowWithdrawModal] = useState(false); useEffect(() => { fetchCurrentSubscription(); @@ -396,8 +400,8 @@ export default function Billing() { setShowDepositModal(true)} + onWithdraw={() => setShowWithdrawModal(true)} onViewHistory={() => {}} loading={loadingWallet} /> @@ -449,6 +453,27 @@ export default function Billing() { )} + + {/* Wallet Deposit Modal */} + setShowDepositModal(false)} + onSuccess={() => { + setShowDepositModal(false); + fetchWallet(); + }} + /> + + {/* Wallet Withdraw Modal */} + setShowWithdrawModal(false)} + onSuccess={() => { + setShowWithdrawModal(false); + fetchWallet(); + }} + availableBalance={wallet?.availableBalance || 0} + /> ); } diff --git a/src/modules/payments/pages/CheckoutCancel.tsx b/src/modules/payments/pages/CheckoutCancel.tsx new file mode 100644 index 0000000..1a416df --- /dev/null +++ b/src/modules/payments/pages/CheckoutCancel.tsx @@ -0,0 +1,89 @@ +/** + * CheckoutCancel Page + * Displayed when user cancels Stripe checkout + */ + +import React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { XCircle, ArrowLeft, HelpCircle, MessageCircle } from 'lucide-react'; + +export default function CheckoutCancel() { + const [searchParams] = useSearchParams(); + const reason = searchParams.get('reason'); + + return ( +
+
+ {/* Cancel Icon */} +
+ +
+ + {/* Title */} +

+ Pago Cancelado +

+ +

+ {reason === 'expired' + ? 'La sesion de pago ha expirado. Por favor intenta de nuevo.' + : 'Has cancelado el proceso de pago. No se ha realizado ningun cargo a tu cuenta.'} +

+ + {/* Help Section */} +
+

+ + Tuviste algun problema? +

+
    +
  • + + + Si tu tarjeta fue rechazada, verifica los datos o intenta con otro metodo de pago. + +
  • +
  • + + + Puedes contactarnos si necesitas ayuda con el proceso de pago. + +
  • +
  • + + + Todos los pagos son procesados de forma segura a traves de Stripe. + +
  • +
+
+ + {/* Actions */} +
+ + + Volver a Planes + + + + Contactar Soporte + +
+ + {/* FAQ Link */} +

+ Revisa nuestras{' '} + + preguntas frecuentes sobre pagos + +

+
+
+ ); +} diff --git a/src/modules/payments/pages/CheckoutSuccess.tsx b/src/modules/payments/pages/CheckoutSuccess.tsx new file mode 100644 index 0000000..7b2adbc --- /dev/null +++ b/src/modules/payments/pages/CheckoutSuccess.tsx @@ -0,0 +1,130 @@ +/** + * CheckoutSuccess Page + * Displayed after successful Stripe checkout + */ + +import React, { useEffect, useState } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { CheckCircle, Loader2, ArrowRight, Sparkles } from 'lucide-react'; +import { usePaymentStore } from '../../../stores/paymentStore'; + +export default function CheckoutSuccess() { + const [searchParams] = useSearchParams(); + const [loading, setLoading] = useState(true); + const { fetchCurrentSubscription, currentSubscription } = usePaymentStore(); + + const sessionId = searchParams.get('session_id'); + + useEffect(() => { + const loadSubscription = async () => { + try { + await fetchCurrentSubscription(); + } catch (error) { + console.error('Error fetching subscription:', error); + } finally { + setLoading(false); + } + }; + + // Give Stripe webhook time to process + const timer = setTimeout(loadSubscription, 2000); + return () => clearTimeout(timer); + }, [fetchCurrentSubscription]); + + if (loading) { + return ( +
+
+ +

+ Procesando tu pago... +

+

+ Por favor espera mientras confirmamos tu suscripcion +

+
+
+ ); + } + + return ( +
+
+ {/* Success Icon */} +
+
+ +
+ + +
+ + {/* Title */} +

+ Pago Exitoso +

+ +

+ Tu suscripcion ha sido activada correctamente. Ya puedes disfrutar de todas las funcionalidades premium. +

+ + {/* Subscription Details */} + {currentSubscription && ( +
+

Detalles de tu suscripcion

+
+
+ Plan + + {currentSubscription.plan?.name || 'Premium'} + +
+
+ Ciclo + + {currentSubscription.billingCycle === 'yearly' ? 'Anual' : 'Mensual'} + +
+
+ Estado + Activo +
+ {currentSubscription.currentPeriodEnd && ( +
+ Proxima factura + + {new Date(currentSubscription.currentPeriodEnd).toLocaleDateString('es-ES')} + +
+ )} +
+
+ )} + + {/* Actions */} +
+ + Ir al Dashboard + + + + Ver Detalles de Facturacion + +
+ + {/* Session ID for debugging */} + {sessionId && ( +

+ ID de sesion: {sessionId.slice(0, 20)}... +

+ )} +
+
+ ); +}