// 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 = () => (
);
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 && (
)}
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)" />
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 });