Why
Right now a merchant signs up, gets a Stellar settlement wallet auto-provisioned (PR #136), but can only create payment links via curl. The dashboard `/links` page exists but was never tested against the new API response shape — it'll likely crash on first render now that the contract changed in PR #136.
This is the highest-leverage unshipped work for the demo: unblocks every "show me how a merchant uses this" conversation.
Scope
Build two things in `apps/dashboard/src/app/(dashboard)/links/`:
- List view — paginated table of the merchant's payment links with filter + search
- Create form — modal or drawer that POSTs a new link and adds it to the list optimistically
API endpoints (already shipped)
| Verb |
Path |
Auth |
| GET |
`/v1/payment-links?page=1&limit=20&status=active|expired|deactivated|all` |
JWT |
| POST |
`/v1/payment-links` |
API key (CombinedAuthGuard) — see acceptance criteria #6 |
| DELETE |
`/v1/payment-links/:id` |
JWT |
| GET |
`/v1/payment-links/:id/stats` |
JWT |
Response shape from `@useroutr/types`:
```ts
interface PaymentLink {
id: string; // "lnk_..."
amount?: number;
currency: string;
description?: string;
type: "single-use" | "multi-use";
status: "active" | "expired" | "deactivated";
usageCount: number;
expiresAt?: string; // ISO 8601
url: string; // pay.useroutr.com/...
createdAt: string;
updatedAt: string;
}
```
`POST` body (note: still snake_case for input — it's the merchant-facing contract):
```json
{
"amount": 25,
"currency": "USD",
"description": "...",
"single_use": false,
"expires_at": "2026-06-01T00:00:00Z"
}
```
Files to read first
- `apps/dashboard/src/app/(dashboard)/links/page.tsx` — existing page (crashes on render right now)
- `apps/dashboard/src/components/links/{LinkCard,LinkStatusBadge,CreateLinkModal}.tsx` — existing components
- `apps/dashboard/src/hooks/` — add `useLinks.ts` here following the pattern in `useSettings.ts`
- `packages/types/src/payment.types.ts` — source of truth for `PaymentLink`
What to build
List page (`/links`)
- Card grid (the existing `LinkCard` already renders the right shape — fix what doesn't)
- Filter chips: All / Active / Expired / Deactivated (drives the `?status=` query param)
- Search input (client-side filter on `description` for now, server-side later)
- Empty state with a "Create your first link" CTA
- "+ New link" button top-right opens the create modal
Create modal
- Form fields: amount (optional → "open amount" checkbox), currency, description, single-use toggle, expires-at date picker (optional)
- Submit POSTs to `/v1/payment-links`, on success closes modal + optimistically inserts into the list + toasts the URL + copies to clipboard
- Handle 422 (insufficient liquidity), 400 (validation), 401 (auth), surface API `error.message` from the envelope
Detail link (each card)
- Clicking a card or its ID navigates to `/links/[id]` — that page is a separate issue (PR 9)
Acceptance criteria
Estimated effort
~1 day for a frontend dev familiar with TanStack Query + the existing dashboard component library.
Design
Match the existing `/payments` and `/payouts` pages in this app — same `surface` chrome, same toast pattern, same skeleton loaders. Look at `apps/dashboard/src/app/(dashboard)/payments/page.tsx` as the template.
Why
Right now a merchant signs up, gets a Stellar settlement wallet auto-provisioned (PR #136), but can only create payment links via curl. The dashboard `/links` page exists but was never tested against the new API response shape — it'll likely crash on first render now that the contract changed in PR #136.
This is the highest-leverage unshipped work for the demo: unblocks every "show me how a merchant uses this" conversation.
Scope
Build two things in `apps/dashboard/src/app/(dashboard)/links/`:
API endpoints (already shipped)
Response shape from `@useroutr/types`:
```ts
interface PaymentLink {
id: string; // "lnk_..."
amount?: number;
currency: string;
description?: string;
type: "single-use" | "multi-use";
status: "active" | "expired" | "deactivated";
usageCount: number;
expiresAt?: string; // ISO 8601
url: string; // pay.useroutr.com/...
createdAt: string;
updatedAt: string;
}
```
`POST` body (note: still snake_case for input — it's the merchant-facing contract):
```json
{
"amount": 25,
"currency": "USD",
"description": "...",
"single_use": false,
"expires_at": "2026-06-01T00:00:00Z"
}
```
Files to read first
What to build
List page (`/links`)
Create modal
Detail link (each card)
Acceptance criteria
Estimated effort
~1 day for a frontend dev familiar with TanStack Query + the existing dashboard component library.
Design
Match the existing `/payments` and `/payouts` pages in this app — same `surface` chrome, same toast pattern, same skeleton loaders. Look at `apps/dashboard/src/app/(dashboard)/payments/page.tsx` as the template.