diff --git a/src/app/dashboard/wallets/[id]/addresses/page.tsx b/src/app/dashboard/wallets/[id]/addresses/page.tsx index 5d0b17a..163d0a7 100644 --- a/src/app/dashboard/wallets/[id]/addresses/page.tsx +++ b/src/app/dashboard/wallets/[id]/addresses/page.tsx @@ -202,7 +202,14 @@ function AddressRow({ return ( { + if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + onSelect(); + } + }} + tabIndex={0} + className="cursor-pointer transition-colors hover:bg-surface-raised focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-burgundy-bright" > {address.customer_ref ?? —} diff --git a/src/app/dashboard/wallets/[id]/payment-links/page.tsx b/src/app/dashboard/wallets/[id]/payment-links/page.tsx index 106374a..7e746bf 100644 --- a/src/app/dashboard/wallets/[id]/payment-links/page.tsx +++ b/src/app/dashboard/wallets/[id]/payment-links/page.tsx @@ -13,7 +13,7 @@ import { } from "@/lib/payment-links"; import { asAuthToken, asWalletId } from "@/lib/brands"; import { formatStroops, sumStroops } from "@/lib/amount"; -import { Modal, CopyField } from "@/components/dashboard/Modal"; +import { Modal, CopyField, ConfirmModal } from "@/components/dashboard/Modal"; import { Stat, ActionButton, Panel, Empty } from "@/components/dashboard/WalletUI"; import { Pagination } from "@/components/dashboard/Pagination"; import { PageSpinner } from "@/components/OctoSpinner"; @@ -45,6 +45,7 @@ export default function PaymentLinksPage({ const [created, setCreated] = useState(null); const [selected, setSelected] = useState(null); const [error, setError] = useState(null); + const [deactivating, setDeactivating] = useState(null); // Cursor pagination: `cursors[i]` is the `before` cursor for page i+1 (page 1 has none). const [cursors, setCursors] = useState<(string | null)[]>([null]); @@ -114,6 +115,12 @@ export default function PaymentLinksPage({ load(target ?? null); } + // Deactivating cuts off anyone on the checkout, so it asks first. Reactivating does not. + function requestToggle(link: PaymentLink) { + if (link.active) setDeactivating(link); + else handleToggleActive(link); + } + async function handleToggleActive(link: PaymentLink) { if (!token) return; try { @@ -202,7 +209,8 @@ export default function PaymentLinksPage({ @@ -220,6 +228,21 @@ export default function PaymentLinksPage({ + {deactivating && ( + { + const link = deactivating; + setDeactivating(null); + handleToggleActive(link); + }} + onClose={() => setDeactivating(null)} + /> + )} + {showCreate && ( { + if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + onSelect(); + } + }} + tabIndex={0} + className="cursor-pointer transition-colors hover:bg-surface-raised focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-burgundy-bright" >
diff --git a/src/components/payment-links/PaymentLinkDetail.tsx b/src/components/payment-links/PaymentLinkDetail.tsx index 9db7de0..52674a4 100644 --- a/src/components/payment-links/PaymentLinkDetail.tsx +++ b/src/components/payment-links/PaymentLinkDetail.tsx @@ -28,12 +28,15 @@ export function PaymentLinkDetail({ }) { const [payments, setPayments] = useState(null); const [paymentsError, setPaymentsError] = useState(null); + const [nextCursor, setNextCursor] = useState(null); + const [loadingMore, setLoadingMore] = useState(false); useEffect(() => { if (!token) return; listPaymentLinkPayments(asAuthToken(token), asWalletId(walletId), link.id, { limit: 20 }) .then((page) => { setPayments(page.data); + setNextCursor(page.next_cursor); setPaymentsError(null); }) .catch((e) => @@ -43,6 +46,23 @@ export function PaymentLinkDetail({ ); }, [token, walletId, link.id]); + async function loadMore() { + if (!token || !nextCursor) return; + setLoadingMore(true); + try { + const page = await listPaymentLinkPayments(asAuthToken(token), asWalletId(walletId), link.id, { + limit: 20, + before: nextCursor, + }); + setPayments((prev) => [...(prev ?? []), ...page.data]); + setNextCursor(page.next_cursor); + } catch (e) { + setPaymentsError(e instanceof Error ? e.message : "Could not load payments."); + } finally { + setLoadingMore(false); + } + } + return (
@@ -90,6 +110,16 @@ export function PaymentLinkDetail({
+ {nextCursor && ( + + )}
diff --git a/src/components/payment-links/PaymentLinkList.tsx b/src/components/payment-links/PaymentLinkList.tsx index 21e8190..aa05436 100644 --- a/src/components/payment-links/PaymentLinkList.tsx +++ b/src/components/payment-links/PaymentLinkList.tsx @@ -7,28 +7,40 @@ import { RelativeTime } from "@/components/RelativeTime"; function PaymentLinkRow({ link, + walletId, onSelect, onToggleActive, }: { link: PaymentLink; + walletId: string; onSelect: (link: PaymentLink) => void; onToggleActive: (link: PaymentLink) => void; }) { return ( - onSelect(link)} className="cursor-pointer transition-colors hover:bg-surface-raised"> + onSelect(link)} + onKeyDown={(e) => { + if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + onSelect(link); + } + }} + tabIndex={0} + className="cursor-pointer transition-colors hover:bg-surface-raised focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-burgundy-bright" + > {link.name} {link.amount_usdc_stroops !== null ? `$${formatStroops(link.amount_usdc_stroops)}` : "Flexible"} {link.active ? "●" : "○"} {link.active ? "Active" : "Inactive"} ${formatStroops(link.collected_usdc_stroops)} - e.stopPropagation()} className="mr-3 text-xs text-muted hover:text-foreground">Edit + e.stopPropagation()} className="mr-3 text-xs text-muted hover:text-foreground">Edit ); } -export function PaymentLinkList({ links, onSelect, onToggleActive }: { links: PaymentLink[]; onSelect: (link: PaymentLink) => void; onToggleActive: (link: PaymentLink) => void }) { - return {links.map((link) => )}; +export function PaymentLinkList({ links, walletId, onSelect, onToggleActive }: { links: PaymentLink[]; walletId: string; onSelect: (link: PaymentLink) => void; onToggleActive: (link: PaymentLink) => void }) { + return {links.map((link) => )}; } diff --git a/src/components/payment-links/PaymentsTable.tsx b/src/components/payment-links/PaymentsTable.tsx index 01a95a4..43d975c 100644 --- a/src/components/payment-links/PaymentsTable.tsx +++ b/src/components/payment-links/PaymentsTable.tsx @@ -1,11 +1,13 @@ "use client"; import { formatStroops } from "@/lib/amount"; +import { explorerTxUrl } from "@/lib/network"; +import { RelativeTime } from "@/components/RelativeTime"; import type { PaymentLinkPayment } from "@/lib/payment-links"; export function PaymentsTable({ payments, error }: { payments: PaymentLinkPayment[] | null; error: string | null }) { if (error) return

{error}

; if (payments === null) return

Loading…

; if (payments.length === 0) return

No payments yet. Payers appear here as soon as they start a payment.

; - return
{payments.map((p) => )}
PayerAmountStatus

{p.payer_name ?? "—"}

{p.payer_email &&

{p.payer_email}

}
${formatStroops(p.amount_usdc_stroops)}{p.status === "confirmed" ? "✓ Confirmed" : "• Pending"}
; + return
{payments.map((p) => )}
PayerAmountStatusDateTransaction

{p.payer_name ?? "—"}

{p.payer_email &&

{p.payer_email}

}
${formatStroops(p.amount_usdc_stroops)}{p.status === "confirmed" ? "✓ Confirmed" : "• Pending"}{p.transaction_id && View}
; }