Frontend Development
The bopen frontend is a statically-exported Next.js 16 application at apps/web. It serves two distinct UIs: the Checkout Widget (consumer-facing) and the Acquirer Portal (B2B admin).
Tech stack
- Next.js 16 with App Router,
output: "export"(static HTML) - TypeScript strict mode
- Tailwind CSS v4 (CSS-first configuration)
qrcode— client-side QR code generation for desktop bank handoff
Project structure
apps/web/src/
├── app/
│ ├── layout.tsx # Root layout (fonts, globals.css)
│ ├── page.tsx # Home page
│ ├── globals.css # Tailwind v4 entry + CSS custom properties
│ │
│ ├── checkout/ # Consumer checkout widget
│ │ ├── page.tsx
│ │ └── CheckoutClient.tsx # Multi-step: summary → picker → QR/handoff
│ │
│ ├── payments/return/ # SCA callback landing page
│ │ ├── page.tsx
│ │ └── PaymentReturnClient.tsx # Fee breakdown, end_to_end_id, auto-redirect
│ │
│ ├── portal/ # Acquirer Portal (auth-gated)
│ │ ├── layout.tsx # Auth guard + 15-min session management
│ │ ├── page.tsx # Redirect → /portal/transactions
│ │ ├── login/
│ │ │ ├── page.tsx
│ │ │ └── LoginClient.tsx # Email + password + 6-digit TOTP
│ │ ├── transactions/
│ │ │ ├── page.tsx
│ │ │ └── TransactionsClient.tsx # Ledger table + slide-out audit panel
│ │ ├── settlements/
│ │ │ ├── page.tsx
│ │ │ └── SettlementsClient.tsx # Balance cards + payout history + CSV export
│ │ └── developers/
│ │ ├── page.tsx
│ │ └── DevelopersClient.tsx # API keys + webhook registration + test
│ │
│ ├── redirect-stub/ # Mock bank redirect (dev only)
│ └── dev/ # Development harnesses
│
├── components/
│ ├── checkout/
│ │ └── CheckoutShell.tsx # Shared layout wrapper for checkout screens
│ └── portal/
│ ├── Sidebar.tsx # Portal navigation sidebar
│ └── TransactionSlideover.tsx # Slide-out detail panel with audit trail
│
└── lib/
├── banks.ts # UK bank list (16 banks) with brand colours
├── payments.ts # Payment API client (createPayment, getPayment, etc.)
└── portal-mock.ts # Mock data for portal (replace with real API calls)
Checkout widget
The checkout flow is managed as a client-side state machine within CheckoutClient.tsx. Session state is persisted to sessionStorage before the external bank redirect so it survives the round-trip.
Screens
| Phase | Description |
|---|---|
summary | Amount, merchant name, SCA legal disclosure (PSR 2017) |
picker | Popular banks grid + searchable list of all 16 UK banks |
creating | Spinner while POST /payments completes |
authorise | Mobile: deep-link "Open app" button; Desktop: QR code via qrcode package |
error | User-facing error with "Try again" |
The callback page (/payments/return) handles the bank redirect, calls sca-complete, polls for terminal status, then shows the receipt with:
- Gross / fee / net amount breakdown
end_to_end_idISO 20022 reference- Append-only audit trail with actor labels (
MERCHANT_API,BANK_CALLBACK) - 5-second countdown auto-redirect to
merchant_return_url
Mobile vs desktop
The widget detects mobile via navigator.userAgent at runtime (the app is statically exported). On mobile, the authorise screen renders an <a href={bankRedirectUrl}> styled as a button. On desktop, it uses qrcode.toDataURL() via dynamic import to render a <img> QR code, keeping the bank redirect URL out of the initial JS bundle.
sessionStorage keys
| Key | Value |
|---|---|
bopen_payment_id | UUID of the created payment intent |
bopen_state | OAuth state token for SCA completion |
These allow /payments/return to recover the correct payment even if the user's browser loses the URL parameters during the bank redirect.
Acquirer Portal
All portal routes are wrapped by portal/layout.tsx, which enforces:
- Auth guard: checks
bopen_portal_tokeninlocalStorage; redirects to/portal/loginif absent or expired - 15-minute inactivity logout: refreshes expiry on
click,keydown,mousemove,touchstart; polls every 60 seconds to catch idle expiry
Views
| Route | Description |
|---|---|
/portal/login | Email + password + 6-digit TOTP form. Any valid email + password ≥ 8 chars + 6-digit code succeeds in dev mode. |
/portal/transactions | Searchable, filterable transactions ledger. Click any row to open the TransactionSlideover showing the full payment_events audit trail. |
/portal/settlements | Balance cards (available, pending, account masked as ****NNNN). Daily payout history table. Client-side CSV export. |
/portal/developers | API key management (cycle / revoke), webhook endpoint registration, event subscription checklist, test webhook button. |
Commands
| Command | Description |
|---|---|
npm run dev | Start development server on port 3000 |
npm run build | Static export to out/ |
npm run lint | ESLint |
Environment variables
Create apps/web/.env.local for local configuration:
NEXT_PUBLIC_PAYMENT_API_URL=http://localhost:8081
The checkout widget and portal both resolve the payment service URL from NEXT_PUBLIC_PAYMENT_API_URL. If unset, it defaults to http://localhost:8081.