// mb-auth.jsx — authentication flow (welcome, login, signup, forgot, otp, reset) const LOGO_SRC = "uploads/logomorarbem2.png"; // Fallback gradients when no slides are configured in the ADM const AUTH_SLIDES_FALLBACK = [ { c: ["#2C4E92", "#141d3c"] }, { c: ["#B97E4F", "#5f3c25"] }, { c: ["#E2762E", "#8f3a1e"] }, { c: ["#1C97AC", "#0d4f5e"] }, { c: ["#C2455B", "#5f1f2c"] }, { c: ["#3C6E55", "#1d3729"] }, ]; function BgCarousel({ band }) { const [idx, setIdx] = React.useState(0); const [imgSlides, setImgSlides] = React.useState(null); // null = loading // Load slides from API React.useEffect(() => { API.designAssets() .then(d => setImgSlides((d.login_slides || []).filter(s => s && s.startsWith("http")))) .catch(() => setImgSlides([])); }, []); const slides = imgSlides && imgSlides.length > 0 ? imgSlides : null; const count = slides ? slides.length : AUTH_SLIDES_FALLBACK.length; React.useEffect(() => { const t = setInterval(() => setIdx(x => (x + 1) % count), 4000); return () => clearInterval(t); }, [count]); return (
{slides // Real images from ADM ? slides.map((url, i) => (
)) // Gradient fallback while loading or when no slides configured : AUTH_SLIDES_FALLBACK.map((s, i) => (
)) }
{Array.from({ length: count }).map((_, i) => ( ))}
); } function AuthInput({ label, value, onChange, placeholder, type = "text", icon, password }) { const [show, setShow] = React.useState(false); const t = password ? (show ? "text" : "password") : type; return (
{label &&
{label}
}
{icon && } onChange(e.target.value)} style={{ width: "100%", boxSizing: "border-box", border: "1.5px solid var(--line)", borderRadius: 13, padding: icon ? "14px 44px 14px 42px" : "14px 14px", font: "600 14.5px var(--ui)", color: "var(--text)", background: "var(--surface)", outline: "none" }} /> {password && ( )}
); } function OtpInput({ value, onChange }) { const refs = React.useRef([null, null, null, null]); const focus = (i) => refs.current[i] && refs.current[i].focus(); const set = (i, raw) => { const digits = raw.replace(/\D/g, ""); if (!digits) { // apagou — limpa esta posição const arr = value.split(""); arr[i] = ""; onChange(arr.join("").slice(0, 4)); return; } // suporta colar / autocompletar vários dígitos de uma vez if (digits.length > 1) { const arr = value.split(""); digits.split("").forEach((d, j) => { if (i + j < 4) arr[i + j] = d; }); onChange(arr.join("").slice(0, 4)); const next = Math.min(i + digits.length, 3); focus(next); return; } const arr = value.split(""); arr[i] = digits[0]; onChange(arr.join("").slice(0, 4)); if (i < 3) focus(i + 1); }; return (
{[0, 1, 2, 3].map(i => ( refs.current[i] = el} inputMode="numeric" maxLength={4} value={value[i] || ""} onChange={e => set(i, e.target.value)} onKeyDown={e => { if (e.key === "Backspace" && !value[i] && i > 0) { const arr = value.split(""); arr[i - 1] = ""; onChange(arr.join("").slice(0, 4)); focus(i - 1); } }} style={{ width: 56, height: 64, textAlign: "center", border: value[i] ? "1.5px solid var(--accent)" : "1.5px solid var(--line)", borderRadius: 15, font: "800 26px var(--ui)", color: "var(--text)", background: "var(--surface)", outline: "none" }} /> ))}
); } function AuthHeader({ onBack, title, sub }) { return (
{onBack && }

{title}

{sub &&

{sub}

}
); } const Divider = () => (
ou
); function AuthFlow({ onAuthed, onToast, onCancel }) { const [screen, setScreen] = React.useState(Auth.isLogged() ? "autologin" : "welcome"); const [email, setEmail] = React.useState(""); const [pwd, setPwd] = React.useState(""); const [name, setName] = React.useState(""); const [phone, setPhone] = React.useState(""); const [cpf, setCpf] = React.useState(""); const [agree, setAgree] = React.useState(false); const [otp, setOtp] = React.useState(""); const [npwd, setNpwd] = React.useState(""); const [purpose, setPurpose] = React.useState("signup"); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(""); const err = (e) => { const status = e?.status; let msg = e?.detail || e?.title || "Algo deu errado. Tente novamente."; if (status === 409) msg = "E-mail ou CPF já cadastrado. Verifique seus dados ou faça login."; if (status === 400 && (msg.includes("OTP") || msg.includes("otp") || msg.includes("invalid"))) msg = "Código inválido ou expirado. Reenvie um novo código."; if (status === 403) msg = "E-mail ou senha incorretos."; setError(msg); onToast(msg); setLoading(false); }; // Auto-login com token existente React.useEffect(() => { if (screen === "autologin") { API.me.get().then(user => onAuthed(user)).catch(() => { Auth.clear(); setScreen("welcome"); }); } }, []); const goOtp = (p) => { setPurpose(p); setOtp(""); setError(""); setScreen("otp"); }; // ── Welcome ── if (screen === "welcome" || screen === "autologin") { return (
{onCancel && (
)}
Morar Bem

O guia da sua cidade — descubra parceiros, peça com cashback e cuide do seu lar.

{screen === "autologin" ?
:
setScreen("signup")}>Criar conta
}
Ao continuar você aceita os Termos de uso
e a Política de privacidade.
); } // ── Login ── if (screen === "login") { const doLogin = async () => { if (!email || !pwd) return; setLoading(true); setError(""); try { const { customer, tokens } = await API.auth.login({ email, password: pwd }); Auth.set(tokens.access_token); Auth.setRefresh(tokens.refresh_token); onAuthed(customer); } catch (e) { err(e); } finally { setLoading(false); } }; return (
setScreen("welcome")} size={40} bg="rgba(255,255,255,.92)" />
Morar Bem

Entrar

Que bom te ver de novo!

{ setEmail(v); setError(""); }} placeholder="voce@email.com" icon="mail" /> { setPwd(v); setError(""); }} placeholder="Sua senha" icon="lock" password /> {error &&
{error}
} {loading ? "Entrando…" : "Entrar"}
setScreen("signup")} />
); } // ── Signup ── if (screen === "signup") { const ok = name && email && phone && cpf && pwd && agree; const doSignup = async () => { if (!ok) return; setLoading(true); setError(""); try { await API.auth.forgot({ email, purpose: "signup" }); goOtp("signup"); } catch (e) { err(e); } finally { setLoading(false); } }; return ( setScreen("welcome")} title="Criar conta" sub="Leva menos de um minuto e o cashback já começa." />
{error &&
{error}
} {loading ? "Aguarde…" : "Criar conta"}
setScreen("login")} />
); } // ── Forgot ── if (screen === "forgot") { const doForgot = async () => { if (!email) return; setLoading(true); setError(""); try { await API.auth.forgot({ email }); goOtp("reset"); } catch (e) { err(e); } finally { setLoading(false); } }; return ( setScreen("login")} title="Esqueceu a senha?" sub="Informe seu e-mail e enviaremos um código para você criar uma nova senha." />
{loading ? "Enviando…" : "Enviar código"}
); } // ── OTP ── if (screen === "otp") { const doOtp = async () => { if (otp.length < 4) return; setLoading(true); setError(""); let navigated = false; try { await API.auth.verifyOtp({ email, otp }); if (purpose === "signup") { const { customer, tokens } = await API.auth.register({ name, email, phone, cpf, password: pwd }); Auth.set(tokens.access_token); Auth.setRefresh(tokens.refresh_token); navigated = true; onToast("Conta criada! Bem-vindo(a)"); onAuthed(customer); } else { navigated = true; setNpwd(""); setPwd(""); setScreen("reset"); } } catch (e) { err(e); } finally { if (!navigated) setLoading(false); } }; return ( setScreen(purpose === "signup" ? "signup" : "forgot")} title="Verificação" sub={`Enviamos um código de 4 dígitos para ${email || phone || "seu contato"}.`} />
{error &&
{error}
}
Não recebeu?
{loading ? "Verificando…" : "Confirmar"}
); } // ── Reset ── if (screen === "reset") { const doReset = async () => { if (npwd.length < 6 || npwd !== pwd) return; setLoading(true); setError(""); try { await API.auth.reset({ email, otp, password: npwd }); onToast("Senha alterada com sucesso"); setScreen("login"); } catch (e) { err(e); } }; return ( setScreen("login")} title="Nova senha" sub="Crie uma senha nova para acessar sua conta." />
{npwd && pwd && npwd !== pwd &&
As senhas não coincidem
} {loading ? "Salvando…" : "Salvar nova senha"}
); } return null; } function AuthScreen({ children }) { return
{children}
; } function AuthFooter({ q, a, onClick }) { return (
{q}
); } Object.assign(window, { AuthFlow, AuthInput, OtpInput });