diff --git a/src/app/(projects)/academy/payment/failed/page.tsx b/src/app/(projects)/academy/payment/failed/page.tsx index c6aba2d..506fdc5 100644 --- a/src/app/(projects)/academy/payment/failed/page.tsx +++ b/src/app/(projects)/academy/payment/failed/page.tsx @@ -23,6 +23,7 @@ function FailedProject() { const [project, setProject] = useState(); const [typeList, setTypeList] = useState(null); const [price, setPrice] = useState(""); + const [paying, setPaying] = useState(false); const getDisplayTypeLabel = (projectType?: string) => { if (projectType === "normal") { @@ -68,6 +69,9 @@ function FailedProject() { }, [project, typeList]); const payHandler = async () => { + // قبلاً هیچ گاردی نداشت — دابل‌کلیک دو درخواستِ initiate-payment-web هم‌زمان می‌فرستاد + if (paying) return; + setPaying(true); try { const response = await request<{ authority?: string; type?: string }>( "POST", @@ -88,6 +92,7 @@ function FailedProject() { } } catch (error: unknown) { console.log(error); + setPaying(false); } }; @@ -122,6 +127,7 @@ function FailedProject() { onClick={() => { payHandler(); }} + disabled={paying} variant="primary" className="w-32 h-9" > diff --git a/src/app/(projects)/projects/payment/failed/page.tsx b/src/app/(projects)/projects/payment/failed/page.tsx index e6e695f..c60324f 100644 --- a/src/app/(projects)/projects/payment/failed/page.tsx +++ b/src/app/(projects)/projects/payment/failed/page.tsx @@ -23,6 +23,7 @@ function FailedProject() { const [project, setProject] = useState(); const [typeList, setTypeList] = useState(null); const [price, setPrice] = useState(""); + const [paying, setPaying] = useState(false); const getDisplayTypeLabel = (projectType?: string) => { if (projectType === "normal" || projectType === "free") { @@ -68,6 +69,9 @@ function FailedProject() { }, [project, typeList]); const payHandler = async () => { + // قبلاً هیچ گاردی نداشت — دابل‌کلیک دو درخواستِ initiate-payment-web هم‌زمان می‌فرستاد + if (paying) return; + setPaying(true); try { const response = await request<{ authority?: string; @@ -88,12 +92,14 @@ function FailedProject() { window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`; } else { toast.error(response?.message || t("projects.payment.initFailed")); + setPaying(false); } } catch (error: unknown) { const axiosErr = error as { response?: { data?: { message?: string } } }; toast.error( axiosErr?.response?.data?.message || t("projects.payment.initFailed") ); + setPaying(false); } }; @@ -126,6 +132,7 @@ function FailedProject() { diff --git a/src/app/billboards/new/[id]/page.tsx b/src/app/billboards/new/[id]/page.tsx index 2644434..e045d51 100644 --- a/src/app/billboards/new/[id]/page.tsx +++ b/src/app/billboards/new/[id]/page.tsx @@ -26,6 +26,7 @@ function BillboardPayment({ params }: IBillboardProps) { const [advertising, setAdvertising] = useState(); const [typeList, setTypeList] = useState(null); const [price, setPrice] = useState(""); + const [paying, setPaying] = useState(false); useEffect(() => { const fetchAd = async () => { @@ -50,6 +51,9 @@ function BillboardPayment({ params }: IBillboardProps) { }, [id, request]); const payHandler = async () => { + // قبلاً هیچ گاردی نداشت — دابل‌کلیک دو درخواستِ initiate-payment-web هم‌زمان می‌فرستاد + if (paying) return; + setPaying(true); try { const response = await request<{ authority?: string }>( "POST", @@ -72,6 +76,7 @@ function BillboardPayment({ params }: IBillboardProps) { } } catch (error: unknown) { console.log(error); + setPaying(false); } }; @@ -103,6 +108,7 @@ function BillboardPayment({ params }: IBillboardProps) { {t("billboards.payment.title")} diff --git a/src/app/billboards/payment/failed/page.tsx b/src/app/billboards/payment/failed/page.tsx index e448d2d..58ea433 100644 --- a/src/app/billboards/payment/failed/page.tsx +++ b/src/app/billboards/payment/failed/page.tsx @@ -27,6 +27,7 @@ function FailedBillboard() { const price = searchParams.get("price") || "0"; const [advertising, setAdvertising] = useState(); + const [paying, setPaying] = useState(false); useEffect(() => { const fetchAd = async () => { @@ -39,7 +40,11 @@ function FailedBillboard() { fetchAd(); }, [projectId, request]); + // قبلاً هیچ‌کدومِ این دو تابع گاردِ submitting نداشتن — دابل‌کلیکِ «تلاشِ دوباره» + // دو درخواستِ پرداختِ هم‌زمان می‌فرستاد const payHandler = async () => { + if (paying) return; + setPaying(true); try { const response = await request<{ authority?: string }>( "POST", @@ -62,10 +67,13 @@ function FailedBillboard() { } } catch (error: unknown) { console.log(error); + setPaying(false); } }; const republishHandler = async () => { + if (paying) return; + setPaying(true); try { const response = await request<{ authority?: string }>( "POST", @@ -88,6 +96,7 @@ function FailedBillboard() { } } catch (error: unknown) { console.log(error); + setPaying(false); } }; @@ -126,6 +135,7 @@ function FailedBillboard() { payHandler(); } }} + disabled={paying} variant="primary" className="w-32 h-9" > diff --git a/src/app/globals.css b/src/app/globals.css index d9a95ac..7118307 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -641,11 +641,17 @@ select { border-color: rgba(255, 255, 255, 0.15); } +/* env(safe-area-inset-bottom) روی اندروید (برخلافِ iOS) تقریباً همیشه 0px + برمی‌گرده، حتی وقتی نوار جست‌وجوی سیستم/نشانگرِ ژستِ اندروید همون چند پیکسلِ + پایینِ صفحه رو اشغال کرده — یعنی قبلاً تکیه‌ی تنها به env() باعث می‌شد دکمه‌ی + ارسال/ویس روی خیلی از گوشی‌های اندروید دقیقاً روی لبه‌ی پایینیِ صفحه بشینه و + زیرِ نوارِ سیستم بره. max() یه حداقلِ ثابت (که این نوارها رو پوشش می‌ده) رو + تضمین می‌کنه، مستقل از این‌که env() چی برگردونه */ .chat-input-area { - padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px)); + padding-bottom: max(1.5rem, calc(0.75rem + env(safe-area-inset-bottom, 0px))); } .chat-input-area--thread { - padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + padding-bottom: max(1.5rem, calc(1rem + env(safe-area-inset-bottom, 0px))); } .ig-dm-send-btn { diff --git a/src/app/settings/chats/[username]/[id]/page.tsx b/src/app/settings/chats/[username]/[id]/page.tsx index 8097eb3..553c65e 100644 --- a/src/app/settings/chats/[username]/[id]/page.tsx +++ b/src/app/settings/chats/[username]/[id]/page.tsx @@ -363,14 +363,23 @@ function TicketChat({ params }: ITicketChatProps) { const isFileUpload = fileForUpload && fileType !== "location"; try { - await ensureIdentityKeys(currentUserId); - const encrypted = await encryptDmContent( - currentUserId, - targetReceiverId, - textContent - ); - if (!encrypted) throw new Error("E2EE encryption failed"); - const sendContent = encrypted; + // ارسالِ فایل/عکس/ویس/ویدیو فعلاً بدونِ e2ee می‌ره (طبقِ تصمیمِ فعلی، رمزنگاریِ + // مدیا بعداً اضافه می‌شه) — قبلاً حتی برای همین آپلودها هم این مرحله اجباری بود + // و اگه encryptDmContent به هر دلیلی (مثلاً هنوز کلیدِ عمومیِ طرفِ مقابل واکشی + // نشده) null برمی‌گردوند، کلِ ارسالِ فایل هم با «E2EE encryption failed» شکست + // می‌خورد — دقیقاً مثلِ اندروید که از اول اصلاً این مرحله رو برای فایل صدا نمی‌زد + const sendContent = isFileUpload + ? textContent + : await (async () => { + await ensureIdentityKeys(currentUserId); + const encrypted = await encryptDmContent( + currentUserId, + targetReceiverId, + textContent + ); + if (!encrypted) throw new Error("E2EE encryption failed"); + return encrypted; + })(); let messagePayload: Record = { content: sendContent, diff --git a/src/app/settings/chats/rooms/[roomId]/page.tsx b/src/app/settings/chats/rooms/[roomId]/page.tsx index 32b3fe0..e85817e 100644 --- a/src/app/settings/chats/rooms/[roomId]/page.tsx +++ b/src/app/settings/chats/rooms/[roomId]/page.tsx @@ -622,10 +622,17 @@ export default function ChatRoomThreadPage({ } try { - await ensureIdentityKeys(senderId); - const encrypted = await encryptRoomContent(senderId, roomId, text); - if (!encrypted) throw new Error("E2EE encryption failed"); - const sendContent = encrypted; + // ارسالِ فایل/عکس/ویس/ویدیو توی روم هم فعلاً بدونِ e2ee می‌ره — عینِ دلیلِ همین + // تغییر توی چتِ مستقیم (chats/[username]/[id]/page.tsx): اگه encryptRoomContent + // به هر دلیلی null برگردونه، قبلاً کلِ آپلودِ فایل هم با همین خطا شکست می‌خورد + const sendContent = fileForUpload + ? text + : await (async () => { + await ensureIdentityKeys(senderId); + const encrypted = await encryptRoomContent(senderId, roomId, text); + if (!encrypted) throw new Error("E2EE encryption failed"); + return encrypted; + })(); let res: { message?: RoomMessage } | null = null; if (fileForUpload) { diff --git a/src/app/settings/financial/page.tsx b/src/app/settings/financial/page.tsx index 9d778a0..b9ba471 100644 --- a/src/app/settings/financial/page.tsx +++ b/src/app/settings/financial/page.tsx @@ -29,8 +29,12 @@ export default function Financial() { limit: 15, }); + // قبلاً flatMap روی خودِ page.financial (یه آبجکت {docs, totalPages}، نه آرایه) اجرا + // می‌شد، پس هر صفحه فقط به یه آیتم تبدیل می‌شد و allTransactions[0]?.docs همیشه فقط + // صفحه‌ی اول رو می‌داد — اسکرول به پایین صفحه‌های بعدی رو دانلود می‌کرد ولی هیچ‌وقت + // نمایش نمی‌داد const allTransactions = - data?.pages.flatMap((page) => page.financial || []) ?? []; + data?.pages.flatMap((page) => page.financial?.docs ?? []) ?? []; const getTransactionType = (type: string) => { const known = [ @@ -98,7 +102,7 @@ export default function Financial() { ref={containerRef} className="space-y-4 max-h-[65vh] overflow-y-auto scrollbar-thin scrollbar-thumb-neutral-300 dark:scrollbar-thumb-neutral-600" > - {allTransactions[0]?.docs?.map((item: IFinancial) => ( + {allTransactions.map((item: IFinancial) => (
{ }, }); - useEffect(() => { - if (isEditing && formik.values.selectedType) { - formik.submitForm(); - } - }, [isEditing, formik.values.selectedType]); + // این افکت قبلاً با هر تغییرِ selectedType (یعنی هر کلیکِ کاربر روی یه کارتِ نوعِ + // دیگه، توی حالتِ ویرایش) دوباره اجرا می‌شد و formik.submitForm() رو صدا می‌زد — + // یعنی هر کلیک یه PATCH واقعی به /advertising/edit می‌فرستاد، نه فقط کلیکِ دکمه‌ی + // ثبتِ واقعی. حذف شد — کاربر توی حالتِ ویرایش هم باید مثلِ حالتِ عادی، دکمه‌ی + // ارسال/ثبتِ پایینِ همین فرم رو بزنه return (
diff --git a/src/components/projects/NewProject/Step4.tsx b/src/components/projects/NewProject/Step4.tsx index 4a9dd33..2a8325e 100644 --- a/src/components/projects/NewProject/Step4.tsx +++ b/src/components/projects/NewProject/Step4.tsx @@ -183,7 +183,15 @@ const Step4 = () => { {formik.touched.selectedType && formik.errors.selectedType && (

{formik.errors.selectedType}

)} - + {/* قبلاً هیچ گاردی نبود — فرمیک isSubmitting رو خودش درست مدیریت می‌کنه (چون + onSubmit یه تابعِ async هست)، فقط به دکمه وصل نشده بود؛ دابل‌کلیک دو + POST /projects/create هم‌زمان می‌فرستاد و دو پروژه‌ی تکراری می‌ساخت */} + {t("projects.newProject.submitRequest")}