From 4b197325dca4c87efef749784f02732579e31672 Mon Sep 17 00:00:00 2001 From: rdj-savyy Date: Sat, 17 Oct 2026 19:25:21 +0100 Subject: [PATCH 1/4] Show longer wallet address prefix and suffix so accounts are distinguishable (#922) --- comebackhere-frontend/src/components/WalletBar.tsx | 3 ++- comebackhere-frontend/src/tests/WalletBar.test.tsx | 2 +- comebackhere-frontend/src/utils/truncateAddress.ts | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 comebackhere-frontend/src/utils/truncateAddress.ts diff --git a/comebackhere-frontend/src/components/WalletBar.tsx b/comebackhere-frontend/src/components/WalletBar.tsx index 8533be5..3d891c2 100644 --- a/comebackhere-frontend/src/components/WalletBar.tsx +++ b/comebackhere-frontend/src/components/WalletBar.tsx @@ -1,5 +1,6 @@ import type { FC, ReactNode } from "react" import type { WalletState } from "../hooks/useWallet" +import { truncateAddress } from "../utils/truncateAddress" interface WalletBarProps { wallet: WalletState @@ -9,7 +10,7 @@ interface WalletBarProps { } function shortAddress(address: string): string { - return `${address.slice(0, 6)}...${address.slice(-4)}` + return truncateAddress(address) } /** Human-readable name for a network passphrase, e.g. "Test SDF Network". */ diff --git a/comebackhere-frontend/src/tests/WalletBar.test.tsx b/comebackhere-frontend/src/tests/WalletBar.test.tsx index f177991..6a696c0 100644 --- a/comebackhere-frontend/src/tests/WalletBar.test.tsx +++ b/comebackhere-frontend/src/tests/WalletBar.test.tsx @@ -127,7 +127,7 @@ describe("WalletBar — connected state", () => { it("renders the truncated address with the full address accessible", () => { renderBar(STATES.connected) const addr = screen.getByTestId("wallet-address") - expect(addr).toHaveTextContent(`GBDXOE...${ADDRESS.slice(-4)}`) + expect(addr).toHaveTextContent(`${ADDRESS.slice(0, 8)}...${ADDRESS.slice(-8)}`) expect(addr).toHaveAttribute("aria-label", `Wallet connected: ${ADDRESS}`) }) diff --git a/comebackhere-frontend/src/utils/truncateAddress.ts b/comebackhere-frontend/src/utils/truncateAddress.ts new file mode 100644 index 0000000..d750390 --- /dev/null +++ b/comebackhere-frontend/src/utils/truncateAddress.ts @@ -0,0 +1,5 @@ +/** Shorten a wallet address while keeping enough characters to tell accounts apart. */ +export function truncateAddress(address: string, head = 8, tail = 8): string { + if (address.length <= head + tail + 3) return address + return `${address.slice(0, head)}...${address.slice(-tail)}` +} From 6b069b9e5dfdce5c4f939d106f5e47826c023024 Mon Sep 17 00:00:00 2001 From: rdj-savyy Date: Sat, 17 Oct 2026 19:25:21 +0100 Subject: [PATCH 2/4] Block resubmitting a completed payment after browser back navigation (#921) --- .../src/components/InvoicePayment.tsx | 5 +++- .../src/hooks/useCompletedPaymentGuard.ts | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 comebackhere-frontend/src/hooks/useCompletedPaymentGuard.ts diff --git a/comebackhere-frontend/src/components/InvoicePayment.tsx b/comebackhere-frontend/src/components/InvoicePayment.tsx index 83359d4..bf0d46b 100644 --- a/comebackhere-frontend/src/components/InvoicePayment.tsx +++ b/comebackhere-frontend/src/components/InvoicePayment.tsx @@ -8,10 +8,12 @@ import { PayConfirmationModal } from "./PayConfirmationModal" import { CancelConfirmationModal } from "./CancelConfirmationModal" import { TransactionHistory } from "./TransactionHistory" import { InvoiceQRCode } from "./InvoiceQRCode" +import { useCompletedPaymentGuard } from "../hooks/useCompletedPaymentGuard" import { formatAmount, USDC_DECIMALS } from "../utils/format" export function InvoicePayment() { const { invoice, loading, error, loadInvoice, pay, cancel } = useInvoice() + const { isCompleted, markCompleted } = useCompletedPaymentGuard() const { address, connected, connecting, connect, notReadyReason } = useWallet() const [invoiceId, setInvoiceId] = useState("") const [showConfirm, setShowConfirm] = useState(false) @@ -80,9 +82,10 @@ export function InvoicePayment() { } const handleConfirmPayment = async () => { - if (!address) return + if (!address || submitting || (invoice && isCompleted(invoice.id))) return setSubmitting(true) const res = await pay(address) + if (res.success && invoice) markCompleted(invoice.id) setSubmitting(false) setShowConfirm(false) setResult({ diff --git a/comebackhere-frontend/src/hooks/useCompletedPaymentGuard.ts b/comebackhere-frontend/src/hooks/useCompletedPaymentGuard.ts new file mode 100644 index 0000000..84d9e3d --- /dev/null +++ b/comebackhere-frontend/src/hooks/useCompletedPaymentGuard.ts @@ -0,0 +1,28 @@ +import { useCallback } from "react" + +const KEY = "cbh:completedPayments" + +function read(): string[] { + try { + const parsed = JSON.parse(sessionStorage.getItem(KEY) ?? "[]") + return Array.isArray(parsed) ? parsed.map(String) : [] + } catch { + return [] + } +} + +/** + * Remembers invoices paid in this browser session so that back/forward + * navigation (including bfcache restores) cannot submit the payment again. + */ +export function useCompletedPaymentGuard() { + const isCompleted = useCallback((id: string | number) => read().includes(String(id)), []) + const markCompleted = useCallback((id: string | number) => { + try { + sessionStorage.setItem(KEY, JSON.stringify([...read(), String(id)])) + } catch { + // storage unavailable; the on-chain status check remains the backstop + } + }, []) + return { isCompleted, markCompleted } +} From 485825593550d667f8a98a79a21438d480a5e54c Mon Sep 17 00:00:00 2001 From: rdj-savyy Date: Sat, 17 Oct 2026 19:25:21 +0100 Subject: [PATCH 3/4] Time out invoice retrieval so the loading state always clears (#919) --- comebackhere-frontend/src/hooks/useInvoice.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/comebackhere-frontend/src/hooks/useInvoice.ts b/comebackhere-frontend/src/hooks/useInvoice.ts index ee6d0d4..1e1d532 100644 --- a/comebackhere-frontend/src/hooks/useInvoice.ts +++ b/comebackhere-frontend/src/hooks/useInvoice.ts @@ -15,6 +15,8 @@ interface UseInvoiceReturn { release: (publicKey: string) => Promise } +const LOAD_TIMEOUT_MS = 30_000 + export function useInvoice(): UseInvoiceReturn { const [invoice, setInvoice] = useState(null) const [loading, setLoading] = useState(false) @@ -23,12 +25,17 @@ export function useInvoice(): UseInvoiceReturn { const loadInvoice = useCallback(async (id: number) => { setLoading(true) setError(null) + let timer: ReturnType | undefined try { - const result = await fetchInvoice(CONTRACT_ID, id) + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("Invoice request timed out")), LOAD_TIMEOUT_MS) + }) + const result = await Promise.race([fetchInvoice(CONTRACT_ID, id), timeout]) setInvoice(result) } catch (err: unknown) { setError(err instanceof Error ? err.message : "Failed to load invoice") } finally { + clearTimeout(timer) setLoading(false) } }, []) From a3cd1d88b1e4dceb4c686ff095fad9738f864253 Mon Sep 17 00:00:00 2001 From: rdj-savyy Date: Sat, 17 Oct 2026 19:25:21 +0100 Subject: [PATCH 4/4] Clear cached invoice details when the connected account changes (#920) --- .../src/components/InvoicePayment.tsx | 2 +- comebackhere-frontend/src/hooks/useInvoice.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/comebackhere-frontend/src/components/InvoicePayment.tsx b/comebackhere-frontend/src/components/InvoicePayment.tsx index bf0d46b..709085d 100644 --- a/comebackhere-frontend/src/components/InvoicePayment.tsx +++ b/comebackhere-frontend/src/components/InvoicePayment.tsx @@ -12,9 +12,9 @@ import { useCompletedPaymentGuard } from "../hooks/useCompletedPaymentGuard" import { formatAmount, USDC_DECIMALS } from "../utils/format" export function InvoicePayment() { - const { invoice, loading, error, loadInvoice, pay, cancel } = useInvoice() const { isCompleted, markCompleted } = useCompletedPaymentGuard() const { address, connected, connecting, connect, notReadyReason } = useWallet() + const { invoice, loading, error, loadInvoice, pay, cancel } = useInvoice(address) const [invoiceId, setInvoiceId] = useState("") const [showConfirm, setShowConfirm] = useState(false) const [showCancelConfirm, setShowCancelConfirm] = useState(false) diff --git a/comebackhere-frontend/src/hooks/useInvoice.ts b/comebackhere-frontend/src/hooks/useInvoice.ts index 1e1d532..d3d00da 100644 --- a/comebackhere-frontend/src/hooks/useInvoice.ts +++ b/comebackhere-frontend/src/hooks/useInvoice.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react" +import { useState, useCallback, useEffect, useRef } from "react" import { InvoiceStatus, type Invoice, type PaymentResult } from "../types" import { fetchInvoice, payInvoice, cancelInvoice, requestRefund, releaseEscrow } from "../utils/soroban" @@ -17,11 +17,21 @@ interface UseInvoiceReturn { const LOAD_TIMEOUT_MS = 30_000 -export function useInvoice(): UseInvoiceReturn { +export function useInvoice(accountKey?: string | null): UseInvoiceReturn { const [invoice, setInvoice] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + // Drop cached invoice details when the connected account changes. + const prevAccount = useRef(accountKey) + useEffect(() => { + if (prevAccount.current !== accountKey) { + prevAccount.current = accountKey + setInvoice(null) + setError(null) + } + }, [accountKey]) + const loadInvoice = useCallback(async (id: number) => { setLoading(true) setError(null)