From c0f4db621599315858414075d6c7d3023dd4510a Mon Sep 17 00:00:00 2001 From: Freezyyy-arch Date: Sat, 17 Oct 2026 19:26:56 +0100 Subject: [PATCH 1/4] Derive transaction explorer link from configured network (#918) --- .../src/components/InvoiceTimeline.tsx | 3 ++- .../src/components/PaymentReceipt.tsx | 5 +++-- .../src/tests/explorerUrl.test.ts | 17 +++++++++++++++++ comebackhere-frontend/src/utils/explorerUrl.ts | 16 ++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 comebackhere-frontend/src/tests/explorerUrl.test.ts create mode 100644 comebackhere-frontend/src/utils/explorerUrl.ts diff --git a/comebackhere-frontend/src/components/InvoiceTimeline.tsx b/comebackhere-frontend/src/components/InvoiceTimeline.tsx index f85e4d1..6c294e8 100644 --- a/comebackhere-frontend/src/components/InvoiceTimeline.tsx +++ b/comebackhere-frontend/src/components/InvoiceTimeline.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from "react" import { useT } from "../i18n" +import { getExplorerTxBaseUrl } from "../utils/explorerUrl" // --------------------------------------------------------------------------- // Types matching the GET /invoices/:id/events endpoint shape @@ -90,7 +91,7 @@ async function fetchEvents( // Component // --------------------------------------------------------------------------- -const DEFAULT_EXPLORER = "https://stellar.expert/explorer/testnet/tx" +const DEFAULT_EXPLORER = getExplorerTxBaseUrl() export function InvoiceTimeline({ invoiceId, diff --git a/comebackhere-frontend/src/components/PaymentReceipt.tsx b/comebackhere-frontend/src/components/PaymentReceipt.tsx index 3575aa5..9fa9357 100644 --- a/comebackhere-frontend/src/components/PaymentReceipt.tsx +++ b/comebackhere-frontend/src/components/PaymentReceipt.tsx @@ -2,17 +2,18 @@ import { useCallback } from "react" import type { Invoice } from "../types" import { useT } from "../i18n" import { CopyableText } from "./CopyableText" +import { getExplorerTxBaseUrl } from "../utils/explorerUrl" interface PaymentReceiptProps { invoice: Invoice transactionHash: string /** Unix timestamp (seconds) when the payment was confirmed. */ paidAt: number - /** Base URL for the block explorer — defaults to Stellar Expert testnet. */ + /** Base URL for the block explorer — defaults to the explorer for the configured network. */ explorerBaseUrl?: string } -const DEFAULT_EXPLORER = "https://stellar.expert/explorer/testnet/tx" +const DEFAULT_EXPLORER = getExplorerTxBaseUrl() export function PaymentReceipt({ invoice, diff --git a/comebackhere-frontend/src/tests/explorerUrl.test.ts b/comebackhere-frontend/src/tests/explorerUrl.test.ts new file mode 100644 index 0000000..630ee6b --- /dev/null +++ b/comebackhere-frontend/src/tests/explorerUrl.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest" +import { getExplorerTxBaseUrl } from "../utils/explorerUrl" + +describe("getExplorerTxBaseUrl", () => { + it("uses the public network segment for mainnet", () => { + expect(getExplorerTxBaseUrl("Public Global Stellar Network ; September 2015")).toBe( + "https://stellar.expert/explorer/public/tx", + ) + }) + + it("uses the testnet segment for testnet and unknown networks", () => { + expect(getExplorerTxBaseUrl("Test SDF Network ; September 2015")).toBe( + "https://stellar.expert/explorer/testnet/tx", + ) + expect(getExplorerTxBaseUrl("something else")).toBe("https://stellar.expert/explorer/testnet/tx") + }) +}) diff --git a/comebackhere-frontend/src/utils/explorerUrl.ts b/comebackhere-frontend/src/utils/explorerUrl.ts new file mode 100644 index 0000000..4964f7e --- /dev/null +++ b/comebackhere-frontend/src/utils/explorerUrl.ts @@ -0,0 +1,16 @@ +/** + * Block-explorer URL helpers. The explorer network segment is derived from the + * configured network passphrase so links never point at the wrong network. + */ + +const EXPLORER_NETWORK_SEGMENTS: Record = { + "Public Global Stellar Network ; September 2015": "public", + "Test SDF Network ; September 2015": "testnet", +} + +/** Stellar Expert base URL for transactions on the given network passphrase. */ +export function getExplorerTxBaseUrl(passphrase?: string): string { + const value = passphrase ?? import.meta.env.VITE_NETWORK_PASSPHRASE ?? "" + const segment = EXPLORER_NETWORK_SEGMENTS[value] ?? "testnet" + return `https://stellar.expert/explorer/${segment}/tx` +} From adac780b9b82dbf01bd49dc36dd27b43dd6d756a Mon Sep 17 00:00:00 2001 From: Freezyyy-arch Date: Sat, 17 Oct 2026 19:27:02 +0100 Subject: [PATCH 2/4] Prevent mobile layout from clipping invoice identifier copy control (#917) --- comebackhere-frontend/src/App.css | 12 ++++++++++++ .../src/components/CopyableText.tsx | 2 +- .../src/tests/CopyableText.test.tsx | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/comebackhere-frontend/src/App.css b/comebackhere-frontend/src/App.css index 8b0d546..6f58f7a 100644 --- a/comebackhere-frontend/src/App.css +++ b/comebackhere-frontend/src/App.css @@ -1607,3 +1607,15 @@ h3 { justify-content: center; } } + +/* Keep the copy icon visible on narrow screens: long identifiers wrap + instead of pushing the control out of the viewport. */ +.copyable-text { + max-width: 100%; +} + +.copyable-text__value { + min-width: 0; + overflow-wrap: anywhere; + word-break: break-all; +} diff --git a/comebackhere-frontend/src/components/CopyableText.tsx b/comebackhere-frontend/src/components/CopyableText.tsx index 1bc8fed..9e15bbb 100644 --- a/comebackhere-frontend/src/components/CopyableText.tsx +++ b/comebackhere-frontend/src/components/CopyableText.tsx @@ -38,7 +38,7 @@ export function CopyableText({ text, label, className }: CopyableTextProps) { aria-label={`${label ?? "Copy"}: ${text}`} style={{ cursor: "pointer", position: "relative", display: "inline-flex", alignItems: "center", gap: "4px" }} > - {text} + {text} { expect(writeText).toHaveBeenCalledWith("clipboard-value") }) }) + +describe("CopyableText layout", () => { + it("marks the value span so long identifiers can wrap beside the copy icon", () => { + render() + expect(screen.getByText("G".repeat(56))).toHaveClass("copyable-text__value") + }) +}) From 28ef77ff3d963217e85bb05db4a836f31c7ca6fb Mon Sep 17 00:00:00 2001 From: Freezyyy-arch Date: Sat, 17 Oct 2026 19:27:10 +0100 Subject: [PATCH 3/4] Document and test valid fallback route after authentication expires (#916) --- comebackhere-frontend/src/tests/useHashTab.test.ts | 8 ++++++++ docs/drips-notes/916-invalid-route-after-auth-expiry.md | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 docs/drips-notes/916-invalid-route-after-auth-expiry.md diff --git a/comebackhere-frontend/src/tests/useHashTab.test.ts b/comebackhere-frontend/src/tests/useHashTab.test.ts index 84a5198..c89d2a0 100644 --- a/comebackhere-frontend/src/tests/useHashTab.test.ts +++ b/comebackhere-frontend/src/tests/useHashTab.test.ts @@ -94,3 +94,11 @@ describe("useHashTab", () => { expect(result.current[0]).toBe("payment") }) }) + +describe("parseTabHash after authentication expires", () => { + it("falls back to the default tab for stale authenticated-only routes", () => { + expect(parseTabHash("#/session-expired")).toBe(DEFAULT_TAB) + expect(parseTabHash("#login")).toBe(DEFAULT_TAB) + expect(parseTabHash("#%E0%A4%A")).toBe(DEFAULT_TAB) + }) +}) diff --git a/docs/drips-notes/916-invalid-route-after-auth-expiry.md b/docs/drips-notes/916-invalid-route-after-auth-expiry.md new file mode 100644 index 0000000..eaf6e30 --- /dev/null +++ b/docs/drips-notes/916-invalid-route-after-auth-expiry.md @@ -0,0 +1,9 @@ +# #916: Redirect after authentication expires + +Note to maintainers: the frontend has no separate session/login route. When the +wallet session ends (disconnect, locked or expired Freighter authorization), +`handleDisconnect` in `comebackhere-frontend/src/App.tsx` calls `setTab("payment")`, +and `useHashTab` (`parseTabHash`) maps any empty, malformed or unknown hash to +`DEFAULT_TAB` ("payment"). The app therefore always lands on a valid route, so +no behavior change was needed. A regression test in +`src/tests/useHashTab.test.ts` pins the fallback for stale/unknown routes. From afff2d074786a6fb945161ba23a44dab5c97cad7 Mon Sep 17 00:00:00 2001 From: Freezyyy-arch Date: Sat, 17 Oct 2026 19:27:17 +0100 Subject: [PATCH 4/4] Refresh invoice list after a successful payment (#915) --- .../src/components/InvoiceList.tsx | 8 ++++++++ .../src/components/PaymentReceipt.tsx | 8 +++++++- .../src/tests/invoiceEvents.test.ts | 13 +++++++++++++ comebackhere-frontend/src/utils/invoiceEvents.ts | 7 +++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 comebackhere-frontend/src/tests/invoiceEvents.test.ts create mode 100644 comebackhere-frontend/src/utils/invoiceEvents.ts diff --git a/comebackhere-frontend/src/components/InvoiceList.tsx b/comebackhere-frontend/src/components/InvoiceList.tsx index aeb3d96..740a104 100644 --- a/comebackhere-frontend/src/components/InvoiceList.tsx +++ b/comebackhere-frontend/src/components/InvoiceList.tsx @@ -4,6 +4,7 @@ import { StatusBadge } from "./StatusBadge" import { InvoiceListSkeleton } from "./Skeleton" import { listInvoices, ApiError, INVOICE_PAGE_SIZE, type InvoicePage } from "../utils/api" import { FILTERABLE_STATUSES, statusBadgeClass, statusLabel } from "../utils/invoiceStatus" +import { PAYMENT_COMPLETED_EVENT } from "../utils/invoiceEvents" import { INVOICE_TOKENS, fromSmallestUnit } from "../utils/invoiceForm" interface InvoiceListProps { @@ -52,6 +53,13 @@ export function InvoiceList({ merchantAddress, onOpenInvoice, onCreateInvoice }: return () => controller.abort() }, [merchantAddress, status, page, reloadKey]) + // Refetch when a payment completes elsewhere in the app. + useEffect(() => { + const onPaid = () => setReloadKey((k) => k + 1) + window.addEventListener(PAYMENT_COMPLETED_EVENT, onPaid) + return () => window.removeEventListener(PAYMENT_COMPLETED_EVENT, onPaid) + }, []) + const handleFilter = useCallback((next: InvoiceStatus | null) => { setStatus(next) setPage(1) diff --git a/comebackhere-frontend/src/components/PaymentReceipt.tsx b/comebackhere-frontend/src/components/PaymentReceipt.tsx index 9fa9357..94d4391 100644 --- a/comebackhere-frontend/src/components/PaymentReceipt.tsx +++ b/comebackhere-frontend/src/components/PaymentReceipt.tsx @@ -1,7 +1,8 @@ -import { useCallback } from "react" +import { useCallback, useEffect } from "react" import type { Invoice } from "../types" import { useT } from "../i18n" import { CopyableText } from "./CopyableText" +import { emitPaymentCompleted } from "../utils/invoiceEvents" import { getExplorerTxBaseUrl } from "../utils/explorerUrl" interface PaymentReceiptProps { @@ -23,6 +24,11 @@ export function PaymentReceipt({ }: PaymentReceiptProps) { const t = useT() + // The receipt only renders after a confirmed payment; tell lists to refetch. + useEffect(() => { + emitPaymentCompleted(String(invoice.id)) + }, [invoice.id, transactionHash]) + const explorerUrl = `${explorerBaseUrl}/${transactionHash}` const handlePrint = useCallback(() => { diff --git a/comebackhere-frontend/src/tests/invoiceEvents.test.ts b/comebackhere-frontend/src/tests/invoiceEvents.test.ts new file mode 100644 index 0000000..e304fb5 --- /dev/null +++ b/comebackhere-frontend/src/tests/invoiceEvents.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect, vi } from "vitest" +import { emitPaymentCompleted, PAYMENT_COMPLETED_EVENT } from "../utils/invoiceEvents" + +describe("emitPaymentCompleted", () => { + it("dispatches a window event with the invoice id", () => { + const handler = vi.fn() + window.addEventListener(PAYMENT_COMPLETED_EVENT, handler) + emitPaymentCompleted("42") + window.removeEventListener(PAYMENT_COMPLETED_EVENT, handler) + expect(handler).toHaveBeenCalledTimes(1) + expect((handler.mock.calls[0][0] as CustomEvent).detail).toEqual({ invoiceId: "42" }) + }) +}) diff --git a/comebackhere-frontend/src/utils/invoiceEvents.ts b/comebackhere-frontend/src/utils/invoiceEvents.ts new file mode 100644 index 0000000..f207733 --- /dev/null +++ b/comebackhere-frontend/src/utils/invoiceEvents.ts @@ -0,0 +1,7 @@ +/** Window event fired once a payment has been confirmed, so lists can refetch. */ +export const PAYMENT_COMPLETED_EVENT = "comebackhere:payment-completed" + +export function emitPaymentCompleted(invoiceId?: string): void { + if (typeof window === "undefined") return + window.dispatchEvent(new CustomEvent(PAYMENT_COMPLETED_EVENT, { detail: { invoiceId } })) +}