SUBTASK-005 (Investment): - Rewrite Investment.tsx with summary cards and performance chart - Add pagination to Transactions.tsx (10 items per page) - Add PDF/CSV export dropdown to Reports.tsx - Fix quick amount buttons in DepositForm.tsx - Fix Max button in WithdrawForm.tsx - Add full KYC verification system (3 steps) - Add KYCVerification page with route /investment/kyc SUBTASK-006 (Payments): - Add AlternativePaymentMethods component (OXXO, SPEI, Card) - Extend payment types for regional methods - Update PaymentMethodsList exports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
612 lines
23 KiB
TypeScript
612 lines
23 KiB
TypeScript
/**
|
|
* KYCVerificationPanel Component
|
|
* Complete KYC verification flow with document upload
|
|
* Epic: OQI-004 Cuentas de Inversion
|
|
*/
|
|
|
|
import React, { useState, useRef } from 'react';
|
|
import {
|
|
Shield,
|
|
User,
|
|
FileText,
|
|
Upload,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Camera,
|
|
X,
|
|
Loader2,
|
|
Clock,
|
|
MapPin,
|
|
Briefcase,
|
|
} from 'lucide-react';
|
|
import { KYCStatusBadge, KYCStatus } from './KYCStatusBadge';
|
|
|
|
interface PersonalData {
|
|
fullName: string;
|
|
dateOfBirth: string;
|
|
nationality: string;
|
|
address: string;
|
|
city: string;
|
|
state: string;
|
|
postalCode: string;
|
|
country: string;
|
|
occupation: string;
|
|
incomeSource: string;
|
|
}
|
|
|
|
interface Document {
|
|
id: string;
|
|
type: 'id_front' | 'id_back' | 'proof_of_address' | 'selfie';
|
|
fileName: string;
|
|
status: 'pending' | 'approved' | 'rejected';
|
|
previewUrl?: string;
|
|
rejectionReason?: string;
|
|
}
|
|
|
|
interface KYCVerificationPanelProps {
|
|
status: KYCStatus;
|
|
personalData?: Partial<PersonalData>;
|
|
documents?: Document[];
|
|
rejectionReason?: string;
|
|
onSubmit?: (data: { personalData: PersonalData; documents: File[] }) => Promise<void>;
|
|
onCancel?: () => void;
|
|
}
|
|
|
|
type Step = 'personal' | 'documents' | 'review';
|
|
|
|
const DOCUMENT_TYPES = [
|
|
{ id: 'id_front', label: 'ID Frontal', description: 'INE, Pasaporte o Licencia de conducir (frente)', icon: FileText },
|
|
{ id: 'id_back', label: 'ID Reverso', description: 'Parte posterior del documento', icon: FileText },
|
|
{ id: 'proof_of_address', label: 'Comprobante de Domicilio', description: 'Recibo de luz, agua o estado de cuenta (no mayor a 3 meses)', icon: MapPin },
|
|
{ id: 'selfie', label: 'Selfie con ID', description: 'Foto tuya sosteniendo tu identificación', icon: Camera },
|
|
];
|
|
|
|
const NATIONALITY_OPTIONS = [
|
|
'Mexico', 'United States', 'Canada', 'Spain', 'Argentina',
|
|
'Colombia', 'Chile', 'Peru', 'Brazil', 'United Kingdom', 'Germany', 'France',
|
|
];
|
|
|
|
const OCCUPATION_OPTIONS = [
|
|
'Empleado', 'Empresario', 'Profesionista Independiente', 'Comerciante',
|
|
'Estudiante', 'Jubilado', 'Otro',
|
|
];
|
|
|
|
const INCOME_SOURCE_OPTIONS = [
|
|
'Salario', 'Negocio Propio', 'Inversiones', 'Herencia',
|
|
'Ahorros', 'Pensión', 'Otro',
|
|
];
|
|
|
|
export const KYCVerificationPanel: React.FC<KYCVerificationPanelProps> = ({
|
|
status,
|
|
personalData: initialPersonalData,
|
|
documents: existingDocuments = [],
|
|
rejectionReason,
|
|
onSubmit,
|
|
onCancel,
|
|
}) => {
|
|
const [currentStep, setCurrentStep] = useState<Step>(status === 'not_started' ? 'personal' : 'review');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const [currentDocType, setCurrentDocType] = useState<string | null>(null);
|
|
|
|
const [personalData, setPersonalData] = useState<PersonalData>({
|
|
fullName: initialPersonalData?.fullName || '',
|
|
dateOfBirth: initialPersonalData?.dateOfBirth || '',
|
|
nationality: initialPersonalData?.nationality || '',
|
|
address: initialPersonalData?.address || '',
|
|
city: initialPersonalData?.city || '',
|
|
state: initialPersonalData?.state || '',
|
|
postalCode: initialPersonalData?.postalCode || '',
|
|
country: initialPersonalData?.country || '',
|
|
occupation: initialPersonalData?.occupation || '',
|
|
incomeSource: initialPersonalData?.incomeSource || '',
|
|
});
|
|
|
|
const [uploadedDocuments, setUploadedDocuments] = useState<Map<string, { file: File; preview: string }>>(new Map());
|
|
|
|
const handlePersonalDataChange = (field: keyof PersonalData, value: string) => {
|
|
setPersonalData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const isPersonalDataValid = () => {
|
|
return (
|
|
personalData.fullName.length >= 3 &&
|
|
personalData.dateOfBirth !== '' &&
|
|
personalData.nationality !== '' &&
|
|
personalData.address.length >= 5 &&
|
|
personalData.city.length >= 2 &&
|
|
personalData.country !== '' &&
|
|
personalData.occupation !== '' &&
|
|
personalData.incomeSource !== ''
|
|
);
|
|
};
|
|
|
|
const handleFileSelect = (docType: string) => {
|
|
setCurrentDocType(docType);
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file || !currentDocType) return;
|
|
|
|
if (!file.type.startsWith('image/') && file.type !== 'application/pdf') {
|
|
setError('Solo se permiten imágenes o archivos PDF');
|
|
return;
|
|
}
|
|
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
setError('El archivo no debe exceder 10MB');
|
|
return;
|
|
}
|
|
|
|
const preview = URL.createObjectURL(file);
|
|
setUploadedDocuments(prev => {
|
|
const newMap = new Map(prev);
|
|
const existing = prev.get(currentDocType);
|
|
if (existing?.preview) {
|
|
URL.revokeObjectURL(existing.preview);
|
|
}
|
|
newMap.set(currentDocType, { file, preview });
|
|
return newMap;
|
|
});
|
|
|
|
setError(null);
|
|
setCurrentDocType(null);
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
};
|
|
|
|
const removeDocument = (docType: string) => {
|
|
setUploadedDocuments(prev => {
|
|
const newMap = new Map(prev);
|
|
const existing = prev.get(docType);
|
|
if (existing?.preview) {
|
|
URL.revokeObjectURL(existing.preview);
|
|
}
|
|
newMap.delete(docType);
|
|
return newMap;
|
|
});
|
|
};
|
|
|
|
const isDocumentsComplete = () => {
|
|
return DOCUMENT_TYPES.every(doc => uploadedDocuments.has(doc.id) || existingDocuments.some(d => d.type === doc.id && d.status === 'approved'));
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!onSubmit) return;
|
|
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const files = Array.from(uploadedDocuments.values()).map(d => d.file);
|
|
await onSubmit({ personalData, documents: files });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Error al enviar la verificación');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const renderStepIndicator = () => (
|
|
<div className="flex items-center justify-center gap-4 mb-8">
|
|
{(['personal', 'documents', 'review'] as Step[]).map((step, index) => {
|
|
const isActive = step === currentStep;
|
|
const isCompleted =
|
|
(step === 'personal' && (currentStep === 'documents' || currentStep === 'review')) ||
|
|
(step === 'documents' && currentStep === 'review');
|
|
|
|
return (
|
|
<React.Fragment key={step}>
|
|
<div className="flex flex-col items-center">
|
|
<div
|
|
className={`w-10 h-10 rounded-full flex items-center justify-center transition-colors ${
|
|
isActive ? 'bg-blue-600 text-white' :
|
|
isCompleted ? 'bg-green-600 text-white' :
|
|
'bg-gray-200 dark:bg-gray-700 text-gray-500'
|
|
}`}
|
|
>
|
|
{isCompleted ? <CheckCircle className="w-5 h-5" /> : index + 1}
|
|
</div>
|
|
<span className={`text-xs mt-1 ${isActive ? 'text-white' : 'text-gray-400'}`}>
|
|
{step === 'personal' ? 'Datos' : step === 'documents' ? 'Documentos' : 'Revisión'}
|
|
</span>
|
|
</div>
|
|
{index < 2 && (
|
|
<div className={`w-16 h-0.5 ${isCompleted ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-700'}`} />
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
const renderPersonalDataStep = () => (
|
|
<div className="space-y-4">
|
|
<div className="bg-blue-900/20 border border-blue-800/50 rounded-lg p-4 mb-6">
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="w-5 h-5 text-blue-400" />
|
|
<span className="text-blue-400 font-medium">Información Personal</span>
|
|
</div>
|
|
<p className="text-slate-400 text-sm mt-2">
|
|
Proporciona tus datos personales como aparecen en tu identificación oficial.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Nombre Completo</label>
|
|
<input
|
|
type="text"
|
|
value={personalData.fullName}
|
|
onChange={(e) => handlePersonalDataChange('fullName', e.target.value)}
|
|
placeholder="Como aparece en tu identificación"
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Fecha de Nacimiento</label>
|
|
<input
|
|
type="date"
|
|
value={personalData.dateOfBirth}
|
|
onChange={(e) => handlePersonalDataChange('dateOfBirth', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Nacionalidad</label>
|
|
<select
|
|
value={personalData.nationality}
|
|
onChange={(e) => handlePersonalDataChange('nationality', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
>
|
|
<option value="">Seleccionar</option>
|
|
{NATIONALITY_OPTIONS.map(option => (
|
|
<option key={option} value={option}>{option}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Dirección</label>
|
|
<input
|
|
type="text"
|
|
value={personalData.address}
|
|
onChange={(e) => handlePersonalDataChange('address', e.target.value)}
|
|
placeholder="Calle y número"
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Ciudad</label>
|
|
<input
|
|
type="text"
|
|
value={personalData.city}
|
|
onChange={(e) => handlePersonalDataChange('city', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Estado/Provincia</label>
|
|
<input
|
|
type="text"
|
|
value={personalData.state}
|
|
onChange={(e) => handlePersonalDataChange('state', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Código Postal</label>
|
|
<input
|
|
type="text"
|
|
value={personalData.postalCode}
|
|
onChange={(e) => handlePersonalDataChange('postalCode', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">País</label>
|
|
<select
|
|
value={personalData.country}
|
|
onChange={(e) => handlePersonalDataChange('country', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
>
|
|
<option value="">Seleccionar</option>
|
|
{NATIONALITY_OPTIONS.map(option => (
|
|
<option key={option} value={option}>{option}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Ocupación</label>
|
|
<select
|
|
value={personalData.occupation}
|
|
onChange={(e) => handlePersonalDataChange('occupation', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
>
|
|
<option value="">Seleccionar</option>
|
|
{OCCUPATION_OPTIONS.map(option => (
|
|
<option key={option} value={option}>{option}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-1">Fuente de Ingresos</label>
|
|
<select
|
|
value={personalData.incomeSource}
|
|
onChange={(e) => handlePersonalDataChange('incomeSource', e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-blue-500 focus:outline-none"
|
|
>
|
|
<option value="">Seleccionar</option>
|
|
{INCOME_SOURCE_OPTIONS.map(option => (
|
|
<option key={option} value={option}>{option}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderDocumentsStep = () => (
|
|
<div className="space-y-4">
|
|
<div className="bg-amber-900/20 border border-amber-800/50 rounded-lg p-4 mb-6">
|
|
<div className="flex items-center gap-2">
|
|
<FileText className="w-5 h-5 text-amber-400" />
|
|
<span className="text-amber-400 font-medium">Documentos Requeridos</span>
|
|
</div>
|
|
<p className="text-slate-400 text-sm mt-2">
|
|
Sube fotos claras de tus documentos. Asegúrate de que toda la información sea legible.
|
|
</p>
|
|
</div>
|
|
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*,application/pdf"
|
|
onChange={handleFileChange}
|
|
className="hidden"
|
|
/>
|
|
|
|
<div className="grid gap-4">
|
|
{DOCUMENT_TYPES.map((docType) => {
|
|
const uploaded = uploadedDocuments.get(docType.id);
|
|
const existing = existingDocuments.find(d => d.type === docType.id);
|
|
const Icon = docType.icon;
|
|
|
|
return (
|
|
<div
|
|
key={docType.id}
|
|
className={`p-4 rounded-lg border transition-colors ${
|
|
uploaded || (existing?.status === 'approved')
|
|
? 'border-green-500/50 bg-green-900/10'
|
|
: existing?.status === 'rejected'
|
|
? 'border-red-500/50 bg-red-900/10'
|
|
: 'border-slate-700 bg-slate-800/50 hover:border-slate-600'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-3">
|
|
<div className={`p-2 rounded-lg ${
|
|
uploaded || (existing?.status === 'approved')
|
|
? 'bg-green-900/30 text-green-400'
|
|
: existing?.status === 'rejected'
|
|
? 'bg-red-900/30 text-red-400'
|
|
: 'bg-slate-700 text-slate-400'
|
|
}`}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-white">{docType.label}</p>
|
|
<p className="text-sm text-slate-400">{docType.description}</p>
|
|
{existing?.status === 'rejected' && existing.rejectionReason && (
|
|
<p className="text-sm text-red-400 mt-1">
|
|
<AlertCircle className="w-3 h-3 inline mr-1" />
|
|
{existing.rejectionReason}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{uploaded && (
|
|
<>
|
|
<img
|
|
src={uploaded.preview}
|
|
alt="Preview"
|
|
className="w-12 h-12 rounded object-cover"
|
|
/>
|
|
<button
|
|
onClick={() => removeDocument(docType.id)}
|
|
className="p-1 text-red-400 hover:text-red-300"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</>
|
|
)}
|
|
{!uploaded && existing?.status === 'approved' && (
|
|
<CheckCircle className="w-5 h-5 text-green-400" />
|
|
)}
|
|
{!uploaded && (!existing || existing.status === 'rejected') && (
|
|
<button
|
|
onClick={() => handleFileSelect(docType.id)}
|
|
className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-500 transition-colors"
|
|
>
|
|
<Upload className="w-4 h-4" />
|
|
Subir
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderReviewStep = () => (
|
|
<div className="space-y-6">
|
|
<div className="text-center mb-6">
|
|
<KYCStatusBadge status={status} rejectionReason={rejectionReason} size="lg" />
|
|
<p className="text-slate-400 mt-3">
|
|
{status === 'pending' && 'Tu documentación ha sido enviada y está pendiente de revisión.'}
|
|
{status === 'in_review' && 'Nuestro equipo está revisando tu documentación.'}
|
|
{status === 'approved' && 'Tu identidad ha sido verificada exitosamente.'}
|
|
{status === 'rejected' && 'Por favor, corrige los documentos rechazados y vuelve a enviar.'}
|
|
</p>
|
|
</div>
|
|
|
|
{status === 'approved' && (
|
|
<div className="bg-green-900/20 border border-green-800/50 rounded-lg p-6 text-center">
|
|
<CheckCircle className="w-12 h-12 text-green-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold text-white mb-2">Verificación Completa</h3>
|
|
<p className="text-slate-400">
|
|
Ya puedes realizar retiros y acceder a todas las funcionalidades de inversión.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{(status === 'pending' || status === 'in_review') && (
|
|
<div className="bg-blue-900/20 border border-blue-800/50 rounded-lg p-6 text-center">
|
|
<Clock className="w-12 h-12 text-blue-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold text-white mb-2">En Proceso</h3>
|
|
<p className="text-slate-400">
|
|
La verificación puede tomar entre 24 y 48 horas. Te notificaremos cuando esté lista.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{status === 'rejected' && (
|
|
<div className="bg-red-900/20 border border-red-800/50 rounded-lg p-6">
|
|
<AlertCircle className="w-12 h-12 text-red-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold text-white mb-2 text-center">Verificación Rechazada</h3>
|
|
{rejectionReason && (
|
|
<p className="text-red-400 text-center mb-4">{rejectionReason}</p>
|
|
)}
|
|
<button
|
|
onClick={() => setCurrentStep('personal')}
|
|
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500 transition-colors"
|
|
>
|
|
Reiniciar Verificación
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{personalData.fullName && (
|
|
<div className="bg-slate-800/50 rounded-lg p-4 border border-slate-700">
|
|
<h4 className="text-sm font-medium text-slate-400 mb-3">Datos Personales</h4>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
<div>
|
|
<span className="text-slate-500">Nombre:</span>
|
|
<span className="text-white ml-2">{personalData.fullName}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-slate-500">Nacionalidad:</span>
|
|
<span className="text-white ml-2">{personalData.nationality}</span>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<span className="text-slate-500">Dirección:</span>
|
|
<span className="text-white ml-2">
|
|
{personalData.address}, {personalData.city}, {personalData.country}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="bg-slate-900 rounded-xl border border-slate-800 max-w-2xl mx-auto">
|
|
<div className="p-6 border-b border-slate-800">
|
|
<h2 className="text-white text-xl font-semibold flex items-center gap-2">
|
|
<Shield className="w-5 h-5 text-blue-400" />
|
|
Verificación de Identidad (KYC)
|
|
</h2>
|
|
<p className="text-slate-400 text-sm mt-1">
|
|
Completa la verificación para habilitar retiros y acceder a todas las funcionalidades
|
|
</p>
|
|
</div>
|
|
|
|
{status === 'not_started' || status === 'rejected' ? (
|
|
<>
|
|
<div className="p-6 border-b border-slate-800">
|
|
{renderStepIndicator()}
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
{currentStep === 'personal' && renderPersonalDataStep()}
|
|
{currentStep === 'documents' && renderDocumentsStep()}
|
|
{currentStep === 'review' && renderReviewStep()}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="px-6 pb-4">
|
|
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
|
<p className="text-sm text-red-400">{error}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-6 border-t border-slate-800 flex justify-between">
|
|
<button
|
|
onClick={currentStep === 'personal' ? onCancel : () => setCurrentStep(currentStep === 'documents' ? 'personal' : 'documents')}
|
|
className="px-4 py-2 text-slate-300 hover:text-white transition-colors"
|
|
>
|
|
{currentStep === 'personal' ? 'Cancelar' : 'Atrás'}
|
|
</button>
|
|
|
|
{currentStep === 'review' ? (
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={isSubmitting}
|
|
className="flex items-center gap-2 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Enviando...
|
|
</>
|
|
) : (
|
|
<>
|
|
<CheckCircle className="w-4 h-4" />
|
|
Enviar Verificación
|
|
</>
|
|
)}
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={() => setCurrentStep(currentStep === 'personal' ? 'documents' : 'review')}
|
|
disabled={
|
|
(currentStep === 'personal' && !isPersonalDataValid()) ||
|
|
(currentStep === 'documents' && !isDocumentsComplete())
|
|
}
|
|
className="flex items-center gap-2 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
Continuar
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="p-6">
|
|
{renderReviewStep()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default KYCVerificationPanel;
|