This commit is contained in:
payacom
2026-10-02 18:12:58 +03:30
parent 9fb454b0a7
commit d6386b779f
11 changed files with 86 additions and 23 deletions

View File

@@ -23,6 +23,7 @@ function FailedProject() {
const [project, setProject] = useState<Project>();
const [typeList, setTypeList] = useState<IProjectType[] | null>(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"
>

View File

@@ -23,6 +23,7 @@ function FailedProject() {
const [project, setProject] = useState<Project>();
const [typeList, setTypeList] = useState<IProjectType[] | null>(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() {
</RoundedDiv>
<RoundedButton
onClick={payHandler}
disabled={paying}
variant="primary"
className="h-9 w-32"
>

View File

@@ -26,6 +26,7 @@ function BillboardPayment({ params }: IBillboardProps) {
const [advertising, setAdvertising] = useState<IAdvertising>();
const [typeList, setTypeList] = useState<IAdvertisingType[] | null>(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) {
<RoundedButton
variant="primary"
onClick={payHandler}
disabled={paying}
className="h-9 w-32"
>
{t("billboards.payment.title")}

View File

@@ -27,6 +27,7 @@ function FailedBillboard() {
const price = searchParams.get("price") || "0";
const [advertising, setAdvertising] = useState<IAdvertising>();
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"
>

View File

@@ -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 {

View File

@@ -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<string, unknown> = {
content: sendContent,

View File

@@ -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) {

View File

@@ -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) => (
<div
key={item._id}
className="p-5 border rounded-3xl border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 shadow-sm hover:shadow-md transition-all duration-200"

View File

@@ -200,11 +200,11 @@ const Step5 = () => {
},
});
useEffect(() => {
if (isEditing && formik.values.selectedType) {
formik.submitForm();
}
}, [isEditing, formik.values.selectedType]);
// این افکت قبلاً با هر تغییرِ selectedType (یعنی هر کلیکِ کاربر روی یه کارتِ نوعِ
// دیگه، توی حالتِ ویرایش) دوباره اجرا می‌شد و formik.submitForm() رو صدا می‌زد —
// یعنی هر کلیک یه PATCH واقعی به /advertising/edit می‌فرستاد، نه فقط کلیکِ دکمه‌ی
// ثبتِ واقعی. حذف شد — کاربر توی حالتِ ویرایش هم باید مثلِ حالتِ عادی، دکمه‌ی
// ارسال/ثبتِ پایینِ همین فرم رو بزنه
return (
<form

View File

@@ -223,7 +223,7 @@ const MessageInput = ({
return (
<div
className={`pointer-events-none fixed left-0 right-0 z-50 flex flex-col items-center bg-transparent px-3 sm:px-4 ${
className={`pointer-events-none fixed left-0 right-0 z-50 flex flex-col items-center bg-transparent px-2 sm:px-3 ${
isChatThread ? "bottom-0 chat-input-area--thread" : "bottom-[5.5rem] chat-input-area"
}`}
>

View File

@@ -183,7 +183,15 @@ const Step4 = () => {
{formik.touched.selectedType && formik.errors.selectedType && (
<p className="text-red-500">{formik.errors.selectedType}</p>
)}
<RoundedButton type="submit" variant="primary" className="mt-4 px-8 py-2">
{/* قبلاً هیچ گاردی نبود — فرمیک isSubmitting رو خودش درست مدیریت می‌کنه (چون
onSubmit یه تابعِ async هست)، فقط به دکمه وصل نشده بود؛ دابل‌کلیک دو
POST /projects/create هم‌زمان می‌فرستاد و دو پروژه‌ی تکراری می‌ساخت */}
<RoundedButton
type="submit"
variant="primary"
disabled={formik.isSubmitting}
className="mt-4 px-8 py-2"
>
{t("projects.newProject.submitRequest")}
</RoundedButton>
</form>