// app.jsx — main shell, navigation, tweaks panel wiring const { useState: _a_useState, useEffect: _a_useEffect, useMemo: _a_useMemo } = React; // Tweak defaults (must be valid JSON inside markers) const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "userState": "free_low", "campaign": "default", "fortuneStyle": "wheel", "accent": "purple-cyan", "lang": "en", "frame": "iphone", "firstRun": false }/*EDITMODE-END*/; const USER_STATES = { new_user: { premium: 'free', balance: { photo: 20, video: 0 }, label: 'New user (free starter)' }, free_low: { premium: 'free', balance: { photo: 5, video: 0 }, label: 'Free, low balance' }, free_normal: { premium: 'free', balance: { photo: 15, video: 0 }, label: 'Free, normal' }, paid: { premium: 'premium', balance: { photo: 120, video: 80 }, label: 'Premium' }, ref_premium: { premium: 'ref_premium', balance: { photo: 80, video: 25 }, label: 'Referral Premium' }, returning: { premium: 'returning', balance: { photo: 8, video: 2 }, label: 'Old payer returning' }, }; const CAMPAIGNS = [ ['default', 'Default'], ['invoice_reminder', 'Invoice reminder'], ['post_result_upsell', 'Post-result upsell'], ['reactivation', 'Reactivation'], ['referral_challenge', 'Referral challenge'], ]; const ACCENTS = { 'purple-cyan': { a: '#9B5CFF', b: '#22D3EE' }, 'purple-gold': { a: '#9B5CFF', b: '#F6C85F' }, 'cyan-gold': { a: '#22D3EE', b: '#F6C85F' }, }; function App(){ // Tweaks state const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const mini = window.RavanaMiniApp || {}; const isDebug = Boolean(mini.debug); const [sessionState, setSessionState] = _a_useState({ loading: true, error: null, data: null, }); // Derive starting tab/screen from campaign context const contextToScreen = (context) => { if (!context) return null; if (context === 'referral_challenge') return 'referrals'; if (context && context.startsWith('fortune')) return 'fortune'; if (context && context.startsWith('invoice')) return 'invoice'; if (context && context.startsWith('post_result')) return 'credits'; if (context === 'reactivation_old_payer' || context === 'reactivation') return 'home'; if (context && context.includes('no_balance')) return 'credits'; if (context && context.includes('premium')) return 'credits'; if (context === 'faceswap') return 'faceswap'; if (context === 'multiface') return 'multiface'; if (context === 'imagegen') return 'imagegen'; return null; }; const routeFromContext = contextToScreen(mini.context) || (t.campaign === 'referral_challenge' ? 'referrals' : 'home'); const initialTab = ['home', 'create', 'fortune', 'credits', 'referrals'].includes(routeFromContext) ? routeFromContext : 'home'; const [tab, setTab] = _a_useState(initialTab); const [screenStack, setScreenStack] = _a_useState([routeFromContext]); // navigation history within tab const [screenParams, setScreenParams] = _a_useState({}); // App-level state const us = USER_STATES[t.userState] || USER_STATES.free_low; const [balance, setBalance] = _a_useState(us.balance); const sessionUser = sessionState.data && sessionState.data.user ? sessionState.data.user : null; const premium = sessionUser && sessionUser.premium ? 'premium' : us.premium; const [fortune, setFortune] = _a_useState({ spins: t.userState === 'paid' ? 2 : 0, points: t.userState === 'paid' ? 380 : 120, dailyReady: true, cooldown: '23h 12m', }); // Reset balance when user state changes via tweak _a_useEffect(() => { if (sessionUser && !isDebug) return; setBalance(USER_STATES[t.userState].balance); setFortune(f => ({ ...f, spins: t.userState === 'paid' ? 2 : 0, points: t.userState === 'paid' ? 380 : 120, })); }, [t.userState, sessionUser, isDebug]); _a_useEffect(() => { let cancelled = false; async function loadSession() { if (!mini.fetchSession) { setSessionState({ loading: false, error: null, data: null }); return; } try { const data = await mini.fetchSession(); if (cancelled) return; if (data && data.ok !== false && data.user && !isDebug) { setBalance(data.user.balance || { photo: 0, video: 0 }); } setSessionState({ loading: false, error: data && data.error ? data.error : null, data }); if (mini.track) mini.track('miniapp_opened', { context: mini.context }); } catch (error) { if (cancelled) return; setSessionState({ loading: false, error: error.message || 'Mini App session failed.', data: null }); } } loadSession(); return () => { cancelled = true; }; }, []); // Apply accent colors _a_useEffect(() => { const acc = ACCENTS[t.accent] || ACCENTS['purple-cyan']; document.documentElement.style.setProperty('--accent-a', acc.a); document.documentElement.style.setProperty('--accent-b', acc.b); }, [t.accent]); const T = (key, lang, vars) => window.t(key, lang || t.lang, vars); const navigate = (screen, params) => { setScreenParams(params || {}); // Update tab if root-level const tabMap = { home: 'home', create: 'create', fortune: 'fortune', credits: 'credits', referrals: 'referrals' }; if (tabMap[screen]) setTab(tabMap[screen]); setScreenStack(s => { if (s[s.length - 1] === screen) return s; return [...s, screen]; }); }; const goBack = () => { if (screenStack.length > 1) { setScreenStack(s => s.slice(0, -1)); } }; const switchTab = (newTab) => { setTab(newTab); setScreenStack([newTab]); setScreenParams({}); }; const currentScreen = screenStack[screenStack.length - 1]; // Active jobs depend on user state const jobs = useMemo(() => { if (!isDebug) return []; if (premium === 'free') return []; return window.RAVANA_DATA.active_jobs_default; }, [premium, isDebug]); const paymentMethods = ( sessionState.data && Array.isArray(sessionState.data.payment_methods) ? sessionState.data.payment_methods : ['telegram'] ); const features = (sessionState.data && sessionState.data.config && sessionState.data.config.features) || {}; const ctx = { t: T, lang: t.lang, user: sessionUser, balance, setBalance, premium, fortune, setFortune, navigate, goBack, campaign: t.campaign, miniContext: mini.context, paymentMethods, features, fortuneStyle: t.fortuneStyle, setLang: (l) => setTweak('lang', l), jobs, }; // Onboarding state — separate so re-runs don't depend on tweak persistence const onboardedKey = 'ravana_miniapp_onboarded_v1'; const shouldShowOnboarding = !isDebug && !window.localStorage.getItem(onboardedKey); const [onboardingOpen, setOnboardingOpen] = _a_useState(t.firstRun || shouldShowOnboarding); _a_useEffect(() => { if (t.firstRun) setOnboardingOpen(true); }, [t.firstRun]); const closeOnboarding = () => { setOnboardingOpen(false); window.localStorage.setItem(onboardedKey, '1'); if (t.firstRun) setTweak('firstRun', false); }; // Render correct screen let body; switch (currentScreen) { case 'home': body = ; break; case 'create': body = ; break; case 'faceswap': body = ; break; case 'multiface': body = ; break; case 'imagegen': body = ; break; case 'fortune': body = ; break; case 'credits': body = ; break; case 'invoice': body = ; break; case 'manage_subscription': body = ; break; case 'referrals': body = ; break; case 'profile': body = ; break; case 'support': body = ; break; case 'manual': body = ; break; case 'terms': body = ; break; default: body = ; } // Topbar balance summary (always visible) const topBalance = ( ); return ( <>
{T('brand')}
{topBalance}
{(sessionState.loading || sessionState.error) && (
{sessionState.loading ? ( <> Checking Telegram session... ) : ( <> {sessionState.error} )}
)} {body}
{isDebug && ( setTweak('userState', v)} options={Object.keys(USER_STATES).map(k => ({value: k, label: USER_STATES[k].label}))}/> setTweak('campaign', v)} options={CAMPAIGNS.map(([v,l])=>({value:v, label:l}))}/> setTweak('firstRun', v)}/> setTweak('fortuneStyle', v)} options={[ {value: 'wheel', label: 'Wheel'}, {value: 'reel', label: 'Slot reel'}, ]}/> setTweak('accent', v)} options={[ {value: 'purple-cyan', label: 'P/Cyan'}, {value: 'purple-gold', label: 'P/Gold'}, {value: 'cyan-gold', label: 'C/Gold'}, ]}/> setTweak('lang', v)} options={[ {value: 'en', label: 'English'}, {value: 'ru', label: 'Русский'}, ]}/> { setOnboardingOpen(true); }}>Replay onboarding navigate('faceswap', { prefill: true })}>Jump to Face Swap review navigate('imagegen', { prefill: true })}>Jump to Image Gen review navigate('multiface')}>Open MultiFace flow navigate('invoice')}>Open invoice resume navigate('manage_subscription')}>Manage subscription navigate('terms')}>Terms )} ); } // Boot const root = ReactDOM.createRoot(document.getElementById('phone-mount')); root.render();