- The wait screen counts down to the next heart second by second, fills the first heart as time passes, shows when all five are back, and asks the server when the time comes; 'continue' reloads the quiz. Packs and 'ask Pi meanwhile' (asking costs no hearts) are offered as ways on. - The server now also refuses to finish a lesson/island without hearts and answers (403 no_hearts); the quiz shows the same screen then. - Invoices know the two admin-set statuses, canceled and refunded. - Account deletion waits for all six digits of the SMS code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
6.6 KiB
TypeScript
143 lines
6.6 KiB
TypeScript
import { useState } from 'react';
|
||
import { ChevronLeft, Copy, Receipt } from 'lucide-react';
|
||
import { api, type Invoice } from '@/api';
|
||
import { useNav } from '@/nav/Navigator';
|
||
import { useAsync } from '@/lib/useAsync';
|
||
import { useToast } from '@/components/Toast';
|
||
import { Sheet } from '@/components/Sheet';
|
||
import { ErrorState, LoadingState, PrimaryButton, Screen, ScreenBody, ScreenHeader, Sep } from '@/components/ui';
|
||
import { formatJalaliDate, formatToman } from '@/lib/format';
|
||
|
||
/**
|
||
* «پرداختها و فاکتورها» (۱۴۰۵-۰۷): همهی سفارشهای کاربر، با فاکتورِ هر خرید.
|
||
* سفارشِ «در انتظار» و «ناموفق» هم نشان داده میشود تا کسی که پولش کم شده ولی
|
||
* اشتراکش فعال نشده، شمارهی فاکتور را به پشتیبانی بدهد. عددها از GET /payment/invoices.
|
||
*/
|
||
const STATUS: Record<Invoice['status'], { label: string; className: string }> = {
|
||
paid: { label: 'پرداختشده', className: 'bg-[#EAF8F0] text-[#2E7D5B]' },
|
||
pending: { label: 'در انتظارِ پرداخت', className: 'bg-[#FFF8E1] text-[#8A6100]' },
|
||
failed: { label: 'ناموفق', className: 'bg-soft text-muted' },
|
||
canceled: { label: 'لغوشده', className: 'bg-soft text-muted' },
|
||
refunded: { label: 'بازپرداختشده', className: 'bg-[#EEF1FF] text-indigo' },
|
||
};
|
||
|
||
const PROVIDER: Record<string, string> = { gateway: 'درگاهِ زرینپال', bazaar: 'کافهبازار' };
|
||
|
||
const dateOf = (invoice: Invoice) => formatJalaliDate(invoice.paidAt ?? invoice.createdAt);
|
||
|
||
export function InvoicesScreen() {
|
||
const nav = useNav();
|
||
const list = useAsync(() => api.subscription.invoices(), []);
|
||
const [open, setOpen] = useState<Invoice | null>(null);
|
||
|
||
return (
|
||
<Screen>
|
||
<ScreenHeader title="پرداختها و فاکتورها" subtitle="همهی خریدهات، با شمارهی فاکتور و کدِ پیگیری" color="#4C5FD5" back />
|
||
<ScreenBody className="flex flex-col gap-3">
|
||
{list.loading ? (
|
||
<LoadingState />
|
||
) : list.error || !list.data ? (
|
||
<ErrorState error={list.error} onRetry={list.reload} />
|
||
) : list.data.length === 0 ? (
|
||
<div className="flex flex-col items-center gap-4 rounded-3xl border border-line bg-white px-5 py-8 text-center">
|
||
<Receipt size={32} className="text-faint" />
|
||
<p className="text-sm text-body">هنوز خریدی نکردی.</p>
|
||
<PrimaryButton onClick={() => nav.switchTab('subscription')}>دیدنِ اشتراکها</PrimaryButton>
|
||
</div>
|
||
) : (
|
||
list.data.map((invoice) => (
|
||
<button
|
||
key={invoice.orderId}
|
||
type="button"
|
||
onClick={() => setOpen(invoice)}
|
||
className="flex items-center gap-3 rounded-3xl border border-line bg-white p-4 text-right active:scale-[0.99]"
|
||
>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm font-black text-ink">{invoice.planTitle || 'اشتراک'}</span>
|
||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-bold ${STATUS[invoice.status].className}`}>
|
||
{STATUS[invoice.status].label}
|
||
</span>
|
||
</div>
|
||
<div className="mt-1 text-xs text-muted">
|
||
{dateOf(invoice)}<Sep /><span dir="ltr">{invoice.invoiceNumber}</span>
|
||
</div>
|
||
</div>
|
||
<span className="text-sm font-black text-ink">{formatToman(invoice.amount)}</span>
|
||
<ChevronLeft size={16} className="text-faint" />
|
||
</button>
|
||
))
|
||
)}
|
||
</ScreenBody>
|
||
<InvoiceSheet invoice={open} onClose={() => setOpen(null)} />
|
||
</Screen>
|
||
);
|
||
}
|
||
|
||
function InvoiceSheet({ invoice, onClose }: { invoice: Invoice | null; onClose: () => void }) {
|
||
const toast = useToast();
|
||
|
||
async function copy(text: string) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
toast('کپی شد', 'success');
|
||
} catch {
|
||
toast(text, 'success');
|
||
}
|
||
}
|
||
|
||
const rows: [string, React.ReactNode][] = invoice
|
||
? [
|
||
['شمارهی فاکتور', <span dir="ltr">{invoice.invoiceNumber}</span>],
|
||
['تاریخ', dateOf(invoice)],
|
||
['وضعیت', STATUS[invoice.status].label],
|
||
['اشتراک', invoice.planTitle || '—'],
|
||
...(invoice.listPrice !== null ? [['قیمت', formatToman(invoice.listPrice)] as [string, React.ReactNode]] : []),
|
||
...(invoice.discountAmount ? [[
|
||
invoice.discountCode ? `تخفیف (کدِ ${invoice.discountCode})` : 'تخفیف',
|
||
formatToman(invoice.discountAmount),
|
||
] as [string, React.ReactNode]] : []),
|
||
['مبلغِ پرداختی', <b>{formatToman(invoice.amount)}</b>],
|
||
['روشِ پرداخت', PROVIDER[invoice.provider] ?? invoice.provider],
|
||
]
|
||
: [];
|
||
|
||
return (
|
||
<Sheet open={invoice !== null} onClose={onClose} title="فاکتور">
|
||
{invoice && (
|
||
<div className="flex flex-col gap-4">
|
||
<table className="w-full text-sm">
|
||
<tbody>
|
||
{rows.map(([label, value]) => (
|
||
<tr key={label} className="border-b border-line last:border-0">
|
||
<td className="py-2.5 text-muted">{label}</td>
|
||
<td className="py-2.5 text-left text-ink">{value}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
{invoice.refId ? (
|
||
<button
|
||
type="button"
|
||
onClick={() => void copy(invoice.refId)}
|
||
className="flex items-center justify-between rounded-2xl bg-soft px-4 py-3 text-sm"
|
||
>
|
||
<span className="text-muted">کدِ پیگیری</span>
|
||
<span className="flex items-center gap-2 font-bold text-ink" dir="ltr">
|
||
{/* کد همانطور که کپی میشود: با رقمِ فارسی صفرها نقطه دیده میشدند. */}
|
||
{invoice.refId} <Copy size={14} />
|
||
</span>
|
||
</button>
|
||
) : (
|
||
(invoice.status === 'pending' || invoice.status === 'failed') && (
|
||
<p className="text-xs leading-6 text-muted">
|
||
اگه پولی از حسابت کم شده ولی این خرید «پرداختشده» نیست، شمارهی فاکتور رو به پشتیبانی بده.
|
||
</p>
|
||
)
|
||
)}
|
||
</div>
|
||
)}
|
||
</Sheet>
|
||
);
|
||
}
|