// ===== 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 }) => (
social(id, label)} disabled={!!loading} className="no-select" style={{
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 11, minHeight: 52,
border: `1.5px solid ${t.border}`, background: t.card, color: t.text, borderRadius: 14,
fontSize: 14.5, fontWeight: 700, opacity: loading && loading !== id ? 0.5 : 1, position: 'relative',
}}>
{loading === id
?
: mark}
{loading === id ? 'Connecting…' : `Continue with ${label}`}
);
return (
{/* Hero */}
NRSA
National Road Safety Alliance
Sign in to access live traffic, crash reporting, and road-safety tools.
{/* Card */}
{step === 'form' ? (adminMode ? (
{ setAdminMode(false); setError(''); }} className="no-select" style={{ display: 'flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', color: t.subtext, fontWeight: 700, fontSize: 13.5, padding: '16px 0 8px' }}>
Back to sign in
🛡️
Administrator sign in
Restricted access for NRSA system administrators.
{NRSAApi.isMock() && (
💡
Demo: any email + password grants admin access.
)}
) : (
{/* Tabs */}
{[['login', 'Log in'], ['signup', 'Sign up']].map(([id, label]) => (
{ setMode(id); setError(''); }} className="no-select" style={{
flex: 1, border: 'none', borderRadius: 11, padding: '11px', minHeight: 44, fontSize: 14, fontWeight: 700,
background: mode === id ? t.card : 'transparent', color: mode === id ? t.text : t.subtext,
boxShadow: mode === id ? t.shadow : 'none', transition: 'all 0.2s',
}}>{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 */}
{/* Mobile form */}
{mode === 'signup' && (
FULL NAME
{ setName(e.target.value); setError(''); }} placeholder="Juan Dela Cruz" style={inputBase} />
)}
{error &&
{error}
}
{loading === 'phone'
?
: 'Send verification code'}
By continuing you agree to NRSA's{' '}
setLegalDoc('terms')} className="no-select" style={{ background: 'none', border: 'none', padding: 0, font: 'inherit', fontSize: 11, fontWeight: 700, color: t.mid, textDecoration: 'underline', cursor: 'pointer' }}>Terms of Service
{' '}and{' '}
setLegalDoc('privacy')} className="no-select" style={{ background: 'none', border: 'none', padding: 0, font: 'inherit', fontSize: 11, fontWeight: 700, color: t.mid, textDecoration: 'underline', cursor: 'pointer' }}>Privacy Policy .
{ setAdminMode(true); setError(''); }} className="no-select" style={{ background: 'none', border: 'none', color: t.subtext, fontWeight: 700, fontSize: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
Sign in as Administrator
)) : (
{ setStep('form'); setError(''); }} className="no-select" style={{ display: 'flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', color: t.subtext, fontWeight: 700, fontSize: 13.5, padding: '16px 0 8px' }}>
Back
📲
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
: { const g = String(Math.floor(100000 + Math.random() * 900000)); setCode(g); setTimer(30); setError(''); setDigits(['','','','','','']); }} className="no-select" style={{ background: 'none', border: 'none', color: t.mid, fontWeight: 800, fontSize: 13 }}>Resend code }
)}
{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.sections.map(([h, body], i) => (
))}
Got it
);
}
Object.assign(window, { AuthFlow, AUTH_KEY });