From 4fdac75ba3f8c856e350ca629f3d78c5680b7ce2 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 5 Sep 2026 10:50:02 -0400 Subject: [PATCH] fix(checkout): return the customer to the merchant when payment is detected A merchant that supplies `redirectURL` and `redirectAutomatically` is asking for its customer back, and the moment to do that is detection rather than final depth. The payment is already irreversible from the payer's side, and the merchant's own page is where an order status belongs. Waiting for the confirmation target instead parks the customer on a payment page for as long as the coin takes. Only the classic style acted on those fields, and it held its own copy of the rule. The standard style had no automatic return at all, and its way back appeared only once the invoice was confirmed. Both styles now render one shared component, so the rule lives in one place, and the return appears as soon as the payment is seen. The receipt still waits for confirmation, because that is when there is a settled payment to describe. Also ignores the tsbuildinfo a typecheck leaves behind. --- .gitignore | 1 + ui/app/i/[invoiceId]/page.tsx | 27 +++++++++--- ui/components/btcpay-auto-return.tsx | 53 +++++++++++++++++++++++ ui/components/btcpay-classic-checkout.tsx | 22 +++------- 4 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 ui/components/btcpay-auto-return.tsx diff --git a/.gitignore b/.gitignore index c742db9..2b8868f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ ui/node_modules/ nginx/certs/ backups/ +ui/tsconfig.tsbuildinfo diff --git a/ui/app/i/[invoiceId]/page.tsx b/ui/app/i/[invoiceId]/page.tsx index a5a6016..78d0259 100644 --- a/ui/app/i/[invoiceId]/page.tsx +++ b/ui/app/i/[invoiceId]/page.tsx @@ -6,6 +6,7 @@ import InvoicePaymentDetails from "../../../components/invoice-payment-details"; import InvoiceStatusAutoRefresh from "../../../components/invoice-status-auto-refresh"; import { formatRelativeTime } from "../../../components/relative-time"; import StatusRefreshButton from "../../../components/status-refresh-button"; +import BtcpayAutoReturn from "../../../components/btcpay-auto-return"; export const dynamic = "force-dynamic"; export const metadata: Metadata = { @@ -149,7 +150,12 @@ export default async function BtcpayModalInvoicePage({ const isBtcpayInvoice = Boolean(invoice.btcpay_amount && invoice.btcpay_currency); const useClassicCheckout = isBtcpayInvoice && invoice.btcpay_checkout_style === "btcpay_classic"; - const shouldShowBtcpayActions = isBtcpayInvoice && invoice.status === "confirmed"; + // The merchant's own order page is where a customer belongs once the payment has been seen, so + // the way back appears at detection. The receipt still waits for the confirmation target, because + // that is the point at which there is a settled payment to write one about. + const shouldShowBtcpayActions = + isBtcpayInvoice && + (invoice.status === "payment_detected" || invoice.status === "confirmed"); const checkoutContinueAvailable = !isBtcpayInvoice && invoice.status === "confirmed" && @@ -257,6 +263,11 @@ export default async function BtcpayModalInvoicePage({ ) : null} +
{shouldShowBtcpayActions ? (
- - View receipt - + {invoice.status === "confirmed" ? ( + + View receipt + + ) : null} {invoice.btcpay_redirect_url ? ( { + if ( + (status !== "confirmed" && status !== "payment_detected") || + !redirectUrl || + !redirectAutomatically + ) { + return; + } + const timer = window.setTimeout(() => { + try { + window.top?.location.assign(redirectUrl); + } catch { + window.location.assign(redirectUrl); + } + }, delayMs); + return () => { + window.clearTimeout(timer); + }; + }, [delayMs, redirectAutomatically, redirectUrl, status]); + + return null; +} diff --git a/ui/components/btcpay-classic-checkout.tsx b/ui/components/btcpay-classic-checkout.tsx index 3b0ed53..adbc817 100644 --- a/ui/components/btcpay-classic-checkout.tsx +++ b/ui/components/btcpay-classic-checkout.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import QRCode from "qrcode"; import { formatXmrAmount } from "../lib/formatting"; +import BtcpayAutoReturn from "./btcpay-auto-return"; type InvoiceStatus = | "pending" @@ -129,22 +130,6 @@ export default function BtcpayClassicCheckout({ ? `1 WOW = ${formatFiatAmount(quote.rate, quote.fiat_currency)}` : null; - useEffect(() => { - if ((status !== "confirmed" && status !== "payment_detected") || !redirectUrl || !redirectAutomatically) { - return; - } - const timer = window.setTimeout(() => { - try { - window.top?.location.assign(redirectUrl); - } catch { - window.location.assign(redirectUrl); - } - }, 3000); - return () => { - window.clearTimeout(timer); - }; - }, [redirectAutomatically, redirectUrl, status]); - const handleCopy = async (value: string, field: string) => { try { await navigator.clipboard.writeText(value); @@ -285,6 +270,11 @@ export default function BtcpayClassicCheckout({ return (
+

Payment request