Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions comebackhere-frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion comebackhere-frontend/src/components/CopyableText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" }}
>
<span ref={textRef}>{text}</span>
<span ref={textRef} className="copyable-text__value">{text}</span>
<svg
width="14"
height="14"
Expand Down
8 changes: 8 additions & 0 deletions comebackhere-frontend/src/components/InvoiceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion comebackhere-frontend/src/components/InvoiceTimeline.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 10 additions & 3 deletions comebackhere-frontend/src/components/PaymentReceipt.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
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 {
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,
Expand All @@ -22,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(() => {
Expand Down
7 changes: 7 additions & 0 deletions comebackhere-frontend/src/tests/CopyableText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ describe("CopyableText", () => {
expect(writeText).toHaveBeenCalledWith("clipboard-value")
})
})

describe("CopyableText layout", () => {
it("marks the value span so long identifiers can wrap beside the copy icon", () => {
render(<CopyableText text={"G".repeat(56)} label="Copy value" />)
expect(screen.getByText("G".repeat(56))).toHaveClass("copyable-text__value")
})
})
17 changes: 17 additions & 0 deletions comebackhere-frontend/src/tests/explorerUrl.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
13 changes: 13 additions & 0 deletions comebackhere-frontend/src/tests/invoiceEvents.test.ts
Original file line number Diff line number Diff line change
@@ -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" })
})
})
8 changes: 8 additions & 0 deletions comebackhere-frontend/src/tests/useHashTab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
16 changes: 16 additions & 0 deletions comebackhere-frontend/src/utils/explorerUrl.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
"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`
}
7 changes: 7 additions & 0 deletions comebackhere-frontend/src/utils/invoiceEvents.ts
Original file line number Diff line number Diff line change
@@ -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 } }))
}
9 changes: 9 additions & 0 deletions docs/drips-notes/916-invalid-route-after-auth-expiry.md
Original file line number Diff line number Diff line change
@@ -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.
Loading