// screens-onboarding.jsx — splash, phone, OTP, profile setup function SplashScreen({ onNext }) { React.useEffect(() => { const t = setTimeout(onNext, 2400); return () => clearTimeout(t); }, []); return (
{/* gold particles bg */}
WHERE TRUST MEETS OPPORTUNITY
); } function PhoneScreen({ onNext, t, lang }) { const [phone, setPhone] = React.useState(''); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const [errorCode, setErrorCode] = React.useState(null); const submit = async () => { if (loading) return; setError(null); setErrorCode(null); // Garde-fou : numéro doit avoir au moins 8 chiffres const digits = (phone || '').replace(/\D/g, ''); if (digits.length < 8) { setError(lang === 'ka' ? 'არასწორი ნომერი' : 'Numéro invalide'); return; } if (!window.API_AVAILABLE) { // Mode dev/preview : pas d'API, on passe direct onNext(phone); return; } setLoading(true); try { const res = await window.TrustaAPI.requestOtp(phone, lang); onNext(res.phone || phone); } catch (err) { setError(errorMessage(err, lang)); setErrorCode(err?.code || null); setLoading(false); } }; // Mode "SMS down" : affiché quand notre opérateur SMS bloque les envois const smsDown = errorCode === 'sms_failed'; return (
{t('signin_register')}
setPhone(e.target.value)} type="tel" inputMode="tel" disabled={loading} placeholder="+33 6 12 34 56 78" style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', color: '#fff', fontFamily: 'Inter, system-ui', fontSize: 16, fontWeight: 500, letterSpacing: 0.3, opacity: loading ? 0.5 : 1, }} />
{error && !smsDown && (
{error}
)} {loading ? '...' : t('continue')} {smsDown ? (
{lang === 'ka' ? '⚠️ SMS დროებით მიუწვდომელია' : '⚠️ SMS temporairement indisponible'}
{lang === 'ka' ? 'ჩვენმა SMS ოპერატორმა გაშვების გამო ყველა გაგზავნა ანტი-სპამის შემოწმებაში გადაიტანა. ვამუშავებთ. სცადე ხვალ დილით ან მომწერე პირადში Facebook-ზე — დაგეხმარები ხელით.' : 'Notre opérateur SMS a temporairement bloqué les envois (vérification anti-spam suite au lancement). On rétablit ça dans la nuit. Réessaie demain matin, ou écris-moi en privé sur Facebook — je te connecte à la main 💛'}
) : (
{t('sms_disclaimer')}
)}
{t('no_account')} {t('register_link')}
); } function errorMessage(err, lang) { const msgs = { invalid_phone: { fr: 'Numéro invalide', ka: 'არასწორი ნომერი' }, rate_limited: { fr: 'Trop de demandes, réessayez plus tard', ka: 'ძალიან ბევრი მოთხოვნა, სცადეთ მოგვიანებით' }, too_many_requests_phone: { fr: 'Trop de demandes pour ce numéro', ka: 'ძალიან ბევრი მოთხოვნა ამ ნომრისთვის' }, too_many_requests_ip: { fr: 'Trop de demandes depuis votre IP', ka: 'ძალიან ბევრი მოთხოვნა' }, phone_not_verified_trial:{ fr: 'Numéro non vérifié (compte Twilio trial)', ka: 'ნომერი არ არის გადამოწმებული' }, invalid_code: { fr: 'Code incorrect ou expiré', ka: 'არასწორი ან ვადაგასული კოდი' }, network_error: { fr: 'Pas de connexion internet', ka: 'ინტერნეტის გარეშე' }, sms_failed: { fr: 'Échec envoi SMS, réessayez', ka: 'SMS-ის გაგზავნა ვერ მოხერხდა' }, }; const code = err?.code; if (code && msgs[code]) return msgs[code][lang] || msgs[code].fr; // Pas de mapping connu → on affiche le vrai message backend (utile pour debug) if (err?.message) return err.message; return msgs.sms_failed[lang] || msgs.sms_failed.fr; } window.errorMessage = errorMessage; function OtpScreen({ onNext, onBack, t, lang, phone }) { const [code, setCode] = React.useState(['', '', '', '', '', '']); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const refs = React.useRef([]); // Garde-fou : si on arrive sur cet écran sans phone, retour à PhoneScreen React.useEffect(() => { if (!phone && onBack) { console.warn('[OtpScreen] No phone provided, going back to PhoneScreen'); onBack(); } }, [phone]); const verify = async (digits) => { if (loading) return; setError(null); if (!window.API_AVAILABLE) { // Mode dev/preview : pas de vérification, on passe direct setTimeout(() => onNext({ isNewUser: true }), 350); return; } setLoading(true); try { const res = await window.TrustaAPI.verifyOtp(phone, digits); onNext({ isNewUser: res.is_new_user, user: res.user }); } catch (err) { setError(window.errorMessage(err, lang)); setCode(['', '', '', '', '', '']); refs.current[0]?.focus(); setLoading(false); } }; const setDigit = (i, v) => { if (!/^\d?$/.test(v)) return; const nc = [...code]; nc[i] = v; setCode(nc); if (v && i < 5) refs.current[i + 1]?.focus(); if (nc.every(d => d) && nc.join('').length === 6) { verify(nc.join('')); } }; return (
{t('enter_code')}
{t('code_sent_to')} {phone || '...'}
{[0, 1, 2, 3, 4, 5].map(i => ( refs.current[i] = el} value={code[i]} onChange={e => setDigit(i, e.target.value)} maxLength={1} inputMode="numeric" disabled={loading} style={{ width: 44, height: 56, borderRadius: 12, background: COLORS.card, border: `1px solid ${error ? COLORS.red : (code[i] ? COLORS.gold : COLORS.borderSoft)}`, color: COLORS.gold, fontFamily: '"Playfair Display", serif', fontSize: 24, fontWeight: 600, textAlign: 'center', outline: 'none', opacity: loading ? 0.5 : 1, }} autoFocus={i === 0} /> ))}
{error && (
{error}
)}
verify(code.join(''))} disabled={loading || code.join('').length < 6}> {loading ? '...' : t('verify')}
{t('resend_code')}
); } function Field({ label, children }) { return (
{label}
{children}
); } function ProfileSetupScreen({ onNext, t, lang }) { const [role, setRole] = React.useState('looking'); const [name, setName] = React.useState(lang === 'ka' ? 'ანი გ.' : 'Ani G.'); const [city, setCity] = React.useState(lang === 'ka' ? 'ლიონი' : 'Lyon'); const [referral, setReferral] = React.useState(''); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const finish = async () => { if (loading) return; setError(null); if (!window.API_AVAILABLE) { onNext(); return; } setLoading(true); try { await window.TrustaAPI.updateMe({ name, city, role: role === 'looking' ? 'worker' : 'employer', language: lang, referred_by: referral || undefined, }); onNext(); } catch (err) { setError(window.errorMessage(err, lang)); setLoading(false); } }; const inputStyle = { height: 48, borderRadius: 12, background: COLORS.card, border: `1px solid ${COLORS.borderSoft}`, padding: '0 14px', color: '#fff', fontFamily: 'Inter, system-ui', fontSize: 15, outline: 'none', }; return (
{[ { id: 'looking', label: t('i_am_looking') }, { id: 'offering', label: t('i_am_offering') }, ].map(r => ( ))}
setName(e.target.value)} style={inputStyle} /> setCity(e.target.value)} style={inputStyle} /> setReferral(e.target.value.toUpperCase())} placeholder="" style={inputStyle} /> {error && (
{error}
)}
{loading ? '...' : t('finish')}
); } window.SplashScreen = SplashScreen; window.PhoneScreen = PhoneScreen; window.OtpScreen = OtpScreen; window.ProfileSetupScreen = ProfileSetupScreen;