+ {s.active ? (
+ <>
+ {s.expiresAt && تا {formatJalaliDate(s.expiresAt)}{low && ' · وقتِ تمدیده'}}
+ قلبِ نامحدود · {toFa(s.aiDailyQuota)} سوال در روز
+ >
+ ) : (
+ فصلِ اولِ هر پایه رایگانه؛ با اشتراک همهی فصلها و قلبِ نامحدود باز میشه.
+ )}
+
+ {s.questionCredits > 0 && (
+
+ {toFa(s.questionCredits)} سوالِ خریدهشده در اعتبارت
+
+ )}
+
+ );
+}
+
function Menu({ children }: { children: ReactNode }) {
return
{children}
;
}
@@ -273,6 +347,7 @@ function LogoutSheet({ open, switching = false, onClose, onSignedOut }: { open:
{switching && 'اول از این حساب خارج میشی، بعد با شمارهی دیگه وارد شو. '}
هر وقت خواستی با همین شماره دوباره وارد شو؛ پیشرفتت سرِ جاشه.
+
ممنون که با تدریسینو یاد میگیری 💛
نه، میمونم void logout()}>
diff --git a/src/screens/QuizScreen.tsx b/src/screens/QuizScreen.tsx
index 7a35d6c..09bfec2 100644
--- a/src/screens/QuizScreen.tsx
+++ b/src/screens/QuizScreen.tsx
@@ -95,7 +95,12 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
if (h && !h.isUnlimited && h.currentHearts <= 0) setPhase({ kind: 'noHearts', nextRefillAt: h.nextRefillAt, fullRefillAt: h.fullRefillAt });
else setPhase({ kind: 'playing' });
})
- .catch((error: unknown) => alive && setPhase({ kind: 'error', error }));
+ .catch((error: unknown) => {
+ if (!alive) return;
+ // فصلِ غیرِ اول بیاشتراک (۴۰۲)؛ صفحهی خطا معنا ندارد، کاربر یکراست به اشتراکها میرود.
+ if (error instanceof ApiError && error.code === 'subscription_required') nav.replace({ name: 'subscription', view: 'subscription' });
+ else setPhase({ kind: 'error', error });
+ });
return () => {
alive = false;
};
diff --git a/src/screens/SplashScreen.tsx b/src/screens/SplashScreen.tsx
index 10e04dd..f5a48b9 100644
--- a/src/screens/SplashScreen.tsx
+++ b/src/screens/SplashScreen.tsx
@@ -1,21 +1,35 @@
import { motion } from 'motion/react';
import { SeyarMascot } from '@/mascots/SeyarMascot';
+import { BrandMark } from '@/components/BrandMark';
-/** تا وقتی نشستِ قبلی بررسی میشود نمایش داده میشود. */
+/** نمادهای ریاضی که آرام از پایینِ صفحه بالا میروند؛ فقط CSS (transform/opacity)، بیبارِ اضافه. */
+const GLYPHS = [
+ { c: 'π', x: 12, d: 0 }, { c: '+', x: 28, d: 2.6 }, { c: '√', x: 46, d: 1.2 },
+ { c: '÷', x: 64, d: 3.8 }, { c: '∑', x: 80, d: 0.6 }, { c: '×', x: 90, d: 4.6 },
+];
+
+/** تا وقتی نشستِ قبلی بررسی میشود نمایش داده میشود؛ عمداً تأخیرِ ساختگی ندارد. */
export function SplashScreen() {
return (
-
+
+
+ {GLYPHS.map((g) => (
+
+ {g.c}
+
+ ))}
+
-
تدریسینو
-
ریاضی، قدمبهقدم، با همراههات
+
+
ریاضی، قدمبهقدم، از روی کتابِ درسی
= 360 && days <= 370) return 'یکساله';
if (days === 90) return 'سهماهه';
+ if (days === 180) return 'ششماهه';
return days ? `${toFa(days)} روزه` : '';
}
+type View = 'subscription' | 'pack';
+
/**
- * «بستهی سوال» (از ۱۴۰۵-۰۷ بهجای اشتراکِ ماهانه/سالانه). هر کس به اندازهی مصرفش سوال از
- * دستیار میخرد؛ اعتبار منقضی نمیشود و هر بسته چند روز قلبِ نامحدود هم هدیه دارد. هر روز
- * اول سوالهای رایگانِ همان روز خرج میشوند، بعد اعتبار.
+ * فروشگاه دو محصول دارد: «اشتراک» (یک/سه/ششماهه؛ همهی فصلها، قلبِ نامحدود، سقفِ روزانهی دستیار)
+ * و «بستهی سوال» (سوالِ بیانقضا از دستیار). خواستِ سینیور: پیش از فهرست دو گزینه بیاید تا شش کارت
+ * یکجا کاربر را گیج نکند. از فصلِ قفل یکراست به فهرستِ اشتراک میآید (`initialView`).
*/
-export function SubscriptionScreen() {
+export function SubscriptionScreen({ initialView }: { initialView?: View }) {
const plans = useAsync(() => api.subscription.plans(), []);
const status = useAsync(() => api.subscription.status(), []);
const quota = useAsync(() => api.ai.quota(), []);
+ const [view, setView] = useState(initialView ?? null);
const [buying, setBuying] = useState(null);
- // داخلِ اپِ بازار، طبقِ قانونِ بازار، خرید فقط با پرداختِ درونبرنامهایِ خودش است؛ بستهای که
+ // داخلِ اپِ بازار، طبقِ قانونِ بازار، خرید فقط با پرداختِ درونبرنامهایِ خودش است؛ محصولی که
// SKU ندارد آنجا خریدنی نیست (به زرینپال فرستاده نمیشود).
const bazaar = inBazaarApp();
const credits = status.data?.questionCredits ?? quota.data?.credits ?? 0;
const heartsUntil = status.data?.unlimitedHeartsUntil ?? null;
const reload = () => { status.reload(); quota.reload(); };
+ const list = plans.data?.plans.filter((p) => p.kind === view) ?? [];
+ const monthly = plans.data?.plans.find((p) => p.kind === 'subscription' && p.durationDays <= 31);
return (
-
+
-
-
-
-
-
اعتبارِ تو
-
{toFa(credits)} سوال
-
- {quota.data && (
-
- رایگانِ امروز
- {toFa(quota.data.remainingQuota)} از {toFa(quota.data.freeDaily)}
-
- )}
-
-
- هر روز {toFa(plans.data?.freeAiDailyQuota ?? 3)} سوالِ اول رایگانه؛ بعدش از اعتبارت کم میشه. اعتبار هیچوقت منقضی نمیشه.
-
- {heartsUntil && (
-
- قلبِ نامحدود تا {formatJalaliDate(heartsUntil)}
-
- )}
- {status.data?.active && (
-
- اشتراکِ قبلیت هم فعاله{status.data.expiresAt && <> تا {formatJalaliDate(status.data.expiresAt)}>}.
-
@@ -233,7 +311,7 @@ function CheckoutSheet({ plan, bazaar, onClose, onPaid }: { plan: Plan | null; b
{done ? (
<>
- {done === 'code' ? 'کدِ تخفیفت کلِ مبلغ رو پوشوند و همین الان به حسابت اضافه شد.' : 'پرداختت در بازار تأیید شد و سوالها به اعتبارت اضافه شدن.'}
+ {done === 'code' ? 'کدِ تخفیفت کلِ مبلغ رو پوشوند و همین الان به حسابت اضافه شد.' : 'پرداختت در بازار تأیید شد و به حسابت اضافه شد.'}