/* iLPF v2 — Login + MFA variants Three side-by-side designs sharing the same step machine. Step 1: ID + kata laluan Step 2: SMS OTP (6 or 8 digit) — switches to backup-code sub-mode Step 3: Disahkan · redirecting */ const { useState, useEffect, useRef, useCallback } = React; /* ---------- icons (inline SVG) -------------------------------------- */ const Ic = { shield: (p) => ( ), phone: (p) => ( ), eye: (p) => ( ), eyeOff: (p) => ( ), check: (p) => ( ), arrowR: (p) => ( ), arrowL: (p) => ( ), key: (p) => ( ), alert: (p) => ( ), }; /* ---------- shared step machine ------------------------------------- */ function useLoginFlow(otpLen, onSubmitCreds, onSubmitOtp) { const [step, setStep] = useState(1); const [mode, setMode] = useState("sms"); // 'sms' | 'backup' const [userId, setUserId] = useState(""); const [password, setPassword] = useState(""); const [showPass, setShowPass] = useState(false); const [credBusy, setCredBusy] = useState(false); const [credErr, setCredErr] = useState(""); const otpRef = useRef(Array(otpLen).fill("")); const [otp, setOtpState] = useState(() => Array(otpLen).fill("")); const setOtp = useCallback((v) => { otpRef.current = v; setOtpState(v); }, []); const [otpBusy, setOtpBusy] = useState(false); const [otpErr, setOtpErr] = useState(""); const [resendIn, setResendIn] = useState(32); const [backup, setBackup] = useState(["", "", "", ""]); const [backupBusy, setBackupBusy] = useState(false); const [backupErr, setBackupErr] = useState(""); // Reset OTP array when length changes useEffect(() => { const fresh = Array(otpLen).fill(""); otpRef.current = fresh; setOtpState(fresh); }, [otpLen]); // Resend countdown useEffect(() => { if (step !== 2 || mode !== "sms") return; if (resendIn <= 0) return; const t = setTimeout(() => setResendIn((s) => s - 1), 1000); return () => clearTimeout(t); }, [step, mode, resendIn]); const submitCreds = useCallback( (e) => { e && e.preventDefault(); setCredErr(""); setCredBusy(true); if (onSubmitCreds) { onSubmitCreds({ userId, password }, ({ ok, error }) => { setCredBusy(false); if (ok) { setStep(2); setMode("sms"); setResendIn(32); } else setCredErr(error || "E-mel atau kata laluan tidak sah."); }); } else { setTimeout(() => { setCredBusy(false); setStep(2); setMode("sms"); setResendIn(32); }, 700); } }, [userId, password, onSubmitCreds] ); const submitOtp = useCallback(() => { setOtpBusy(true); setOtpErr(""); if (onSubmitOtp) { onSubmitOtp({ code: otpRef.current.join("") }, ({ ok, error, redirect }) => { setOtpBusy(false); if (ok) { setStep(3); if (redirect) setTimeout(() => { window.location.href = redirect; }, 1200); } else { setOtpErr(error || "Kod OTP tidak sah."); } }); } else { setTimeout(() => { setOtpBusy(false); setStep(3); }, 900); } }, [onSubmitOtp]); const submitBackup = useCallback(() => { setBackupBusy(true); setBackupErr(""); setTimeout(() => { setBackupBusy(false); setStep(3); }, 900); }, []); const resend = () => { setResendIn(32); setOtp(Array(otpLen).fill("")); setOtpErr(""); }; const restart = () => { setStep(1); setMode("sms"); setOtp(Array(otpLen).fill("")); setBackup(["", "", "", ""]); setOtpErr(""); setBackupErr(""); setCredErr(""); setResendIn(32); }; return { step, setStep, mode, setMode, userId, setUserId, password, setPassword, showPass, setShowPass, credBusy, credErr, submitCreds, otp, setOtp, otpBusy, otpErr, submitOtp, resendIn, resend, backup, setBackup, backupBusy, backupErr, submitBackup, restart, }; } /* ---------- OTP grid component -------------------------------------- */ function OtpGrid({ value, onChange, onComplete, dark, errored, len }) { const refs = useRef([]); const set = (i, v) => { const next = [...value]; next[i] = v; onChange(next); if (v && i < len - 1) refs.current[i + 1]?.focus(); if (next.every((d) => d !== "") && next.join("").length === len) { onComplete && onComplete(next.join("")); } }; const handleKey = (i, e) => { if (e.key === "Backspace" && !value[i] && i > 0) { refs.current[i - 1]?.focus(); } else if (e.key === "ArrowLeft" && i > 0) { refs.current[i - 1]?.focus(); } else if (e.key === "ArrowRight" && i < len - 1) { refs.current[i + 1]?.focus(); } }; const handlePaste = (e) => { const txt = (e.clipboardData.getData("text") || "") .replace(/\D/g, "") .slice(0, len); if (!txt) return; e.preventDefault(); const next = Array(len).fill(""); for (let i = 0; i < txt.length; i++) next[i] = txt[i]; onChange(next); const focusAt = Math.min(txt.length, len - 1); refs.current[focusAt]?.focus(); if (txt.length === len) onComplete && onComplete(txt); }; return (
Pengesahan dua langkah aktif untuk semua ALPF.
{f.mode === "sms" ? `Kod ${otpLen}-digit dihantar ke nombor berdaftar anda.` : "Masukkan kod sandaran 16-aksara dari pendaftaran anda."}
{f.mode === "sms" ? ( <>Selamat datang kembali. Membuka ruang kerja anda.
Ruang kerja Ahli Lembaga Penapisan Filem. Tenang. Padat. Decisif. Setiap tugasan, satu klik daripada selesai.
Masukkan butiran rasmi anda untuk meneruskan.
{f.mode === "sms" ? `Kami menghantar kod ${otpLen}-digit ke nombor berdaftar anda. Kod sah selama 5 minit.` : "Masukkan kod sandaran 16-aksara dari pendaftaran anda."}
{f.mode === "sms" ? ( <>Ruang kerja ALPF. Akses terhad kepada anggota Lembaga Penapisan Filem yang dilantik.
{f.mode === "sms" ? `Kod ${otpLen}-digit dihantar. Sah 5 minit.` : "Kod sandaran 16-aksara. Sekali guna sahaja."}
{f.mode === "sms" ? ( <>Sesi aktif. Membuka ruang kerja anda.