Skip to content
Merged
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
9 changes: 8 additions & 1 deletion src/app/dashboard/wallets/[id]/addresses/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,14 @@ function AddressRow({
return (
<tr
onClick={onSelect}
className="cursor-pointer transition-colors hover:bg-surface-raised"
onKeyDown={(e) => {
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"
>
<td className="py-3 pr-4 text-foreground">
{address.customer_ref ?? <span className="text-muted">—</span>}
Expand Down
27 changes: 25 additions & 2 deletions src/app/dashboard/wallets/[id]/payment-links/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -45,6 +45,7 @@ export default function PaymentLinksPage({
const [created, setCreated] = useState<PaymentLink | null>(null);
const [selected, setSelected] = useState<PaymentLink | null>(null);
const [error, setError] = useState<string | null>(null);
const [deactivating, setDeactivating] = useState<PaymentLink | null>(null);

// Cursor pagination: `cursors[i]` is the `before` cursor for page i+1 (page 1 has none).
const [cursors, setCursors] = useState<(string | null)[]>([null]);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -202,7 +209,8 @@ export default function PaymentLinksPage({
<PaymentLinkList
links={links}
onSelect={setSelected}
onToggleActive={handleToggleActive}
onToggleActive={requestToggle}
walletId={id}
/>
</table>
</div>
Expand All @@ -220,6 +228,21 @@ export default function PaymentLinksPage({

</WalletPageShell>

{deactivating && (
<ConfirmModal
title="Deactivate payment link?"
message="Anyone currently on the checkout will see “Link not found” straight away."
confirmLabel="Deactivate"
destructive
onConfirm={() => {
const link = deactivating;
setDeactivating(null);
handleToggleActive(link);
}}
onClose={() => setDeactivating(null)}
/>
)}

{showCreate && (
<CreatePaymentLinkForm
walletId={id}
Expand Down
9 changes: 8 additions & 1 deletion src/app/dashboard/wallets/[id]/transactions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,14 @@ function TxRow({
return (
<tr
onClick={onSelect}
className="cursor-pointer transition-colors hover:bg-surface-raised"
onKeyDown={(e) => {
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"
>
<td className="py-3 pr-4">
<div className="flex flex-wrap items-center gap-1.5">
Expand Down
30 changes: 30 additions & 0 deletions src/components/payment-links/PaymentLinkDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ export function PaymentLinkDetail({
}) {
const [payments, setPayments] = useState<PaymentLinkPayment[] | null>(null);
const [paymentsError, setPaymentsError] = useState<string | null>(null);
const [nextCursor, setNextCursor] = useState<string | null>(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) =>
Expand All @@ -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 (
<Modal title="Payment link details" onClose={onClose}>
<div className="space-y-4">
Expand Down Expand Up @@ -90,6 +110,16 @@ export function PaymentLinkDetail({
<ExportPaymentLinkPaymentsCsvButton token={token} walletId={walletId} linkId={link.id} />
</div>
<PaymentsTable payments={payments} error={paymentsError} />
{nextCursor && (
<button
type="button"
onClick={loadMore}
disabled={loadingMore}
className="mt-2 w-full rounded-lg border border-border py-2 text-xs font-medium text-foreground hover:bg-surface-sunken disabled:opacity-60"
>
{loadingMore ? "Loading…" : "Load more"}
</button>
)}
</div>
</div>
</Modal>
Expand Down
20 changes: 16 additions & 4 deletions src/components/payment-links/PaymentLinkList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<tr onClick={() => onSelect(link)} className="cursor-pointer transition-colors hover:bg-surface-raised">
<tr
onClick={() => 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"
>
<td className="py-3 pr-4 text-foreground">{link.name}</td>
<td className="py-3 pr-4 text-foreground">{link.amount_usdc_stroops !== null ? `$${formatStroops(link.amount_usdc_stroops)}` : "Flexible"}</td>
<td className="py-3 pr-4"><span className={`inline-flex items-center gap-1 text-xs ${link.active ? "text-success" : "text-muted"}`}>{link.active ? "●" : "○"} {link.active ? "Active" : "Inactive"}</span></td>
<td className="py-3 pr-4 font-medium text-foreground">${formatStroops(link.collected_usdc_stroops)}</td>
<td className="py-3 pr-4 text-muted"><RelativeTime date={link.created_at} /></td>
<td className="py-3 text-right">
<Link href={`payment-links/${link.id}/edit`} onClick={(e) => e.stopPropagation()} className="mr-3 text-xs text-muted hover:text-foreground">Edit</Link>
<Link href={`/dashboard/wallets/${walletId}/payment-links/${encodeURIComponent(link.id)}/edit`} onClick={(e) => e.stopPropagation()} className="mr-3 text-xs text-muted hover:text-foreground">Edit</Link>
<button type="button" onClick={(e) => { e.stopPropagation(); onToggleActive(link); }} className="text-xs text-muted hover:text-foreground">{link.active ? "Deactivate" : "Activate"}</button>
</td>
</tr>
);
}

export function PaymentLinkList({ links, onSelect, onToggleActive }: { links: PaymentLink[]; onSelect: (link: PaymentLink) => void; onToggleActive: (link: PaymentLink) => void }) {
return <tbody className="divide-y divide-divider">{links.map((link) => <PaymentLinkRow key={link.id} link={link} onSelect={onSelect} onToggleActive={onToggleActive} />)}</tbody>;
export function PaymentLinkList({ links, walletId, onSelect, onToggleActive }: { links: PaymentLink[]; walletId: string; onSelect: (link: PaymentLink) => void; onToggleActive: (link: PaymentLink) => void }) {
return <tbody className="divide-y divide-divider">{links.map((link) => <PaymentLinkRow key={link.id} link={link} walletId={walletId} onSelect={onSelect} onToggleActive={onToggleActive} />)}</tbody>;
}
4 changes: 3 additions & 1 deletion src/components/payment-links/PaymentsTable.tsx
Original file line number Diff line number Diff line change
@@ -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 <p className="mt-2 rounded-lg border border-danger-border bg-danger-bg px-3 py-2 text-xs text-danger">{error}</p>;
if (payments === null) return <p className="mt-2 text-xs text-muted">Loading…</p>;
if (payments.length === 0) return <p className="mt-2 text-xs text-muted">No payments yet. Payers appear here as soon as they start a payment.</p>;
return <div className="mt-2 max-h-56 overflow-y-auto rounded-lg border border-border"><table className="w-full text-xs"><thead className="sticky top-0 bg-burgundy-soft/60"><tr className="text-left text-[10px] uppercase tracking-wide text-muted"><th className="px-3 py-2 font-medium">Payer</th><th className="px-3 py-2 font-medium">Amount</th><th className="px-3 py-2 font-medium">Status</th></tr></thead><tbody className="divide-y divide-divider">{payments.map((p) => <tr key={p.id}><td className="px-3 py-2"><p className="text-foreground">{p.payer_name ?? "—"}</p>{p.payer_email && <p className="text-[10px] text-muted">{p.payer_email}</p>}</td><td className="px-3 py-2 font-medium text-foreground">${formatStroops(p.amount_usdc_stroops)}</td><td className="px-3 py-2"><span className={p.status === "confirmed" ? "text-success" : "text-warning"}>{p.status === "confirmed" ? "✓ Confirmed" : "• Pending"}</span></td></tr>)}</tbody></table></div>;
return <div className="mt-2 max-h-56 overflow-y-auto rounded-lg border border-border"><table className="w-full text-xs"><thead className="sticky top-0 bg-burgundy-soft/60"><tr className="text-left text-[10px] uppercase tracking-wide text-muted"><th className="px-3 py-2 font-medium">Payer</th><th className="px-3 py-2 font-medium">Amount</th><th className="px-3 py-2 font-medium">Status</th><th className="px-3 py-2 font-medium">Date</th><th className="px-3 py-2 font-medium">Transaction</th></tr></thead><tbody className="divide-y divide-divider">{payments.map((p) => <tr key={p.id}><td className="px-3 py-2"><p className="text-foreground">{p.payer_name ?? "—"}</p>{p.payer_email && <p className="text-[10px] text-muted">{p.payer_email}</p>}</td><td className="px-3 py-2 font-medium text-foreground">${formatStroops(p.amount_usdc_stroops)}</td><td className="px-3 py-2"><span className={p.status === "confirmed" ? "text-success" : "text-warning"}>{p.status === "confirmed" ? "✓ Confirmed" : "• Pending"}</span></td><td className="px-3 py-2 text-muted"><RelativeTime date={p.created_at} /></td><td className="px-3 py-2">{p.transaction_id && <a href={explorerTxUrl(p.transaction_id)} target="_blank" rel="noopener noreferrer" className="text-burgundy-bright hover:underline">View</a>}</td></tr>)}</tbody></table></div>;
}