[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:
parent
d87c2b8e54
commit
ee307ee91a
@ -54,6 +54,8 @@ const Quiz = lazy(() => import('./modules/education/pages/Quiz'));
|
|||||||
// Lazy load modules - Payments
|
// Lazy load modules - Payments
|
||||||
const Pricing = lazy(() => import('./modules/payments/pages/Pricing'));
|
const Pricing = lazy(() => import('./modules/payments/pages/Pricing'));
|
||||||
const Billing = lazy(() => import('./modules/payments/pages/Billing'));
|
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
|
// Lazy load modules - Notifications
|
||||||
const NotificationsPage = lazy(() => import('./modules/notifications/pages/NotificationsPage'));
|
const NotificationsPage = lazy(() => import('./modules/notifications/pages/NotificationsPage'));
|
||||||
@ -120,6 +122,8 @@ function App() {
|
|||||||
{/* Payments */}
|
{/* Payments */}
|
||||||
<Route path="/pricing" element={<Pricing />} />
|
<Route path="/pricing" element={<Pricing />} />
|
||||||
<Route path="/billing" element={<Billing />} />
|
<Route path="/billing" element={<Billing />} />
|
||||||
|
<Route path="/payments/success" element={<CheckoutSuccess />} />
|
||||||
|
<Route path="/payments/cancel" element={<CheckoutCancel />} />
|
||||||
|
|
||||||
{/* Settings */}
|
{/* Settings */}
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
|||||||
239
src/components/payments/WalletDepositModal.tsx
Normal file
239
src/components/payments/WalletDepositModal.tsx
Normal 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;
|
||||||
222
src/components/payments/WalletWithdrawModal.tsx
Normal file
222
src/components/payments/WalletWithdrawModal.tsx
Normal 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;
|
||||||
@ -7,3 +7,5 @@ export { PricingCard } from './PricingCard';
|
|||||||
export { SubscriptionCard } from './SubscriptionCard';
|
export { SubscriptionCard } from './SubscriptionCard';
|
||||||
export { WalletCard } from './WalletCard';
|
export { WalletCard } from './WalletCard';
|
||||||
export { UsageProgress } from './UsageProgress';
|
export { UsageProgress } from './UsageProgress';
|
||||||
|
export { WalletDepositModal } from './WalletDepositModal';
|
||||||
|
export { WalletWithdrawModal } from './WalletWithdrawModal';
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import {
|
|||||||
SubscriptionCard,
|
SubscriptionCard,
|
||||||
UsageProgress,
|
UsageProgress,
|
||||||
WalletCard,
|
WalletCard,
|
||||||
|
WalletDepositModal,
|
||||||
|
WalletWithdrawModal,
|
||||||
} from '../../../components/payments';
|
} from '../../../components/payments';
|
||||||
|
|
||||||
type TabType = 'overview' | 'payment-methods' | 'invoices' | 'wallet';
|
type TabType = 'overview' | 'payment-methods' | 'invoices' | 'wallet';
|
||||||
@ -54,6 +56,8 @@ export default function Billing() {
|
|||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('overview');
|
const [activeTab, setActiveTab] = useState<TabType>('overview');
|
||||||
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
||||||
|
const [showDepositModal, setShowDepositModal] = useState(false);
|
||||||
|
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCurrentSubscription();
|
fetchCurrentSubscription();
|
||||||
@ -396,8 +400,8 @@ export default function Billing() {
|
|||||||
<WalletCard
|
<WalletCard
|
||||||
wallet={wallet}
|
wallet={wallet}
|
||||||
recentTransactions={walletTransactions}
|
recentTransactions={walletTransactions}
|
||||||
onDeposit={openBillingPortal}
|
onDeposit={() => setShowDepositModal(true)}
|
||||||
onWithdraw={openBillingPortal}
|
onWithdraw={() => setShowWithdrawModal(true)}
|
||||||
onViewHistory={() => {}}
|
onViewHistory={() => {}}
|
||||||
loading={loadingWallet}
|
loading={loadingWallet}
|
||||||
/>
|
/>
|
||||||
@ -449,6 +453,27 @@ export default function Billing() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/modules/payments/pages/CheckoutCancel.tsx
Normal file
89
src/modules/payments/pages/CheckoutCancel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
src/modules/payments/pages/CheckoutSuccess.tsx
Normal file
130
src/modules/payments/pages/CheckoutSuccess.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user