// mb-app.jsx — root app: routing, cart, themes, tweaks const THEMES = { marinho: { accent: "oklch(0.52 0.17 264)", turbo: "linear-gradient(135deg,#FF8A1E,#FF2E63)", ts: "rgba(255,60,99,.4)" }, litoral: { accent: "oklch(0.60 0.115 205)", turbo: "linear-gradient(135deg,#13C6B4,#0E86C4)", ts: "rgba(14,134,196,.4)" }, coral: { accent: "oklch(0.64 0.165 33)", turbo: "linear-gradient(135deg,#FFB020,#FF3D77)", ts: "rgba(255,61,119,.4)" }, }; const FONTS = { "Plus Jakarta Sans": "'Plus Jakarta Sans'", "DM Sans": "'DM Sans'", Poppins: "'Poppins'", Manrope: "'Manrope'" }; const LIGHT = { bg: "#F3F5FA", surface: "#FFFFFF", text: "#15173A", muted: "#71748F", line: "#E8EAF2", brand: "#181B57", cbbg: "#E7F6EE", cbink: "#0A6B43", ph1: "#EEF1F6", ph2: "#E4E8F0", pht: "#9AA0B4" }; const DARK = { bg: "#0D0F1E", surface: "#191C30", text: "#ECEEF7", muted: "#9A9EBE", line: "#2A2E48", brand: "#232652", cbbg: "#143226", cbink: "#7DE0AC", ph1: "#222640", ph2: "#1B1E33", pht: "#71768F" }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "marinho", "font": "Plus Jakarta Sans", "appearance": "Claro", }/*EDITMODE-END*/; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const th = THEMES[t.theme] || THEMES.marinho; const pal = t.appearance === "Escuro" ? DARK : LIGHT; const rootVars = { "--ui": (FONTS[t.font] || FONTS["DM Sans"]) + ", system-ui, sans-serif", "--mono": "'DM Mono', ui-monospace, monospace", "--accent": th.accent, "--accent-ink": "#fff", "--accent-50": "color-mix(in srgb, var(--accent) 13%, " + pal.surface + ")", "--accent-shadow": "color-mix(in srgb, var(--accent) 38%, transparent)", "--turbo-grad": th.turbo, "--turbo-shadow": th.ts, "--bg": pal.bg, "--surface": pal.surface, "--text": pal.text, "--muted": pal.muted, "--line": pal.line, "--brand": pal.brand, "--cb": "#0FA968", "--cb-bg": pal.cbbg, "--cb-ink": pal.cbink, "--ph1": pal.ph1, "--ph2": pal.ph2, "--ph-text": pal.pht, }; // ── navigation ── const [tab, setTab] = React.useState("home"); const [filt, setFilt] = React.useState({ region: "Todas", seg: null, cat: null, radius: 8, mode: "todos" }); const [filtersOpen, setFiltersOpen] = React.useState(false); const [cashbackOpen, setCashbackOpen] = React.useState(false); const [storeId, setStoreId] = React.useState(null); const [storeMode, setStoreMode] = React.useState("delivery"); const [product, setProduct] = React.useState(null); const [cartOpen, setCartOpen] = React.useState(false); const [checkoutOpen, setCheckoutOpen] = React.useState(false); const [success, setSuccess] = React.useState(null); const [review, setReview] = React.useState(null); const [replaceItem, setReplaceItem] = React.useState(null); const [toast, setToast] = React.useState(null); // ── cart ── const [cart, setCart] = React.useState({ partnerId: null, items: {} }); const cartCount = Object.values(cart.items).reduce((s, e) => s + e.qty, 0); const cartTotal = Object.values(cart.items).reduce((s, e) => s + e.item.price * e.qty, 0); const [novoLarOpen, setNovoLarOpen] = React.useState(false); const [servicesOpen, setServicesOpen] = React.useState(false); const [gastroOpen, setGastroOpen] = React.useState(false); const [blogInitialPost, setBlogInitialPost] = React.useState(null); const openBlog = (post) => { if (post) setBlogInitialPost(post); setTab("blog"); }; const [bookInfo, setBookInfo] = React.useState(null); // {partner, service} const [leadInfo, setLeadInfo] = React.useState(null); // {partner, listing} const [profileView, setProfileView] = React.useState(null); const [agendaDetail, setAgendaDetail] = React.useState(null); const [authed, setAuthed] = React.useState(true); const [perfilAuthOpen, setPerfilAuthOpen] = React.useState(false); const [notifsOpen, setNotifsOpen] = React.useState(false); // ── PWA install modal ── const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent) && !window.MSStream; const isAndroid = /Android/.test(navigator.userAgent); const isMobile = isIOS || isAndroid; // Safari em qualquer plataforma (iOS ou Mac) — não tem beforeinstallprompt const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // Mostra o botão "Instalar App" para quem pode instalar: prompt nativo, mobile ou Safari const canShowInstallUI = (pwaCanInstall || isMobile || isSafari) && !window.mbIsStandalone?.(); const [pwaCanInstall, setPwaCanInstall] = React.useState(false); const [pwaDismissed, setPwaDismissed] = React.useState(() => !!localStorage.getItem("pwa-modal-dismissed")); const [pwaInstalled, setPwaInstalled] = React.useState(false); const [pwaGuideOpen, setPwaGuideOpen] = React.useState(false); const [updateReady, setUpdateReady] = React.useState(false); const [updateStatus, setUpdateStatus] = React.useState(null); // null | "checking" | "done" | "error" React.useEffect(() => { if (window.mbCanInstallPWA && window.mbCanInstallPWA()) setPwaCanInstall(true); const onInstallable = () => setPwaCanInstall(true); const onInstalled = () => { setPwaCanInstall(false); }; const onUpdateAvail = () => setUpdateReady(true); window.addEventListener("pwa-installable", onInstallable); window.addEventListener("pwa-installed", onInstalled); window.addEventListener("pwa-update-available", onUpdateAvail); return () => { window.removeEventListener("pwa-installable", onInstallable); window.removeEventListener("pwa-installed", onInstalled); window.removeEventListener("pwa-update-available", onUpdateAvail); }; }, []); // Aciona a atualização: tenta via SW, se não tiver, limpa cache e recarrega const doAppUpdate = async () => { setUpdateStatus("checking"); try { const triggered = await window.mbTriggerUpdate?.(); if (!triggered) { // Sem SW esperando — limpa tudo e recarrega na força await window.mbClearCaches?.(); setUpdateStatus("done"); setTimeout(() => window.location.reload(true), 1200); } // Se triggered: o controllerchange vai recarregar automaticamente } catch { setUpdateStatus("error"); setTimeout(() => setUpdateStatus(null), 3000); } }; const [splashDone, setSplashDone] = React.useState(false); const [notifList, setNotifList] = React.useState([]); const [clubMember, setClubMember] = React.useState(false); const [clubOpen, setClubOpen] = React.useState(false); // ── endereços salvos (compartilhado com FiltersSheet e ShopScreen) ── const [savedAddresses, setSavedAddresses] = React.useState([]); React.useEffect(() => { if (!Auth.isLogged()) return; API.me.addresses().then(d => setSavedAddresses(d.addresses || [])).catch(() => {}); }, [authed]); // Carrega dados se já estiver logado ao abrir o app (modo guest bypass) React.useEffect(() => { if (Auth.isLogged()) loadUserData(); }, []); // ── segmentos da API (substitui o SEGMENTS hardcoded nos filhos) ── const [apiSegments, setApiSegments] = React.useState(null); // null = carregando React.useEffect(() => { API.segments().then(d => setApiSegments(d.segments || [])).catch(() => setApiSegments(SEGMENTS)); }, []); // ── feature flags ── const [featureFlags, setFeatureFlags] = React.useState({ loja: false, cashback: false, compras: false }); const [pilotEmails, setPilotEmails] = React.useState([]); React.useEffect(() => { API.home().then(d => { if (d?.feature_flags) setFeatureFlags(d.feature_flags); if (d?.pilot_emails) setPilotEmails(d.pilot_emails.map(e => e.toLowerCase())); }).catch(() => {}); }, []); const isPilot = (email) => pilotEmails.includes((email || "").toLowerCase()); // ── dados reais da API ── const [apiUser, setApiUser] = React.useState(null); const [apiWallet, setApiWallet] = React.useState(null); // Cache de parceiros da API: id → objeto parceiro (formato apiCompanyToPartner) const [partnerMap, setPartnerMap] = React.useState({}); const cachePartners = (list) => setPartnerMap(m => { const next = { ...m }; list.forEach(p => { next[p.id] = p; }); return next; }); // Resolve parceiro: cache da API primeiro, fallback estático const resolvePartner = (id) => partnerMap[id] || partnerById(id); const storePartner = resolvePartner(storeId); const cartPartner = resolvePartner(cart.partnerId); const flashToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 1800); }; // Handle MP payment return (?payment=success|failure|pending&order=uuid) React.useEffect(() => { const params = new URLSearchParams(window.location.search); const payStatus = params.get("payment"); const orderId = params.get("order"); if (!payStatus) return; // Clean URL window.history.replaceState({}, "", window.location.pathname); if (payStatus === "success") { flashToast("Pagamento confirmado! Pedido em andamento."); setTab("pedidos"); } else if (payStatus === "failure") { flashToast("Pagamento não concluído. Tente novamente."); } else if (payStatus === "pending") { flashToast("Pagamento em processamento. Avisaremos quando confirmar."); setTab("pedidos"); } if (orderId) { // Optionally open the order detail console.log("Payment return for order:", orderId, "status:", payStatus); } }, []); // Carrega wallet, notificações e perfil quando logado const loadUserData = async () => { try { const [walletData, txData, notifData, meData] = await Promise.all([ API.wallet.get(), API.wallet.transactions(), API.me.notifications().catch(() => ({ notifications: [] })), API.me.get().catch(() => null), ]); setApiWallet(apiWalletToMock(walletData, txData.transactions)); setNotifList((notifData.notifications || []).map(n => ({ id: n.id, kind: n.kind, title: n.title, text: n.body, time: new Date(n.created_at).toLocaleDateString("pt-BR"), group: "Recentes", unread: !n.read, }))); if (meData) setApiUser(meData); } catch (_) {} }; const openStore = (id, partnerObj) => { // partnerObj opcional: pré-popula o cache para evitar lookup estático if (partnerObj) cachePartners([partnerObj]); const p = partnerObj || resolvePartner(id); if (!p) { setStoreId(id); return; } setStoreMode(p.delivery ? "delivery" : "pickup"); setStoreId(id); }; const reallyAdd = (item, qty, note, pid) => { setCart(c => { const base = c.partnerId === pid ? c.items : {}; const ex = base[item.id]; return { partnerId: pid, items: { ...base, [item.id]: { item, qty: (ex ? ex.qty : 0) + qty, note: note || (ex && ex.note) || "" } } }; }); }; const confirmAdd = (qty, note) => { const item = product; setProduct(null); if (cart.partnerId && cart.partnerId !== storeId && cartCount > 0) { setReplaceItem({ item, qty, note }); } else { reallyAdd(item, qty, note, storeId); flashToast("Adicionado à sacola"); } }; const inc = (id) => setCart(c => ({ ...c, items: { ...c.items, [id]: { ...c.items[id], qty: c.items[id].qty + 1 } } })); const dec = (id) => setCart(c => { const q = c.items[id].qty - 1; const items = { ...c.items }; if (q <= 0) delete items[id]; else items[id] = { ...items[id], qty: q }; return { partnerId: Object.keys(items).length ? c.partnerId : null, items }; }); const getQty = (id) => cart.items[id] ? cart.items[id].qty : 0; const placeOrder = (o) => { setCheckoutOpen(false); setCartOpen(false); setSuccess({ ...o, partnerId: storeId }); setCart({ partnerId: null, items: {} }); }; const closeSuccess = () => { setSuccess(null); setStoreId(null); }; const enterApp = (userFromApi, goToPerfil = false) => { if (userFromApi) setApiUser(userFromApi); setAuthed(true); setPerfilAuthOpen(false); setTab(goToPerfil ? "perfil" : "home"); loadUserData(); }; const subscribeClub = () => { setClubMember(true); flashToast("Bem-vinda ao Clube Morar Bem!"); }; const cancelClub = () => { setClubMember(false); flashToast("Assinatura cancelada"); }; const logout = () => { Auth.clear(); setApiUser(null); setApiWallet(null); setProfileView(null); setStoreId(null); setNovoLarOpen(false); setServicesOpen(false); setGastroOpen(false); setNotifsOpen(false); setCartOpen(false); setCheckoutOpen(false); setSuccess(null); setClubOpen(false); setTab("home"); setCart({ partnerId: null, items: {} }); setAuthed(false); }; // Gate: perfil requer login const handleTab = (id) => { if (id === "perfil" && !Auth.isLogged()) { setPerfilAuthOpen(true); return; } setTab(id); }; const confirmBooking = async ({ dateISO, dateLabel, time, local }) => { const partner = bookInfo.partner; const service = bookInfo.service; const companyId = partner._raw?.id || partner.id; const serviceId = service._raw?.id || service.id; try { await API.appointments.create({ company_id: companyId, service_id: typeof serviceId === "string" && serviceId.includes("-") ? serviceId : undefined, date: dateISO, time, local, }); } catch (_) {} setSuccess({ kind: "agendamento", partnerId: partner.id, service: service.name, date: dateLabel, time, local }); setBookInfo(null); }; const confirmLead = async ({ pref, msg }) => { const partner = leadInfo.partner; const listing = leadInfo.listing; const companyId = partner._raw?.id || partner.id; const user = apiUser || USER; try { await API.leads.create({ company_id: companyId, listing_id: listing?._raw?.id || listing?.id || undefined, name: user.name, phone: user.phone, message: msg || "", channel: pref === "whats" ? "whatsapp" : "ligacao", }); } catch (_) {} setSuccess({ kind: "contato", partnerId: partner.id, pref }); setLeadInfo(null); }; return (
Sua sacola tem itens de {cartPartner && cartPartner.name}. Cada pedido é de um parceiro por vez. Deseja esvaziar e começar uma nova sacola em {storePartner && storePartner.name}?