// App principal — orquestra estado, navegação e telas const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": "cream", "serifSize": 26, "showTrialBanner": true, "verseLayout": "centered" }/*EDITMODE-END*/; const PALETTES = { cream: { '--cream':'#faf5e9','--cream-2':'#f5ecd3','--paper':'#fffaf0','--ink':'#2a2114','--ink-soft':'#5a4a30','--gold':'#b58a48','--gold-deep':'#7a5a30','--gold-soft':'#e7d3a3','--night':'#2a2540' }, ivory: { '--cream':'#f9f6f0','--cream-2':'#efe8d8','--paper':'#ffffff','--ink':'#1f1d1a','--ink-soft':'#4a463e','--gold':'#a8895a','--gold-deep':'#6e5836','--gold-soft':'#dccba6','--night':'#1d2536' }, rose: { '--cream':'#faf2ec','--cream-2':'#f3e2d4','--paper':'#fffaf6','--ink':'#2a1d1a','--ink-soft':'#5a3f36','--gold':'#b87862','--gold-deep':'#85503e','--gold-soft':'#e8c5b0','--night':'#3a2530' }, sage: { '--cream':'#f3f3ea','--cream-2':'#dde4d2','--paper':'#fdfdf7','--ink':'#1f261a','--ink-soft':'#3f4a36','--gold':'#83936a','--gold-deep':'#576443','--gold-soft':'#c4d2a8','--night':'#1f3030' }, }; // Hotmart checkout links (one per plan) const HOTMART_LINKS = { monthly: 'https://pay.hotmart.com/A105754709Y?off=2rvj1rlg', yearly: 'https://pay.hotmart.com/A105754709Y?off=h0efzjr5', }; function goToHotmart(plan, email) { const base = HOTMART_LINKS[plan] || HOTMART_LINKS.yearly; const sep = base.includes('?') ? '&' : '?'; // Pre-fill buyer email on Hotmart checkout so the webhook can match the account const url = `${base}${sep}email=${encodeURIComponent(email || '')}&checkoutMode=10`; window.location.href = url; } const { useState, useEffect, useMemo, useRef } = React; function App() { const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS); const [lang, setLangRaw] = useState(() => { const saved = localStorage.getItem('mv_lang'); if (saved === 'pt' || saved === 'en' || saved === 'es') return saved; const nav = (navigator.language || 'pt').toLowerCase(); if (nav.startsWith('en')) return 'en'; if (nav.startsWith('es')) return 'es'; return 'pt'; }); const setLang = (v) => { localStorage.setItem('mv_lang', v); setLangRaw(v); }; const [route, setRoute] = useState('landing'); // landing | auth | app | paywall | checkout const [authMode, setAuthMode] = useState('signup'); const [plan, setPlan] = useState('yearly'); // monthly | yearly const [tab, setTab] = useState('home'); // home | special | history | favorites | settings const [user, setUser] = useState(null); const [profile, setProfile] = useState(null); const [favs, setFavs] = useState([]); const [hist, setHist] = useState([]); const [share, setShare] = useState(null); const [favDetail, setFavDetail] = useState(null); const [randomSeed, setRandomSeed] = useState(Math.random()); const [verifyingPayment, setVerifyingPayment] = useState(false); const [newPassword, setNewPassword] = useState(''); const [resettingPw, setResettingPw] = useState(false); const [resetDone, setResetDone] = useState(false); // boot: restaurar sessão + detectar retorno do Hotmart (?paid=1) useEffect(() => { (async () => { // ⚠️ Verificar recovery PRIMEIRO — antes de qualquer getSession const hash = window.location.hash; const searchParams = new URLSearchParams(window.location.search); if (hash.includes('type=recovery') || searchParams.get('type') === 'recovery' || (hash.includes('access_token') && !searchParams.get('paid'))) { setRoute('reset-password'); window.history.replaceState({}, '', window.location.pathname); return; } const s = await window.DB.getSession(); if (s) { setUser(s.user); setProfile(s.profile); if (s.profile?.language) setLang(s.profile.language); setFavs(await window.DB.listFavorites()); setHist(await window.DB.listHistory()); setRoute('app'); // Setup push também ao restaurar sessão } // Quando voltar do checkout Hotmart, mostra "verificando" e re-busca o perfil const params = new URLSearchParams(window.location.search); if (params.get('paid') === '1') { setVerifyingPayment(true); const start = Date.now(); const tick = async () => { const ns = await window.DB.getSession(); if (ns?.profile?.subscription_status === 'active') { setProfile(ns.profile); setUser(ns.user); setVerifyingPayment(false); setRoute('app'); window.history.replaceState({}, '', window.location.pathname); return; } if (Date.now() - start < 90000) setTimeout(tick, 3000); else { setVerifyingPayment(false); window.history.replaceState({}, '', window.location.pathname); } }; setTimeout(tick, 1500); } })(); }, []); const t = window.I18N[lang]; // Aplicar paleta useEffect(() => { const p = PALETTES[tw.palette] || PALETTES.cream; Object.entries(p).forEach(([k, v]) => document.documentElement.style.setProperty(k, v)); }, [tw.palette]); // Trial logic const trialDaysLeft = useMemo(() => { if (!profile?.trial_started_at) return 0; const started = new Date(profile.trial_started_at); const now = new Date(); const days = Math.floor((now - started) / 86400000); return Math.max(0, 7 - days); }, [profile]); const isTrialExpired = profile?.subscription_status !== 'active' && trialDaysLeft === 0 && !!profile; // Persistir lang quando muda useEffect(() => { if (profile && profile.language !== lang) { window.DB.updateProfile({ language: lang }).then(setProfile); } }, [lang]); // Re-busca perfil periodicamente e quando usuário volta para a aba // Garante que trial expirado e assinaturas ativas sejam detectados sem recarregar useEffect(() => { if (!user) return; async function refreshProfile() { try { const s = await window.DB.getSession(); if (s?.profile) setProfile(s.profile); } catch (e) {} } // A cada 30 minutos const interval = setInterval(refreshProfile, 30 * 60 * 1000); // Imediatamente ao voltar para a aba const onVisible = () => { if (document.visibilityState === 'visible') refreshProfile(); }; document.addEventListener('visibilitychange', onVisible); return () => { clearInterval(interval); document.removeEventListener('visibilitychange', onVisible); }; }, [user]); // Registrar visualização no histórico useEffect(() => { if (route !== 'app' || !user) return; const today = new Date(); const data = tab === 'special' ? window.getRandomVerse(randomSeed) : window.getVerseForDate(today); if (tab === 'home' || tab === 'special') { window.DB.addHistory({ verse_ref: data.verse.ref[lang], theme_id: data.theme.id, kind: tab === 'special' ? 'random' : 'daily', }).then(setHist); } }, [tab, randomSeed, route, user, lang]); function handleAuth(res) { setUser(res.user); setProfile(res.profile); if (res.profile?.language) setLang(res.profile.language); setRoute('app'); setTab('home'); window.DB.listFavorites().then(setFavs); window.DB.listHistory().then(setHist); } async function handleSignOut() { await window.DB.signOut(); setUser(null); setProfile(null); setRoute('landing'); } const isFav = (ref) => favs.some(f => f.verse_ref === ref); async function toggleFav(item) { try { if (isFav(item.verse_ref)) setFavs(await window.DB.removeFavorite(item.verse_ref)); else setFavs(await window.DB.addFavorite(item)); } catch (e) { console.error('[favorites] toggle failed:', e); alert(e.message || 'Não foi possível salvar o favorito.'); } } async function completeSubscription(chosenPlan = 'yearly') { const until = new Date(); if (chosenPlan === 'monthly') until.setMonth(until.getMonth() + 1); else until.setFullYear(until.getFullYear() + 1); const np = await window.DB.updateProfile({ subscription_status: 'active', subscription_until: until.toISOString(), plan: chosenPlan, }); setProfile(np); setRoute('app'); } async function setupPushNotifications(userId, userLang) { if (!('Notification' in window) || !('serviceWorker' in navigator)) return; const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); const isStandalone = window.navigator.standalone === true; if (isIOS && !isStandalone) { const msg = { en: '📱 To receive daily verse notifications, add this app to your Home Screen.\n\nTap Share → "Add to Home Screen"', pt: '📱 Para receber notificações do versículo diário, adicione este app à sua tela inicial.\n\nToque em Compartilhar → "Adicionar à tela de início"', es: '📱 Para recibir notificaciones del versículo diario, agrega esta app a tu pantalla de inicio.\n\nToca Compartir → "Agregar a la pantalla de inicio"', }[userLang] || ''; setTimeout(() => alert(msg), 1000); return; } try { // Registra o SW se ainda não estiver const reg = await navigator.serviceWorker.register('/sw.js'); await navigator.serviceWorker.ready; // Pede permissão const permission = await Notification.requestPermission(); if (permission !== 'granted') return; // Verifica se já tem subscription const existing = await reg.pushManager.getSubscription(); if (existing) return; // Cria subscription const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(window.__VAPID_PUBLIC_KEY__), }); // Salva via DB já autenticado await window.DB.savePushSubscription({ user_id: userId, subscription: sub.toJSON(), lang: userLang }); } catch (e) { console.error('[push]', e); } } function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = atob(base64); return new Uint8Array([...raw].map(c => c.charCodeAt(0))); } // Render let screen; if (route === 'landing') { screen = { setAuthMode('signup'); setRoute('auth'); }} onSignIn={() => { setAuthMode('signin'); setRoute('auth'); }} />; } else if (route === 'auth') { screen = setRoute('landing')} onForgot={() => setRoute('forgot')} onToggle={() => setAuthMode(authMode === 'signup' ? 'signin' : 'signup')} />; } else if (route === 'reset-password') { screen = (

{lang==='pt' ? 'Nova senha' : lang==='es' ? 'Nueva contraseña' : 'New password'}

{lang==='pt' ? 'Digite sua nova senha abaixo.' : lang==='es' ? 'Ingresa tu nueva contraseña.' : 'Enter your new password below.'}

{resetDone ? (

✓ {lang==='pt' ? 'Senha atualizada!' : lang==='es' ? '¡Contraseña actualizada!' : 'Password updated!'}

) : (
)}
); } else if (route === 'forgot') { screen = { setAuthMode('signin'); setRoute('auth'); }} />; } else if (route === 'paywall') { screen = { setPlan(p); goToHotmart(p, user?.email); }} onClose={() => setRoute('app')} />; } else { // App if (isTrialExpired && tab !== 'settings') { screen = { setPlan(p); goToHotmart(p, user?.email); }} onClose={() => setTab('settings')} />; } else { screen = (
Meu Versículo
{tw.showTrialBanner && profile?.subscription_status !== 'active' && trialDaysLeft > 0 && (
{trialDaysLeft} {t.trial_left}
)}
{tab === 'home' && } {tab === 'special' && setRandomSeed(Math.random())} animateOnMount={true} />} {tab === 'history' && ( setTab('home')} /> )} {tab === 'favorites' && ( setFavDetail(item)} /> )} {tab === 'settings' && setRoute('paywall')} onEnableNotifications={() => setupPushNotifications(user?.id, lang)} />}
{share && setShare(null)} />} {favDetail && setFavDetail(null)} onShare={(item) => { setShare({ text: item.text, ref: item.verse_ref, theme: window.THEMES?.find(x => x.id === item.theme_id)?.[lang] || '', themeId: item.theme_id }); }} />}
); } } // Frame só envolve o app autenticado const showFrame = route !== 'landing'; const stage = (
Meu Versículo

{t.landing_title}

{window.DB_MODE === 'supabase' ? 'Conectado ao Supabase.' : 'Modo local — dados salvos no navegador para o protótipo.'}

{showFrame ? (
{screen}
) : null}
); return ( <> {verifyingPayment && (
{lang==='en'?'Verifying your payment…':lang==='es'?'Verificando tu pago…':'Verificando seu pagamento…'}
{lang==='en'?'This usually takes a few seconds.':lang==='es'?'Esto suele tardar unos segundos.':'Isso costuma levar alguns segundos.'}
)} {route === 'landing' ? screen : stage} setTweak('palette', v)} /> setTweak('serifSize', v)} /> setTweak('showTrialBanner', v)} /> ); } function TabBtn({ label, on, onClick, icon }) { return ( ); } ReactDOM.createRoot(document.getElementById('root')).render();