import { lazy, Suspense, useEffect, useRef, useState, type ComponentType } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { CheckCircle2, XCircle } from 'lucide-react'; import { api, type Grade, type UserProfile } from '@/api'; import { AppDataProvider, useAppData } from '@/state/AppData'; import { NavigatorProvider, useNav } from '@/nav/Navigator'; import { isTab, type Route } from '@/nav/routes'; import { BottomNav } from '@/components/BottomNav'; import { Sheet } from '@/components/Sheet'; import { LoadingState, PrimaryButton } from '@/components/ui'; import { HomeScreen } from '@/screens/HomeScreen'; import type { PaymentReturn } from './launchParams'; /** * خانه همراهِ خودِ اپ بار می‌شود؛ بقیه‌ی صفحه‌ها هر کدام فایلِ جدا دارند تا بارِ اول * سبک باشد (قبلاً کلِ اپ یک فایلِ ۵۰۵ کیلوبایتی بود، و دستیار با ماژولِ شکل‌ها * بزرگ‌ترین تکه‌اش). بعد از بالا آمدنِ خانه همه در پس‌زمینه گرفته می‌شوند * (preloadScreens)، پس رفتن به صفحه‌ی بعدی منتظرِ شبکه نمی‌ماند. */ const loaders: (() => Promise)[] = []; function screen>, K extends keyof M>(load: () => Promise, name: K) { loaders.push(load); return lazy(async () => ({ default: (await load())[name] })); } const AskScreen = screen(() => import('@/screens/AskScreen'), 'AskScreen'); const CharactersScreen = screen(() => import('@/screens/CharactersScreen'), 'CharactersScreen'); const InviteScreen = screen(() => import('@/screens/InviteScreen'), 'InviteScreen'); const InvoicesScreen = screen(() => import('@/screens/InvoicesScreen'), 'InvoicesScreen'); const LeaderboardScreen = screen(() => import('@/screens/LeaderboardScreen'), 'LeaderboardScreen'); const LessonScreen = screen(() => import('@/screens/LessonScreen'), 'LessonScreen'); const LessonsScreen = screen(() => import('@/screens/LessonsScreen'), 'LessonsScreen'); const NotificationsScreen = screen(() => import('@/screens/NotificationsScreen'), 'NotificationsScreen'); const ProfileScreen = screen(() => import('@/screens/ProfileScreen'), 'ProfileScreen'); const ReportScreen = screen(() => import('@/screens/ReportScreen'), 'ReportScreen'); const QuizScreen = screen(() => import('@/screens/QuizScreen'), 'QuizScreen'); const StreakScreen = screen(() => import('@/screens/StreakScreen'), 'StreakScreen'); const SubscriptionScreen = screen(() => import('@/screens/SubscriptionScreen'), 'SubscriptionScreen'); const SupportScreen = screen(() => import('@/screens/SupportScreen'), 'SupportScreen'); const UnitScreen = screen(() => import('@/screens/UnitScreen'), 'UnitScreen'); let preloaded = false; function preloadScreens() { if (preloaded) return; preloaded = true; const run = () => loaders.forEach((load) => void load().catch(() => {})); if ('requestIdleCallback' in window) window.requestIdleCallback(run, { timeout: 2500 }); else setTimeout(run, 1200); } interface Props { profile: UserProfile; grades: Grade[]; payment: PaymentReturn | null; onSignedOut: () => void; } /** اپِ اصلی بعد از ورود: تب‌ها، صفحه‌ها و داده‌ی مشترک. */ export function MainApp({ profile, grades, payment, onSignedOut }: Props) { useEffect(preloadScreens, []); return ( {payment && } ); } function Screens({ onSignedOut }: Pick) { const nav = useNav(); const { route } = nav; const showNav = isTab(route.name) && nav.tab === route.name; // صفحه‌ی اولِ بعد از ورود بی‌انیمیشنِ ورود می‌آید، ولی این را روی خودِ motion.div می‌گذاریم نه // ``: آن یکی به همه‌ی فرزندهای motionِ صفحه‌ی اول هم سرایت می‌کند و // انیمیشن‌های تکرارشونده‌شان (ماسکوتِ بالا‌پایین، حلقه‌ی جزیره، دکمه‌ی «شروع») تا اولین تعویضِ تب شروع نمی‌شدند. const firstRender = useRef(true); useEffect(() => { firstRender.current = false; }, []); return ( <>
}>
{showNav && } ); } function RouteView({ route, onSignedOut }: { route: Route } & Pick) { switch (route.name) { case 'home': return ; case 'lessons': return ; case 'ask': return ; case 'subscription': return ; case 'profile': return ; case 'unit': return ; case 'lesson': return ; case 'quiz': return ; case 'notifications': return ; case 'leaderboard': return ; case 'characters': return ; case 'streak': return ; case 'support': return ; case 'report': return ; case 'invoices': return ; case 'invite': return ; } } /** * نتیجه‌ی برگشت از درگاه. فقط پاسخِ سرور تعیین می‌کند پرداخت موفق بوده یا نه؛ `status` در آدرس را هر * لینکِ ساختگی می‌تواند «paid» بگذارد. */ function PaymentResult({ payment }: { payment: PaymentReturn }) { const { refreshSummary } = useAppData(); const [open, setOpen] = useState(true); const [state, setState] = useState<'checking' | 'paid' | 'failed' | 'unknown'>('checking'); useEffect(() => { api.subscription .order(payment.orderId) .then((order) => setState(order.status === 'paid' ? 'paid' : 'failed')) .catch((err: unknown) => { console.warn('payment result lookup failed', err); setState('unknown'); }); void refreshSummary(); }, [payment.orderId, refreshSummary]); if (state === 'checking') { return ( setOpen(false)}> ); } const paid = state === 'paid'; return ( setOpen(false)}>
{paid ? : }

{paid ? 'پرداخت موفق بود! 🎉' : state === 'unknown' ? 'وضعیتِ پرداخت معلوم نشد' : 'پرداخت انجام نشد'}

{paid ? 'اشتراکت فعال شد؛ از الان قلب‌هات نامحدوده.' : state === 'unknown' ? 'چند دقیقه‌ی دیگه از «پرداخت‌ها و فاکتورها» در پروفایل نگاه کن. اگه مبلغی کم شده، یا خریدت فعال می‌شه یا پول خودکار برمی‌گرده.' : 'اگه مبلغی از حسابت کم شده، نگران نباش؛ طیِ چند ساعت خودکار برمی‌گرده یا اشتراکت فعال می‌شه.'}

setOpen(false)}>باشه
); }