The whole app lived in one 3,071-line App.tsx on a Figma scaffold (60 unused packages, 45 unused UI components, no type-check). The live site could not reach the backend, Persian-keyboard digits were erased from the phone field, guests were signed out after an hour, "mark read" hit a method the server does not accept (405), and iOS Safari blocked the payment window. What changed: - src/ split into api/ (one typed client: timeouts, GET retry, single-flight refresh, SSE), screens/ (one file each), components/, state/, nav/, lib/. - The API address is configuration only: public/config.js at runtime, then VITE_API_BASE_URL at build time, then same-origin /api. No domain in code. - Responsive shell instead of a fixed 390x844 phone frame; self-hosted Vazirmatn instead of Google Fonts; strict TypeScript, `npm run build` type-checks first. - Everything on the handoff list: tutor answers keep their line breaks, have thumbs up/down, follow-ups, "from the book" badge, retry, history that survives tab switches and reloads, "why was I wrong" from a wrong answer with questionId; question reports; hearts with maxHearts/unlimited and a no-hearts screen with the refill time; result screen and a visible error when lesson completion fails; subscription status, badge, discount code before the gateway and the payment-return message; guest refresh token; 6-digit OTP with resend timer; age saved as a Jalali birthYear; PATCH read-all; leaderboard refetched on open with "you" highlighted; selected companion shown across the app; logout that revokes the refresh token. - New screens for endpoints that had none: profile, streak calendar, support tickets and bug reports, parents, invite-a-friend (with ?ref= tracking), guest-to-phone upgrade. - Browser/phone back button walks the screen stack. Checked with a headless-Chrome walkthrough against a local backend at 390x844 and 360x740 (fresh guest: onboarding, setup, a full lesson, map, island sheet, every tab and sub-screen, one real tutor question, reload, logout): no console errors, no failed API calls. Real SMS login and a real Zarinpal payment were not exercised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
30 lines
1.1 KiB
TypeScript
30 lines
1.1 KiB
TypeScript
import { KEYS, store } from '@/lib/storage';
|
|
|
|
/** برگشت از درگاهِ پرداخت: سرور کاربر را به `/?orderId=…&status=…` برمیگرداند. */
|
|
export interface PaymentReturn {
|
|
orderId: string;
|
|
status: string;
|
|
}
|
|
|
|
/**
|
|
* پارامترهای آدرسِ ورود را یکبار میخواند و از نوارِ آدرس پاک میکند، تا با
|
|
* رفرش دوباره پیامِ پرداخت نشان داده نشود:
|
|
* ?ref=CODE کدِ معرف (لینکِ دعوت)
|
|
* ?orderId=…&status=… نتیجهی پرداخت
|
|
*/
|
|
function readLaunchParams(): PaymentReturn | null {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const ref = params.get('ref');
|
|
const orderId = params.get('orderId');
|
|
const status = params.get('status');
|
|
|
|
if (ref) store.set(KEYS.referralCode, ref.slice(0, 40));
|
|
if (ref || orderId) {
|
|
window.history.replaceState(null, '', window.location.pathname);
|
|
}
|
|
return orderId ? { orderId, status: status ?? '' } : null;
|
|
}
|
|
|
|
/** یکبار، هنگامِ بار شدنِ اپ. */
|
|
export const launchPayment = readLaunchParams();
|