Skip to content

Fix/356 back navigation#446

Closed
Emmanex01 wants to merge 53 commits into
Pi-Defi-world:devfrom
Emmanex01:fix/356-back-navigation
Closed

Fix/356 back navigation#446
Emmanex01 wants to merge 53 commits into
Pi-Defi-world:devfrom
Emmanex01:fix/356-back-navigation

Conversation

@Emmanex01
Copy link
Copy Markdown
Contributor

@Emmanex01 Emmanex01 commented Jun 2, 2026

Summary

Fixes #356.

After completing the Send or Mint 3-step flow (Form → Confirm → Result), pressing the browser Back button previously returned users to the Confirm step.

This change cleans up navigation history after successful completion so users return to the originating page instead of re-entering completed confirmation screens.

Changes

  • Updated navigation handling in Send flow.
  • Updated navigation handling in Mint flow.
  • Replaced history entries for terminal flow navigation where appropriate.
  • Preserved normal navigation during in-progress steps.

Testing

Send Flow

  1. Open Send flow.
  2. Complete Form → Confirm → Result.
  3. Press browser Back.
  4. Verify Confirm page is skipped.

Mint Flow

  1. Open Mint flow.
  2. Complete Form → Confirm → Result.
  3. Press browser Back.
  4. Verify Confirm page is skipped.

Fixes #356.

Summary by CodeRabbit

  • New Features

    • Added multi-language support (English with region variants)
    • Enhanced wallet encryption for improved security
    • Improved error messaging and accessibility across key features
    • Refined send, mint, and burn transaction flows with better UX
  • Bug Fixes

    • Resolved error handling across various pages
    • Fixed auth context and wallet initialization
    • Improved form validation and error display
  • Accessibility

    • Added comprehensive accessibility testing
    • Enhanced screen reader support throughout app
    • Improved keyboard navigation and ARIA attributes
  • Refactoring

    • Streamlined error handling system
    • Simplified deposit/withdrawal flows
    • Optimized authentication context

dubemoyibe-star and others added 30 commits April 23, 2026 08:50
- Add proper htmlFor/id pairing to all form inputs across mint, burn, send,
  and savings withdraw pages
- Implement ARIA attributes (aria-describedby, aria-label, aria-invalid)
  for screen reader support
- Fix viewport zoom issue by removing maximum-scale=1 to allow text scaling
- Improve color contrast for loading states and muted text to meet WCAG 2.1 AA
  (contrast ratio now 7.2:1 vs required 4.5:1)
- Add role=alert and role=status for dynamic content announcements
- Add focus management and keyboard navigation improvements
- Implement axe-core Playwright tests to prevent future regressions

Changes include:
- app/mint/page.tsx: Label associations for select and amount inputs
- app/burn/page.tsx: Complete form labeling with hint text
- app/send/page.tsx: Dialog and form accessibility enhancements
- app/savings/withdraw/page.tsx: Proper label-id pairing
- app/layout.tsx: Updated viewport config to allow zooming
- app/globals.css: Adjusted muted-foreground colors for contrast compliance
- tests/accessibility.spec.ts: New axe-core test suite

All money forms now have zero axe-core critical violations.

Closes #217
implemeted  i18n / localization "F-045 — No i18n / localization
fix(a11y): resolve accessibility violations on money forms (F-046)
fix: replace unstable list keys and fix burn page build errors
fix(theme): improve send status badge contrast in dark mode
fix(frontend): prevent PII leaks in production console logs
…ings-deposit-handler

Revert "fix: wire savings deposit dialog to API and show pending/completed st…"
Junman140 and others added 23 commits April 28, 2026 03:13
Enhance Multi-Currency Bank Validation and Burn Page Form Logic
…ryption

Fix savings URI resolution, reserves unit labels, and wallet secret encryption
#231 F-060 — Performance: send page callbacks not memoized
- mintSource dropdown: only show Stellar USDC (backend-supported)
- i18n hydration: add suppressHydrationWarning to <html>
- Amount overflow: add break-words to formatted amount previews
- Keystroke validation: add useDebounce hook for 300ms debounce

Closes #322, #329, #332, #334
#172 — remove ignoreBuildErrors: true from next.config.mjs and fix all
surfaced TypeScript errors across 20+ files:
- Add token field back to RequestOptions; export setToken no-op
- Re-export useApiError from hooks/use-api-error (correct hook)
- Fix useApiError destructuring (error/handleError → uiError/setApiError)
  in burn, fiat, reserves, wallet, send/[id], savings/deposit,
  savings/withdraw, rates, send, transactions/[id]
- Resolve merge conflicts in lib/wallet-storage.ts (keep security branch)
- Remove stray git command from contexts/auth-context.tsx
- Add clearPasscode import to auth-context
- Fix duplicate declarations in mint/page.tsx and burn/page.tsx
- Fix missing imports (lucide icons, ApiErrorDisplay, Tooltip, etc.)
- Fix lib/stellar/burning.ts: errorResultXdr → errorResult
- Fix auth/2fa: wrong login() arity
- Install @tanstack/react-virtual (missing dep used by send/page.tsx)
- Update test files to match current hook/type APIs

#373 — add segment-level layout.tsx with unique title + description for
all 38 pages (activity, auth, bills, burn, business, currency, fiat,
help, lending, me, me/settings, mint, rates, recovery, reserves,
savings, send, test-locale, transactions, wallet)
fix: resolve mintSource dropdown, i18n hydration, amount overflow, and keystroke validation
…ge-metadata

fix: enable TS build errors and add per-page metadata (#172, #373)
…oduction

fix: suppress console logs in production to prevent user data exposure
…history

fix: prevent double-tap nav pushing duplicate history entries on mobile
Fix mobile toast overlap with bottom navigation
 Toaster position overlaps mobile navigation – M
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR modernizes the ACBU frontend by adding internationalization via next-intl, refactoring authentication to manage API keys in sessionStorage, converting multi-step flows (mint/send) to use URL query parameters for proper history handling, simplifying error boundaries and error pages, implementing comprehensive accessibility improvements, and updating form validation with debounced inputs and currency-specific rules.

Changes

i18n Infrastructure, Auth Refactor, Dialog Flow Modernization, and Accessibility Enhancements

Layer / File(s) Summary
i18n Core Setup
i18n/messages/en.json, i18n/messages/en-NG.json, i18n/messages/en-KE.json, i18n/request.ts, next.config.mjs, middleware.ts
Configure locale routing, add next-intl plugin, export locale definitions, and provide English plus region-specific translation message files.
Root Layout with i18n
app/[locale]/layout.tsx
Add locale param handling, load translation messages, wrap app with NextIntlClientProvider, ErrorBoundary, AuthProvider, and AuthGuard, set HTML lang attribute.
Route-Level Layout Metadata
app/[locale]/auth/layout.tsx, app/[locale]/activity/layout.tsx, app/bills/layout.tsx, app/burn/layout.tsx, app/fiat/layout.tsx, app/help/layout.tsx, app/lending/layout.tsx, app/me/layout.tsx, app/me/settings/layout.tsx, app/mint/layout.tsx, app/rates/layout.tsx, app/recovery/layout.tsx, app/reserves/layout.tsx, app/savings/layout.tsx, app/send/layout.tsx, app/test-locale/layout.tsx, app/transactions/layout.tsx, app/wallet/layout.tsx, app/business/layout.tsx, app/currency/layout.tsx
Add metadata exports and default layout components that render children for all route pages to support App Router metadata hierarchy.
Home Page Localization
app/[locale]/page.tsx
Use next-intl hooks for translated feature tiles, localized balance card labels, useFormatter for number formatting, and replace skeleton loading with placeholder text.
Auth Context with API Key Storage
contexts/auth-context.tsx
Add apiKey to auth state, persist/hydrate from sessionStorage, compute isAuthenticated from both userId and apiKey, call setToken during hydration, remove API call validation on mount.
2FA and Login Flow
app/[locale]/auth/2fa/page.tsx
Update successful 2FA verification to call login with user_id and stellar_address instead of api_key.
API Client Token Handling
lib/api/client.ts
Add optional token field to RequestOptions, use only opts.token for authorization header, and deprecate client-side setToken as no-op.
Wallet Storage Encryption
lib/wallet-storage.ts
Implement PBKDF2/AES-256-GCM encryption/decryption helpers, update storeWalletSecret/getWalletSecret to persist encrypted JSON payloads, remove encrypted-slot decryption path.
Mint Page URL-Driven Flow
app/mint/page.tsx, app/mint/layout.tsx, app/mint/mint.test.tsx
Wire mint/burn dialogs to flowStep URL param, add debounced fiat amount for validation, use router.replace on success, switch to local mintError/burnError state, format rates with String conversion.
Send Page URL-Driven Flow and Accessibility
app/send/page.tsx, app/send/layout.tsx, app/send/send.test.tsx
Wire send dialog to flowStep URL param, add debounced amount validation, implement Stellar payment submission via submitAcbuPaymentClient (local wallet or connected wallet), refresh data on success, enhance history tab with ARIA semantics, refactor status badge logic, add form control accessibility attributes.
Debounce Hook
hooks/use-debounce.ts
Add useDebounce hook to delay state updates by configurable interval for debounced validation and API calls.
Burn Form Validation
app/burn/page.tsx, app/burn/layout.tsx
Use superRefine with currency-specific NGN/KES patterns, add defensive formatCurrency locale handling, currency-conditional account number/bank code descriptions, detailed 400 error-field mapping via form.setError.
Currency Page Debounced Inputs
app/currency/page.tsx, app/currency/layout.tsx
Add debounced mint/burn/international amounts, wire balance loading and toast, reduce mint source to USDC only, update button disable logic to depend on debounced amounts.
Error Boundary Simplification
components/error-boundary.tsx
Reduce Props to children and optional fallback, simplify State to hasError and error, remove errorInfo, eliminate level-based rendering, remove development error details and PageErrorBoundary/ComponentErrorBoundary wrappers.
Error Page UI
app/error.tsx
Replace previous layout with compact presentation: red icon, "Something went wrong", conditional error.digest, "Try again" button that calls reset.
useApiError Hook Extraction
hooks/use-api.ts, hooks/__tests__/use-api-error.test.ts
Extract useApiError to separate hooks/use-api-error.ts module, re-export from use-api.ts, update test imports and assertions for new API surface.
Pages with Local Error State
app/fiat/page.tsx, app/rates/page.tsx, app/wallet/page.tsx, app/transactions/[id]/page.tsx
Replace useApiError with local error state, update error display to read error.message, simplify error handling logic.
Savings and Transfer Pages
app/savings/deposit/page.tsx, app/savings/withdraw/page.tsx, app/send/[id]/page.tsx
Introduce local error/success state, add local resolveUserUri helper for recipient resolution, simplify error/success rendering, remove useApiError dependency.
Accessibility Test Suite
tests/accessibility.spec.ts
Add Playwright + Axe test suite with page-readiness helpers, auth mocking, and violation checks for mint/burn/send/savings-withdraw pages using WCAG rules, plus interaction tests.
Mobile Navigation
components/mobile-nav.tsx
Replace Next Link with router.push, add useTransition and useRef-based guard to prevent duplicate navigation, update nav items to buttons with aria-current and disabled state.
Status Color Theming
app/globals.css
Add --status-success/warning/neutral and foreground/border variants to :root and .dark, expose --color-status-* tokens in @theme inline, update muted-foreground baseline.
Dependencies and Configuration
package.json, playwright.config.ts, .gitignore, next.config.mjs
Add @tanstack/react-virtual, next-intl, stellar-sdk; remove Vitest packages; update Playwright to ./tests directory and Chromium-only; change webServer command to npm run dev; update .gitignore patterns.
Build and Type Configuration
app/layout.tsx, global.d.ts
Simplify viewport (remove maximumScale, add userScalable), simplify RootLayout to return children directly, add CSS module declaration.
Supporting Updates
app/business/page.tsx, app/help/page.tsx, app/test-locale/page.tsx, app/savings/page.tsx, app/reserves/page.tsx, components/ui/toast.tsx, app/me/page.tsx, lib/stellar/burning.ts
Update business/sme nav link to /sme, change FAQ accordion keying, remove unused formatCurrency import, add SAVINGS_ACCOUNT_TYPES constant, update reserves copy/tooltips, adjust toast positioning, remove local KycBadge, update Stellar XDR error handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #323: Modifications to app/currency/page.tsx with debounced input handling and form enable/disable logic directly affect form state during tab switches that may relate to currency-selection issues.

Possibly related PRs

Poem

🐰 A rabbit hops through locales with glee,
Auth keys now stored, where they should be!
URLs guide dialogs through their dance,
Forms debounce inputs at first glance,
Accessibility shines bright and clear—
The frontend's evolved, a new frontier! ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/356-back-navigation

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
app/send/page.tsx (2)

523-531: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

contactsParentRef is undefined.

The code references contactsParentRef but there's no useRef declaration visible for it. This was likely part of the removed virtualizer setup.

-                      <div
-                        ref={contactsParentRef}
-                        style={{
-                          height: `${virtualizer.getTotalSize()}px`,
-                          width: '100%',
-                          position: 'relative',
-                        }}
-                      >
-                        {virtualizedContacts}
-                      </div>
+                      {contactOptions}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/send/page.tsx` around lines 523 - 531, contactsParentRef is referenced
but not declared; add a React ref declaration (e.g., const contactsParentRef =
useRef<HTMLDivElement | null>(null)) in the same component where virtualizer and
virtualizedContacts are used, attach it to the container div (the element that
relies on virtualizer.getTotalSize()), and ensure any virtualizer setup that
expects an element reference reads contactsParentRef.current; update imports to
include useRef if missing.

336-336: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix missing/undefined status helper(s) in app/send/page.tsx

getStatusColor is used at line 335, and getStatusBadgeClassName is used at lines 468 and 709—but neither getStatusColor nor getStatusBadgeClassName is defined or imported anywhere in the repo. Update the references to the correct existing helper or add the missing helper/import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/send/page.tsx` at line 336, The file app/send/page.tsx references
undefined helpers getStatusColor and getStatusBadgeClassName; locate the
existing status helper(s) in the codebase (for example a utils/status,
lib/statusHelpers, or components/status file) and either import the correct
functions into page.tsx or implement the missing helpers with the expected
signatures; update references to use the actual exported names (or export
getStatusColor/getStatusBadgeClassName from the helper module) so the calls in
page.tsx (getStatusColor at the usage around line 335 and
getStatusBadgeClassName at the usages around lines 468 and 709) resolve to
defined functions.
app/me/page.tsx (2)

189-196: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate KycBadge render in the header.

The KYC badge is rendered twice for the same user?.kyc_status — once inline beside the display name (Line 191) and again below the email (Line 195). Unless two badges are intended, drop one.

🐛 Proposed fix (remove the redundant render)
               <div className="flex items-center gap-2 mb-0.5">
                 <h1 className="text-lg font-bold text-foreground truncate">{displayName}</h1>
                 <KycBadge status={user?.kyc_status} />
               </div>
               <p className="text-xs text-muted-foreground truncate">{user?.email || user?.phone_e164 || '—'}</p>
-              <div className="mt-1.5">
-                <KycBadge status={user?.kyc_status} />
-              </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/me/page.tsx` around lines 189 - 196, The KYC badge is being rendered
twice for the same user status; remove the redundant KycBadge usage so it only
appears once next to the displayName. Locate the two KycBadge renders in the
component (the inline one alongside displayName and the one inside the div below
the email) and delete the lower/duplicate KycBadge (the second instance
rendering user?.kyc_status) so only the intended inline KycBadge remains.

24-68: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused KYC helper/types in app/me/page.tsx

getKycBadgeConfig, KycBadgeConfig, and KycStatus are defined but never used by the page; the JSX directly renders <KycBadge status={user?.kyc_status} />. Deleting the unused helper/types will also make the Lucide icon imports (CheckCircle2, Clock3, XCircle, AlertCircle) removable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/me/page.tsx` around lines 24 - 68, Remove the unused KYC helper types and
function: delete KycStatus, KycBadgeConfig, and getKycBadgeConfig from
app/me/page.tsx and also remove the unused Lucide icon imports CheckCircle2,
Clock3, XCircle, and AlertCircle; verify there are no remaining references to
these symbols (the JSX uses <KycBadge status={user?.kyc_status} /> so leave
KycBadge and its import intact) and run typecheck to ensure no other code relies
on those types or the helper.
app/wallet/page.tsx (1)

283-286: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use clearError() to clear, not setError("").

setError("") resolves to handleError(new Error("")), which sets an empty error object on uiError rather than clearing it. It only renders blank because uiError?.message happens to be falsy. Call clearError() to actually reset the error state.

🧹 Proposed fix
                 onClick={() => {
                   setOption(null);
-                  setError("");
+                  clearError();
                 }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/wallet/page.tsx` around lines 283 - 286, The onClick handler currently
calls setOption(null) and setError(""), but setError("") invokes handleError(new
Error("")) and leaves an empty Error object in uiError; replace the setError("")
call with clearError() so the error state is actually reset; update the handler
that references setOption and setError in the component (page.tsx) to call
clearError() instead of setError("") and ensure clearError is imported/available
where used.
.github/workflows/frontend-qa.yml (2)

4-7: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Workflow only triggers on main; this PR targets dev.

With push/pull_request scoped to main, the QA job will not run for PRs into dev. If dev is the integration branch, add it to the trigger branches so checks actually execute for these PRs.

♻️ Suggested trigger update
 on:
   push:
-    branches: [ main ]
+    branches: [ main, dev ]
   pull_request:
-    branches: [ main ]
+    branches: [ main, dev ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/frontend-qa.yml around lines 4 - 7, The workflow's
triggers only include the main branch so QA won't run for PRs targeting dev;
update the GitHub Actions trigger blocks by adding "dev" to the branches arrays
under the "push" and "pull_request" keys (i.e., modify the branches: [ main ]
entries in the push and pull_request sections to include dev) so the workflow
runs for dev-targeted pushes and PRs.

36-81: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Workflow is invalid YAML and will fail to run.

Three problems were introduced into the single quality-gate job:

  • Line 36-37: a Run component tests step with no run/uses, immediately followed by a second steps: key (a job can only have one steps).
  • Lines 38-59: a duplicated checkout/pnpm/node/install/typecheck/lint block.
  • Lines 69-81: a package.json scripts object pasted into the workflow.

The real Run component tests step already exists at lines 60-61, so the duplicated block and the trailing scripts object should be removed entirely. Note the original Set up Node.js at line 22 also uses the non-existent actions/node-js@v4; the correct action is actions/setup-node@v4.

🛠️ Proposed fix to restore a single valid steps list
       - name: Run lint
         run: pnpm lint
 
-      - name: Run component tests
-    steps:
-      - uses: actions/checkout@v4
-
-      - name: Install pnpm
-        uses: pnpm/action-setup@v4
-        with:
-          version: 10
-
-      - name: Set up Node.js
-        uses: actions/setup-node@v4
-        with:
-          node-version: 20
-          cache: pnpm
-
-      - name: Install dependencies
-        run: pnpm install
-
-      - name: Run typecheck
-        run: pnpm typecheck
-
-      - name: Run lint
-        run: pnpm lint
-
       - name: Run component tests
         run: pnpm test
 
       - name: Install Playwright Browsers
         run: pnpm exec playwright install --with-deps
 
       - name: Run E2E smoke tests
         run: pnpm test:e2e
-
-"scripts": {
-  "build": "next build",
-  "dev": "next dev",
-  "lint": "eslint .",
-  "start": "next start",
-  "test:a11y": "playwright test accessibility.spec.ts",
-  "test:a11y:ci": "playwright test accessibility.spec.ts --reporter=github",
-  "test:a11y:report": "playwright show-report",
-  "a11y": "playwright test",
-  "typecheck": "tsc --noEmit",
-  "test": "echo \"Test passed\"",
-  "test:e2e": "echo \"E2E tests passed\""
-}

Also fix the original line 22:

       - name: Set up Node.js
-        uses: actions/node-js@v4
+        uses: actions/setup-node@v4
         with:
           node-version: 20
           cache: 'pnpm'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/frontend-qa.yml around lines 36 - 81, The workflow's job
"quality-gate" contains a malformed/duplicated steps list and a stray
package.json scripts block; remove the extra incomplete step named "Run
component tests" (the first occurrence without run/uses), delete the duplicated
checkout/pnpm/setup-node/install/typecheck/lint/test/Playwright block and the
pasted "scripts" object, and ensure the real "Run component tests" step remains
(the one that runs pnpm test). Also replace any usage of actions/node-js@v4 with
the correct actions/setup-node@v4 in the Node.js setup step so the job has a
single valid steps array.
lib/__tests__/api-error.test.ts (1)

90-94: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

500 fallback assertion not migrated to .message.

The 400/401/404 cases were updated to read ?.message, but the 500 case still compares the returned UIError object directly to a string, so it fails against the current mapApiError contract.

🐛 Use the mapped message
   it('passes through the error message for 500', () => {
-    expect(mapApiError(makeApiError(500, 'internal server error'))).toBe(
-      'internal server error',
-    );
+    expect(mapApiError(makeApiError(500, 'internal server error'))?.message).toBe(
+      'internal server error',
+    );
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/__tests__/api-error.test.ts` around lines 90 - 94, Update the 500-case
assertion in the test to assert against the mapped error message like the other
cases: call mapApiError(makeApiError(500, 'internal server error')) and compare
its .message (or the returned UIError.message) to 'internal server error' so the
test matches the current mapApiError contract; locate the assertion in the it
block referencing mapApiError and makeApiError and change the expectation to use
the returned object's message property.
.gitignore (1)

20-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Redundant .env entry; also diverges from the summary.

.env on Line 31 is already covered by .env* on Line 20, so it has no effect. The AI summary states .env* was narrowed to .env, but the code keeps .env*, so all env variants remain ignored (good — no secret-leak risk). Consider dropping the duplicate.

🧹 Proposed cleanup
 .pnpm-store/
 .history/*
-.env
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 20 - 31, Remove the redundant `.env` entry since
`.env*` already covers it; edit the .gitignore to keep the `.env*` pattern and
delete the duplicate `.env` line to avoid clutter and inconsistency between the
file and the summary (references: `.env*` and `.env` entries shown in the diff).
package.json (1)

49-71: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove unused legacy stellar-sdk and ensure CI runs real tests

  • package.json declares both @stellar/stellar-sdk and legacy stellar-sdk, but legacy stellar-sdk is not imported anywhere in the codebase—remove it to avoid unnecessary dependency/install bloat and version ambiguity.
  • .github/workflows/frontend-qa.yml runs pnpm test and pnpm test:e2e, but those scripts only echo success—CI gates currently don’t execute the Playwright suites (e.g., test:a11y/a11y). Update scripts/workflow to run real tests.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 49 - 71, package.json currently lists both
"`@stellar/stellar-sdk`" and legacy "stellar-sdk" (the latter unused) and CI
invokes placeholder scripts; remove the unused "stellar-sdk" dependency from
package.json to eliminate duplication and ambiguity, then update the npm scripts
(the ones run by "pnpm test" and "pnpm test:e2e") to invoke the real
Playwright/Playwright-a11y test targets (e.g., "test:a11y" or "a11y" and the
actual e2e Playwright suite) and modify .github/workflows/frontend-qa.yml to run
those real scripts instead of the echo stubs so CI executes the Playwright
suites. Ensure you reference and update the exact script names in package.json
and the workflow job steps that call them.
🟡 Minor comments (6)
app/savings/page.tsx-99-102 (1)

99-102: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unused state variables: showDialog and showDepositDialog are set but never read.

These state variables are updated in handleSelectAccount and handleDeposit but no corresponding Dialog components are conditionally rendered based on them. Either remove the unused state and handlers, or add the missing dialog rendering logic.

🔧 If these are truly unused, remove them
-  const [selectedAccount, setSelectedAccount] = useState<(typeof SAVINGS_ACCOUNT_TYPES)[0] | null>(null);
-  const [showDialog, setShowDialog] = useState(false);
-  const [showDepositDialog, setShowDepositDialog] = useState(false);
-  const [depositAmount, setDepositAmount] = useState('');

And remove the unused handlers handleSelectAccount, handleDeposit, and handleConfirmDeposit (lines 142-159).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/savings/page.tsx` around lines 99 - 102, The state variables showDialog
and showDepositDialog are never read; remove their declarations and all related
state setters, and also delete the unused handlers handleSelectAccount,
handleDeposit, and handleConfirmDeposit to eliminate dead code; alternatively,
if dialogs are intended, add conditional rendering of the appropriate Dialog
components tied to showDialog/showDepositDialog and wire their onClose/confirm
actions to setShowDialog/setShowDepositDialog and use depositAmount/state
updates in the confirm handler (referenced symbols: showDialog,
showDepositDialog, handleSelectAccount, handleDeposit, handleConfirmDeposit,
depositAmount).
i18n/messages/en-KE.json-2-14 (1)

2-14: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing home.approx_usd translation key to en-KE (also missing in en-NG)

app/[locale]/page.tsx uses useTranslations('home') and renders t('approx_usd') (i.e., home.approx_usd). en-KE.json is missing home.approx_usd (and so is en-NG.json) even though base en.json defines it as "≈ USD", leading to next-intl missing-message fallback behavior.

🌐 Proposed fix to align keys with base locale
     "some_currencies_missing_rate": "Some currencies missing a rate",
-    "usd": "USD"
+    "usd": "USD",
+    "approx_usd": "≈ USD"
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@i18n/messages/en-KE.json` around lines 2 - 14, The locale file en-KE.json is
missing the home.approx_usd key used by app/[locale]/page.tsx
(useTranslations('home') → t('approx_usd')), causing fallback warnings; add the
"approx_usd" key under the "home" object in en-KE.json (and the same missing key
in en-NG.json) with the same or appropriate localized value (e.g., the base
en.json value "≈ USD") so the translations align with the base locale and
t('approx_usd') resolves.
i18n/request.ts-8-11 (1)

8-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace @ts-ignore with @ts-expect-error and drop as any.

Per the ESLint hints, @ts-ignore silently no-ops if the next line has no error; @ts-expect-error is preferred. The as any on Line 11 can be removed by narrowing against the locales tuple type. Both are resolved by the fix above (which removes the need for the suppression comment entirely once the v4 signature is used).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@i18n/request.ts` around lines 8 - 11, Replace the silent suppression and
unsafe cast: change the "// `@ts-ignore`" above the getRequestConfig call to "//
`@ts-expect-error`" and remove the "as any" cast on the locales.includes check;
instead narrow the checked value to the locales tuple type (e.g. ensure the
incoming locale is typed/checked as typeof locales[number] or adjust the
getRequestConfig signature so locale is that union) and keep the runtime guard
if (!locales.includes(locale)) notFound(); reference getRequestConfig, locales,
and notFound when making the change.
test-results/.last-run.json-1-4 (1)

1-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don’t commit Playwright-generated test-results artifacts.
test-results/.last-run.json is Playwright run output and shouldn’t be tracked; add test-results/ to .gitignore and remove this file from version control.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-results/.last-run.json` around lines 1 - 4, Remove the
Playwright-generated artifact from version control and ignore future runs:
delete test-results/.last-run.json from the repo (git rm --cached if needed) and
add test-results/ to .gitignore so Playwright output is not tracked going
forward; commit the .gitignore change and the removal together.
tests/accessibility.spec.ts-30-37 (1)

30-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Set a JSON content type on the mocked auth response.

route.fulfill sends a JSON string body without contentType, so it defaults to text/plain. If app code reads response.json() based on the header, the mock may not behave as intended.

🔧 Proposed fix
       await route.fulfill({
         status: 200,
+        contentType: 'application/json',
         body: JSON.stringify({ 
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/accessibility.spec.ts` around lines 30 - 37, The mocked auth response
in tests uses route.fulfill with a JSON-stringified body but omits the
contentType, causing the response to default to text/plain; update the
route.fulfill call in tests/accessibility.spec.ts (the route.fulfill invocation)
to include contentType: 'application/json; charset=utf-8' (or set appropriate
headers) so that response.json() in app code behaves correctly.
tests/accessibility.spec.ts-8-8 (1)

8-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reduce flakiness: remove networkidle + fixed waitForTimeout in accessibility tests

  • tests/accessibility.spec.ts uses page.waitForLoadState('networkidle') (line 8) and multiple fixed sleeps (waitForTimeout(1000/2000/3000/100) in the helper and interaction tests). Playwright guidance discourages networkidle and arbitrary timeouts for synchronization.
  • Replace the sleeps with web-first assertions / locator.waitFor() on concrete UI states (e.g., wait for the loading indicator to be hidden, dialogs/buttons to be visible, etc.). If a specific API call gates readiness, use page.waitForResponse() instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/accessibility.spec.ts` at line 8, Remove the brittle
page.waitForLoadState('networkidle') and all fixed page.waitForTimeout(...)
usages in the accessibility tests; instead synchronize on concrete UI or network
conditions by replacing page.waitForLoadState('networkidle') with explicit waits
such as locator.waitFor({ state: 'visible'|'hidden' }) for loading indicators,
buttons or dialogs referenced in the test, or use page.waitForResponse() for a
specific API call that gates readiness; update any helper functions (the shared
helper referenced in the tests) and interaction tests to expose or use these
concrete locators or response matchers so tests wait deterministically on UI
state rather than arbitrary timeouts.
🧹 Nitpick comments (12)
lib/wallet-storage.ts (1)

2-2: 💤 Low value

Unused import: getPasscode is no longer used.

The getPasscode import is retained but never called in the updated implementation. The comment at lines 12-16 explains passcodes must be passed explicitly, so this import should be removed.

♻️ Remove unused import
 import localforage from 'localforage';
-import { getPasscode } from './passcode-manager';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/wallet-storage.ts` at line 2, The import getPasscode in
lib/wallet-storage.ts is unused after the passcode handling was changed; remove
the unused import line "import { getPasscode } from './passcode-manager';" so
the file no longer references getPasscode and lints cleanly, leaving existing
passcode comments and explicit passcode parameters intact.
app/savings/deposit/page.tsx (1)

16-26: ⚡ Quick win

Duplicated resolveUserUri helper across savings pages.

This helper is copy-pasted in app/savings/deposit/page.tsx, app/savings/withdraw/page.tsx, and app/savings/page.tsx. Consider extracting to a shared utility (e.g., lib/resolve-user.ts) to avoid divergence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/savings/deposit/page.tsx` around lines 16 - 26, The resolveUserUri helper
is duplicated; extract it into a shared module (e.g., create
lib/resolve-user.ts) that exports the async function resolveUserUri(raw: string,
opts: Parameters<typeof resolveRecipient>[1]): Promise<string> with the same
try/catch behavior (call resolveRecipient, return resolved.pay_uri ??
resolved.alias ?? raw, catch => raw). Replace the inline resolveUserUri
implementations in app/savings/deposit/page.tsx, app/savings/withdraw/page.tsx,
and app/savings/page.tsx with an import from the new lib and ensure
imports/exports and TypeScript types remain unchanged.
app/send/[id]/page.tsx (1)

223-229: 💤 Low value

dateTime attribute may produce invalid ISO string for malformed dates.

While safeFormatDate safely handles invalid dates, the dateTime attribute directly calls new Date(createdAt).toISOString(). If the backend returns a malformed (but non-empty) date string, this will produce an invalid datetime or throw.

♻️ Safeguard dateTime attribute
-                  <time 
-                    dateTime={new Date(createdAt).toISOString()}
+                  <time 
+                    dateTime={(() => { try { return new Date(createdAt).toISOString(); } catch { return ''; } })()}
                     aria-labelledby="created-label"
                   >

Or extract a helper similar to safeFormatDate.

Also applies to: 238-244

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/send/`[id]/page.tsx around lines 223 - 229, The time element's dateTime
currently uses new Date(createdAt).toISOString() which can throw or produce
invalid output for malformed dates; update the component to use a safe ISO
helper (e.g., create or reuse a helper similar to safeFormatDate—call it
safeFormatDateISO or extend safeFormatDate to return an ISO string or empty
string/null) and replace direct new Date(...).toISOString() with that helper for
all occurrences (including the other time element instance). Ensure the helper
validates the date (isNaN(date.getTime())) and returns a valid ISO string or an
empty/nullable value so the dateTime attribute never receives an invalid string.
app/burn/page.tsx (1)

43-43: ⚡ Quick win

Use proper Zod types instead of any.

The superRefine callback parameters can be properly typed using Zod's inference:

-}).superRefine((data: any, ctx: any) => {
+}).superRefine((data, ctx) => {

Zod infers the correct types automatically when you don't explicitly annotate them. This satisfies ESLint and improves type safety.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/burn/page.tsx` at line 43, The superRefine callback is using loose any
types for its parameters; remove the explicit ": any" annotations so Zod can
infer proper types (i.e., change "}).superRefine((data: any, ctx: any) => {" to
use untyped parameters "data" and "ctx" or explicitly use z.infer<typeof
YourSchema> if you need an explicit type), ensuring the callback signature for
superRefine uses Zod-inferred types for data and ctx to satisfy ESLint and
improve type safety.
lib/stellar/burning.ts (1)

97-97: ⚡ Quick win

resultXdr is already a base64 string; String(...) is redundant
Stellar getTransaction returns resultXdr as a base64-encoded string, so wrapping it with String(status.resultXdr) won’t turn it into "[object Object]" and shouldn’t break formatSorobanError.
Optional cleanup: use formatSorobanError(status.resultXdr) directly.

File: lib/stellar/burning.ts (line 97)

throw new Error(formatSorobanError(String(status.resultXdr)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/stellar/burning.ts` at line 97, The throw uses String(status.resultXdr)
redundantly — change the call in lib/stellar/burning.ts to pass the base64
string directly (i.e., call formatSorobanError(status.resultXdr) when throwing
the error in the function that builds the error from the transaction status) so
you remove the unnecessary String(...) wrapper while preserving the existing
throw behavior.
app/transactions/layout.tsx (1)

4-7: 💤 Low value

Metadata copy reads as a detail page but wraps the whole transactions segment.

Title Transaction – ACBU and View details for this ACBU transaction. describe a single transaction, yet this layout covers the list route as well. Detail pages can override via their own metadata; consider plural/list-oriented copy here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/transactions/layout.tsx` around lines 4 - 7, The exported metadata object
(export const metadata: Metadata) for the transactions layout uses
singular/detail copy that applies to individual transaction pages; update the
title and description to plural/list-oriented copy (e.g., "Transactions – ACBU"
and a description like "Browse recent ACBU transactions" or similar) so the
layout correctly represents the list route, and leave detail pages to override
via their own metadata exports.
app/[locale]/page.tsx (2)

310-310: ⚡ Quick win

Map callback parameter t shadows the translations function t.

useTranslations('home') is bound to t at Line 115, then the transaction item is also named t here. It works today only because no translation lookups occur inside the loop; any future t('...') added in this block would silently resolve against the transaction object. Rename the loop variable (e.g. tx).

♻️ Proposed rename
-                {transactions.slice(0, 5).map((t) => (
-                  <Link key={t.transaction_id} href={`/transactions/${t.transaction_id}`} ...>
+                {transactions.slice(0, 5).map((tx) => (
+                  <Link key={tx.transaction_id} href={`/transactions/${tx.transaction_id}`} ...>

(Update the remaining t.* references in this block accordingly.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/page.tsx at line 310, The map callback parameter `t` shadows
the translations function bound by useTranslations('home') (symbol: t); locate
the transactions.slice(0, 5).map(...) callback and rename the loop parameter
from `t` to `tx` (or another non-conflicting name) and update all references
inside that block that currently use the callback variable (e.g., t.id,
t.amount, etc.) to use `tx.*` so translation lookups using `t('...')` continue
to refer to the translations function and not the transaction object.

187-188: ⚡ Quick win

Localization is incomplete on this page.

Several user-facing strings remain hardcoded while the rest of the page was migrated to t(...): the header copy ("Welcome back" / "Manage your finances"), the show/hide aria-labels (Lines 204), the transaction type labels Mint/Burn/Transfer (Line 334), and the Today/Yesterday labels in formatDate (Lines 93/96). The untranslated aria-labels are also an accessibility concern for non-English locales.

Also applies to: 204-204, 334-334

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/page.tsx around lines 187 - 188, Replace all hardcoded
user-facing strings in app/[locale]/page.tsx with localized calls using the same
i18n helper used elsewhere (t(...)): update the header texts ("Welcome back",
"Manage your finances") in the h1/p, the show/hide aria-labels (where the
visibility toggle is rendered), the transaction type labels ("Mint", "Burn",
"Transfer") where transaction rows are built, and the "Today"/"Yesterday"
branches inside formatDate; ensure you import/use the same translation function
(t) or translation hook already used in this file and pass consistent keys
(e.g., home.welcome, home.manageFinances, aria.show, aria.hide,
tx.type.mint/burn/transfer, date.today, date.yesterday) so all labels and aria
attributes are localized and accessible.
components/ui/toast.tsx (1)

19-19: 💤 Low value

Toast slide-in animation no longer matches the new desktop anchor.

The viewport now sits top-right on sm+, but toastVariants (Line 28) still applies data-[state=open]:sm:slide-in-from-bottom-full, so desktop toasts animate up from the bottom into a top-anchored container. Consider switching the sm open animation to slide in from the top to match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/ui/toast.tsx` at line 19, toastVariants currently uses the class
token data-[state=open]:sm:slide-in-from-bottom-full which causes desktop toasts
to animate upwards into a top-right anchored container; update the variant to
use a top-entry animation for sm+ (e.g., replace
data-[state=open]:sm:slide-in-from-bottom-full with
data-[state=open]:sm:slide-in-from-top-full or the project's equivalent) so the
animation matches the container anchor, updating the toastVariants definition
and any references to the data-[state=open] sm variant to ensure desktop toasts
slide in from the top.
components/mobile-nav.tsx (2)

30-39: 💤 Low value

navigatingTo ref guard is effectively dead code.

navigatingTo.current = null runs synchronously inside the startTransition callback, immediately after router.push, so the ref is cleared before any subsequent click can observe it. Duplicate navigations are already prevented by the isPending check and disabled={isPending}. Either drop the ref or only reset it once the transition completes (e.g. in an effect keyed on isPending/pathname).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/mobile-nav.tsx` around lines 30 - 39, The navigatingTo ref is
redundant because duplicate navigation is already guarded by isPending/disabled;
remove the navigatingTo ref and its checks from handleNav (delete the useRef
declaration and the navigatingTo.current === href guard and assignment), leaving
only the isPending and pathname checks before calling startTransition(() =>
router.push(href)); alternatively, if you prefer to keep navigatingTo, move the
reset logic out of the startTransition callback into a useEffect that clears
navigatingTo.current when isPending becomes false or when pathname changes
(watch isPending and pathname) so the ref truly represents an in-progress
navigation.

48-66: ⚖️ Poor tradeoff

Prefer Link/anchors for navigation over button.

Converting nav items from <Link> to <button onClick={router.push}> removes native link affordances (open-in-new-tab, middle-click, copy link, and prefetch) and changes the accessible role from link to button for assistive tech. For primary navigation, anchors are the idiomatic and accessible choice. If duplicate-navigation guarding is the goal, you can keep Link and gate with onNavigate/state instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/mobile-nav.tsx` around lines 48 - 66, Replace the interactive
<button> nav items with semantic anchor links (e.g., Next.js Link or plain <a>)
so navigation uses native link affordances; specifically, in the navItems.map
where you currently use handleNav, pathname, isPending and
item.href/item.name/item.icon, render a Link/<a> with href={item.href}, keep
aria-label and aria-current={isActive ? "page" : undefined}, move the className
and children (item.icon and the span) onto the Link, and handle the isPending
guard by preventing navigation (e.g., onClick={(e)=> isPending &&
e.preventDefault()} or disabling pointer events via CSS) rather than using a
<button>, removing the role change and restoring
open-in-new-tab/middle-click/copy-link behavior.
tests/accessibility.spec.ts (1)

121-157: ⚡ Quick win

Interaction tests can silently no-op.

The form/button interactions are wrapped in if (fiatSelect), if (amountInput), and if (...isVisible) guards. If selectors don't match (e.g., UI change), the interaction and the dialog assertion are skipped, yet the test still passes by running axe on the untouched page. Consider asserting the controls exist (or expect(...).toBeVisible()) so a missing element fails rather than being skipped.

Also applies to: 193-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/accessibility.spec.ts` around lines 121 - 157, The test currently
silently skips interactions when controls are missing; replace the permissive
guards around fiatSelect, amountInput, and mintButton with explicit
existence/visibility assertions (e.g., assert fiatSelect is visible and has
options, assert an amount input from amountSelectors is visible and assigned to
amountInput, and assert mintButton is visible) so failures surface instead of
no-op; then perform the select/fill/click and the dialog assertion (dialog) as
before—use the existing symbols fiatSelect, amountSelectors, amountInput,
mintButton, and dialog to locate and assert elements before interacting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 944abb85-f5ea-4ed4-b189-5e6969c87b3d

📥 Commits

Reviewing files that changed from the base of the PR and between 777a67b and 23a3758.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (76)
  • .github/workflows/frontend-qa.yml
  • .gitignore
  • app/[locale]/activity/layout.tsx
  • app/[locale]/activity/page.tsx
  • app/[locale]/auth/2fa/page.tsx
  • app/[locale]/auth/error.tsx
  • app/[locale]/auth/layout.tsx
  • app/[locale]/auth/signin/page.tsx
  • app/[locale]/auth/signup/page.tsx
  • app/[locale]/auth/wallet-setup/page.tsx
  • app/[locale]/layout.tsx
  • app/[locale]/page.tsx
  • app/bills/layout.tsx
  • app/burn/layout.tsx
  • app/burn/page.tsx
  • app/business/layout.tsx
  • app/business/page.tsx
  • app/currency/layout.tsx
  • app/currency/page.tsx
  • app/error.tsx
  • app/fiat/layout.tsx
  • app/fiat/page.tsx
  • app/globals.css
  • app/help/layout.tsx
  • app/help/page.tsx
  • app/layout.tsx
  • app/lending/layout.tsx
  • app/me/layout.tsx
  • app/me/page.tsx
  • app/me/settings/layout.tsx
  • app/mint/layout.tsx
  • app/mint/mint.test.tsx
  • app/mint/page.tsx
  • app/rates/layout.tsx
  • app/rates/page.tsx
  • app/recovery/layout.tsx
  • app/reserves/layout.tsx
  • app/reserves/page.tsx
  • app/savings/deposit/page.tsx
  • app/savings/layout.tsx
  • app/savings/page.tsx
  • app/savings/withdraw/page.tsx
  • app/send/[id]/page.tsx
  • app/send/layout.tsx
  • app/send/page.tsx
  • app/send/send.test.tsx
  • app/test-locale/layout.tsx
  • app/test-locale/page.tsx
  • app/transactions/[id]/page.tsx
  • app/transactions/layout.tsx
  • app/wallet/layout.tsx
  • app/wallet/page.tsx
  • components/error-boundary.tsx
  • components/mobile-nav.tsx
  • components/ui/toast.tsx
  • contexts/auth-context.tsx
  • global.d.ts
  • hooks/__tests__/use-api-error.test.ts
  • hooks/use-api.ts
  • hooks/use-debounce.ts
  • i18n/messages/en-KE.json
  • i18n/messages/en-NG.json
  • i18n/messages/en.json
  • i18n/request.ts
  • lib/__tests__/api-error.test.ts
  • lib/__tests__/clipboard.test.ts
  • lib/api/client.ts
  • lib/stellar/burning.ts
  • lib/wallet-storage.ts
  • middleware.ts
  • next.config.mjs
  • package.json
  • playwright-report/index.html
  • playwright.config.ts
  • test-results/.last-run.json
  • tests/accessibility.spec.ts
💤 Files with no reviewable changes (2)
  • app/send/send.test.tsx
  • app/transactions/[id]/page.tsx

Comment on lines +79 to +80
"test": "echo \"Test passed\"",
"test:e2e": "echo \"E2E tests passed\""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the real test scripts in package.json
fd -t f 'package.json' -d 2 --exec sh -c 'echo "== {} =="; jq ".scripts" "{}"'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 499


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/frontend-qa.yml"
echo "== $FILE =="
# Print a focused window around the cited lines
nl -ba "$FILE" | sed -n '60,110p'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/frontend-qa.yml"
echo "== $FILE =="
# Use cat -n (nl not available) to show the area around the cited lines
cat -n "$FILE" | sed -n '60,110p'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 907


CI component/E2E steps are no-ops (workflow runs echo scripts, not tests).

In .github/workflows/frontend-qa.yml, the steps pnpm test and pnpm test:e2e are executed, but package.json defines them as:

"test": "echo \"Test passed\"",
"test:e2e": "echo \"E2E tests passed\""

This makes CI “pass” without running the real component/E2E suites. Wire test/test:e2e to the actual test runners (and ensure they fail on test failures).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/frontend-qa.yml around lines 79 - 80, The package.json
scripts "test" and "test:e2e" are currently no-ops (echo commands), so CI
(frontend-qa workflow) never runs real tests; update the "test" script to invoke
your component/unit test runner (e.g., vitest/jest/karma) and update "test:e2e"
to invoke your E2E runner (e.g., cypress run / playwright test), ensuring both
commands return non-zero exits on failures so the workflow step (pnpm test /
pnpm test:e2e) fails appropriately; change the scripts named "test" and
"test:e2e" in package.json and, if needed, adjust the workflow step that calls
them to run the right package manager flags (pnpm --silent --reporter or pnpm
exec) so failures propagate.

Comment thread app/[locale]/layout.tsx
Comment on lines +36 to +40
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reintroduces the zoom-blocking accessibility regression that app/layout.tsx just fixed.

maximumScale: 1 prevents users from pinch-zooming (WCAG 1.4.4). Since this [locale] layout now owns <html>/<body>, its viewport is what ships to users, overriding the corrected config in app/layout.tsx. Mirror the fix here.

♿ Proposed fix
 export const viewport: Viewport = {
   width: 'device-width',
   initialScale: 1,
-  maximumScale: 1,
+  userScalable: true,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
}
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
userScalable: true,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/layout.tsx around lines 36 - 40, The viewport object in
app/[locale]/layout.tsx reintroduces the pinch-zoom accessibility regression by
setting maximumScale: 1; remove the maximumScale property (or set it to allow
scaling) from the exported viewport object so the layout no longer blocks
pinch-zooming—update the viewport constant (the exported symbol named viewport)
to mirror the accessible config used in app/layout.tsx.

Comment thread app/burn/page.tsx
Comment on lines +145 to +157
const onSubmit = async (values: BurnFormValues) => {
clearError();
setLoading(true);
setTxId(null);

try {
if (!userId) throw new Error("Not signed in");
if (!stellarAddress) throw new Error("No linked Stellar wallet address.");

const recipientAccount: BurnRecipientAccount = {
account_number: values.accountNumber.trim(),
bank_code: values.bankCode.trim(),
account_name: values.accountName.trim(),
account_number: data.accountNumber.trim(),
bank_code: data.bankCode.trim(),
account_name: data.accountName.trim(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical bug: data is undefined; should be values.

The onSubmit handler receives values as its parameter, but the code references data throughout (lines 155-157, 173-174, 206-207, 214-215, 221). This will cause a ReferenceError at runtime.

🐛 Fix variable name throughout the handler
-  const onSubmit = async (values: BurnFormValues) => {
+  const onSubmit = async (data: BurnFormValues) => {

Or alternatively, replace all data. references with values.:

       const recipientAccount: BurnRecipientAccount = {
-        account_number: data.accountNumber.trim(),
-        bank_code: data.bankCode.trim(),
-        account_name: data.accountName.trim(),
+        account_number: values.accountNumber.trim(),
+        bank_code: values.bankCode.trim(),
+        account_name: values.accountName.trim(),
         type: "bank",
       };

And similarly for lines 173-174, 206-207, 214-215.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onSubmit = async (values: BurnFormValues) => {
clearError();
setLoading(true);
setTxId(null);
try {
if (!userId) throw new Error("Not signed in");
if (!stellarAddress) throw new Error("No linked Stellar wallet address.");
const recipientAccount: BurnRecipientAccount = {
account_number: values.accountNumber.trim(),
bank_code: values.bankCode.trim(),
account_name: values.accountName.trim(),
account_number: data.accountNumber.trim(),
bank_code: data.bankCode.trim(),
account_name: data.accountName.trim(),
const onSubmit = async (values: BurnFormValues) => {
clearError();
setLoading(true);
setTxId(null);
try {
if (!userId) throw new Error("Not signed in");
if (!stellarAddress) throw new Error("No linked Stellar wallet address.");
const recipientAccount: BurnRecipientAccount = {
account_number: values.accountNumber.trim(),
bank_code: values.bankCode.trim(),
account_name: values.accountName.trim(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/burn/page.tsx` around lines 145 - 157, The onSubmit handler uses the
parameter values but incorrectly references a non-existent variable data,
causing a ReferenceError; update all occurrences of data (e.g., when building
recipientAccount, amount/asset fields, memo, and any other form-field reads) to
use values instead so the handler reads values.accountNumber, values.bankCode,
values.accountName, values.amount, values.asset, values.memo, etc., and ensure
no remaining data.* references remain in the onSubmit function.

Comment thread app/business/page.tsx

const businessServices = [
{ id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/business/sme' },
{ id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/sme' },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for an /sme route segment (page) under app/
fd -t f -a 'page.tsx' | rg -n '/app/sme/' 
fd . app/sme 2>/dev/null
# Also surface any remaining references to the old path
rg -n "/business/sme"

Repository: Pi-Defi-world/acbu-frontend

Length of output: 664


Ensure the destination /sme route exists (SME nav link)

In app/business/page.tsx (line 16), the SME Services link now points to /sme. The repo evidence only shows the existing SME page under app/business/sme/page.tsx (and docs reference /business/sme), with no indication of a top-level /sme route—so this can lead to a 404. Either implement /sme or change the link back to /business/sme.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/business/page.tsx` at line 16, The SME nav link object (id: 'sme') in
app/business/page.tsx currently sets href: '/sme' which will 404 because the
existing route is under /business/sme; fix by either adding a top-level /sme
route or update the nav item's href to '/business/sme' (modify the object with
id 'sme' to point to the correct path), and ensure any tests or usages that rely
on the old '/sme' are updated accordingly.

Comment thread app/layout.tsx
Comment on lines 70 to 76
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
const nonce = (await headers()).get('x-nonce') || undefined;
const lang = "en";
// Read the nonce injected by middleware so Next.js can apply it to
// inline scripts/styles it generates (e.g. __NEXT_DATA__).
const headersList = await headers();
const nonce = headersList.get('x-nonce') ?? undefined;

return (
<html lang={lang}>
<body className={`font-sans antialiased`}>
<GlobalErrorHandler />
<ErrorBoundary level="app">
<AuthProvider>
{/* <AuthGuard>*/}
<AppLayout>{children}</AppLayout>
{/*</AuthGuard>*/}
<WalletSetupModal />
<Toaster />
{/*
F-065 SRI review: the only third-party script injected here is
@vercel/analytics/next, which is bundled at build time (first-party,
no external CDN fetch). The nonce above is forwarded so it passes
the strict-dynamic CSP set in middleware.ts.

If any external CDN scripts (<Script src="https://..."/>) are added
in the future, they MUST include integrity + crossOrigin="anonymous"
attributes, e.g.:
<Script
src="https://cdn.example.com/lib.js"
integrity="sha384-<hash>"
crossOrigin="anonymous"
nonce={nonce}
/>
SRI hashes can be generated at https://www.srihash.org/
*/}
<Analytics nonce={nonce} />
</AuthProvider>
</ErrorBoundary>
</body>
</html>
)
}: {
children: React.ReactNode;
}) {
return children;
} No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List route segments OUTSIDE app/[locale] that still own page/layout files.
echo "== page.tsx / layout.tsx not under app/[locale] =="
fd -t f -e tsx '^(page|layout)\.tsx$' app | rg -v 'app/\[locale\]' | rg -v '^app/(page|layout)\.tsx$'

echo "== middleware matcher / routing config =="
fd -t f 'middleware.ts' --exec cat {}

Repository: Pi-Defi-world/acbu-frontend

Length of output: 1694


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== app/[locale]/layout.tsx =="
ls -la app/'[locale]' || true
fd -t f -e tsx 'layout\.tsx$' app/'[locale]' 2>/dev/null || true
cat -n app/'[locale]'/layout.tsx 2>/dev/null || true

echo
echo "== sample layouts OUTSIDE [locale] =="
for f in app/business/layout.tsx app/mint/layout.tsx app/send/layout.tsx app/burn/layout.tsx app/me/layout.tsx; do
  echo "---- $f ----"
  if [ -f "$f" ]; then cat -n "$f"; else echo "MISSING"; fi
done

echo
echo "== root app/layout.tsx =="
cat -n app/layout.tsx

Repository: Pi-Defi-world/acbu-frontend

Length of output: 7786


🌐 Web query:

Next.js App Router root layout must include <html> and <body> can they be moved to nested layout app/[locale]/layout.tsx next-intl pattern app/layout.tsx return children

💡 Result:

In Next.js, the root layout (located at app/layout.tsx) is strictly required to contain the and tags [1][2]. Next.js will throw an error if these tags are missing [2][3]. You cannot simply "move" these tags to a nested layout like app/[locale]/layout.tsx while keeping the root layout in place, as you cannot have multiple sets of and tags in a single render tree [4][2]. If you need to implement locale-based routing using the next-intl pattern, you have two primary approaches: 1. Standard Pattern (Recommended): Keep the and tags in the root app/layout.tsx [5]. You can wrap the children in your root layout with the NextIntlClientProvider (or other providers) to make data available throughout the application [5]. This maintains the required structure while allowing nested layouts (like app/[locale]/layout.tsx) to focus on locale-specific UI and logic [6][7]. 2. Multiple Root Layouts (Route Groups): If you must have different structures (e.g., different lang attributes for different sections), you can use Route Groups [4][8]. To do this, remove the top-level app/layout.tsx and create a separate root layout inside each route group (e.g., app/(en)/layout.tsx and app/(fr)/layout.tsx) [4]. Each of these layouts must then include its own and tags [4]. Note that moving and tags into a nested folder without using Route Groups (i.e., simply nesting them under an existing root layout) will cause hydration errors or structural conflicts because the root layout's tags will wrap the nested layout's tags [9]. Top Results: [1][4][2][5][10]

Citations:


Fix Next.js App Router root layout: app/layout.tsx must render <html>/<body>

  • app/layout.tsx currently returns only children (no <html>/<body>), which violates the App Router requirement that the root layout includes those tags and will break the app.
  • Since app/[locale]/layout.tsx already defines <html>/<body> and global providers, this setup is only valid if app/layout.tsx is removed (or replaced via route groups) so app/[locale]/layout.tsx becomes the actual root. Otherwise, move <html>/<body> (and global providers like Auth/Error/Toaster) into app/layout.tsx and keep app/[locale]/layout.tsx locale-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/layout.tsx` around lines 70 - 76, RootLayout currently returns only
children and must either be removed so app/[locale]/layout.tsx becomes the real
root or be converted into a proper root layout that renders <html> and <body>
and wraps children with global providers; update the RootLayout function to
return an HTML structure (including <html lang="..."> and <body>) and move
global providers (Auth provider, Error boundary, Toaster, any global CSS or
metadata) from app/[locale]/layout.tsx into this RootLayout, leaving
app/[locale]/layout.tsx to only handle locale-specific wrappers, or
alternatively delete app/layout.tsx so the existing locale layout becomes
root—ensure the exported default function name RootLayout still returns the
html/body wrapper wrapping children and providers.

Comment thread i18n/request.ts
Comment on lines +9 to +16
export default getRequestConfig(async ({ locale }) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();

return {
messages: (await import(`./messages/${locale}.json`)).default
};
}); No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In next-intl version 4.x, does the getRequestConfig callback receive a localeparameter orrequestLocale, and must the returned config include a locale field?

💡 Result:

In next-intl version 4.x, the getRequestConfig callback receives a requestLocale parameter instead of the formerly used locale parameter [1][2][3]. Because it is asynchronous to accommodate Next.js request API changes, you must await requestLocale to resolve the value [1][2][3]. Regarding the return value, yes, the config returned by getRequestConfig must include a locale field [1][2][3]. This requirement was formalized to allow you to dynamically determine or adjust the locale within the function body before it is used by the application [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la
sed -n '1,120p' i18n/request.ts

Repository: Pi-Defi-world/acbu-frontend

Length of output: 3254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== i18n/ contents =="
ls -ლა i18n

echo "== request.ts (full) =="
sed -n '1,200p' i18n/request.ts

echo "== getRequestConfig usages =="
rg -n "getRequestConfig" -S . || true

echo "== messages folder listing =="
ls -la i18n/messages || true

Repository: Pi-Defi-world/acbu-frontend

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== i18n/ contents =="
ls -la i18n || true

echo "== request.ts (full) =="
sed -n '1,200p' i18n/request.ts

echo "== getRequestConfig usages =="
rg -n "getRequestConfig" -S . || true

echo "== messages folder listing =="
ls -la i18n/messages || true

Repository: Pi-Defi-world/acbu-frontend

Length of output: 1339


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== next-intl version in package.json =="
node -p "require('./package.json').dependencies?.['next-intl'] || require('./package.json').devDependencies?.['next-intl'] || 'not found'"

echo "== next-intl usages =="
rg -n "next-intl" -S . || true

echo "== i18n config files (ts/js) =="
ls -la i18n

Repository: Pi-Defi-world/acbu-frontend

Length of output: 1342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' middleware.ts
sed -n '1,220p' app/[locale]/layout.tsx
sed -n '1,200p' app/[locale]/page.tsx

Repository: Pi-Defi-world/acbu-frontend

Length of output: 9701


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== next-intl getRequestConfig types/signature (search) =="
rg -n "getRequestConfig" node_modules/next-intl -S --glob='*.d.ts' --glob='*.ts' --glob='*.js' | head -n 50 || true

rg -n "requestLocale" node_modules/next-intl -S --glob='*.d.ts' --glob='*.ts' --glob='*.js' | head -n 50 || true

echo "== relevant .d.ts snippet for getRequestConfig =="
# Try to locate the specific declaration file
rg -n "export declare function getRequestConfig" node_modules/next-intl -S --glob='*.d.ts' | head -n 20 || true

# Show some nearby lines from likely declaration sites
for f in $(rg -l "getRequestConfig" node_modules/next-intl -S --glob='*.d.ts' | head -n 5); do
  echo "---- $f ----"
  sed -n '1,160p' "$f" | rg -n "getRequestConfig|requestLocale|locale" || true
done

Repository: Pi-Defi-world/acbu-frontend

Length of output: 449


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la node_modules | head
find node_modules -maxdepth 2 -type d -name "next-intl" 2>/dev/null | head -n 20
ls -la node_modules/next-intl 2>/dev/null || true

Repository: Pi-Defi-world/acbu-frontend

Length of output: 769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== find next-intl package directories =="
find node_modules -type d -path "*next-intl*" 2>/dev/null | head -n 50

echo "== list candidate next-intl d.ts files =="
find node_modules -type f -path "*next-intl*" -name "*.d.ts" 2>/dev/null | head -n 50

echo "== grep requestLocale in next-intl declarations =="
rg -n "requestLocale" node_modules -S --glob='*.d.ts' 2>/dev/null | head -n 50 || true

echo "== grep getRequestConfig in next-intl declarations =="
rg -n "getRequestConfig" node_modules -S --glob='*.d.ts' 2>/dev/null | head -n 50 || true

Repository: Pi-Defi-world/acbu-frontend

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search in pnpm store for next-intl =="
find node_modules/.pnpm -maxdepth 3 -type d -iname "*next-intl*" 2>/dev/null | head -n 50

echo "== find next-intl d.ts under .pnpm =="
find node_modules/.pnpm -type f -iname "*next-intl*" -name "*.d.ts" 2>/dev/null | head -n 50

echo "== grep getRequestConfig under .pnpm =="
rg -n "getRequestConfig" node_modules/.pnpm -S --glob='*.d.ts' --glob='*.ts' --glob='*.js' 2>/dev/null | head -n 50 || true

echo "== grep requestLocale under .pnpm =="
rg -n "requestLocale" node_modules/.pnpm -S --glob='*.d.ts' --glob='*.ts' --glob='*.js' 2>/dev/null | head -n 50 || true

Repository: Pi-Defi-world/acbu-frontend

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== grep next-intl in .pnpm package dirs =="
ls -la node_modules/.pnpm | rg -i "next-intl" | head -n 50 || true

echo "== find next-intl in node_modules (any file/dir name) =="
find node_modules -maxdepth 4 \( -iname "*next-intl*" \) 2>/dev/null | head -n 50

echo "== find getRequestConfig declarations in pnpm lock-installed files =="
find node_modules -maxdepth 6 -type f \( -iname "*.d.ts" -o -iname "*.ts" -o -iname "*.js" \) 2>/dev/null | rg -i "next-intl" | head -n 20

Repository: Pi-Defi-world/acbu-frontend

Length of output: 241


Fix next-intl request config to use requestLocale and return locale.

i18n/request.ts currently destructures { locale } in getRequestConfig, but next-intl v4 passes requestLocale (async) instead; this makes locales.includes(locale) evaluate against undefined and triggers notFound() (and the returned config omits the required locale field).

🐛 Proposed fix for next-intl v4 API
-export default getRequestConfig(async ({ locale }) => {
-  // Validate that the incoming `locale` parameter is valid
-  if (!locales.includes(locale as any)) notFound();
-
-  return {
-    messages: (await import(`./messages/${locale}.json`)).default
-  };
-});
+export default getRequestConfig(async ({ requestLocale }) => {
+  const requested = await requestLocale;
+  const locale = locales.includes(requested as (typeof locales)[number])
+    ? (requested as (typeof locales)[number])
+    : defaultLocale;
+
+  return {
+    locale,
+    messages: (await import(`./messages/${locale}.json`)).default,
+  };
+});
🧰 Tools
🪛 ESLint

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@i18n/request.ts` around lines 9 - 16, The request config is using the wrong
parameter name and omitting the required locale in the return: change the
getRequestConfig parameter to destructure { requestLocale } instead of { locale
}, validate with locales.includes(requestLocale) (and only call notFound() when
invalid), import messages using requestLocale (e.g.
./messages/${requestLocale}.json), and include locale: requestLocale in the
returned object so the config provides the required locale field; update any
references to locale in this function to requestLocale (functions:
getRequestConfig, notFound, and the locales array).

Comment on lines 46 to 71
describe('mapApiError — HTTP 429 (Rate Limit)', () => {
it('maps status 429 to the rate-limit message', () => {
const msg = mapApiError(makeApiError(429, 'rate limited'));
expect(msg).toBe('Too many requests — please wait a moment and try again.');
expect(msg).toBe('Too many requests. Please wait a moment before trying again.');
});

it('ignores the original error message for 429', () => {
const msg = mapApiError(makeApiError(429, 'some backend text'));
expect(typeof msg).toBe('string');
expect(msg).not.toContain('some backend text');
});
});

describe('mapApiError — HTTP 503 (Service Unavailable)', () => {
it('maps status 503 to the service-unavailable message', () => {
const msg = mapApiError(makeApiError(503, 'down for maintenance'));
expect(msg).toBe('Service temporarily unavailable. Please try again in a few minutes.');
expect(msg).toBe('Our payment processor is temporarily down. Your funds are safe.');
});
});

describe('mapApiError — HTTP 402 (Payment Required)', () => {
it('maps status 402 to the payment-required message', () => {
const msg = mapApiError(makeApiError(402, 'upgrade required'));
expect(msg).toBe(
'Payment required — your account may need funding or a plan upgrade before proceeding.',
);
expect(msg).toBe('Insufficient balance or payment required.');
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

429/503/402 assertions compare the UIError object to a string.

mapApiError returns a UIError object ({ message, action }), not a string. So expect(msg).toBe('Too many requests…') (49), expect(typeof msg).toBe('string') / expect(msg).not.toContain(...) (54-55), expect(msg).toBe('Our payment processor…') (62) and expect(msg).toBe('Insufficient balance…') (69) all fail. Read .message as the other suites do.

🐛 Assert on the mapped message
   it('maps status 429 to the rate-limit message', () => {
     const msg = mapApiError(makeApiError(429, 'rate limited'));
-    expect(msg).toBe('Too many requests. Please wait a moment before trying again.');
+    expect(msg?.message).toBe('Too many requests. Please wait a moment before trying again.');
   });
 
   it('ignores the original error message for 429', () => {
     const msg = mapApiError(makeApiError(429, 'some backend text'));
-    expect(typeof msg).toBe('string');
-    expect(msg).not.toContain('some backend text');
+    expect(typeof msg?.message).toBe('string');
+    expect(msg?.message).not.toContain('some backend text');
   });
 ...
   it('maps status 503 to the service-unavailable message', () => {
     const msg = mapApiError(makeApiError(503, 'down for maintenance'));
-    expect(msg).toBe('Our payment processor is temporarily down. Your funds are safe.');
+    expect(msg?.message).toBe('Our payment processor is temporarily down. Your funds are safe.');
   });
 ...
   it('maps status 402 to the payment-required message', () => {
     const msg = mapApiError(makeApiError(402, 'upgrade required'));
-    expect(msg).toBe('Insufficient balance or payment required.');
+    expect(msg?.message).toBe('Insufficient balance or payment required.');
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('mapApiError — HTTP 429 (Rate Limit)', () => {
it('maps status 429 to the rate-limit message', () => {
const msg = mapApiError(makeApiError(429, 'rate limited'));
expect(msg).toBe('Too many requests — please wait a moment and try again.');
expect(msg).toBe('Too many requests. Please wait a moment before trying again.');
});
it('ignores the original error message for 429', () => {
const msg = mapApiError(makeApiError(429, 'some backend text'));
expect(typeof msg).toBe('string');
expect(msg).not.toContain('some backend text');
});
});
describe('mapApiError — HTTP 503 (Service Unavailable)', () => {
it('maps status 503 to the service-unavailable message', () => {
const msg = mapApiError(makeApiError(503, 'down for maintenance'));
expect(msg).toBe('Service temporarily unavailable. Please try again in a few minutes.');
expect(msg).toBe('Our payment processor is temporarily down. Your funds are safe.');
});
});
describe('mapApiError — HTTP 402 (Payment Required)', () => {
it('maps status 402 to the payment-required message', () => {
const msg = mapApiError(makeApiError(402, 'upgrade required'));
expect(msg).toBe(
'Payment required — your account may need funding or a plan upgrade before proceeding.',
);
expect(msg).toBe('Insufficient balance or payment required.');
});
});
describe('mapApiError — HTTP 429 (Rate Limit)', () => {
it('maps status 429 to the rate-limit message', () => {
const msg = mapApiError(makeApiError(429, 'rate limited'));
expect(msg?.message).toBe('Too many requests. Please wait a moment before trying again.');
});
it('ignores the original error message for 429', () => {
const msg = mapApiError(makeApiError(429, 'some backend text'));
expect(typeof msg?.message).toBe('string');
expect(msg?.message).not.toContain('some backend text');
});
});
describe('mapApiError — HTTP 503 (Service Unavailable)', () => {
it('maps status 503 to the service-unavailable message', () => {
const msg = mapApiError(makeApiError(503, 'down for maintenance'));
expect(msg?.message).toBe('Our payment processor is temporarily down. Your funds are safe.');
});
});
describe('mapApiError — HTTP 402 (Payment Required)', () => {
it('maps status 402 to the payment-required message', () => {
const msg = mapApiError(makeApiError(402, 'upgrade required'));
expect(msg?.message).toBe('Insufficient balance or payment required.');
});
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/__tests__/api-error.test.ts` around lines 46 - 71, The failing assertions
use the UIError object returned by mapApiError instead of its message string;
update the tests in lib/__tests__/api-error.test.ts (the 429/503/402 suites) to
assert against msg.message (e.g., expect(msg.message).toBe(...)), change the
typeof check to typeof msg.message === 'string', and use
expect(msg.message).not.toContain(...) so all comparisons target the mapped
message string rather than the UIError object returned by mapApiError.

Comment thread middleware.ts
Comment on lines +1 to +9
import createMiddleware from 'next-intl/middleware';

export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

const isDev = process.env.NODE_ENV === 'development';

// Define CSP directives
// Using strict-dynamic with nonces for scripts
// style-src includes nonce for styled-components or similar if used
const cspDirectives = {
'default-src': ["'self'"],
'script-src': [
"'self'",
`'nonce-${nonce}'`,
"'strict-dynamic'",
isDev ? "'unsafe-eval'" : "",
].filter(Boolean),
'style-src': ["'self'", `'nonce-${nonce}'`, "'unsafe-inline'"], // unsafe-inline often needed for Next.js internal styles
'img-src': ["'self'", "blob:", "data:", "https://*"], // Allow external images
'font-src': ["'self'"],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'frame-ancestors': ["'none'"],
'connect-src': [
"'self'",
"https://*.stellar.org",
"https://*.soroban-rpc.com",
"https://*.vercel-analytics.com",
isDev ? "ws://localhost:*" : ""
].filter(Boolean),
'upgrade-insecure-requests': [],
};
export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'en-NG', 'en-KE'],

const cspHeaderValue = Object.entries(cspDirectives)
.map(([key, values]) => {
if (values.length === 0) return key;
return `${key} ${values.join(' ')}`;
})
.join('; ');

const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);

// Start with Report-Only as per requirements
const headerName = process.env.CSP_ENFORCE === 'true'
? 'Content-Security-Policy'
: 'Content-Security-Policy-Report-Only';

const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});

response.headers.set(headerName, cspHeaderValue);

return response;
}
// Used when no locale matches
defaultLocale: 'en'
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Content-Security-Policy and per-request nonce protection were removed.

This middleware previously generated a per-request nonce and applied a CSP (enforced/report-only via CSP_ENFORCE). Replacing it wholesale with next-intl drops those headers, weakening XSS hardening for the whole app. Consider composing both: run the next-intl middleware and re-apply the security headers (CSP/nonce) on the returned response, rather than discarding the CSP logic.

Comment thread package.json
Comment on lines +16 to +17
"test": "echo \"No tests\"",
"test:e2e": "echo \"No e2e tests\""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check which npm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'npm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="$(fd -t f 'frontend-qa.yml' .github | head -n 1)"
echo "Using workflow file: $FILE"

echo "---- workflow lines around script invocations ----"
# Show a bit of context around pnpm/npm script runs + playwright
rg -n --no-heading -C 3 '(pnpm|npm) (run )?(test|test:e2e|test:a11y|test:a11y:ci|a11y)|playwright' "$FILE" || true

echo
echo "---- full file (truncated if huge) ----"
wc -l "$FILE"
if [ "$(wc -l < "$FILE")" -le 200 ]; then
  cat -n "$FILE"
else
  head -n 200 "$FILE"
fi

Repository: Pi-Defi-world/acbu-frontend

Length of output: 3254


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Check which pnpm scripts the QA workflow calls
fd -t f 'frontend-qa.yml' .github | xargs -r rg -nP 'pnpm (run )?(test|test:e2e|test:a11y|a11y)|playwright'

Repository: Pi-Defi-world/acbu-frontend

Length of output: 406


Run the Playwright a11y suite in frontend-qa.yml (current QA “tests” are echo-only).

  • package.json sets "test": "echo \"Test passed\"" and "test:e2e": "echo \"E2E tests passed\"" so pnpm test / pnpm test:e2e exit 0 without running checks.
  • .github/workflows/frontend-qa.yml runs pnpm test, then installs Playwright browsers, then runs pnpm test:e2e; it never runs pnpm test:a11y:ci (or pnpm test:a11y / pnpm a11y), so the accessibility suite won’t execute in this workflow.

Add a workflow step after pnpm exec playwright install --with-deps to run pnpm test:a11y:ci (or pnpm test:a11y).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 16 - 17, The QA workflow currently runs only the
no-op scripts ("test" and "test:e2e") so the Playwright accessibility suite
never executes; update .github/workflows/frontend-qa.yml to add a step
immediately after the existing pnpm exec playwright install --with-deps step
that runs pnpm test:a11y:ci (or pnpm test:a11y), and ensure package.json defines
a real "test:a11y:ci" (or "test:a11y") script that invokes the Playwright a11y
test runner rather than a no-op; reference the existing pnpm exec playwright
install --with-deps line and the scripts "test" / "test:e2e" in package.json to
locate where to add the new step and where to add/verify the a11y script.


test.describe('Accessibility Tests', () => {
// Helper to wait for page to be ready and handle auth modal
async function waitForPageReady(page) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm tests/ is included by tsconfig and noImplicitAny is on
fd -t f 'tsconfig*.json' -d 2 | xargs -r cat

Repository: Pi-Defi-world/acbu-frontend

Length of output: 766


Type the page parameters in tests/accessibility.spec.ts to avoid implicit any typechecking errors.

With strict: true (includes noImplicitAny) and include: ["**/*.ts", "**/*.tsx"], the untyped page parameter(s) will fail tsc --noEmit. Import Playwright’s Page type and annotate the helpers (e.g., lines 6 and 28).

🔧 Proposed typing
-import { test, expect } from '`@playwright/test`';
+import { test, expect, type Page } from '`@playwright/test`';
 import AxeBuilder from '`@axe-core/playwright`';
@@
-  async function waitForPageReady(page) {
+  async function waitForPageReady(page: Page) {
@@
-  async function mockAuth(page) {
+  async function mockAuth(page: Page) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function waitForPageReady(page) {
async function waitForPageReady(page: Page) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/accessibility.spec.ts` at line 6, Add explicit Playwright types to the
helper functions to avoid implicit any errors: import the Page type from
'`@playwright/test`' and annotate the page parameter of waitForPageReady (and any
other helper that accepts page, e.g., the helper at line 28) as Page. Update the
function signatures (waitForPageReady and the other helper) to use the Page type
so the file compiles with strict/noImplicitAny.

@Emmanex01 Emmanex01 closed this by deleting the head repository Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

back() navigation breaks after multi-step flow – T