// ===== AUTH: login / sign up · social + mobile OTP ===== const AUTH_KEY = 'nrsa_auth'; function GoogleMark({ size = 20 }) { return ( ); } function FacebookMark({ size = 20 }) { return ( ); } function AppleMark({ size = 20, color = '#000' }) { return ( ); } function AuthFlow({ t, dark, onAuth }) { const [mode, setMode] = useState('login'); // login | signup const [step, setStep] = useState('form'); // form | otp const [adminMode, setAdminMode] = useState(false); const [adminEmail, setAdminEmail] = useState(''); const [adminPass, setAdminPass] = useState(''); const [name, setName] = useState(''); const [phone, setPhone] = useState(''); const [loading, setLoading] = useState(''); // provider id while "connecting" const [code, setCode] = useState(''); const [digits, setDigits] = useState(['', '', '', '', '', '']); const [timer, setTimer] = useState(0); const [error, setError] = useState(''); const [verifying, setVerifying] = useState(false); const [legalDoc, setLegalDoc] = useState(null); // 'terms' | 'privacy' const boxRefs = useRef([]); // Maps a backend user object → the shape the app's onAuth expects. const mapUser = (u) => ({ name: u.fullName || u.full_name || 'NRSA User', method: u.authProvider || u.auth_provider || 'mobile', role: u.role || 'driver', email: u.email || undefined, phone: u.phone || undefined, verified: !!(u.isVerified ?? u.is_verified), occupation: u.occupation, school: u.school, industry: u.industry, age: u.age, address: u.address, since: new Date().getFullYear(), }); const navy = t.navy, accent = t.accent; const phoneClean = phone.replace(/[^\d]/g, ''); const phoneValid = phoneClean.length >= 10; useEffect(() => { if (timer <= 0) return; const id = setTimeout(() => setTimer(s => s - 1), 1000); return () => clearTimeout(id); }, [timer]); const social = (provider, label) => { if (loading) return; setLoading(provider); if (NRSAApi.isLive()) { // NOTE: replace this dev placeholder token with a real OAuth token obtained // from the provider's SDK/popup before production. The dev backend accepts // "mock:" tokens in non-production so this path is testable locally. const token = `mock:${provider}@nrsa.org.ph:${label}`; NRSAApi.socialLogin(provider, token) .then(r => onAuth(mapUser(r.user))) .catch(e => { setError(e.message); setLoading(''); }); return; } setTimeout(() => { onAuth({ name: label, method: provider, since: new Date().getFullYear() }); }, 950); }; const adminLogin = () => { if (!adminEmail.trim() || !adminPass.trim()) { setError('Enter your admin email and password'); return; } if (!/.+@.+\..+/.test(adminEmail)) { setError('Enter a valid email address'); return; } setError(''); setLoading('admin'); if (NRSAApi.isLive()) { NRSAApi.adminLogin(adminEmail.trim(), adminPass) .then(r => onAuth(mapUser(r.user))) .catch(e => { setError(e.message); setLoading(''); }); return; } setTimeout(() => { const nm = adminEmail.split('@')[0].replace(/[._]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); onAuth({ name: nm || 'NRSA Admin', method: 'admin', role: 'admin', email: adminEmail.trim(), verified: true, since: new Date().getFullYear() }); }, 950); }; const sendOtp = () => { if (!phoneValid) { setError('Enter a valid mobile number'); return; } if (mode === 'signup' && !name.trim()) { setError('Please enter your full name'); return; } setError(''); setLoading('phone'); if (NRSAApi.isLive()) { // Live: the backend texts the code; it is never returned to the client. NRSAApi.requestOtp('+63' + phoneClean) .then(() => { setCode(''); // unknown on the client in live mode setDigits(['', '', '', '', '', '']); setTimer(30); setStep('otp'); setLoading(''); setTimeout(() => boxRefs.current[0] && boxRefs.current[0].focus(), 120); }) .catch(e => { setError(e.message); setLoading(''); }); return; } setTimeout(() => { const gen = String(Math.floor(100000 + Math.random() * 900000)); setCode(gen); setDigits(['', '', '', '', '', '']); setTimer(30); setStep('otp'); setLoading(''); setTimeout(() => boxRefs.current[0] && boxRefs.current[0].focus(), 120); }, 900); }; const setDigit = (i, v) => { const d = v.replace(/[^\d]/g, '').slice(-1); setDigits(prev => { const next = [...prev]; next[i] = d; return next; }); setError(''); if (d && i < 5) boxRefs.current[i + 1] && boxRefs.current[i + 1].focus(); }; const onKey = (i, e) => { if (e.key === 'Backspace' && !digits[i] && i > 0) boxRefs.current[i - 1] && boxRefs.current[i - 1].focus(); }; const onPaste = (e) => { const txt = (e.clipboardData.getData('text') || '').replace(/[^\d]/g, '').slice(0, 6); if (!txt) return; e.preventDefault(); const next = ['', '', '', '', '', '']; txt.split('').forEach((c, i) => next[i] = c); setDigits(next); const last = Math.min(txt.length, 6) - 1; if (last >= 0) boxRefs.current[Math.min(last + 1, 5)] && boxRefs.current[Math.min(last + 1, 5)].focus(); }; const verify = () => { const entered = digits.join(''); if (entered.length < 6) { setError('Enter the full 6-digit code'); return; } setVerifying(true); if (NRSAApi.isLive()) { NRSAApi.verifyOtp('+63' + phoneClean, entered) .then(r => onAuth(mapUser(r.user))) .catch(e => { setError(e.message || 'Incorrect code. Please try again.'); setDigits(['', '', '', '', '', '']); boxRefs.current[0] && boxRefs.current[0].focus(); setVerifying(false); }); return; } setTimeout(() => { if (entered === code) { onAuth({ name: mode === 'signup' ? name.trim() : 'NRSA Responder', method: 'mobile', phone: '+63 ' + phoneClean, since: new Date().getFullYear() }); } else { setError('Incorrect code. Please try again.'); setDigits(['', '', '', '', '', '']); boxRefs.current[0] && boxRefs.current[0].focus(); setVerifying(false); } }, 800); }; const inputBase = { width: '100%', border: `1.5px solid ${t.border}`, background: t.inputBg, color: t.text, borderRadius: 14, padding: '14px 15px', outline: 'none', fontWeight: 500, fontSize: 15, }; const SocialBtn = ({ id, mark, label }) => ( ); return (
{/* Hero */}
NRSA
NRSA
National Road Safety Alliance
Sign in to access live traffic, crash reporting, and road-safety tools.
{/* Card */}
{step === 'form' ? (adminMode ? (
🛡️

Administrator sign in

Restricted access for NRSA system administrators.

{ setAdminEmail(e.target.value); setError(''); }} type="email" inputMode="email" placeholder="admin@nrsa.org.ph" style={inputBase} />
{ setAdminPass(e.target.value); setError(''); }} type="password" placeholder="••••••••" style={inputBase} />
{error &&
{error}
} {loading === 'admin' ? : 'Sign in to console'}
{NRSAApi.isMock() && (
💡 Demo: any email + password grants admin access.
)}
) : (
{/* Tabs */}
{[['login', 'Log in'], ['signup', 'Sign up']].map(([id, label]) => ( ))}

{mode === 'login' ? 'Welcome back' : 'Create your account'}

{mode === 'login' ? 'Log in to continue keeping the roads safer.' : 'Join the NRSA road-safety community in seconds.'}

{/* Social */}
} label="Google" /> } label="Facebook" /> } label="Apple" />
{/* Divider */}
OR USE MOBILE NUMBER
{/* Mobile form */}
{mode === 'signup' && (
{ setName(e.target.value); setError(''); }} placeholder="Juan Dela Cruz" style={inputBase} />
)}
🇵🇭 +63
{ setPhone(e.target.value); setError(''); }} inputMode="tel" placeholder="917 123 4567" style={{ ...inputBase, flex: 1 }} />
{error &&
{error}
} {loading === 'phone' ? : 'Send verification code'}

By continuing you agree to NRSA's{' '} {' '}and{' '} .

)) : (
📲

Verify your number

We sent a 6-digit code to +63 {phoneClean}.

{/* demo hint — only in mock mode (live mode texts the code) */} {NRSAApi.isMock() && (
💡 Demo code: {code}
)} {/* OTP boxes */}
{digits.map((d, i) => ( boxRefs.current[i] = el} value={d} onChange={e => setDigit(i, e.target.value)} onKeyDown={e => onKey(i, e)} onPaste={onPaste} inputMode="numeric" maxLength={1} style={{ width: '100%', aspectRatio: '1 / 1', minWidth: 0, textAlign: 'center', fontSize: 24, fontWeight: 800, border: `2px solid ${error ? t.danger : d ? t.mid : t.border}`, background: t.inputBg, color: t.text, borderRadius: 14, outline: 'none', fontVariantNumeric: 'tabular-nums', transition: 'border-color 0.18s', }} /> ))}
{error &&
{error}
} {verifying ? : 'Verify & continue'}
Didn't get the code?{' '} {timer > 0 ? Resend in {timer}s : }
)}
{legalDoc && setLegalDoc(null)} />}
); } const LEGAL_CONTENT = { terms: { title: 'Terms of Service', icon: '📜', updated: 'Last updated: May 2026', sections: [ ['1. Acceptance of Terms', 'By creating an account or using the NRSA app, you agree to these Terms of Service. If you do not agree, please do not use the app.'], ['2. Purpose of the Service', 'NRSA provides road-safety tools including live traffic, crash reporting, education, emergency contacts, and community features. The app is provided for public safety and informational purposes.'], ['3. Eligibility & Accounts', 'You must provide accurate information when registering. You are responsible for keeping your account secure and for all activity under it. Certain roles (e.g. Crash Responder) require verification.'], ['4. Acceptable Use', 'You agree not to submit false crash reports, misuse the SOS/emergency features, harass other users, or attempt to disrupt or gain unauthorized access to the service.'], ['5. Emergency Disclaimer', 'NRSA is not a substitute for official emergency services. In an emergency, always call 911 or the appropriate hotline directly. Traffic, weather, and routing data may not always be accurate or real-time.'], ['6. User Content', 'You retain ownership of content you submit (reports, posts, photos) but grant NRSA a license to use it to operate and improve road-safety services.'], ['7. Limitation of Liability', 'The service is provided "as is." NRSA is not liable for damages arising from reliance on app information, road decisions, or service interruptions.'], ['8. Changes', 'We may update these Terms. Continued use after changes constitutes acceptance. Material changes will be notified in-app.'], ['9. Contact', 'For questions about these Terms, contact support@nrsa.org.ph.'], ], }, privacy: { title: 'Privacy Policy', icon: '🔐', updated: 'Compliant with RA 10173 (Data Privacy Act of 2012)', sections: [ ['1. Who We Are', 'The National Road Safety Alliance (NRSA) is the Personal Information Controller responsible for your data under the Data Privacy Act of 2012 (RA 10173).'], ['2. Information We Collect', 'Identity (name, age, government ID), contact details (mobile, email, address), background (study/work, school or industry), location data (for traffic, SOS, crash reports), and any incident reports you submit.'], ['3. How We Use It', 'To verify users, power road-safety tools, respond to incidents, improve the service, and—only with your optional consent—send road-safety updates.'], ['4. Legal Basis', 'We process your data based on your consent and NRSA\u2019s legitimate road-safety mandate.'], ['5. How We Protect It', 'Data is encrypted, access-controlled, and never sold to third parties. We apply data minimization and retention limits.'], ['6. Your Rights', 'You may access, correct, object to processing, withdraw consent, or request erasure of your data, and lodge a complaint with the National Privacy Commission.'], ['7. Retention', 'We keep data only as long as necessary for these purposes or as required by law.'], ['8. Data Protection Officer', 'To exercise your rights or for privacy concerns, contact our DPO at dpo@nrsa.org.ph.'], ], }, }; function LegalSheet({ t, doc, onClose }) { const c = LEGAL_CONTENT[doc]; return (
e.stopPropagation()} style={{ width: '100%', background: t.bg, borderTopLeftRadius: 26, borderTopRightRadius: 26, padding: '10px 20px max(22px, env(safe-area-inset-bottom))', maxHeight: '86%', display: 'flex', flexDirection: 'column' }}>
{c.icon}

{c.title}

{c.updated}
{c.sections.map(([h, body], i) => (
{h}

{body}

))}
Got it
); } Object.assign(window, { AuthFlow, AUTH_KEY });