fix: zarinpal silent failure + show payment error in checkout
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m15s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 43s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 4m45s
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m15s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 43s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 4m45s
Previously the subscribe mutation had no onError handler, so any payment initiation failure (wrong merchant ID, ZarinPal API error, disabled payment method) would silently re-enable the button with no user feedback. Now errors are shown below the Pay button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1019,7 +1019,8 @@
|
|||||||
"total": "المبلغ المستحق",
|
"total": "المبلغ المستحق",
|
||||||
"secureNote": "تتم المعالجة عبر بوابة دفع بنكية آمنة.",
|
"secureNote": "تتم المعالجة عبر بوابة دفع بنكية آمنة.",
|
||||||
"payTotal": "ادفع {total}",
|
"payTotal": "ادفع {total}",
|
||||||
"redirecting": "جارٍ التحويل إلى البوابة..."
|
"redirecting": "جارٍ التحويل إلى البوابة...",
|
||||||
|
"paymentFailed": "فشل الدفع. الرجاء المحاولة مرة أخرى."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -1091,7 +1091,8 @@
|
|||||||
"total": "Amount due",
|
"total": "Amount due",
|
||||||
"secureNote": "Payment is processed through a secure bank gateway.",
|
"secureNote": "Payment is processed through a secure bank gateway.",
|
||||||
"payTotal": "Pay {total}",
|
"payTotal": "Pay {total}",
|
||||||
"redirecting": "Redirecting to gateway..."
|
"redirecting": "Redirecting to gateway...",
|
||||||
|
"paymentFailed": "Payment failed. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -1092,7 +1092,8 @@
|
|||||||
"total": "مبلغ قابل پرداخت",
|
"total": "مبلغ قابل پرداخت",
|
||||||
"secureNote": "پرداخت از طریق درگاه امن بانکی انجام میشود.",
|
"secureNote": "پرداخت از طریق درگاه امن بانکی انجام میشود.",
|
||||||
"payTotal": "پرداخت {total}",
|
"payTotal": "پرداخت {total}",
|
||||||
"redirecting": "در حال انتقال به درگاه..."
|
"redirecting": "در حال انتقال به درگاه...",
|
||||||
|
"paymentFailed": "پرداخت ناموفق بود. لطفاً دوباره امتحان کنید."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export function CheckoutScreen() {
|
|||||||
|
|
||||||
const [months, setMonths] = useState(1);
|
const [months, setMonths] = useState(1);
|
||||||
const [paymentMethod, setPaymentMethod] = useState("");
|
const [paymentMethod, setPaymentMethod] = useState("");
|
||||||
|
const [payError, setPayError] = useState<string | null>(null);
|
||||||
|
|
||||||
const numberLocale =
|
const numberLocale =
|
||||||
typeof document !== "undefined" && document.documentElement.lang === "en"
|
typeof document !== "undefined" && document.documentElement.lang === "en"
|
||||||
@@ -76,8 +77,13 @@ export function CheckoutScreen() {
|
|||||||
mutationFn: (body: { planTier: string; months: number; paymentMethod: string }) =>
|
mutationFn: (body: { planTier: string; months: number; paymentMethod: string }) =>
|
||||||
apiPost<SubscribeResponse>("/api/billing/subscribe", body),
|
apiPost<SubscribeResponse>("/api/billing/subscribe", body),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
setPayError(null);
|
||||||
window.location.href = data.paymentUrl;
|
window.location.href = data.paymentUrl;
|
||||||
},
|
},
|
||||||
|
onError: (err: unknown) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
setPayError(msg || tc("paymentFailed"));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!cafeId) return null;
|
if (!cafeId) return null;
|
||||||
@@ -255,10 +261,15 @@ export function CheckoutScreen() {
|
|||||||
|
|
||||||
{/* Pay action */}
|
{/* Pay action */}
|
||||||
<div className="flex flex-col gap-3 border-t border-border/80 bg-muted/20 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 border-t border-border/80 bg-muted/20 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<div className="flex flex-col gap-1">
|
||||||
<ShieldCheck className="h-4 w-4 text-[#0F6E56]" aria-hidden />
|
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
{tc("secureNote")}
|
<ShieldCheck className="h-4 w-4 text-[#0F6E56]" aria-hidden />
|
||||||
</p>
|
{tc("secureNote")}
|
||||||
|
</p>
|
||||||
|
{payError && (
|
||||||
|
<p className="text-xs text-destructive">{payError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
|
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
|
||||||
disabled={subscribe.isPending}
|
disabled={subscribe.isPending}
|
||||||
|
|||||||
Reference in New Issue
Block a user