Merchant dashboard and customer payment portal for the DupDub crypto-to-fiat settlement platform.
This is the Next.js app merchants use to manage payments and settlements, and the page customers land on to pay a merchant with USDC on Stellar. It talks exclusively to the dupdap-backend REST API — there is no direct on-chain write path from the frontend itself beyond what the customer's wallet signs.
Related repos:
dupdap-backend— the API this app calls (NEXT_PUBLIC_API_URL)dupdapp_stellar— the Soroban contracts (payment_escrow, etc.) that the customer payment flow ultimately settles into
The customer-facing payment flow here is built around Stellar, not a generic multi-chain wallet connector:
- One asset, one chain to reason about — the payment page only needs to handle Stellar/USDC, so there's no chain-switching UX, no gas-token juggling, and no per-network RPC config on the client.
- Sub-second-feeling confirmations — Stellar's ~5s ledger close means the "waiting for payment" state on
/pay/[paymentId]resolves fast enough to keep a checkout flow feeling responsive. - Escrow, not a bare wallet transfer — the customer flow is approve → deposit into the
payment_escrowSoroban contract, not a raw transfer to a merchant address, so funds are held under contract logic until settlement conditions are met (seedupdapp_stellar). - Fees low enough for small payments — near-zero base fees keep the platform viable for low-ticket merchant transactions.
/ marketing/landing page
/waitlist public waitlist signup
/auth/login merchant login
/auth/register merchant registration
/auth/forgot-password request a password reset link
/auth/reset-password set a new password from a reset link
/pay/[paymentId] customer-facing payment page (approve → deposit → status → receipt)
/dashboard merchant dashboard shell (layout.tsx wraps the routes below)
/dashboard overview
/dashboard/payments payment list/detail
/dashboard/settlements merchant settlement tracking
/dashboard/analytics revenue/conversion analytics
/dashboard/webhooks webhook endpoint management
/dashboard/settings profile, API keys, notification prefs
/dashboard/admin internal/admin views
/dashboard/admin/settlements
src/lib/api.ts— a single Axios instance (NEXT_PUBLIC_API_URL, defaulthttp://localhost:3000/api/v1) with:- a request interceptor that attaches
Authorization: Bearer <token>from the Zustand auth store (single source of truth) - a response interceptor that calls
logout()on the auth store and redirects to/auth/loginon401 - grouped API helpers (
authApi,paymentsApi, …) rather than ad-hoc fetches scattered through components
- a request interceptor that attaches
src/lib/store.ts— Zustand +persistfor auth state (token,merchant), persisted tolocalStorageunder thedupdub-authkey. This is the only global client state; everything else (payment lists, analytics, etc.) is fetched per-page throughapi.ts.
src/lib/api.ts— the Axios instance and grouped API helpers described above.src/lib/store.ts— the Zustand auth store described above.src/lib/errors.ts— the app-wide error-message utility. Pages and components should extract user-facing error text viagetErrorMessagefrom this module.src/lib/utils.ts— shared formatting/className helpers (clsx+tailwind-merge).
Error handling: always import
getErrorMessagefromsrc/lib/errors.ts. It is the single app-wide helper —utils.tsdeliberately does not export a same-named variant, so an import from the wrong module fails the type-check instead of silently changing behavior.
The access token is persisted in localStorage via Zustand. Any XSS vector can read it synchronously. Mitigations in this repo:
- Single storage key — the Axios client reads from
useAuthStore, not a duplicateaccess_tokenkey, so 401 logout and UI auth state stay in sync. - CSP headers —
next.config.jssets a restrictive Content-Security-Policy (plusX-Frame-Options,Referrer-Policy) to reduce XSS blast radius. - Recommended long-term fix — move to an
httpOnly,SameSite=Strictsession cookie issued bydupdap-backend, with the frontend never handling the raw JWT.
Several helpers exist in more than one place in this codebase. To keep new work from adding a third copy, use the canonical source below and extend it in place rather than redefining it per-page:
- Error messages — import
getErrorMessagefromsrc/lib/errors.ts. That is the onlygetErrorMessagein the repo; do not add a same-named helper toutils.tsor any page. If you need to change error-message behavior, change it inerrors.ts. - Status colors/icons — import
STATUS_COLORS,STATUS_ICONS, andDEFAULT_STATUS_COLORfromsrc/lib/utils.ts. Do not redefine per-page status→color or status→icon maps; add new statuses to the shared maps inutils.tsso every page stays consistent. - Destructive confirmations — use the shared
ConfirmDialogcomponent rather thanwindow.confirm. It matches the app's styling, is accessible, and keeps confirmation UX consistent across the dashboard. - Overlays (modal, off-canvas drawer) — wrap the panel with
useFocusTrapfromsrc/lib/useFocusTrap.tsand add an Escape handler. The dashboard's mobile nav drawer does both, so focus moves into the drawer on open, cycles within it on Tab/Shift+Tab, and returns to the hamburger button on close. Panels should carryrole="dialog"andaria-modal="true".
When in doubt, grep for the helper name first — if it already exists in src/lib, reuse it instead of writing a local copy.
- Approve USDC allowance for the escrow contract (
approve(escrow_contract, amount)) - Deposit into escrow (
deposit()), which pulls funds viatransfer_from - Customer confirms/signs in their own wallet — the app never touches a private key
- Frontend polls the backend for payment status (
GET /payments/:id/status) until it's confirmed/settled - Receipt view once settled
There are no <img> tags or image files in the codebase today — icons come from lucide-react and QR codes are rendered inline by qrcode.react. When the first real image is added (merchant logos, avatars, marketing assets), use Next.js's next/image component:
import Image from 'next/image';
// Local asset (placed in /public):
<Image src="/logo.png" alt="Merchant logo" width={120} height={40} />
// Remote asset — hostname must be allow-listed in next.config.js first:
<Image src="https://cdn.example.com/avatar.jpg" alt="Avatar" width={48} height={48} />Why next/image over a plain <img>:
- Automatic format conversion (WebP/AVIF) and responsive
srcsetgeneration - Built-in lazy loading with a low-quality placeholder option
- Prevents Cumulative Layout Shift via required
width/height(orfilllayout)
Adding a remote image domain: edit the remotePatterns array in next.config.js — Next.js throws a hard error at build time for any remote hostname not explicitly listed there. The array is already wired up (currently empty); add an entry for each CDN or image host as you introduce it.
- Framework: Next.js 14 (App Router), React 18, TypeScript
- Styling: Tailwind CSS (custom
brandcolor scale intailwind.config.ts) - State: Zustand (with
persistfor auth) - HTTP: Axios, with interceptors for auth + 401 handling
- Blockchain:
@stellar/stellar-sdk(client-side Stellar operations for the payment flow) - UI utilities:
lucide-react(icons),clsx+tailwind-merge,react-hot-toast(notifications) - Data viz:
recharts(dashboard analytics) - QR codes:
qrcode.react(payment request QR codes) - Dates:
date-fns
Note: this app does not currently use a wallet-connector library (wagmi/viem/RainbowKit) or ship a PWA manifest/service worker — if you've seen references to those in older platform docs, treat them as roadmap items, not what's implemented today.
- Node.js 18+
- A running
dupdap-backendinstance (or a deployed API you can point at)
# Install dependencies
npm install
# Set up environment variables
cp .env.local.example .env.local
# Edit .env.local — see below
# Start development server
npm run dev
# Build for production
npm run build
# Start production server
npm run start
# Lint
npm run lintThe app runs at http://localhost:3001 by default (or whatever port next dev picks if 3000 — the backend's default — is taken).
Full list in .env.local.example:
# Backend API base URL — must include the /api/v1 prefix
NEXT_PUBLIC_API_URL=http://localhost:3000/api/v1That's the only required variable today. next.config.js also falls back to http://localhost:3000/api/v1 if it's unset, so local dev works against a locally-running backend with zero config.
If you're pointing this at a deployed backend, set it to that backend's public URL including the /api/v1 prefix, e.g.:
NEXT_PUBLIC_API_URL=https://api.dupdub.xyz/api/v1This app is a pure client of dupdap-backend's REST API — see that repo's README for the full endpoint list and its Swagger docs (/docs on the backend) for request/response shapes. The helpers in src/lib/api.ts currently cover:
authApi— register/loginpaymentsApi— create/list/get/stats for paymentsadminApi— list/retry/approve settlements (admin views)
Extend api.ts with additional grouped helpers (e.g. settlementsApi, webhooksApi, merchantsApi) as dashboard pages need them, rather than calling api.get(...) directly from components, to keep endpoint paths in one place and so the auth/401 interceptors keep applying everywhere.
npm run type-check # tsc --noEmit
npm run lint # next lint (ESLint, see .eslintrc.json)
npm test # vitest run (single pass)
npm run test:watch # vitest (watch mode)
npm run verify # type-check + lint + testThe suite runs on Vitest with React Testing Library in a jsdom environment — the fastest thing to wire into a Next.js 14 App Router project. Config lives in vitest.config.ts, and vitest.setup.ts registers @testing-library/jest-dom matchers, cleans up between tests, and stubs matchMedia.
Conventions
- Tests are colocated with the code they cover as
*.test.ts/*.test.tsx(e.g.src/lib/utils.ts→src/lib/formatDate.test.ts), plus broader integration suites undersrc/__tests__/. - Mock network boundaries at the module level (
vi.mock('@/lib/api', …)) rather than stubbing fetch, and mock third-party UI libs only for what the component under test actually needs. - Assert on user-visible behavior (roles, labels, text) rather than implementation details, so refactors don't break the suite.
- Any helper added to
src/libshould come with a test next to it.
vercel --prodThe app is a standard Next.js app, so any platform that supports Next.js (Vercel, Railway, etc.) works. The only required runtime config is NEXT_PUBLIC_API_URL pointed at the deployed backend.
- #381: No test coverage exists for the forgot-password or reset-password pages
- #380: No test coverage exists for LandingNav or the landing page
- #293: Login and register forms have no autocomplete attributes for email/password
- #332: StatusPieChart and VolumeBarChart components are fully implemented but never rendered anywhere