/** * 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; documents?: Document[]; rejectionReason?: string; onSubmit?: (data: { personalData: PersonalData; documents: File[] }) => Promise; 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 = ({ status, personalData: initialPersonalData, documents: existingDocuments = [], rejectionReason, onSubmit, onCancel, }) => { const [currentStep, setCurrentStep] = useState(status === 'not_started' ? 'personal' : 'review'); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const fileInputRef = useRef(null); const [currentDocType, setCurrentDocType] = useState(null); const [personalData, setPersonalData] = useState({ 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>(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) => { 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 = () => (
{(['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 (
{isCompleted ? : index + 1}
{step === 'personal' ? 'Datos' : step === 'documents' ? 'Documentos' : 'Revisión'}
{index < 2 && (
)} ); })}
); const renderPersonalDataStep = () => (
Información Personal

Proporciona tus datos personales como aparecen en tu identificación oficial.

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" />
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" />
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" />
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" />
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" />
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" />
); const renderDocumentsStep = () => (
Documentos Requeridos

Sube fotos claras de tus documentos. Asegúrate de que toda la información sea legible.

{DOCUMENT_TYPES.map((docType) => { const uploaded = uploadedDocuments.get(docType.id); const existing = existingDocuments.find(d => d.type === docType.id); const Icon = docType.icon; return (

{docType.label}

{docType.description}

{existing?.status === 'rejected' && existing.rejectionReason && (

{existing.rejectionReason}

)}
{uploaded && ( <> Preview )} {!uploaded && existing?.status === 'approved' && ( )} {!uploaded && (!existing || existing.status === 'rejected') && ( )}
); })}
); const renderReviewStep = () => (

{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.'}

{status === 'approved' && (

Verificación Completa

Ya puedes realizar retiros y acceder a todas las funcionalidades de inversión.

)} {(status === 'pending' || status === 'in_review') && (

En Proceso

La verificación puede tomar entre 24 y 48 horas. Te notificaremos cuando esté lista.

)} {status === 'rejected' && (

Verificación Rechazada

{rejectionReason && (

{rejectionReason}

)}
)} {personalData.fullName && (

Datos Personales

Nombre: {personalData.fullName}
Nacionalidad: {personalData.nationality}
Dirección: {personalData.address}, {personalData.city}, {personalData.country}
)}
); return (

Verificación de Identidad (KYC)

Completa la verificación para habilitar retiros y acceder a todas las funcionalidades

{status === 'not_started' || status === 'rejected' ? ( <>
{renderStepIndicator()}
{currentStep === 'personal' && renderPersonalDataStep()} {currentStep === 'documents' && renderDocumentsStep()} {currentStep === 'review' && renderReviewStep()}
{error && (

{error}

)}
{currentStep === 'review' ? ( ) : ( )}
) : (
{renderReviewStep()}
)}
); }; export default KYCVerificationPanel;