fix(payment): portal checkout dialogs to viewport
Docker Image / Build and Push Docker Image (push) Successful in 3m32s

This commit is contained in:
Codex
2026-07-27 18:50:39 +08:00
parent 8c0d1ad3ce
commit 67f292353e
10 changed files with 610 additions and 235 deletions
@@ -3,6 +3,7 @@
import { useId, type ReactNode } from "react";
import { QRCodeSVG } from "qrcode.react";
import { ModalPortal } from "@/app/_components/core/modal-portal";
import type { PaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow";
import { LazyStripePaymentDialog } from "./lazy-stripe-payment-dialog";
@@ -111,60 +112,61 @@ function QrisPaymentDialog({
const statusMessage = qrisStatusMessage(status, errorMessage);
return (
<div
className={styles.overlay}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
<ModalPortal
open
persistent
onClose={onClose}
scrimClassName={styles.overlay}
panelClassName={styles.dialog}
scrimOpacity={0.5}
ariaLabelledBy={titleId}
>
<div className={styles.dialog}>
<div className={`${styles.header} text-center`}>
<h2 id={titleId} className={styles.title}>
Scan to pay with QRIS
</h2>
<p className={styles.content}>
Open a QRIS-compatible banking or wallet app and scan this code.
</p>
</div>
<div className="mb-4 flex min-h-64 items-center justify-center rounded-2xl bg-white p-4">
{payment.qrData ? (
<QRCodeSVG
value={payment.qrData}
title="QRIS payment QR code"
size={256}
level="M"
marginSize={2}
className="h-auto w-full max-w-64"
/>
) : (
<p className={styles.error} role="alert">
QRIS data is unavailable. Please close this dialog and try again.
</p>
)}
</div>
<div className="mb-4 flex flex-col gap-2 text-center">
<p className={styles.content}>Order No. {payment.orderId}</p>
<p className="m-0 text-xl font-bold text-text-foreground">
{formatQrisAmount(payment.amountCents, payment.currency)}
</p>
<p
className={status === "failed" || status === "expired" ? styles.error : styles.content}
role="status"
>
{statusMessage}
</p>
</div>
<div className={styles.actions}>
<button
type="button"
className={`${styles.button} ${styles.secondary}`}
onClick={onClose}
>
Close
</button>
</div>
<div className={`${styles.header} text-center`}>
<h2 id={titleId} className={styles.title}>
Scan to pay with QRIS
</h2>
<p className={styles.content}>
Open a QRIS-compatible banking or wallet app and scan this code.
</p>
</div>
</div>
<div className="mb-4 flex min-h-64 items-center justify-center rounded-2xl bg-white p-4">
{payment.qrData ? (
<QRCodeSVG
value={payment.qrData}
title="QRIS payment QR code"
size={256}
level="M"
marginSize={2}
className="h-auto w-full max-w-64"
/>
) : (
<p className={styles.error} role="alert">
QRIS data is unavailable. Please close this dialog and try again.
</p>
)}
</div>
<div className="mb-4 flex flex-col gap-2 text-center">
<p className={styles.content}>Order No. {payment.orderId}</p>
<p className="m-0 text-xl font-bold text-text-foreground">
{formatQrisAmount(payment.amountCents, payment.currency)}
</p>
<p
className={status === "failed" || status === "expired" ? styles.error : styles.content}
role="status"
>
{statusMessage}
</p>
</div>
<div className={styles.actions}>
<button
type="button"
className={`${styles.button} ${styles.secondary}`}
onClick={onClose}
>
Close
</button>
</div>
</ModalPortal>
);
}
@@ -188,41 +190,43 @@ function EzpayRedirectConfirmDialog({
const titleId = useId();
return (
<div
className={styles.overlay}
<ModalPortal
open
persistent
onClose={onCancel}
scrimClassName={styles.overlay}
panelClassName={styles.dialog}
scrimOpacity={0.5}
role="alertdialog"
aria-modal="true"
aria-labelledby={titleId}
ariaLabelledBy={titleId}
>
<div className={styles.dialog}>
<div className={styles.header}>
<h2 id={titleId} className={styles.title}>
Continue to payment?
</h2>
<p className={styles.content}>{description}</p>
<p className={styles.content}>Order No. {orderId}</p>
</div>
<div className={styles.actions}>
<button
type="button"
className={`${styles.button} ${styles.secondary}`}
disabled={isConfirming}
onClick={onCancel}
>
Cancel
</button>
<button
type="button"
data-analytics-key={externalCheckoutAnalyticsKey}
data-analytics-label="Continue to external checkout"
className={`${styles.button} ${styles.primary}`}
disabled={isConfirming}
onClick={onConfirm}
>
{isConfirming ? "Opening..." : "Continue"}
</button>
</div>
<div className={styles.header}>
<h2 id={titleId} className={styles.title}>
Continue to payment?
</h2>
<p className={styles.content}>{description}</p>
<p className={styles.content}>Order No. {orderId}</p>
</div>
</div>
<div className={styles.actions}>
<button
type="button"
className={`${styles.button} ${styles.secondary}`}
disabled={isConfirming}
onClick={onCancel}
>
Cancel
</button>
<button
type="button"
data-analytics-key={externalCheckoutAnalyticsKey}
data-analytics-label="Continue to external checkout"
className={`${styles.button} ${styles.primary}`}
disabled={isConfirming}
onClick={onConfirm}
>
{isConfirming ? "Opening..." : "Continue"}
</button>
</div>
</ModalPortal>
);
}