DataScope26/WEB
0
1import { Eye, EyeOff, AtSign, Loader, ShieldCheck } from "lucide-react";2import { useMemo, useRef, useState } from "react";3import { useAccountRecovery } from "../hooks/useAccountRecovery.js";4import ButtonPrimary from "./ButtonPrimary.jsx";5import ButtonSecondary from "./ButtonSecondary.jsx";6import ButtonClose from "./ButtonClose.jsx";7import "./AccountRecoveryModal.css";8 9export default function AccountRecoveryModal({ isOpen, onClose, onLoginClick }) {10 const {11 isLoading: recoveryLoading,12 error: recoveryError,13 success: recoverySuccess,14 sendRecoveryCode,15 verifyRecoveryCode,16 resetPasswordWithRecoveryCode,17 } = useAccountRecovery();18 19 const [showPassword, setShowPassword] = useState(false);20 const [message, setMessage] = useState("");21 const [recoveryStep, setRecoveryStep] = useState("request");22 const [recoveryEmail, setRecoveryEmail] = useState("");23 const [recoveryCode, setRecoveryCode] = useState(Array(6).fill(""));24 const [newPassword, setNewPassword] = useState("");25 const [confirmPassword, setConfirmPassword] = useState("");26 const codeRefs = useRef([]);27 28 const recoveryCodeValue = useMemo(() => recoveryCode.join(""), [recoveryCode]);29 30 const moveToNextCodeInput = (index, value) => {31 if (!/\d/.test(value) && value !== "") return;32 33 const nextCode = [...recoveryCode];34 nextCode[index] = value;35 setRecoveryCode(nextCode);36 37 if (value && index < 5) {38 codeRefs.current[index + 1]?.focus();39 }40 };41 42 const handleRecoveryCodeKeyDown = (index, event) => {43 if (event.key === "Backspace" && !recoveryCode[index] && index > 0) {44 codeRefs.current[index - 1]?.focus();45 }46 };47 48 const handleRecoveryRequest = async () => {49 const trimmedEmail = recoveryEmail.trim().toLowerCase();50 if (!trimmedEmail) {51 setMessage("Ingresá tu email para continuar.");52 return;53 }54 55 const ok = await sendRecoveryCode(trimmedEmail);56 if (ok) {57 setRecoveryStep("code");58 setMessage("");59 }60 };61 62 const handleRecoveryVerification = async () => {63 const trimmedEmail = recoveryEmail.trim().toLowerCase();64 const code = recoveryCodeValue;65 66 if (!trimmedEmail || code.length !== 6) {67 setMessage("Ingresá el código completo de 6 dígitos.");68 return;69 }70 71 const ok = await verifyRecoveryCode(trimmedEmail, code);72 if (ok) {73 setRecoveryStep("reset");74 setMessage("");75 }76 };77 78 const handlePasswordReset = async () => {79 const trimmedEmail = recoveryEmail.trim().toLowerCase();80 const code = recoveryCodeValue;81 82 if (!trimmedEmail || code.length !== 6) {83 setMessage("El código ingresado no es válido.");84 return;85 }86 87 if (newPassword.length < 8 || !/[A-Z]/.test(newPassword) || !/[a-z]/.test(newPassword)) {88 setMessage("La contraseña debe tener al menos 8 caracteres, una mayúscula y una minúscula.");89 return;90 }91 92 if (newPassword !== confirmPassword) {93 setMessage("Las contraseñas no coinciden.");94 return;95 }96 97 const ok = await resetPasswordWithRecoveryCode(trimmedEmail, code, newPassword);98 if (ok) {99 setRecoveryStep("success");100 setMessage("");101 }102 };103 104 const goToLogin = () => {105 setRecoveryStep("request");106 setRecoveryEmail("");107 setRecoveryCode(Array(6).fill(""));108 setNewPassword("");109 setConfirmPassword("");110 setMessage("");111 onLoginClick();112 };113 114 if (!isOpen) return null;115 116 return (117 <>118 <div className="modal-backdrop" onClick={onClose} />119 <div className="recovery-modal">120 <ButtonClose onClick={onClose} />121 122 {recoveryStep === "request" && (123 <>124 <div className="recovery-modal__header">125 <span className="recovery-modal__badge">Seguridad</span>126 <h2>Recuperar acceso</h2>127 </div>128 129 <p className="recovery-modal__description">130 Ingresá tu email para recibir el código de recuperación.131 </p>132 133 <div className="input-group recovery-input-group">134 <label>EMAIL</label>135 <div className="input-icon">136 <input137 type="email"138 value={recoveryEmail}139 onChange={(event) => setRecoveryEmail(event.target.value)}140 placeholder="nombre@usuario.com"141 maxLength={80}142 autoComplete="email"143 />144 <AtSign size={18} className="icon" />145 </div>146 </div>147 148 {(recoveryError || message) && (149 <p className="error-text error-text--animated">{recoveryError || message}</p>150 )}151 152 <div className="recovery-actions">153 <ButtonPrimary className="recovery-btn" onClick={handleRecoveryRequest} disabled={recoveryLoading}>154 {recoveryLoading ? <Loader size={18} className="loader-icon-animation" /> : "Enviar código"}155 </ButtonPrimary>156 <ButtonSecondary className="recovery-btn" onClick={onClose}>Volver</ButtonSecondary>157 </div>158 </>159 )}160 161 {recoveryStep === "code" && (162 <>163 <div className="recovery-modal__header">164 <span className="recovery-modal__badge">Verificación</span>165 <h2>Ingresá el código</h2>166 </div>167 168 <p className="recovery-modal__description">169 Te enviamos un código de 6 dígitos a <strong>{recoveryEmail}</strong>.170 </p>171 172 <div className="recovery-code-grid">173 {recoveryCode.map((digit, index) => (174 <input175 key={index}176 ref={(element) => (codeRefs.current[index] = element)}177 type="text"178 inputMode="numeric"179 maxLength={1}180 value={digit}181 onChange={(event) => moveToNextCodeInput(index, event.target.value.slice(-1))}182 onKeyDown={(event) => handleRecoveryCodeKeyDown(index, event)}183 className="recovery-code-input"184 autoComplete="one-time-code"185 />186 ))}187 </div>188 189 {(recoveryError || message) && (190 <p className="error-text error-text--animated">{recoveryError || message}</p>191 )}192 193 <div className="recovery-actions">194 <ButtonPrimary className="recovery-btn" onClick={handleRecoveryVerification} disabled={recoveryLoading}>195 {recoveryLoading ? <Loader size={18} className="loader-icon-animation" /> : "Confirmar código"}196 </ButtonPrimary>197 <ButtonSecondary className="recovery-btn" onClick={() => setRecoveryStep("request")}>Solicitar otro</ButtonSecondary>198 </div>199 </>200 )}201 202 {recoveryStep === "reset" && (203 <>204 <div className="recovery-modal__header">205 <span className="recovery-modal__badge">Nueva contraseña</span>206 <h2>Actualizá tu acceso</h2>207 </div>208 209 <p className="recovery-modal__description">210 Elegí una contraseña segura para continuar.211 </p>212 213 <div className="input-group recovery-input-group">214 <label>NUEVA CONTRASEÑA</label>215 <div className="input-icon">216 <input217 type={showPassword ? "text" : "password"}218 value={newPassword}219 onChange={(event) => setNewPassword(event.target.value)}220 placeholder="••••••••"221 maxLength={60}222 />223 {showPassword ? (224 <EyeOff size={18} className="icon" style={{ cursor: "pointer" }} onClick={() => setShowPassword(!showPassword)} />225 ) : (226 <Eye size={18} className="icon" style={{ cursor: "pointer" }} onClick={() => setShowPassword(!showPassword)} />227 )}228 </div>229 </div>230 231 <div className="input-group recovery-input-group">232 <label>REPETIR CONTRASEÑA</label>233 <div className="input-icon">234 <input235 type={showPassword ? "text" : "password"}236 value={confirmPassword}237 onChange={(event) => setConfirmPassword(event.target.value)}238 placeholder="••••••••"239 maxLength={60}240 />241 </div>242 </div>243 244 <div className="password-checklist recovery-password-checklist">245 <div className={newPassword.length >= 8 ? "password-checklist__ok" : "password-checklist__warn"}>246 {newPassword.length >= 8 ? "✓" : "•"} Mínimo 8 caracteres247 </div>248 <div className={/[A-Z]/.test(newPassword) && /[a-z]/.test(newPassword) ? "password-checklist__ok" : "password-checklist__warn"}>249 {/[A-Z]/.test(newPassword) && /[a-z]/.test(newPassword) ? "✓" : "•"} Mayúscula y minúscula250 </div>251 </div>252 253 {(recoveryError || message) && (254 <p className="error-text error-text--animated">{recoveryError || message}</p>255 )}256 257 <div className="recovery-actions">258 <ButtonPrimary className="recovery-btn" onClick={handlePasswordReset} disabled={recoveryLoading}>259 {recoveryLoading ? <Loader size={18} className="loader-icon-animation" /> : "Guardar contraseña"}260 </ButtonPrimary>261 <ButtonSecondary className="recovery-btn" onClick={() => setRecoveryStep("code")}>Atrás</ButtonSecondary>262 </div>263 </>264 )}265 266 {recoveryStep === "success" && (267 <>268 <div className="recovery-modal__header">269 <span className="recovery-modal__badge success-badge"><ShieldCheck size={16} /> Listo</span>270 <h2>Contraseña actualizada</h2>271 </div>272 273 <p className="recovery-modal__description">274 Tu contraseña fue actualizada correctamente. Ya podés iniciar sesión con tu nueva clave.275 </p>276 277 <div className="recovery-actions">278 <ButtonPrimary className="recovery-btn" onClick={goToLogin}>Iniciar sesión</ButtonPrimary>279 </div>280 </>281 )}282 </div>283 </>284 );285}286 