UI readability pass and Bazaar in-app purchase bridge

- muted text #9e9ec0 -> #727299 (contrast 2.6 -> 4.5 on white); tab labels 11px
- map: chapter titles wrap to two lines instead of being cut off
- reduced-motion respected (MotionConfig + CSS), focus-visible ring,
  no pull-to-refresh, short vibration on correct/wrong answers
- src/lib/bazaar.ts: bridge to the Android WebView wrapper; checkout uses
  Bazaar IAP only inside that app, Zarinpal everywhere else (unchanged)
- android/: WebView wrapper with Poolakey; written but never built

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:13:12 +03:30
parent adecf25486
commit f8cf86e4e8
19 changed files with 619 additions and 68 deletions

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { AnimatePresence, MotionConfig, motion } from 'motion/react';
import { api, ApiError, http, type Grade, type UserProfile } from '@/api';
import { AppShell } from '@/components/AppShell';
import { ToastProvider } from '@/components/Toast';
@@ -83,52 +83,55 @@ export function App() {
};
return (
<AppShell>
<ToastProvider>
<AnimatePresence mode="wait">
<motion.div
key={flow.kind}
className="flex h-full flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{flow.kind === 'boot' && <SplashScreen />}
{flow.kind === 'bootError' && (
<div className="flex h-full flex-col justify-center">
<ErrorState error={flow.error} onRetry={() => void boot()} />
</div>
)}
{flow.kind === 'onboarding' && (
<OnboardingScreen
onDone={() => {
store.set(KEYS.onboarded, '1');
setFlow({ kind: 'login' });
}}
/>
)}
{flow.kind === 'login' && <LoginScreen onDone={() => void afterLogin()} />}
{flow.kind === 'setup' && (
<SetupScreen
profile={flow.profile}
grades={flow.grades}
onDone={(profile) => setFlow({ kind: 'main', profile, grades: flow.grades })}
/>
)}
{flow.kind === 'main' && (
<MainApp
profile={flow.profile}
grades={flow.grades}
payment={launchPayment}
onSignedOut={() => setFlow({ kind: 'login' })}
onRegistered={trackReferral}
/>
)}
</motion.div>
</AnimatePresence>
</ToastProvider>
</AppShell>
// reducedMotion="user": اگر «کاهشِ حرکت» در گوشی روشن باشد، motion جابه‌جایی‌ها را حذف می‌کند.
<MotionConfig reducedMotion="user">
<AppShell>
<ToastProvider>
<AnimatePresence mode="wait">
<motion.div
key={flow.kind}
className="flex h-full flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{flow.kind === 'boot' && <SplashScreen />}
{flow.kind === 'bootError' && (
<div className="flex h-full flex-col justify-center">
<ErrorState error={flow.error} onRetry={() => void boot()} />
</div>
)}
{flow.kind === 'onboarding' && (
<OnboardingScreen
onDone={() => {
store.set(KEYS.onboarded, '1');
setFlow({ kind: 'login' });
}}
/>
)}
{flow.kind === 'login' && <LoginScreen onDone={() => void afterLogin()} />}
{flow.kind === 'setup' && (
<SetupScreen
profile={flow.profile}
grades={flow.grades}
onDone={(profile) => setFlow({ kind: 'main', profile, grades: flow.grades })}
/>
)}
{flow.kind === 'main' && (
<MainApp
profile={flow.profile}
grades={flow.grades}
payment={launchPayment}
onSignedOut={() => setFlow({ kind: 'login' })}
onRegistered={trackReferral}
/>
)}
</motion.div>
</AnimatePresence>
</ToastProvider>
</AppShell>
</MotionConfig>
);
}