A parent account has no grade, so the app sent the demo mother to the "which grade are you in?" form and she never reached the parents screen. Now an account with a name, no grade and a parent-side family link opens straight on the parents screen (with a logout button, since it has no tab bar), and the setup form offers "I'm a parent, not a student" for new ones. The header's back button only shows when there is somewhere to go back to. Found on the live site with the demo accounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
6.6 KiB
TypeScript
152 lines
6.6 KiB
TypeScript
import { useEffect, useState } 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 { useToast } from '@/components/Toast';
|
||
import { PrimaryButton } from '@/components/ui';
|
||
import { AskScreen } from '@/screens/AskScreen';
|
||
import { CharactersScreen } from '@/screens/CharactersScreen';
|
||
import { FamilyScreen } from '@/screens/FamilyScreen';
|
||
import { HomeScreen } from '@/screens/HomeScreen';
|
||
import { InviteScreen } from '@/screens/InviteScreen';
|
||
import { LeaderboardScreen } from '@/screens/LeaderboardScreen';
|
||
import { LessonScreen } from '@/screens/LessonScreen';
|
||
import { LoginScreen } from '@/screens/LoginScreen';
|
||
import { MapScreen } from '@/screens/MapScreen';
|
||
import { NotificationsScreen } from '@/screens/NotificationsScreen';
|
||
import { ProfileScreen } from '@/screens/ProfileScreen';
|
||
import { QuizScreen } from '@/screens/QuizScreen';
|
||
import { StreakScreen } from '@/screens/StreakScreen';
|
||
import { SubscriptionScreen } from '@/screens/SubscriptionScreen';
|
||
import { SupportScreen } from '@/screens/SupportScreen';
|
||
import { UnitScreen } from '@/screens/UnitScreen';
|
||
import type { PaymentReturn } from './launchParams';
|
||
|
||
interface Props {
|
||
profile: UserProfile;
|
||
grades: Grade[];
|
||
payment: PaymentReturn | null;
|
||
/** صفحهی شروع؛ برای والد بخشِ والدین است. */
|
||
start?: Route;
|
||
onSignedOut: () => void;
|
||
onRegistered: (profile: UserProfile) => Promise<void>;
|
||
}
|
||
|
||
/** اپِ اصلی بعد از ورود: تبها، صفحهها و دادهی مشترک. */
|
||
export function MainApp({ profile, grades, payment, start, onSignedOut, onRegistered }: Props) {
|
||
return (
|
||
<AppDataProvider initialProfile={profile} grades={grades}>
|
||
<NavigatorProvider initial={start ?? { name: payment ? 'subscription' : 'home' }}>
|
||
<Screens onSignedOut={onSignedOut} onRegistered={onRegistered} />
|
||
{payment && <PaymentResult payment={payment} />}
|
||
</NavigatorProvider>
|
||
</AppDataProvider>
|
||
);
|
||
}
|
||
|
||
function Screens({ onSignedOut, onRegistered }: Pick<Props, 'onSignedOut' | 'onRegistered'>) {
|
||
const nav = useNav();
|
||
const { route } = nav;
|
||
const showNav = isTab(route.name) && nav.tab === route.name;
|
||
|
||
return (
|
||
<>
|
||
<div className="relative flex-1 overflow-hidden">
|
||
<AnimatePresence mode="popLayout" initial={false}>
|
||
<motion.div
|
||
key={JSON.stringify(route)}
|
||
className="absolute inset-0 flex flex-col"
|
||
initial={{ opacity: 0, x: -18 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
exit={{ opacity: 0, x: 18 }}
|
||
transition={{ duration: 0.18 }}
|
||
>
|
||
<RouteView route={route} onSignedOut={onSignedOut} onRegistered={onRegistered} />
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
{showNav && <BottomNav active={nav.tab} onChange={nav.switchTab} />}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function RouteView({ route, onSignedOut, onRegistered }: { route: Route } & Pick<Props, 'onSignedOut' | 'onRegistered'>) {
|
||
switch (route.name) {
|
||
case 'home': return <HomeScreen />;
|
||
case 'map': return <MapScreen />;
|
||
case 'ask': return <AskScreen lessonId={route.lessonId} questionId={route.questionId} prompt={route.prompt} />;
|
||
case 'subscription': return <SubscriptionScreen />;
|
||
case 'profile': return <ProfileScreen onSignedOut={onSignedOut} />;
|
||
case 'unit': return <UnitScreen unitId={route.unitId} />;
|
||
case 'lesson': return <LessonScreen lessonId={route.lessonId} nodeId={route.nodeId} />;
|
||
case 'quiz': return <QuizScreen lessonId={route.lessonId} nodeId={route.nodeId} />;
|
||
case 'notifications': return <NotificationsScreen />;
|
||
case 'leaderboard': return <LeaderboardScreen />;
|
||
case 'characters': return <CharactersScreen />;
|
||
case 'streak': return <StreakScreen />;
|
||
case 'support': return <SupportScreen />;
|
||
case 'family': return <FamilyScreen onSignedOut={onSignedOut} />;
|
||
case 'invite': return <InviteScreen />;
|
||
case 'register': return <RegisterView onRegistered={onRegistered} />;
|
||
}
|
||
}
|
||
|
||
/** مهمانی که شماره ثبت میکند؛ همان کاربر میماند و فقط پروفایل تازه میشود. */
|
||
function RegisterView({ onRegistered }: Pick<Props, 'onRegistered'>) {
|
||
const nav = useNav();
|
||
const { setProfile } = useAppData();
|
||
const toast = useToast();
|
||
return (
|
||
<LoginScreen
|
||
upgrade
|
||
onCancel={nav.back}
|
||
onDone={async () => {
|
||
try {
|
||
const profile = await api.profile.get();
|
||
setProfile(profile);
|
||
await onRegistered(profile);
|
||
} catch {
|
||
/* ورود انجام شده؛ پروفایل با بازگشت به اپ تازه میشود */
|
||
}
|
||
toast('شمارهت ثبت شد! پیشرفتت امنه.', 'success');
|
||
nav.back();
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
|
||
/** نتیجهی برگشت از درگاه؛ وضعیتِ سفارش از خودِ سرور هم دوباره خوانده میشود. */
|
||
function PaymentResult({ payment }: { payment: PaymentReturn }) {
|
||
const { refreshSummary } = useAppData();
|
||
const [open, setOpen] = useState(true);
|
||
const [paid, setPaid] = useState(payment.status === 'paid');
|
||
|
||
useEffect(() => {
|
||
api.subscription
|
||
.order(payment.orderId)
|
||
.then((order) => setPaid(order.status === 'paid'))
|
||
.catch(() => {});
|
||
void refreshSummary();
|
||
}, [payment.orderId, refreshSummary]);
|
||
|
||
return (
|
||
<Sheet open={open} onClose={() => setOpen(false)}>
|
||
<div className="flex flex-col items-center gap-3 pb-2 text-center">
|
||
{paid ? <CheckCircle2 size={52} className="text-mint" /> : <XCircle size={52} className="text-red-400" />}
|
||
<h2 className="text-lg font-black text-ink">{paid ? 'پرداخت موفق بود! 🎉' : 'پرداخت انجام نشد'}</h2>
|
||
<p className="text-sm leading-7 text-muted">
|
||
{paid
|
||
? 'اشتراکت فعال شد؛ از الان قلبهات نامحدوده.'
|
||
: 'اگه مبلغی از حسابت کم شده، نگران نباش؛ طیِ چند ساعت خودکار برمیگرده یا اشتراکت فعال میشه.'}
|
||
</p>
|
||
<PrimaryButton color={paid ? '#5FBF8F' : '#ED6956'} onClick={() => setOpen(false)}>باشه</PrimaryButton>
|
||
</div>
|
||
</Sheet>
|
||
);
|
||
}
|