[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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 10:07:14 -06:00
parent d87c2b8e54
commit ee307ee91a
7 changed files with 713 additions and 2 deletions

View File

@ -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 */}
<Route path="/pricing" element={<Pricing />} />
<Route path="/billing" element={<Billing />} />
<Route path="/payments/success" element={<CheckoutSuccess />} />
<Route path="/payments/cancel" element={<CheckoutCancel />} />
{/* Settings */}
<Route path="/settings" element={<Settings />} />

View File

@ -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<WalletDepositModalProps> = ({
isOpen,
onClose,
onSuccess,
currency = 'USD',
}) => {
const { paymentMethods, fetchPaymentMethods, loadingPaymentMethods } = usePaymentStore();
const [amount, setAmount] = useState<string>('100');
const [selectedMethod, setSelectedMethod] = useState<string>('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-gray-800 rounded-xl w-full max-w-md overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-lg font-semibold text-white">Depositar Fondos</h2>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{success ? (
<div className="p-8 text-center">
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-green-400" />
</div>
<h3 className="text-xl font-semibold text-white mb-2">Deposito Exitoso</h3>
<p className="text-gray-400">
Se han agregado ${parseFloat(amount).toFixed(2)} a tu wallet
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="p-4 space-y-4">
{/* Amount Input */}
<div>
<label className="block text-sm text-gray-400 mb-2">Monto a depositar</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">$</span>
<input
type="number"
min="10"
step="0.01"
value={amount}
onChange={(e) => 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"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500">
{currency}
</span>
</div>
</div>
{/* Preset Amounts */}
<div className="flex flex-wrap gap-2">
{PRESET_AMOUNTS.map((preset) => (
<button
key={preset}
type="button"
onClick={() => setAmount(String(preset))}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
amount === String(preset)
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
${preset}
</button>
))}
</div>
{/* Payment Method Selection */}
<div>
<label className="block text-sm text-gray-400 mb-2">Metodo de pago</label>
{loadingPaymentMethods ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="w-6 h-6 text-blue-500 animate-spin" />
</div>
) : paymentMethods.length > 0 ? (
<div className="space-y-2">
{paymentMethods.map((method) => (
<label
key={method.id}
className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
selectedMethod === method.id
? 'bg-blue-600/20 border border-blue-500'
: 'bg-gray-700 border border-transparent hover:bg-gray-600'
}`}
>
<input
type="radio"
name="paymentMethod"
value={method.id}
checked={selectedMethod === method.id}
onChange={(e) => setSelectedMethod(e.target.value)}
className="sr-only"
/>
<CreditCard className="w-5 h-5 text-gray-400" />
<div className="flex-1">
<p className="text-white font-medium">
{method.brand} **** {method.last4}
</p>
{method.expiryMonth && method.expiryYear && (
<p className="text-xs text-gray-500">
Expira {method.expiryMonth}/{method.expiryYear}
</p>
)}
</div>
{method.isDefault && (
<span className="text-xs text-blue-400">Default</span>
)}
</label>
))}
</div>
) : (
<div className="text-center py-4 bg-gray-700 rounded-lg">
<p className="text-gray-400 text-sm mb-2">No tienes metodos de pago guardados</p>
<button
type="button"
onClick={() => window.location.href = '/settings/billing'}
className="text-blue-400 text-sm hover:text-blue-300"
>
Agregar metodo de pago
</button>
</div>
)}
</div>
{/* Error Message */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<p className="text-sm text-red-400">{error}</p>
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={loading || !selectedMethod || paymentMethods.length === 0}
className="w-full py-3 bg-green-600 hover:bg-green-500 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Procesando...
</>
) : (
<>Depositar ${parseFloat(amount || '0').toFixed(2)}</>
)}
</button>
<p className="text-xs text-gray-500 text-center">
Los depositos se procesan de forma segura a traves de Stripe
</p>
</form>
)}
</div>
</div>
);
};
export default WalletDepositModal;

View File

@ -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<WalletWithdrawModalProps> = ({
isOpen,
onClose,
onSuccess,
availableBalance,
currency = 'USD',
}) => {
const [amount, setAmount] = useState<string>('');
const [bankAccount, setBankAccount] = useState<string>('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-gray-800 rounded-xl w-full max-w-md overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-lg font-semibold text-white">Retirar Fondos</h2>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{success ? (
<div className="p-8 text-center">
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-green-400" />
</div>
<h3 className="text-xl font-semibold text-white mb-2">Retiro Solicitado</h3>
<p className="text-gray-400">
Tu retiro de ${parseFloat(amount).toFixed(2)} esta siendo procesado
</p>
<p className="text-sm text-gray-500 mt-2">
El tiempo de procesamiento es de 1-3 dias habiles
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="p-4 space-y-4">
{/* Available Balance */}
<div className="p-4 bg-gray-900/50 rounded-lg">
<p className="text-sm text-gray-400 mb-1">Saldo disponible</p>
<p className="text-2xl font-bold text-white">
${availableBalance.toFixed(2)} {currency}
</p>
</div>
{/* Amount Input */}
<div>
<label className="block text-sm text-gray-400 mb-2">Monto a retirar</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">$</span>
<input
type="number"
min="10"
max={availableBalance}
step="0.01"
value={amount}
onChange={(e) => 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"
/>
<button
type="button"
onClick={handleMaxAmount}
className="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1 bg-gray-700 hover:bg-gray-600 text-sm text-gray-300 rounded transition-colors"
>
MAX
</button>
</div>
</div>
{/* Bank Account */}
<div>
<label className="block text-sm text-gray-400 mb-2">Cuenta bancaria destino</label>
<div className="relative">
<Building2 className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
value={bankAccount}
onChange={(e) => 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"
/>
</div>
<p className="text-xs text-gray-500 mt-1">
Puedes configurar tus cuentas bancarias en el portal de Stripe
</p>
</div>
{/* Warning */}
<div className="flex items-start gap-2 p-3 bg-yellow-900/20 border border-yellow-800/50 rounded-lg">
<AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-yellow-400/80">
<p className="font-medium">Importante:</p>
<ul className="list-disc list-inside mt-1 space-y-1 text-xs">
<li>Los retiros tardan 1-3 dias habiles en procesarse</li>
<li>Se aplicara una comision del 1% (min $1)</li>
<li>El monto minimo de retiro es $10</li>
</ul>
</div>
</div>
{/* Error Message */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<p className="text-sm text-red-400">{error}</p>
</div>
)}
{/* Summary */}
{amount && parseFloat(amount) >= 10 && (
<div className="p-3 bg-gray-700 rounded-lg space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Monto</span>
<span className="text-white">${parseFloat(amount).toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Comision (1%)</span>
<span className="text-red-400">
-${Math.max(parseFloat(amount) * 0.01, 1).toFixed(2)}
</span>
</div>
<div className="flex justify-between pt-2 border-t border-gray-600">
<span className="text-gray-300 font-medium">Recibiras</span>
<span className="text-green-400 font-bold">
${(parseFloat(amount) - Math.max(parseFloat(amount) * 0.01, 1)).toFixed(2)}
</span>
</div>
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={loading || !amount || parseFloat(amount) < 10 || parseFloat(amount) > availableBalance}
className="w-full py-3 bg-red-600 hover:bg-red-500 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Procesando...
</>
) : (
<>Solicitar Retiro</>
)}
</button>
</form>
)}
</div>
</div>
);
};
export default WalletWithdrawModal;

View File

@ -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';

View File

@ -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<TabType>('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() {
<WalletCard
wallet={wallet}
recentTransactions={walletTransactions}
onDeposit={openBillingPortal}
onWithdraw={openBillingPortal}
onDeposit={() => setShowDepositModal(true)}
onWithdraw={() => setShowWithdrawModal(true)}
onViewHistory={() => {}}
loading={loadingWallet}
/>
@ -449,6 +453,27 @@ export default function Billing() {
</div>
</div>
)}
{/* Wallet Deposit Modal */}
<WalletDepositModal
isOpen={showDepositModal}
onClose={() => setShowDepositModal(false)}
onSuccess={() => {
setShowDepositModal(false);
fetchWallet();
}}
/>
{/* Wallet Withdraw Modal */}
<WalletWithdrawModal
isOpen={showWithdrawModal}
onClose={() => setShowWithdrawModal(false)}
onSuccess={() => {
setShowWithdrawModal(false);
fetchWallet();
}}
availableBalance={wallet?.availableBalance || 0}
/>
</div>
);
}

View File

@ -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 (
<div className="min-h-[80vh] flex items-center justify-center px-4">
<div className="max-w-md w-full text-center">
{/* Cancel Icon */}
<div className="w-24 h-24 bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-8">
<XCircle className="w-12 h-12 text-gray-400" />
</div>
{/* Title */}
<h1 className="text-3xl font-bold text-white mb-4">
Pago Cancelado
</h1>
<p className="text-gray-400 mb-8">
{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.'}
</p>
{/* Help Section */}
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-8 text-left">
<h3 className="text-white font-medium mb-4 flex items-center gap-2">
<HelpCircle className="w-5 h-5 text-blue-400" />
Tuviste algun problema?
</h3>
<ul className="space-y-3 text-sm text-gray-400">
<li className="flex items-start gap-2">
<span className="text-blue-400 mt-1"></span>
<span>
Si tu tarjeta fue rechazada, verifica los datos o intenta con otro metodo de pago.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-400 mt-1"></span>
<span>
Puedes contactarnos si necesitas ayuda con el proceso de pago.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-400 mt-1"></span>
<span>
Todos los pagos son procesados de forma segura a traves de Stripe.
</span>
</li>
</ul>
</div>
{/* Actions */}
<div className="space-y-3">
<Link
to="/pricing"
className="flex items-center justify-center gap-2 w-full py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors"
>
<ArrowLeft className="w-5 h-5" />
Volver a Planes
</Link>
<Link
to="/assistant"
className="flex items-center justify-center gap-2 w-full py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors"
>
<MessageCircle className="w-5 h-5" />
Contactar Soporte
</Link>
</div>
{/* FAQ Link */}
<p className="mt-8 text-sm text-gray-500">
Revisa nuestras{' '}
<a href="#" className="text-blue-400 hover:text-blue-300">
preguntas frecuentes sobre pagos
</a>
</p>
</div>
</div>
);
}

View File

@ -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 (
<div className="min-h-[80vh] flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mx-auto mb-4" />
<h2 className="text-xl font-semibold text-white mb-2">
Procesando tu pago...
</h2>
<p className="text-gray-400">
Por favor espera mientras confirmamos tu suscripcion
</p>
</div>
</div>
);
}
return (
<div className="min-h-[80vh] flex items-center justify-center px-4">
<div className="max-w-md w-full text-center">
{/* Success Icon */}
<div className="relative mb-8">
<div className="w-24 h-24 bg-green-500/20 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-12 h-12 text-green-400" />
</div>
<Sparkles className="absolute top-0 right-1/4 w-6 h-6 text-yellow-400 animate-pulse" />
<Sparkles className="absolute bottom-2 left-1/4 w-4 h-4 text-blue-400 animate-pulse delay-300" />
</div>
{/* Title */}
<h1 className="text-3xl font-bold text-white mb-4">
Pago Exitoso
</h1>
<p className="text-gray-400 mb-8">
Tu suscripcion ha sido activada correctamente. Ya puedes disfrutar de todas las funcionalidades premium.
</p>
{/* Subscription Details */}
{currentSubscription && (
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-8 text-left">
<h3 className="text-sm text-gray-400 mb-2">Detalles de tu suscripcion</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-400">Plan</span>
<span className="text-white font-medium">
{currentSubscription.plan?.name || 'Premium'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Ciclo</span>
<span className="text-white font-medium">
{currentSubscription.billingCycle === 'yearly' ? 'Anual' : 'Mensual'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Estado</span>
<span className="text-green-400 font-medium">Activo</span>
</div>
{currentSubscription.currentPeriodEnd && (
<div className="flex justify-between">
<span className="text-gray-400">Proxima factura</span>
<span className="text-white font-medium">
{new Date(currentSubscription.currentPeriodEnd).toLocaleDateString('es-ES')}
</span>
</div>
)}
</div>
</div>
)}
{/* Actions */}
<div className="space-y-3">
<Link
to="/dashboard"
className="flex items-center justify-center gap-2 w-full py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors"
>
Ir al Dashboard
<ArrowRight className="w-5 h-5" />
</Link>
<Link
to="/settings/billing"
className="block w-full py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors"
>
Ver Detalles de Facturacion
</Link>
</div>
{/* Session ID for debugging */}
{sessionId && (
<p className="mt-8 text-xs text-gray-600">
ID de sesion: {sessionId.slice(0, 20)}...
</p>
)}
</div>
</div>
);
}