// Main app — routing + state + iPhone shell const { useState, useEffect, useRef, useCallback } = React; // ─── Persistência local (localStorage) ──────────────────────────── const STORAGE_KEY = 'cdv:state:v1'; const BOOTSTRAP_KEY = 'cdv:bootstrapped:v1'; function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); return parsed && typeof parsed === 'object' ? parsed : null; } catch (e) { return null; } } function loadBootstrap() { try { return localStorage.getItem(BOOTSTRAP_KEY) === '1'; } catch (e) { return false; } } const USER_KEY = 'cdv:user:v1'; function loadUser() { try { const raw = localStorage.getItem(USER_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } function persistUser(user) { try { if (user) localStorage.setItem(USER_KEY, JSON.stringify(user)); else localStorage.removeItem(USER_KEY); } catch (e) {} } function persistState(state) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) { // storage cheio ou bloqueado — ignora silenciosamente } } function resetAllData() { try { localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(BOOTSTRAP_KEY); localStorage.removeItem(USER_KEY); } catch (e) {} } // ─── Preferência de tema ────────────────────────────────────────── const THEME_PREF_KEY = 'cdv:themePref:v1'; function loadThemePref() { try { const v = localStorage.getItem(THEME_PREF_KEY); return v === 'light' || v === 'dark' || v === 'auto' ? v : 'auto'; } catch (e) { return 'auto'; } } function saveThemePref(pref) { try { localStorage.setItem(THEME_PREF_KEY, pref); } catch (e) {} } // Aplica tema sincronamente antes da hidratação do React para evitar flash (function applyInitialTheme() { try { const pref = loadThemePref(); const mode = window.resolveTheme ? window.resolveTheme(pref) : 'dark'; if (window.applyTheme) window.applyTheme(mode); } catch (e) {} })(); // Expor reset global para o cliente poder limpar dados via console se precisar window.CDV_RESET = resetAllData; const PhoneShell = ({ children, dark = true }) => (
{children}
); const App = () => { const [bootstrapped, setBootstrapped] = useState(loadBootstrap); const [user, setUser] = useState(loadUser); const [profile, setProfile] = useState(null); const [route, setRoute] = useState(() => { try { const r = new URLSearchParams(location.search).get('route'); const valid = ['dashboard','tasks','finance','habits','goals','calendar','ai','more','profile','achievements','plans','notifications','admin','vault']; return r && valid.includes(r) ? r : 'dashboard'; } catch (e) { return 'dashboard'; } }); const [toast, setToast] = useState({ text: '', visible: false, icon: 'check' }); const toastTimer = useRef(null); const [syncStatus, setSyncStatus] = useState({ status: 'idle', lastSyncedAt: null }); // Quando aplicamos um update vindo do cloud, evitamos disparar save em loop const skipNextCloudSaveRef = useRef(false); const debouncedSaveRef = useRef(null); // Tema const [themePref, setThemePref] = useState(loadThemePref); const [themeTick, setThemeTick] = useState(0); // força re-render quando tema muda // Push notifications: registra SW + re-agenda timers conforme tarefas mudam useEffect(() => { if (window.Push && window.Push.registerServiceWorker) { window.Push.registerServiceWorker(); } // Listener pra mensagens vindas do SW (clique em notificação) if (navigator && navigator.serviceWorker) { const onMsg = (event) => { const data = event.data || {}; if (data.type === 'NAVIGATE' && data.route) { setRoute(data.route); } }; navigator.serviceWorker.addEventListener('message', onMsg); return () => navigator.serviceWorker.removeEventListener('message', onMsg); } }, []); const applyAndPersistTheme = useCallback((pref) => { setThemePref(pref); saveThemePref(pref); const mode = window.resolveTheme ? window.resolveTheme(pref) : 'dark'; window.applyTheme && window.applyTheme(mode); setThemeTick(t => t + 1); }, []); // Re-aplicar tema quando preferência muda e, em 'auto', reavaliar a cada 5 min useEffect(() => { const mode = window.resolveTheme ? window.resolveTheme(themePref) : 'dark'; window.applyTheme && window.applyTheme(mode); setThemeTick(t => t + 1); if (themePref !== 'auto') return; const interval = setInterval(() => { const next = window.resolveTheme ? window.resolveTheme('auto') : 'dark'; if (next !== (window.CDV && window.CDV._mode)) { window.applyTheme && window.applyTheme(next); setThemeTick(t => t + 1); } }, 5 * 60 * 1000); return () => clearInterval(interval); }, [themePref]); // Sincronizar sessão Supabase ao montar e quando muda em outra aba/redirect OAuth useEffect(() => { if (!window.Auth) return; let mounted = true; window.Auth.getSession().then((u) => { if (mounted && u) { setUser(u); persistUser(u); // Se já tinha bootstrap salvo, mantém — senão considera logado e já bootstrapped if (!loadBootstrap()) setBootstrapped(true); } }); const sub = window.Auth.onAuthStateChange((u) => { setUser(u); persistUser(u); if (u && !loadBootstrap()) setBootstrapped(true); }); return () => { mounted = false; sub && sub.unsubscribe && sub.unsubscribe(); }; }, []); const handleLogout = useCallback(async () => { try { window.Auth && await window.Auth.signOut(); } catch (e) {} setUser(null); setProfile(null); persistUser(null); setBootstrapped(false); setRoute('dashboard'); }, []); // Carregar profile do user logado (papel admin, plano, etc.) useEffect(() => { if (!user || !user.id || user.isMock) { setProfile(null); return; } if (!window.AdminAPI) return; let cancelled = false; (async () => { const p = await window.AdminAPI.ensureProfile(user); if (!cancelled) setProfile(p); })(); return () => { cancelled = true; }; }, [user && user.id]); const [state, setState] = useState(() => { const saved = loadState(); if (saved) return saved; // Sem state salvo: se Supabase está configurado (usuário real), começa LIMPO. // Caso contrário (modo demo), carrega os dados de exemplo. // Atenção: o finance precisa ter a mesma estrutura do initialFinance // (saldo, categoriaGastos, cofrinhos etc) — várias telas leem direto. const cfg = window.Auth && window.Auth.isConfigured(); if (cfg) { return { tasks: [], habits: [], goals: [], finance: { saldo: 0, entradas: 0, saidas: 0, budget: 0, cartao: 0, cartaoLimite: 0, vencimento: '', transactions: [], cofrinhos: [], categoriaGastos: [], cards: [], }, events: [], vault: [], notifications: null, xp: 0, level: 1, streak: 0, }; } return { tasks: initialTasks, habits: initialHabits, goals: initialGoals, finance: initialFinance, events: eventsToday, notifications: null, xp: 1830, level: 12, streak: 47, }; }); // Persistir state e flag de bootstrap a cada mudança useEffect(() => { persistState(state); }, [state]); // Re-agendar notificações sempre que tarefas mudarem useEffect(() => { if (window.Push && window.Push.rescheduleTasks) { window.Push.rescheduleTasks(state.tasks); } }, [state.tasks]); useEffect(() => { try { localStorage.setItem(BOOTSTRAP_KEY, bootstrapped ? '1' : '0'); } catch (e) {} }, [bootstrapped]); // ─── Cloud sync ───────────────────────────────────────────── // Quando o user fica disponível e o CloudSync está configurado: // 1. Inicializa debounced saver // 2. Carrega state da nuvem (resolve conflito com local via updated_at) // 3. Inscreve em UPDATEs realtime (outros dispositivos) useEffect(() => { if (!user || !user.id || user.isMock) return; if (!window.CloudSync || !window.CloudSync.isConfigured()) return; let cancelled = false; debouncedSaveRef.current = window.CloudSync.makeDebouncedSaver(1500); (async () => { setSyncStatus({ status: 'loading', lastSyncedAt: null }); const cloud = await window.CloudSync.loadState(user.id); if (cancelled) return; if (cloud && cloud.data && Object.keys(cloud.data).length > 0) { // Tem dados na nuvem — aplica localmente sem disparar save de volta skipNextCloudSaveRef.current = true; setState(cloud.data); setSyncStatus({ status: 'saved', lastSyncedAt: cloud.updatedAt }); } else { // Primeira sincronização: empurra o state atual pro cloud const res = await window.CloudSync.saveState(user.id, state); if (cancelled) return; setSyncStatus({ status: res.ok ? 'saved' : 'error', lastSyncedAt: new Date().toISOString() }); } })(); const unsubscribe = window.CloudSync.subscribe(user.id, (remoteData, updatedAt) => { // Atualiza local sem reenviar pro cloud skipNextCloudSaveRef.current = true; setState(remoteData); setSyncStatus({ status: 'saved', lastSyncedAt: updatedAt }); }); return () => { cancelled = true; unsubscribe && unsubscribe(); }; }, [user && user.id]); // Salva no cloud quando state muda (debounced) useEffect(() => { if (!user || !user.id || user.isMock) return; if (!window.CloudSync || !window.CloudSync.isConfigured()) return; if (!debouncedSaveRef.current) return; if (skipNextCloudSaveRef.current) { skipNextCloudSaveRef.current = false; return; } debouncedSaveRef.current(user.id, state, (info) => { if (info.status === 'pending') { setSyncStatus(s => ({ ...s, status: 'pending' })); } else if (info.status === 'saving') { setSyncStatus(s => ({ ...s, status: 'saving' })); } else if (info.status === 'saved') { setSyncStatus({ status: 'saved', lastSyncedAt: new Date().toISOString() }); } else if (info.status === 'error') { setSyncStatus(s => ({ ...s, status: 'error' })); } }); }, [state, user && user.id]); const showToast = useCallback((text, icon = 'check') => { clearTimeout(toastTimer.current); setToast({ text, icon, visible: true }); toastTimer.current = setTimeout(() => setToast(t => ({ ...t, visible: false })), 1800); }, []); const navigate = (r) => setRoute(r); const openAI = () => setRoute('ai'); // Map tab IDs to routes; 'more' covers a few inner pages const bottomTab = ['dashboard', 'tasks', 'ai', 'finance', 'more', 'profile', 'achievements', 'plans', 'notifications', 'goals', 'calendar', 'habits', 'admin', 'vault'].includes(route) ? (['profile', 'achievements', 'plans', 'notifications', 'goals', 'habits', 'calendar', 'admin', 'vault'].includes(route) ? 'more' : route) : 'dashboard'; if (!bootstrapped) { return ( { if (payload && payload.user) { setUser(payload.user); persistUser(payload.user); } if (payload && payload.profile) { setState(s => ({ ...s, profile: payload.profile })); } if (payload && payload.paymentMethod && payload.user && payload.user.id && window.AdminAPI) { // Marca trial como ativo no Supabase const trialEnds = new Date(); trialEnds.setDate(trialEnds.getDate() + 3); window.AdminAPI.updateMyProfile(payload.user.id, { plan: 'trial', trial_ends_at: trialEnds.toISOString(), }).catch(() => {}); // Salva info do cartão localmente no state (último 4 + bandeira) setState(s => ({ ...s, paymentMethod: payload.paymentMethod })); } setBootstrapped(true); }} /> ); } // Guard: trial expirado → paywall obrigatório (admins passam direto) const trialStatus = window.Checkout && profile ? window.Checkout.checkTrialStatus(profile) : { expired: false, isPaying: false }; const isAdminUser = profile && profile.role === 'admin'; if (trialStatus.expired && !trialStatus.isPaying && !isAdminUser && window.MobilePaywall) { const refreshProfile = async () => { if (!window.AdminAPI || !user || !user.id) return; const p = await window.AdminAPI.ensureProfile(user); setProfile(p); showToast('Perfil atualizado'); }; return ( ); } return ( {route === 'ai' && ( <>
Sofia ouvindo )} {route === 'dashboard' && ( <> 47 dias · Lv 12 )}
{route === 'dashboard' && } {route === 'tasks' && } {route === 'finance' && } {route === 'habits' && } {route === 'goals' && } {route === 'ai' && } {route === 'calendar' && } {route === 'more' && } {route === 'admin' && } {route === 'vault' && } {route === 'profile' && } {route === 'achievements' && } {route === 'plans' && } {route === 'notifications' && }
); }; ReactDOM.createRoot(document.getElementById('root')).render();