/** * Oveyo PWA — App Root * 5 onglets + vue commerçant + sheets */ const LuApp = window.LucideReact || {}; const { GradientBar: GBar, FlashProvider: FP, useFlash: ufApp, Sheet: ShComp, Spinner: SpApp, } = window.OveyoUI; const { loadState: lS, saveState: sS, defaultState: dS, api: apiApp, useOnlineStatus: uOn, useCatalog: ucApp, } = window.OveyoState; const { HomePage, TopBar, MapPage, ReportPage, PharmacyPage, ProfilePage, MerchantView, } = window.OveyoPages; // ============================================================ // TAB BAR — Vivinter style // ============================================================ const TabBar = ({ active, onChange }) => { const { Home, MapPin, Camera, Cross, User } = LuApp; const tabs = [ { id: 'home', icon: Home, label: 'Accueil' }, { id: 'map', icon: MapPin, label: 'Carte' }, { id: 'report', icon: Camera, label: 'Partager', primary: true }, { id: 'pharmacy', icon: Cross, label: 'Pharmacie' }, { id: 'profile', icon: User, label: 'Moi' }, ]; return (
{tabs.map((tab) => { const Icon = tab.icon; const isActive = active === tab.id; if (tab.primary) { return ( ); } return ( ); })}
); }; // ============================================================ // SHEET : LOGIN OTP WhatsApp // ============================================================ const LoginSheet = ({ open, onClose, onSuccess }) => { const { ChevronRight, Loader2, Lock } = LuApp; const flash = ufApp(); const [step, setStep] = React.useState('phone'); const [phone, setPhone] = React.useState(''); const [code, setCode] = React.useState(''); const [loading, setLoading] = React.useState(false); const requestOtp = async () => { const cleanPhone = '221' + phone.replace(/\D/g, ''); if (cleanPhone.length < 11) { flash.show('error', 'Numéro invalide'); return; } setLoading(true); try { await apiApp('/api/pwa/otp/request', { method: 'POST', body: JSON.stringify({ phone: cleanPhone }), }); setStep('code'); flash.show('success', `Code envoyé sur WhatsApp au +${cleanPhone}`); } catch (e) { flash.show('error', "Impossible d'envoyer le code"); } finally { setLoading(false); } }; const verifyOtp = async () => { const cleanPhone = '221' + phone.replace(/\D/g, ''); setLoading(true); try { const data = await apiApp('/api/pwa/otp/verify', { method: 'POST', body: JSON.stringify({ phone: cleanPhone, code }), }); onSuccess({ phone: cleanPhone, pseudonym: data.pseudonym, neighborhood_slug: data.neighborhood_slug, is_ambassador: data.is_ambassador, }); flash.show('success', '✅ Connexion réussie'); setStep('phone'); setPhone(''); setCode(''); } catch (e) { flash.show('error', 'Code invalide ou expiré'); } finally { setLoading(false); } }; return ( {step === 'phone' ? (

On va t'envoyer un code à 6 chiffres par WhatsApp pour vérifier ton numéro.

setPhone(e.target.value)} placeholder="77 123 45 67" className="flex-1 p-3 rounded-xl text-base outline-none" style={{ background: '#F1F4F9' }} autoComplete="tel-national" inputMode="numeric" />
Format : 77 / 78 / 76 + 7 chiffres
Ton numéro reste privé. Il sert à attribuer tes prix partagés et débloquer ton espace ambassadeur.
) : (

Saisis le code à 6 chiffres reçu sur WhatsApp au +221 {phone}

setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="000000" className="w-full p-4 rounded-xl text-center font-display font-bold text-3xl outline-none" style={{ background: '#F1F4F9', letterSpacing: '0.4em' }} inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoFocus />
)}
); }; // ============================================================ // SHEET : ALERTE FRAUDE // ============================================================ const ReportFraudSheet = ({ open, onClose, state, onOpenSheet }) => { const { Send, Shield, Loader2, Lock } = LuApp; const flash = ufApp(); const [description, setDescription] = React.useState(''); const [shopName, setShopName] = React.useState(''); // V3.8 — champs libres au lieu d'un select des 50 quartiers const [quartierInput, setQuartierInput] = React.useState(''); const [villeInput, setVilleInput] = React.useState('Dakar'); const [loading, setLoading] = React.useState(false); // V4.0 — Login obligatoire pour signaler une fraude // (l'affichage reste anonyme côté communauté, mais on trace côté backend // pour empêcher les abus de signalement) const userPhone = state?.user?.phone; const isLoggedIn = typeof userPhone === 'string' && userPhone.length >= 10; const goToLogin = () => { onClose(); if (typeof onOpenSheet === 'function') { setTimeout(() => onOpenSheet('login'), 300); } }; const submit = async () => { if (!isLoggedIn) { flash.show('info', 'Connecte-toi avec WhatsApp pour signaler un écart de prix'); goToLogin(); return; } // V3.8 — validation client AVANT l'envoi (le backend exige min 15 chars) const desc = description.trim(); if (desc.length < 15) { flash.show('error', 'Décris l\'écart en au moins 15 caractères'); return; } setLoading(true); try { await apiApp('/api/pwa/fraud-alert', { method: 'POST', body: JSON.stringify({ description: desc, shop_name: shopName.trim() || null, neighborhood_name: quartierInput.trim() || null, city_name: villeInput.trim() || null, reported_by: userPhone, // V4.0 — tracé côté backend }), }); flash.show('success', '✅ Alerte envoyée anonymement'); setDescription(''); setShopName(''); setQuartierInput(''); setVilleInput('Dakar'); onClose(); } catch (e) { const status = e?.status; if (status === 422) { flash.show('error', 'Description trop courte ou invalide (min 15 caractères)'); } else if (status === 429) { flash.show('error', "Trop d'alertes récentes, réessaie dans 1h"); } else { flash.show('error', "Erreur d'envoi, réessaie"); } } finally { setLoading(false); } }; // V4.0 — Vue Connexion requise dans le sheet if (!isLoggedIn) { return (
Connexion requise
Pour signaler un écart de prix, tu dois te connecter avec WhatsApp.
Ton signalement reste 100% anonyme pour la communauté. Aucun numéro n'est partagé.
); } return (
🔒 100% anonyme. Aucun numéro de téléphone n'est partagé. Seul le prix compte. On veille à l'intérêt commun, ce n'est pas une dénonciation.