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
NEXT_PUBLIC_API_URL is required for production builds and must be an HTTPS URL. Set it in the deployment environment and CI. For example:
NEXT_PUBLIC_API_URL=https://api.example.com/api/v1Local development may omit it and uses http://localhost:3000/api/v1.
For state-changing API calls, the frontend reads the API-provided csrf_token cookie and sends its value in X-CSRF-Token. The API must validate that this header matches the cookie on every non-GET request. Use SameSite=Strict; Secure for the cookie and allow the header through CORS when the app and API use different origins.
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.
The codebase separates code by responsibility, not by feature folder. Knowing which bucket a new file belongs in keeps the app navigable as it grows.
Files under src/app are route entry points for the Next.js App Router. A page.tsx owns the route's data fetching, layout composition, and route-level state; a layout.tsx wraps a subtree (e.g. the dashboard shell). Page components:
- Are the only place that should call
useQueryfor a route's primary data. - Compose shared components and hooks — they should stay thin and delegate real logic downward.
- Are not imported by other components. If something needs to be reused, it belongs in
src/components.
Reusable, presentational-or-lightly-stateful building blocks used across multiple pages (e.g. ConfirmDialog, status badges, tables, form fields). Shared components:
- Receive data via props and avoid reaching into global stores directly where a prop would do.
- Live in
src/componentsand are imported by pages and other shared components. - Should be generic — if a component is only ever used by one page, keep it colocated with that page until a second consumer appears.
Custom React hooks encapsulate reusable stateful behavior (e.g. useFocusTrap). A hook:
- Is named
useSomethingand lives alongside the code it supports (currentlysrc/lib). - Owns side effects, refs, and lifecycle logic so components stay declarative.
- Should not render UI — it returns values/handlers the component wires up.
Pure, framework-agnostic helpers with no React dependency: formatting, className merging (clsx + tailwind-merge), error-message extraction, and shared constant maps (STATUS_COLORS, etc.). Utils:
- Are plain functions/constants — no hooks, no JSX.
- Are the canonical home for logic used in more than one place; extend the shared module rather than copying it per-page.
- Files and directories:
kebab-case— e.g.confirm-dialog.tsx,use-focus-trap.ts,payment-status-badge.tsx. Route segments follow the same rule (forgot-password,reset-password). - React components:
PascalCase— the exported component isConfirmDialog,PaymentStatusBadge, etc., even though the file isconfirm-dialog.tsx. - Hooks:
camelCasewith auseprefix —useFocusTrap, exported from a kebab-case file. - Utils/constants:
camelCasefor functions (getErrorMessage) andSCREAMING_SNAKE_CASEfor constants (STATUS_COLORS).
- Global client state → Zustand (
src/lib/store.ts). Auth (token,merchant) is the only global state, persisted tolocalStorageunderdupdub-auth. Reach for Zustand only for state that genuinely spans routes (auth, theme, etc.) — not for per-page data. - Server state →
useQuery(TanStack Query). Payment lists, analytics, settlements, and any other backend data are fetched and cached per-page viauseQuery, calling the grouped helpers insrc/lib/api.ts. This gives caching, background refetch, and loading/error states without hand-rolleduseEffectfetching. - Local UI state →
useState/useReducerinside the component that owns it (modals open/closed, form inputs).
Rule of thumb: if the data comes from the API, it's useQuery; if it must survive navigation and isn't server-owned, it's Zustand; otherwise keep it local.
/ 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 (merchant), persisted tolocalStorageunder thedupdub-authkey. Sensitive tokens are not stored in localStorage; session tokens reside exclusively in httpOnly cookies. 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.
To protect against XSS token theft (where scripts running in the browser could steal tokens stored in localStorage), session credentials are managed via secure httpOnly cookies:
- HttpOnly Cookies — The backend issues the JWT access token via
Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Strict. JavaScript cannot read or writeHttpOnlycookies, preventing XSS-based session hijacking. - Automatic Credentials — Axios is configured with
withCredentials: true, so the browser automatically attaches the cookie to API requests. ManualAuthorizationheaders are omitted. - Session Termination — Logging out triggers
POST /auth/logout, instructing the backend to clear the cookie. - State Management — Zustand stores only non-sensitive merchant profile metadata (ID, business name, status) for UI rendering.
- CSP Headers —
next.config.jssets a restrictive Content-Security-Policy (plusX-Frame-Options,Referrer-Policy) to further reduce XSS blast radius.