forked from Tadrisino/MathApp_frontend
Remove every guest remnant: profile badge, register flow, delete-by-word
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,17 +39,16 @@ export const api = {
|
|||||||
http.post<{ success: boolean; devOtp?: string }>('/auth/otp/request', { phone }, { timeoutMs: 45_000 }),
|
http.post<{ success: boolean; devOtp?: string }>('/auth/otp/request', { phone }, { timeoutMs: 45_000 }),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* گامِ ۲: تأییدِ کد. اگر الان مهمان باشد، سرور همان کاربر را نگه میدارد و
|
* گامِ ۲: تأییدِ کد. شمارهی تازه حسابِ تازه میسازد.
|
||||||
* پیشرفتش از دست نمیرود (توکنِ مهمان همراهِ همین درخواست میرود).
|
|
||||||
*/
|
*/
|
||||||
login: (phone: string, otp: string) =>
|
login: (phone: string, otp: string) =>
|
||||||
http.post<T.AuthResponse>('/auth/login', { phone, otp }).then(saveTokens),
|
http.post<T.AuthResponse>('/auth/login', { phone, otp }).then(saveTokens),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* حذفِ همیشگیِ حساب. کاربرِ شمارهدار کدِ تازهی پیامکی میدهد (اول requestOtp با شمارهی خودش)،
|
* حذفِ همیشگیِ حساب. کاربر کدِ تازهی پیامکی میدهد (اول requestOtp با شمارهی خودش).
|
||||||
* مهمان کلمهی «حذف» را. بعد از موفقیت توکنها پاک میشوند؛ دیگر کاربری پشتشان نیست.
|
* بعد از موفقیت توکنها پاک میشوند؛ دیگر کاربری پشتشان نیست.
|
||||||
*/
|
*/
|
||||||
deleteAccount: (body: { otp?: string; confirm?: string }) =>
|
deleteAccount: (body: { otp: string }) =>
|
||||||
http.post<{ success: boolean }>('/user/delete', body).then((r) => {
|
http.post<{ success: boolean }>('/user/delete', body).then((r) => {
|
||||||
http.storage.set(null);
|
http.storage.set(null);
|
||||||
return r;
|
return r;
|
||||||
@@ -208,7 +207,7 @@ export const api = {
|
|||||||
|
|
||||||
affiliate: {
|
affiliate: {
|
||||||
me: () => http.get<T.AffiliateMe>('/affiliate/me'),
|
me: () => http.get<T.AffiliateMe>('/affiliate/me'),
|
||||||
/** کاربرِ تازه (با شماره، نه مهمان) را به صاحبِ کدِ معرف وصل میکند. */
|
/** کاربرِ تازه را به صاحبِ کدِ معرف وصل میکند. */
|
||||||
track: (referralCode: string) => http.post<{ success: boolean }>('/affiliate/track', { referralCode }),
|
track: (referralCode: string) => http.post<{ success: boolean }>('/affiliate/track', { referralCode }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export interface AuthResponse {
|
|||||||
token: string;
|
token: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
isNewUser?: boolean;
|
isNewUser?: boolean;
|
||||||
isGuest?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Grade {
|
export interface Grade {
|
||||||
@@ -33,7 +32,6 @@ export interface UserProfile {
|
|||||||
phone: string | null;
|
phone: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
isGuest: boolean;
|
|
||||||
gradeId: number | null;
|
gradeId: number | null;
|
||||||
birthYear: number | null;
|
birthYear: number | null;
|
||||||
/** تاریخِ تولدِ شمسی «1385-04-12»؛ null وقتی فقط سال داده شده یا هیچ. */
|
/** تاریخِ تولدِ شمسی «1385-04-12»؛ null وقتی فقط سال داده شده یا هیچ. */
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function App() {
|
|||||||
/** پروفایل و پایهها را میگیرد و تصمیم میگیرد کاربر کجا برود. */
|
/** پروفایل و پایهها را میگیرد و تصمیم میگیرد کاربر کجا برود. */
|
||||||
const enter = useCallback(async () => {
|
const enter = useCallback(async () => {
|
||||||
const [profile, grades] = await Promise.all([api.profile.get(), api.grades()]);
|
const [profile, grades] = await Promise.all([api.profile.get(), api.grades()]);
|
||||||
await trackReferral(profile);
|
await trackReferral();
|
||||||
if (profile.name && profile.gradeId) setFlow({ kind: 'main', profile, grades });
|
if (profile.name && profile.gradeId) setFlow({ kind: 'main', profile, grades });
|
||||||
else setFlow({ kind: 'setup', profile, grades });
|
else setFlow({ kind: 'setup', profile, grades });
|
||||||
}, []);
|
}, []);
|
||||||
@@ -124,7 +124,6 @@ export function App() {
|
|||||||
grades={flow.grades}
|
grades={flow.grades}
|
||||||
payment={launchPayment}
|
payment={launchPayment}
|
||||||
onSignedOut={() => setFlow({ kind: 'login' })}
|
onSignedOut={() => setFlow({ kind: 'login' })}
|
||||||
onRegistered={trackReferral}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -135,13 +134,10 @@ export function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** اگر کاربر با لینکِ دعوت آمده بود و حالا وارد شده، معرفی را ثبت کن. */
|
||||||
* اگر کاربر با لینکِ دعوت آمده بود و حالا با شماره وارد شده، معرفی را ثبت کن.
|
async function trackReferral() {
|
||||||
* مهمان را سرور نمیپذیرد، پس کد تا ثبتِ شماره نگه داشته میشود.
|
|
||||||
*/
|
|
||||||
async function trackReferral(profile: UserProfile) {
|
|
||||||
const code = store.get(KEYS.referralCode);
|
const code = store.get(KEYS.referralCode);
|
||||||
if (!code || profile.isGuest) return;
|
if (!code) return;
|
||||||
try {
|
try {
|
||||||
await api.affiliate.track(code);
|
await api.affiliate.track(code);
|
||||||
store.remove(KEYS.referralCode);
|
store.remove(KEYS.referralCode);
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import { NavigatorProvider, useNav } from '@/nav/Navigator';
|
|||||||
import { isTab, type Route } from '@/nav/routes';
|
import { isTab, type Route } from '@/nav/routes';
|
||||||
import { BottomNav } from '@/components/BottomNav';
|
import { BottomNav } from '@/components/BottomNav';
|
||||||
import { Sheet } from '@/components/Sheet';
|
import { Sheet } from '@/components/Sheet';
|
||||||
import { useToast } from '@/components/Toast';
|
|
||||||
import { LoadingState, PrimaryButton } from '@/components/ui';
|
import { LoadingState, PrimaryButton } from '@/components/ui';
|
||||||
import { HomeScreen } from '@/screens/HomeScreen';
|
import { HomeScreen } from '@/screens/HomeScreen';
|
||||||
import { LoginScreen } from '@/screens/LoginScreen';
|
|
||||||
import type { PaymentReturn } from './launchParams';
|
import type { PaymentReturn } from './launchParams';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,23 +52,22 @@ interface Props {
|
|||||||
grades: Grade[];
|
grades: Grade[];
|
||||||
payment: PaymentReturn | null;
|
payment: PaymentReturn | null;
|
||||||
onSignedOut: () => void;
|
onSignedOut: () => void;
|
||||||
onRegistered: (profile: UserProfile) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** اپِ اصلی بعد از ورود: تبها، صفحهها و دادهی مشترک. */
|
/** اپِ اصلی بعد از ورود: تبها، صفحهها و دادهی مشترک. */
|
||||||
export function MainApp({ profile, grades, payment, onSignedOut, onRegistered }: Props) {
|
export function MainApp({ profile, grades, payment, onSignedOut }: Props) {
|
||||||
useEffect(preloadScreens, []);
|
useEffect(preloadScreens, []);
|
||||||
return (
|
return (
|
||||||
<AppDataProvider initialProfile={profile} grades={grades}>
|
<AppDataProvider initialProfile={profile} grades={grades}>
|
||||||
<NavigatorProvider initial={{ name: payment ? 'subscription' : 'home' }}>
|
<NavigatorProvider initial={{ name: payment ? 'subscription' : 'home' }}>
|
||||||
<Screens onSignedOut={onSignedOut} onRegistered={onRegistered} />
|
<Screens onSignedOut={onSignedOut} />
|
||||||
{payment && <PaymentResult payment={payment} />}
|
{payment && <PaymentResult payment={payment} />}
|
||||||
</NavigatorProvider>
|
</NavigatorProvider>
|
||||||
</AppDataProvider>
|
</AppDataProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Screens({ onSignedOut, onRegistered }: Pick<Props, 'onSignedOut' | 'onRegistered'>) {
|
function Screens({ onSignedOut }: Pick<Props, 'onSignedOut'>) {
|
||||||
const nav = useNav();
|
const nav = useNav();
|
||||||
const { route } = nav;
|
const { route } = nav;
|
||||||
const showNav = isTab(route.name) && nav.tab === route.name;
|
const showNav = isTab(route.name) && nav.tab === route.name;
|
||||||
@@ -88,7 +85,7 @@ function Screens({ onSignedOut, onRegistered }: Pick<Props, 'onSignedOut' | 'onR
|
|||||||
transition={{ duration: 0.18 }}
|
transition={{ duration: 0.18 }}
|
||||||
>
|
>
|
||||||
<Suspense fallback={<LoadingState />}>
|
<Suspense fallback={<LoadingState />}>
|
||||||
<RouteView route={route} onSignedOut={onSignedOut} onRegistered={onRegistered} />
|
<RouteView route={route} onSignedOut={onSignedOut} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@@ -98,7 +95,7 @@ function Screens({ onSignedOut, onRegistered }: Pick<Props, 'onSignedOut' | 'onR
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RouteView({ route, onSignedOut, onRegistered }: { route: Route } & Pick<Props, 'onSignedOut' | 'onRegistered'>) {
|
function RouteView({ route, onSignedOut }: { route: Route } & Pick<Props, 'onSignedOut'>) {
|
||||||
switch (route.name) {
|
switch (route.name) {
|
||||||
case 'home': return <HomeScreen />;
|
case 'home': return <HomeScreen />;
|
||||||
case 'map': return <MapScreen />;
|
case 'map': return <MapScreen />;
|
||||||
@@ -116,34 +113,9 @@ function RouteView({ route, onSignedOut, onRegistered }: { route: Route } & Pick
|
|||||||
case 'report': return <ReportScreen />;
|
case 'report': return <ReportScreen />;
|
||||||
case 'invoices': return <InvoicesScreen />;
|
case 'invoices': return <InvoicesScreen />;
|
||||||
case 'invite': return <InviteScreen />;
|
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();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* نتیجهی برگشت از درگاه. فقط پاسخِ سرور تعیین میکند پرداخت موفق بوده یا نه؛ `status` در آدرس را هر
|
* نتیجهی برگشت از درگاه. فقط پاسخِ سرور تعیین میکند پرداخت موفق بوده یا نه؛ `status` در آدرس را هر
|
||||||
* لینکِ ساختگی میتواند «paid» بگذارد.
|
* لینکِ ساختگی میتواند «paid» بگذارد.
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ export type Route =
|
|||||||
| { name: 'report' }
|
| { name: 'report' }
|
||||||
/** پرداختها و فاکتورهای خودِ کاربر. */
|
/** پرداختها و فاکتورهای خودِ کاربر. */
|
||||||
| { name: 'invoices' }
|
| { name: 'invoices' }
|
||||||
| { name: 'invite' }
|
| { name: 'invite' };
|
||||||
| { name: 'register' };
|
|
||||||
|
|
||||||
export type TabName = 'home' | 'map' | 'ask' | 'subscription' | 'profile';
|
export type TabName = 'home' | 'map' | 'ask' | 'subscription' | 'profile';
|
||||||
|
|
||||||
|
|||||||
@@ -11,20 +11,9 @@ const OTP_LENGTH = 6;
|
|||||||
const RESEND_SECONDS = 120;
|
const RESEND_SECONDS = 120;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ورود با شماره (OTP). ورودِ مهمان نداریم: هر کاربر حداقل با شماره ثبتنام میکند.
|
* ورود با شماره (OTP): تنها راهِ ورود است.
|
||||||
*
|
|
||||||
* `upgrade` برای حسابِ مهمانِ قدیمی است (دیگر ساخته نمیشود) که از صفحهی «من»
|
|
||||||
* شماره ثبت میکند؛ سرور همان کاربر را نگه میدارد.
|
|
||||||
*/
|
*/
|
||||||
export function LoginScreen({
|
export function LoginScreen({ onDone, onCancel }: { onDone: () => void; onCancel?: () => void }) {
|
||||||
onDone,
|
|
||||||
onCancel,
|
|
||||||
upgrade = false,
|
|
||||||
}: {
|
|
||||||
onDone: () => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
upgrade?: boolean;
|
|
||||||
}) {
|
|
||||||
const [step, setStep] = useState<'phone' | 'otp'>('phone');
|
const [step, setStep] = useState<'phone' | 'otp'>('phone');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [otp, setOtp] = useState('');
|
const [otp, setOtp] = useState('');
|
||||||
@@ -117,10 +106,10 @@ export function LoginScreen({
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-black">{upgrade ? 'ثبتِ شمارهی موبایل' : 'ورود به تدریسینو'}</h1>
|
<h1 className="text-xl font-black">ورود به تدریسینو</h1>
|
||||||
<p className="mt-1 text-sm text-white/75">
|
<p className="mt-1 text-sm text-white/75">
|
||||||
{step === 'phone'
|
{step === 'phone'
|
||||||
? upgrade ? 'امتیازها و پیشرفتت سرِ جاش میمونه' : 'شمارهی موبایلت رو وارد کن'
|
? 'شمارهی موبایلت رو وارد کن'
|
||||||
: `کدِ ${toFa(OTP_LENGTH)} رقمیِ پیامکشده رو وارد کن`}
|
: `کدِ ${toFa(OTP_LENGTH)} رقمیِ پیامکشده رو وارد کن`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, type ReactNode } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
BarChart3, Bell, CalendarDays, Check, ChevronLeft, Coins, Flame, Gift, GraduationCap, LifeBuoy, LogOut,
|
BarChart3, Bell, CalendarDays, Check, ChevronLeft, Coins, Flame, Gift, GraduationCap, LifeBuoy, LogOut,
|
||||||
FileText, Pencil, Repeat, Smartphone, Sparkles, Star, Trash2, Trophy, Volume2, VolumeX,
|
FileText, Pencil, Repeat, Sparkles, Star, Trash2, Trophy, Volume2, VolumeX,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useAppData } from '@/state/AppData';
|
import { useAppData } from '@/state/AppData';
|
||||||
@@ -48,11 +48,7 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
|||||||
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-white/80">
|
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-white/80">
|
||||||
{gradeTitle && <span className="rounded-full bg-white/15 px-2.5 py-0.5">{gradeTitle}</span>}
|
{gradeTitle && <span className="rounded-full bg-white/15 px-2.5 py-0.5">{gradeTitle}</span>}
|
||||||
{age && <span className="rounded-full bg-white/15 px-2.5 py-0.5">{toFa(age)} ساله</span>}
|
{age && <span className="rounded-full bg-white/15 px-2.5 py-0.5">{toFa(age)} ساله</span>}
|
||||||
{profile.isGuest ? (
|
{profile.phone && <span dir="ltr" className="rounded-full bg-white/15 px-2.5 py-0.5">{toFa(profile.phone)}</span>}
|
||||||
<span className="rounded-full bg-[#FFD23F] px-2.5 py-0.5 font-bold text-ink">مهمان</span>
|
|
||||||
) : (
|
|
||||||
profile.phone && <span dir="ltr" className="rounded-full bg-white/15 px-2.5 py-0.5">{toFa(profile.phone)}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,21 +68,6 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 px-4 py-5">
|
<div className="flex flex-col gap-4 px-4 py-5">
|
||||||
{profile.isGuest && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={go({ name: 'register' })}
|
|
||||||
className="flex items-center gap-3 rounded-3xl bg-[#FFF8E1] p-4 text-right ring-2 ring-[#FFD23F]"
|
|
||||||
>
|
|
||||||
<Smartphone size={22} className="flex-shrink-0 text-gold" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-black text-ink">شمارهت رو ثبت کن</div>
|
|
||||||
<div className="mt-0.5 text-xs leading-6 text-body">تا اگه گوشیت عوض شد، امتیازها و پیشرفتت گم نشه.</div>
|
|
||||||
</div>
|
|
||||||
<ChevronLeft size={18} className="text-muted" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Menu>
|
<Menu>
|
||||||
<Row icon={GraduationCap} label="پایهی تحصیلی" value={gradeTitle} onClick={() => setEditing('grade')} />
|
<Row icon={GraduationCap} label="پایهی تحصیلی" value={gradeTitle} onClick={() => setEditing('grade')} />
|
||||||
<Row
|
<Row
|
||||||
@@ -106,7 +87,7 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
|||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<Menu>
|
<Menu>
|
||||||
{!profile.isGuest && <Row icon={Gift} label="معرفی به دوستان" onClick={go({ name: 'invite' })} />}
|
<Row icon={Gift} label="معرفی به دوستان" onClick={go({ name: 'invite' })} />
|
||||||
<Row icon={FileText} label="پرداختها و فاکتورها" onClick={go({ name: 'invoices' })} />
|
<Row icon={FileText} label="پرداختها و فاکتورها" onClick={go({ name: 'invoices' })} />
|
||||||
<Row icon={LifeBuoy} label="پشتیبانی و گزارشِ مشکل" onClick={go({ name: 'support' })} />
|
<Row icon={LifeBuoy} label="پشتیبانی و گزارشِ مشکل" onClick={go({ name: 'support' })} />
|
||||||
<Row
|
<Row
|
||||||
@@ -273,7 +254,6 @@ function BirthDateSheet({ open, onClose }: { open: boolean; onClose: () => void
|
|||||||
* دیگری میشد، و چند refresh tokenِ ماندگار در مرورگر خطرِ دزدیده شدن را چند برابر میکرد.
|
* دیگری میشد، و چند refresh tokenِ ماندگار در مرورگر خطرِ دزدیده شدن را چند برابر میکرد.
|
||||||
*/
|
*/
|
||||||
function LogoutSheet({ open, switching = false, onClose, onSignedOut }: { open: boolean; switching?: boolean; onClose: () => void; onSignedOut: () => void }) {
|
function LogoutSheet({ open, switching = false, onClose, onSignedOut }: { open: boolean; switching?: boolean; onClose: () => void; onSignedOut: () => void }) {
|
||||||
const { profile } = useAppData();
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
@@ -291,9 +271,7 @@ function LogoutSheet({ open, switching = false, onClose, onSignedOut }: { open:
|
|||||||
<Sheet open={open} onClose={onClose} title={switching ? 'ورود با حسابِ دیگر' : 'از حسابت خارج بشی؟'}>
|
<Sheet open={open} onClose={onClose} title={switching ? 'ورود با حسابِ دیگر' : 'از حسابت خارج بشی؟'}>
|
||||||
<p className="mb-5 text-sm leading-7 text-muted">
|
<p className="mb-5 text-sm leading-7 text-muted">
|
||||||
{switching && 'اول از این حساب خارج میشی، بعد با شمارهی دیگه وارد شو. '}
|
{switching && 'اول از این حساب خارج میشی، بعد با شمارهی دیگه وارد شو. '}
|
||||||
{profile.isGuest
|
هر وقت خواستی با همین شماره دوباره وارد شو؛ پیشرفتت سرِ جاشه.
|
||||||
? 'تو مهمانی: با همین گوشی و مرورگر، «فعلاً مهمان باشم» دوباره تو رو به همین پیشرفت برمیگردونه، ولی از گوشیِ دیگه نه. برای امنتر شدن، اول شمارهت رو ثبت کن.'
|
|
||||||
: 'هر وقت خواستی با همین شماره دوباره وارد شو؛ پیشرفتت سرِ جاشه.'}
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
<SecondaryButton onClick={onClose}>نه، میمونم</SecondaryButton>
|
<SecondaryButton onClick={onClose}>نه، میمونم</SecondaryButton>
|
||||||
@@ -307,7 +285,7 @@ function LogoutSheet({ open, switching = false, onClose, onSignedOut }: { open:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* حذفِ همیشگیِ حساب (POST /user/delete). شمارهدار با کدِ تازهی پیامکی تأیید میکند تا گوشیِ
|
* حذفِ همیشگیِ حساب (POST /user/delete). شمارهدار با کدِ تازهی پیامکی تأیید میکند تا گوشیِ
|
||||||
* بازمانده یا توکنِ دزدیده نتواند حساب را پاک کند؛ مهمان با تایپِ «حذف».
|
* بازمانده یا توکنِ دزدیده نتواند حساب را پاک کند.
|
||||||
*/
|
*/
|
||||||
function DeleteAccountSheet({ open, onClose, onSignedOut }: { open: boolean; onClose: () => void; onSignedOut: () => void }) {
|
function DeleteAccountSheet({ open, onClose, onSignedOut }: { open: boolean; onClose: () => void; onSignedOut: () => void }) {
|
||||||
const { profile } = useAppData();
|
const { profile } = useAppData();
|
||||||
@@ -315,7 +293,6 @@ function DeleteAccountSheet({ open, onClose, onSignedOut }: { open: boolean; onC
|
|||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const needsCode = Boolean(profile.phone);
|
|
||||||
|
|
||||||
async function sendCode() {
|
async function sendCode() {
|
||||||
if (!profile.phone) return;
|
if (!profile.phone) return;
|
||||||
@@ -335,7 +312,7 @@ function DeleteAccountSheet({ open, onClose, onSignedOut }: { open: boolean; onC
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
await api.auth.deleteAccount(needsCode ? { otp: digitsOnly(value, 6) } : { confirm: value.trim() });
|
await api.auth.deleteAccount({ otp: digitsOnly(value, 6) });
|
||||||
askStore.clear();
|
askStore.clear();
|
||||||
onSignedOut();
|
onSignedOut();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -351,8 +328,7 @@ function DeleteAccountSheet({ open, onClose, onSignedOut }: { open: boolean; onC
|
|||||||
امتیاز، زنجیره، پیشرفتِ جزیرهها، گفتگوها با دستیار، سوالهای خریدهشده و قلبِ نامحدودت پاک میشن و برنمیگردن.
|
امتیاز، زنجیره، پیشرفتِ جزیرهها، گفتگوها با دستیار، سوالهای خریدهشده و قلبِ نامحدودت پاک میشن و برنمیگردن.
|
||||||
سوابقِ پرداخت فقط برای حسابداری میمونه.
|
سوابقِ پرداخت فقط برای حسابداری میمونه.
|
||||||
</p>
|
</p>
|
||||||
{needsCode ? (
|
{sent ? (
|
||||||
sent ? (
|
|
||||||
<input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(digitsOnly(e.target.value, 6))}
|
onChange={(e) => setValue(digitsOnly(e.target.value, 6))}
|
||||||
@@ -363,25 +339,17 @@ function DeleteAccountSheet({ open, onClose, onSignedOut }: { open: boolean; onC
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="mb-3 text-xs leading-6 text-muted">برای اطمینان، یک کد به شمارهی خودت پیامک میشه.</p>
|
<p className="mb-3 text-xs leading-6 text-muted">برای اطمینان، یک کد به شمارهی خودت پیامک میشه.</p>
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => setValue(e.target.value.slice(0, 10))}
|
|
||||||
placeholder="برای تأیید بنویس: حذف"
|
|
||||||
className="mb-3 w-full rounded-2xl border-2 border-line px-4 py-3 text-center font-bold outline-none focus:border-red-400"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{error && <div className="mb-3"><InlineError>{error}</InlineError></div>}
|
{error && <div className="mb-3"><InlineError>{error}</InlineError></div>}
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
<SecondaryButton onClick={onClose}>نه، پشیمون شدم</SecondaryButton>
|
<SecondaryButton onClick={onClose}>نه، پشیمون شدم</SecondaryButton>
|
||||||
{needsCode && !sent ? (
|
{!sent ? (
|
||||||
<PrimaryButton color="#ef4444" loading={busy} onClick={() => void sendCode()}>فرستادنِ کد</PrimaryButton>
|
<PrimaryButton color="#ef4444" loading={busy} onClick={() => void sendCode()}>فرستادنِ کد</PrimaryButton>
|
||||||
) : (
|
) : (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
color="#ef4444"
|
color="#ef4444"
|
||||||
loading={busy}
|
loading={busy}
|
||||||
disabled={needsCode ? value.length < 6 : value.trim() !== 'حذف'}
|
disabled={value.length < 6}
|
||||||
onClick={() => void remove()}
|
onClick={() => void remove()}
|
||||||
>
|
>
|
||||||
حذفِ همیشگی
|
حذفِ همیشگی
|
||||||
|
|||||||
Reference in New Issue
Block a user