Fix/f 047 mobile nav aria label#306
Conversation
- 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 Pi-Defi-world#217
…ocalization implemeted i18n / localization "F-045 — No i18n / localization
fix(a11y): resolve accessibility violations on money forms (F-046)
…e-list-keys 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
…ature/fix-savings-deposit-handler Revert "fix: wire savings deposit dialog to API and show pending/completed st…"
Enhance Multi-Currency Bank Validation and Burn Page Form Logic
Use concise destination-based aria-label text on mobile nav icon links so VoiceOver announces each route clearly. Made-with: Cursor
📝 WalkthroughWalkthroughEstablishes internationalization support via Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
|
@DavisVT Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/[locale]/page.tsx (1)
310-354:⚠️ Potential issue | 🟡 MinorVariable shadowing:
tshadows the translation function.The map callback parameter
tshadows theuseTranslationshook resulttfrom line 115. While the current code only accesses transaction properties (t.type,t.status, etc.), any future attempt to use translations inside this callback will silently use the wrongt, causing runtime errors.🔧 Proposed fix: rename the callback parameter
- {transactions.slice(0, 5).map((t) => ( - <Link key={t.transaction_id} href={`/transactions/${t.transaction_id}`} className="block rounded-lg border border-border bg-card p-3 transition-colors active:bg-muted"> + {transactions.slice(0, 5).map((tx) => ( + <Link key={tx.transaction_id} href={`/transactions/${tx.transaction_id}`} className="block rounded-lg border border-border bg-card p-3 transition-colors active:bg-muted"> <div className="flex items-center gap-3 mb-2"> <div className={`p-2 rounded-full flex-shrink-0 ${ - t.type === 'mint' + tx.type === 'mint' ? 'bg-green-100 dark:bg-green-900/30' - : t.type === 'burn' + : tx.type === 'burn' ? 'bg-red-100 dark:bg-red-900/30' : 'bg-blue-100 dark:bg-blue-900/30' }`}Continue renaming all
t.references totx.within this map callback (lines 310-354).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`[locale]/page.tsx around lines 310 - 354, The map callback parameter `t` in the transactions rendering shadows the `t` translation function from useTranslations; rename the callback parameter to `tx` in the transactions.slice(0, 5).map(...) callback and update every reference inside that callback (key, href, tx.type, tx.transaction_id, tx.created_at, tx.acbu_amount_burned, tx.amount_acbu, tx.local_currency, tx.local_amount, tx.status, and any other tx.* uses) so translation `t` remains available and no shadowing occurs while keeping the existing formatting helpers (formatDate, formatAcbu, formatAmount) and Badge/Link usage unchanged.app/burn/page.tsx (1)
145-221:⚠️ Potential issue | 🔴 CriticalCritical:
datais undefined — should bevaluesthroughoutonSubmit.The function parameter is named
values(line 145), but the function body referencesdataat lines 155-157, 173-174, 206-207, and 214-215. This will causeReferenceError: data is not definedwhen the form is submitted.🐛 Fix undefined variable references
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", };const submit = await submitBurnRedeemSingleClient({ userAddress: stellarAddress, - amountAcbu: data.acbuAmount, - currency: data.currency, + amountAcbu: values.acbuAmount, + currency: values.currency, userSecret: secret, });const submit = await submitBurnRedeemSingleClient({ userAddress: stellarAddress, - amountAcbu: data.acbuAmount, - currency: data.currency, + amountAcbu: values.acbuAmount, + currency: values.currency, external: { kit, address }, });const res = await burnApi.burnAcbu( - data.acbuAmount, - data.currency, + values.acbuAmount, + values.currency, recipientAccount, opts, burnTxHash, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/burn/page.tsx` around lines 145 - 221, The onSubmit handler uses an undefined identifier data; replace all usages of data with the function parameter values (e.g., when building recipientAccount.account_number/bank_code/account_name, passing amount/currency to submitBurnRedeemSingleClient and burnApi.burnAcbu, and when calling form.reset) so the function uses the passed BurnFormValues; verify occurrences around recipientAccount construction, the two submitBurnRedeemSingleClient calls, the burnApi.burnAcbu call, setTxId/form.reset lines, and ensure variable names like getWalletSecretAnyLocal, submitBurnRedeemSingleClient, burnApi.burnAcbu, setTxId, and form.reset remain unchanged.
🧹 Nitpick comments (14)
test-results/.last-run.json (1)
1-4: Drop the committed Playwright run state.
.last-run.jsonis machine-generated state. Keeping it in version control will create noisy diffs and merge conflicts without adding any real test coverage; please gitignore it instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test-results/.last-run.json` around lines 1 - 4, Remove the committed Playwright run state file `.last-run.json` from source control and add it to the repository ignore list: stop tracking the file (git rm --cached .last-run.json), commit that removal, and add `.last-run.json` to `.gitignore` (or the project’s appropriate ignore file) so future runs don’t produce noisy diffs or merge conflicts.app/send/[id]/page.tsx (2)
204-214: Redundant ARIA attributes on amount value.The
<span>has botharia-labelledby="amount-label"andaria-describedby="amount-value", butamount-valueis the same element's child, makingaria-describedbypoint to itself. This is unnecessary and could confuse screen readers.♻️ Simplified markup
<span className="font-semibold" aria-labelledby="amount-label" - aria-describedby="amount-value" > - <span id="amount-value"> {isFiatRecord ? `${localCurrency} ${formatAmount(localAmount)}` : `ACBU ${formatAmount(amountAcbu)}`} - </span> </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/send/`[id]/page.tsx around lines 204 - 214, The span rendering the amount (the element with id "amount-value" inside the outer span having aria-labelledby="amount-label") currently includes aria-describedby pointing to its own child, which is redundant and can confuse screen readers; remove the aria-describedby="amount-value" attribute from the outer span (or from whichever element references the same id) so the outer element only uses aria-labelledby="amount-label" and the inner span keeps id="amount-value" for any required reference elsewhere.
263-267: Live region announces status on every render, not just changes.The live region content is always present based on the current
statusvalue, so it will announce on initial page load rather than only when the status changes. If the intent is to announce dynamic updates, consider conditionally rendering only when status transitions occur.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/send/`[id]/page.tsx around lines 263 - 267, The sr-only live region currently renders on every render because it directly reads the current status; change this so it only mounts when status actually changes by tracking the previous status and an announce flag (e.g., add a useState like announce and prevStatus or useRef prevStatus, then in a useEffect compare prevStatus.current to status and set announce true when they differ, update prevStatus.current = status, and optionally clear announce after a short timeout); then render the <div role="status" aria-live="polite" aria-atomic="true"> only when announce is true so the screen reader only receives announcements on transitions, and ensure you clear the announce flag afterwards to avoid repeated announcements.tests/accessibility.spec.ts (3)
121-127: Silent skip when elements aren't found reduces test coverage.When
fiatSelectoramountInputaren't found, the test silently continues without asserting anything meaningful. Consider failing explicitly or logging a warning.♻️ Proposed approach
if (fiatSelect) { // Check if there are options const options = await fiatSelect.locator('option').count(); if (options > 1) { await fiatSelect.selectOption({ index: 1 }); } + } else { + console.warn('Fiat select not found - skipping select interaction'); }Alternatively, use
test.skip()orexpect.soft()to make the skip explicit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/accessibility.spec.ts` around lines 121 - 127, The current block silently ignores missing elements (fiatSelect, amountInput) so tests can pass without asserting behavior; update the test to explicitly fail or mark as skipped when these locators are not found: after obtaining fiatSelect and amountInput (the locator variables used in this snippet), assert their presence with Playwright assertions (e.g., use expect(fiatSelect).not.toBeNull()/toBeVisible() or call test.skip() when intentionally optional) or use expect.soft() to record a soft failure; ensure you also handle the options count via fiatSelect.locator('option').count() only after asserting fiatSelect exists so the test fails/records when elements are missing rather than silently continuing.
17-17: Avoid arbitrarywaitForTimeoutcalls.Hard-coded timeouts (lines 17, 23, 103, 174, 202-204) make tests flaky. Prefer waiting for specific conditions like element visibility or network idle states.
♻️ Example improvement for line 17
- // Wait a bit more for content to stabilize - await page.waitForTimeout(1000); + // Wait for main content to be visible + await page.locator('main, [role="main"], .page-content').first().waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/accessibility.spec.ts` at line 17, The test uses an arbitrary pause via page.waitForTimeout(1000) which makes the spec flaky; replace that call with a targeted wait such as waiting for a specific element or network/state transition (e.g., page.waitForSelector('#selector'), page.waitForLoadState('networkidle'), or page.waitForResponse(...) ) inside the same test function so the test proceeds only when the required condition is met; locate and update the page.waitForTimeout usage in tests/accessibility.spec.ts (search for page.waitForTimeout) and swap each occurrence with an appropriate conditional wait tied to the element or event that the subsequent assertions expect.
6-6: Add TypeScript type annotation forpageparameter.The helper function lacks a type annotation, which reduces type safety.
♻️ Proposed fix
- async function waitForPageReady(page) { + async function waitForPageReady(page: import('@playwright/test').Page) {Apply the same fix to
mockAuthat line 28.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/accessibility.spec.ts` at line 6, Add explicit TypeScript parameter types for the test helpers: annotate the waitForPageReady(page) parameter with the Playwright Page type (e.g., page: Page) and likewise annotate mockAuth's parameter(s) (the helper at line 28) with their appropriate types; import the Page type from your test library (e.g., '@playwright/test' or 'playwright') at the top and update the function signatures for waitForPageReady and mockAuth to use those types to restore type safety.app/[locale]/page.tsx (1)
187-188: Hardcoded strings not using translations.The header contains hardcoded English text while the rest of the page uses
t()for translations. This creates an inconsistent localization experience.♻️ Proposed fix: use translation keys
- <h1 className="text-base font-bold text-foreground">Welcome back</h1> - <p className="text-xs text-muted-foreground">Manage your finances</p> + <h1 className="text-base font-bold text-foreground">{t('welcome_back')}</h1> + <p className="text-xs text-muted-foreground">{t('manage_finances')}</p>Ensure the corresponding keys are added to the locale JSON files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`[locale]/page.tsx around lines 187 - 188, Replace the hardcoded header strings in app/[locale]/page.tsx with translation keys (e.g., use t('welcome_back') for the <h1> and t('manage_finances') for the <p>) so they follow the same localization approach as the rest of the page; update your locale JSON files to include the new keys and corresponding translations, and ensure the component is importing/using the same translation helper (e.g., t or useTranslations) already used elsewhere in this file.i18n/request.ts (1)
8-11: Address TypeScript/ESLint issues for cleaner type handling.Per static analysis: use
@ts-expect-errorinstead of@ts-ignore, and avoidany. The type assertion can be improved.♻️ Proposed fix
- // `@ts-ignore` -export default getRequestConfig(async ({ locale }) => { +export default getRequestConfig(async ({ locale }: { locale: string }) => { // Validate that the incoming `locale` parameter is valid - if (!locales.includes(locale as any)) notFound(); + if (!locales.includes(locale as typeof locales[number])) notFound(); return { messages: (await import(`./messages/${locale}.json`)).default }; });This removes both the
@ts-ignoreandanycast while maintaining type safety.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@i18n/request.ts` around lines 8 - 11, Replace the blanket ignore and the use of `any` by tightening the types around `locale`: remove `// `@ts-ignore``, use `// `@ts-expect-error`` only where a known compiler quirk demands it, and change the `includes` check to use a properly typed guard (e.g., cast `locale` to the union/explicit type used by your `locales` array or run a type-safe predicate) so you call `notFound()` only when locale is not a valid member; update the `getRequestConfig` callback signature or perform a narrow type check against the `locales` symbol to avoid `any` while keeping the `notFound()` behavior intact (refer to getRequestConfig, locales, and notFound).app/send/page.tsx (2)
199-210: Missing"error"status in badge styling.The
getStatusBadgeClassNamehandles"completed","pending","failed", and a default case. If the API returns"error"as a distinct status, it would fall through to the neutral styling rather than destructive. Consider whether"error"should be handled alongside"failed".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/send/page.tsx` around lines 199 - 210, The getStatusBadgeClassName function currently maps "completed", "pending", "failed", and a default but omits the "error" status so "error" falls back to neutral styling; update getStatusBadgeClassName to include a case for "error" (treating it the same as "failed" or using the destructive classes) so API-returned "error" uses the destructive badge styling instead of the neutral default.
89-103: Consider using async/await for consistency.The
loadTransfersandloadContactscallbacks use Promise chains (.then().catch().finally()) while the rest of the codebase usesasync/await. This is functional but slightly inconsistent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/send/page.tsx` around lines 89 - 103, Both loadTransfers and loadContacts use Promise chains instead of async/await; convert each to an async function (keep the useCallback wrappers) and replace .then/.catch/.finally with try { const data = await transfersApi.getTransfers(opts); setTransfers(data.transfers ?? []); setLoadError(""); } catch (e) { setLoadError(e instanceof Error ? e.message : 'Failed to load transfers'); } finally { setLoadingTransfers(false); } (and similarly for loadContacts: await userApi.getContacts(opts), setContacts(...), setLoadingContacts(false)); keep the same state updates and error messages but use try/catch/finally for consistency.app/burn/page.tsx (2)
43-43: Replaceanywith proper Zod refinement types.ESLint flags explicit
anyusage. Zod provides proper types forsuperRefinecallbacks.♻️ Use Zod's refinement context type
-}).superRefine((data: any, ctx: any) => { +}).superRefine((data, ctx) => {TypeScript will infer the correct types from the schema definition. Alternatively, import and use
z.RefinementCtxexplicitly:import { z, RefinementCtx } from "zod"; // ... }).superRefine((data: z.infer<typeof baseSchema>, ctx: RefinementCtx) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/burn/page.tsx` at line 43, Replace the explicit any types in the superRefine callback with Zod refinement types: import z and RefinementCtx from "zod", change the callback signature from (data: any, ctx: any) to (data: z.infer<typeof <yourSchemaVariable>>, ctx: RefinementCtx) where <yourSchemaVariable> is the schema you called .superRefine on (or let TypeScript infer by using z.infer on that schema), and update any usages accordingly so ESLint no longer sees explicit any.
222-245: Type the error handling more strictly.The catch block and error details handling use
anytypes. Consider typing the API error structure based on theApiErrorinterface fromlib/api/client.ts.♻️ Improve error typing
- } catch (e: any) { + } catch (e: unknown) { + const apiErr = e as { status?: number; details?: Record<string, unknown> }; // Handle server-side validation errors if they follow a specific format - if (e?.status === 400 && e?.details) { - const details = e.details as any; + if (apiErr?.status === 400 && apiErr?.details) { + const details = apiErr.details; const errors = details.errors || (details.error && typeof details.error === 'object' ? details.error : null); if (errors && typeof errors === 'object') { - Object.entries(errors).forEach(([key, msg]) => { + Object.entries(errors as Record<string, string>).forEach(([key, msg]) => { const formKey = key === 'account_number' ? 'accountNumber' : key === 'bank_code' ? 'bankCode' : key === 'account_name' ? 'accountName' : key === 'acbu_amount' ? 'acbuAmount' : - key as any; + key as keyof BurnFormValues; if (['accountNumber', 'bankCode', 'accountName', 'acbuAmount', 'currency'].includes(formKey)) { - form.setError(formKey as any, { type: 'server', message: msg as string }); + form.setError(formKey as keyof BurnFormValues, { type: 'server', message: msg }); } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/burn/page.tsx` around lines 222 - 245, The catch currently types the exception as any and processes e.details loosely; import and use the ApiError type from lib/api/client.ts, change the catch parameter to unknown and add a type-guard function isApiError(err: unknown): err is ApiError to validate shape (status, details, details.errors), then narrow e with that guard before accessing details and errors; when iterating errors ensure the value is string (or map object to string) before passing to form.setError (referencing form.setError and setApiError) and fallback to setApiError(e) for non-ApiError cases.app/mint/page.tsx (1)
234-309: Consider extracting duplicated burn execution logic.
handleExecuteBurnduplicates most of the logic fromapp/burn/page.tsx'sonSubmit(wallet secret retrieval, address verification,submitBurnRedeemSingleClientcalls, wallet modal handling). This creates maintenance burden and divergence risk.Consider extracting shared wallet-signing logic into a utility (e.g.,
lib/stellar/wallet-sign.ts) that both pages can use.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/mint/page.tsx` around lines 234 - 309, handleExecuteBurn duplicates wallet-secret retrieval, address verification, wallet-modal flow and submitBurnRedeemSingleClient logic found in app/burn/page.tsx; extract that shared flow into a utility (e.g., lib/stellar/wallet-sign.ts) that exposes a function (e.g., signAndSubmitBurnOrRedeem) which accepts parameters used here (userId, stellarAddress, burnAmount, selectedFiatCurrency, kit, opts) and internally uses getWalletSecretAnyLocal, Keypair.fromSecret checks, kit.openModal wallet selection, and calls submitBurnRedeemSingleClient (or returns transactionHash) so both handleExecuteBurn and the onSubmit in app/burn/page.tsx call the same helper and then call fiatApi.postOffRamp/handle UI state; update both callers to use the new helper and remove the duplicated logic.components/error-boundary.tsx (1)
41-55: Consider adding accessibility attributes to error UI.Given this PR focuses on accessibility, the error fallback UI could benefit from
role="alert"andaria-live="assertive"to announce the error state to screen readers.♿ Suggested accessibility improvement
return ( - <div className="flex min-h-[200px] flex-col items-center justify-center gap-4 p-4 text-center"> + <div + className="flex min-h-[200px] flex-col items-center justify-center gap-4 p-4 text-center" + role="alert" + aria-live="assertive" + > <div className="rounded-full bg-red-100 p-3"> - <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> </svg> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/error-boundary.tsx` around lines 41 - 55, The error fallback UI is missing accessibility attributes; update the fallback container (the div with className "flex min-h-[200px] ... text-center" returned by the ErrorBoundary render) to include role="alert" and aria-live="assertive" so screen readers announce the error immediately, and ensure the interactive Button remains focusable (onClick={this.handleReset})—add these attributes directly on that container element in the ErrorBoundary component to make the error state perceivable to assistive tech.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/business/page.tsx`:
- Line 16: The card entry with id 'sme' currently points to the wrong route;
update its href value from '/sme' to the correct nested path '/business/sme' in
the array/object that contains { id: 'sme', title: 'SME Services', ... } inside
app/business/page.tsx so the card links to the existing page at
app/business/sme/page.tsx.
In `@app/error.tsx`:
- Around line 14-16: The current useEffect uses logger.error('Application
error:', error) which is inconsistent with other error boundaries; replace this
call with errorReporter.reportError and pass structured metadata similar to the
other pages (include page: 'app' or an appropriate page identifier and a digest
computed from the error/stack), i.e., locate the useEffect that references
logger.error and swap it to call errorReporter.reportError(error, { page: 'app',
digest: <computedDigest> }); ensure you import/use the same errorReporter
utility and compute the digest the same way other files do so the format matches
existing reports.
In `@app/globals.css`:
- Line 48: The CSS uses color-mix() for --status-neutral-border which may not be
available for all target browsers; add a safe fallback and a feature check:
declare a fallback value for --status-neutral-border (e.g., a precomputed hex or
rgba blend of --secondary and --border) before the color-mix() line, and wrap
the color-mix() assignment in an `@supports` (color-mix(in oklab, 0%)) {
--status-neutral-border: color-mix(in oklab, var(--secondary) 70%,
var(--border)); } so browsers without color-mix() use the fallback while capable
browsers get the mixed color. Ensure you update both instances where
--status-neutral-border is defined (the two occurrences using --secondary and
--border).
In `@app/layout.tsx`:
- Around line 70-76: RootLayout currently returns children without the required
top-level <html> and <body> elements; move the document root markup from the
nested locale layout into RootLayout by wrapping the returned children inside
the <html lang={...}> and <body> tags (preserve async RootLayout and the
children prop), and remove those tags from app/[locale]/layout.tsx so nested
layouts only render content; ensure any metadata, classNames, or providers that
were in the locale layout are transferred into RootLayout and language selection
is still passed down to children.
In `@app/mint/page.tsx`:
- Around line 582-589: The JSX mapping over rateRows (rateRows.map) uses
key={r.currency ?? r.rate}, which can produce non-unique keys when currency is
missing; change the key to include a stable fallback (e.g., key={`${r.currency
?? r.rate ?? index}`}) by using the map index as the final fallback so each Card
(Card component rendering AC BU/{r.currency ?? 'Rate'}) has a unique key; update
the map callback signature to (r, index) => ... and use that composite key to
avoid React reconciliation issues.
In `@app/savings/deposit/page.tsx`:
- Around line 69-70: The error paragraph rendering ({error}) is not in an
accessible live region; update the JSX that renders the error (the <p
className="text-destructive text-sm">{error}</p> instance) to be an ARIA live
alert by adding role="alert" or aria-live="assertive" (and aria-atomic="true")
so screen readers announce submit failures when the error appears; keep the same
styling and conditional rendering around the error variable.
In `@app/savings/withdraw/page.tsx`:
- Around line 77-78: The error message paragraph rendering the withdraw failure
(the JSX that shows {error} in the withdraw page component) should be marked as
a live region so assistive tech is notified; update the <p> that renders {error}
to include an accessibility attribute such as role="alert" or
aria-live="assertive" (and keep the existing classes and text content) so submit
failures are announced to screen readers.
In `@app/send/`[id]/page.tsx:
- Around line 223-230: The time element is still using new
Date(createdAt).toISOString(), which can produce "Invalid Date" for malformed
input; replace that direct call with the same safe approach used for display:
compute a validated ISO string (e.g., via the existing safeFormatDate helper or
a new safeToISOString utility) and use that for the dateTime attribute so
invalid dates become empty or a safe fallback; apply the same change for the
completedAt usage as well (update the <time> dateTime assignments that reference
createdAt and completedAt).
In `@contexts/auth-context.tsx`:
- Around line 22-24: The public auth API signature for login changed to
login(apiKey, userId, stellarAddress) but callers still pass (userId,
stellarAddress), causing setAuth to persist swapped values; update every call
site that invokes login (notably the sign-in and 2FA/verification handlers) to
pass result.api_key first, then result.user_id, then result.stellar_address
(i.e., login(result.api_key, result.user_id, result.stellar_address)); verify
any helper wrappers that forward parameters to login and update them similarly
so setAuth receives correct values.
In `@i18n/messages/en-KE.json`:
- Around line 2-14: The en-KE messages are missing the home.approx_usd key
present in the base English bundle; add a "approx_usd" entry under the "home"
object (matching the base English meaning, e.g. "Approx. USD") so that the
home.approx_usd lookup succeeds for the en-KE locale; update the "home" object
in the en-KE JSON to include "approx_usd": "<appropriate English text>" (use the
exact string from the base i18n if available).
In `@lib/logger.ts`:
- Around line 5-31: The logger currently uses the forbidden any type; introduce
a LogData type alias (e.g., type LogData = unknown) and replace all occurrences
of any with LogData: update the logMessage signature to logMessage(level:
LogLevel, message: string, data?: LogData), update the logger API methods
(info/warn/error/debug) to accept data?: LogData, and keep the existing logEntry
construction and console branches unchanged so the runtime behavior is identical
while satisfying `@typescript-eslint/no-explicit-any`.
- Around line 5-24: logMessage currently uses JSON.stringify directly and
accepts data: any, which fails on circular structures and strips Error details;
change the signature to use data?: unknown and implement a fail-safe serializer
(e.g., safeStringify) that (1) converts Error instances to plain objects
preserving name, message, stack and enumerable properties, (2) guards against
circular refs using a seen Set (or use util.inspect as fallback) and returns a
stable JSON string, and (3) never throws; then replace all
JSON.stringify(logEntry) calls in logMessage with safeStringify(logEntry) so
logging never fails and error stacks are preserved.
In `@package.json`:
- Around line 73-82: The devDependency versions in package.json are mismatched:
"@eslint/js" is ^10.0.1 while "eslint" is ^9.7.0; update one to align majors.
Either bump "eslint" to a v10 release (e.g., change the "eslint" entry to a
^10.x version) to match "@eslint/js@^10", or downgrade "@eslint/js" to a ^9.x
release so it matches the existing "eslint@^9.7.0"; update the package.json
devDependencies accordingly and run your package manager to refresh lockfiles.
---
Outside diff comments:
In `@app/`[locale]/page.tsx:
- Around line 310-354: The map callback parameter `t` in the transactions
rendering shadows the `t` translation function from useTranslations; rename the
callback parameter to `tx` in the transactions.slice(0, 5).map(...) callback and
update every reference inside that callback (key, href, tx.type,
tx.transaction_id, tx.created_at, tx.acbu_amount_burned, tx.amount_acbu,
tx.local_currency, tx.local_amount, tx.status, and any other tx.* uses) so
translation `t` remains available and no shadowing occurs while keeping the
existing formatting helpers (formatDate, formatAcbu, formatAmount) and
Badge/Link usage unchanged.
In `@app/burn/page.tsx`:
- Around line 145-221: The onSubmit handler uses an undefined identifier data;
replace all usages of data with the function parameter values (e.g., when
building recipientAccount.account_number/bank_code/account_name, passing
amount/currency to submitBurnRedeemSingleClient and burnApi.burnAcbu, and when
calling form.reset) so the function uses the passed BurnFormValues; verify
occurrences around recipientAccount construction, the two
submitBurnRedeemSingleClient calls, the burnApi.burnAcbu call,
setTxId/form.reset lines, and ensure variable names like
getWalletSecretAnyLocal, submitBurnRedeemSingleClient, burnApi.burnAcbu,
setTxId, and form.reset remain unchanged.
---
Nitpick comments:
In `@app/`[locale]/page.tsx:
- Around line 187-188: Replace the hardcoded header strings in
app/[locale]/page.tsx with translation keys (e.g., use t('welcome_back') for the
<h1> and t('manage_finances') for the <p>) so they follow the same localization
approach as the rest of the page; update your locale JSON files to include the
new keys and corresponding translations, and ensure the component is
importing/using the same translation helper (e.g., t or useTranslations) already
used elsewhere in this file.
In `@app/burn/page.tsx`:
- Line 43: Replace the explicit any types in the superRefine callback with Zod
refinement types: import z and RefinementCtx from "zod", change the callback
signature from (data: any, ctx: any) to (data: z.infer<typeof
<yourSchemaVariable>>, ctx: RefinementCtx) where <yourSchemaVariable> is the
schema you called .superRefine on (or let TypeScript infer by using z.infer on
that schema), and update any usages accordingly so ESLint no longer sees
explicit any.
- Around line 222-245: The catch currently types the exception as any and
processes e.details loosely; import and use the ApiError type from
lib/api/client.ts, change the catch parameter to unknown and add a type-guard
function isApiError(err: unknown): err is ApiError to validate shape (status,
details, details.errors), then narrow e with that guard before accessing details
and errors; when iterating errors ensure the value is string (or map object to
string) before passing to form.setError (referencing form.setError and
setApiError) and fallback to setApiError(e) for non-ApiError cases.
In `@app/mint/page.tsx`:
- Around line 234-309: handleExecuteBurn duplicates wallet-secret retrieval,
address verification, wallet-modal flow and submitBurnRedeemSingleClient logic
found in app/burn/page.tsx; extract that shared flow into a utility (e.g.,
lib/stellar/wallet-sign.ts) that exposes a function (e.g.,
signAndSubmitBurnOrRedeem) which accepts parameters used here (userId,
stellarAddress, burnAmount, selectedFiatCurrency, kit, opts) and internally uses
getWalletSecretAnyLocal, Keypair.fromSecret checks, kit.openModal wallet
selection, and calls submitBurnRedeemSingleClient (or returns transactionHash)
so both handleExecuteBurn and the onSubmit in app/burn/page.tsx call the same
helper and then call fiatApi.postOffRamp/handle UI state; update both callers to
use the new helper and remove the duplicated logic.
In `@app/send/`[id]/page.tsx:
- Around line 204-214: The span rendering the amount (the element with id
"amount-value" inside the outer span having aria-labelledby="amount-label")
currently includes aria-describedby pointing to its own child, which is
redundant and can confuse screen readers; remove the
aria-describedby="amount-value" attribute from the outer span (or from whichever
element references the same id) so the outer element only uses
aria-labelledby="amount-label" and the inner span keeps id="amount-value" for
any required reference elsewhere.
- Around line 263-267: The sr-only live region currently renders on every render
because it directly reads the current status; change this so it only mounts when
status actually changes by tracking the previous status and an announce flag
(e.g., add a useState like announce and prevStatus or useRef prevStatus, then in
a useEffect compare prevStatus.current to status and set announce true when they
differ, update prevStatus.current = status, and optionally clear announce after
a short timeout); then render the <div role="status" aria-live="polite"
aria-atomic="true"> only when announce is true so the screen reader only
receives announcements on transitions, and ensure you clear the announce flag
afterwards to avoid repeated announcements.
In `@app/send/page.tsx`:
- Around line 199-210: The getStatusBadgeClassName function currently maps
"completed", "pending", "failed", and a default but omits the "error" status so
"error" falls back to neutral styling; update getStatusBadgeClassName to include
a case for "error" (treating it the same as "failed" or using the destructive
classes) so API-returned "error" uses the destructive badge styling instead of
the neutral default.
- Around line 89-103: Both loadTransfers and loadContacts use Promise chains
instead of async/await; convert each to an async function (keep the useCallback
wrappers) and replace .then/.catch/.finally with try { const data = await
transfersApi.getTransfers(opts); setTransfers(data.transfers ?? []);
setLoadError(""); } catch (e) { setLoadError(e instanceof Error ? e.message :
'Failed to load transfers'); } finally { setLoadingTransfers(false); } (and
similarly for loadContacts: await userApi.getContacts(opts), setContacts(...),
setLoadingContacts(false)); keep the same state updates and error messages but
use try/catch/finally for consistency.
In `@components/error-boundary.tsx`:
- Around line 41-55: The error fallback UI is missing accessibility attributes;
update the fallback container (the div with className "flex min-h-[200px] ...
text-center" returned by the ErrorBoundary render) to include role="alert" and
aria-live="assertive" so screen readers announce the error immediately, and
ensure the interactive Button remains focusable (onClick={this.handleReset})—add
these attributes directly on that container element in the ErrorBoundary
component to make the error state perceivable to assistive tech.
In `@i18n/request.ts`:
- Around line 8-11: Replace the blanket ignore and the use of `any` by
tightening the types around `locale`: remove `// `@ts-ignore``, use `//
`@ts-expect-error`` only where a known compiler quirk demands it, and change the
`includes` check to use a properly typed guard (e.g., cast `locale` to the
union/explicit type used by your `locales` array or run a type-safe predicate)
so you call `notFound()` only when locale is not a valid member; update the
`getRequestConfig` callback signature or perform a narrow type check against the
`locales` symbol to avoid `any` while keeping the `notFound()` behavior intact
(refer to getRequestConfig, locales, and notFound).
In `@test-results/.last-run.json`:
- Around line 1-4: Remove the committed Playwright run state file
`.last-run.json` from source control and add it to the repository ignore list:
stop tracking the file (git rm --cached .last-run.json), commit that removal,
and add `.last-run.json` to `.gitignore` (or the project’s appropriate ignore
file) so future runs don’t produce noisy diffs or merge conflicts.
In `@tests/accessibility.spec.ts`:
- Around line 121-127: The current block silently ignores missing elements
(fiatSelect, amountInput) so tests can pass without asserting behavior; update
the test to explicitly fail or mark as skipped when these locators are not
found: after obtaining fiatSelect and amountInput (the locator variables used in
this snippet), assert their presence with Playwright assertions (e.g., use
expect(fiatSelect).not.toBeNull()/toBeVisible() or call test.skip() when
intentionally optional) or use expect.soft() to record a soft failure; ensure
you also handle the options count via fiatSelect.locator('option').count() only
after asserting fiatSelect exists so the test fails/records when elements are
missing rather than silently continuing.
- Line 17: The test uses an arbitrary pause via page.waitForTimeout(1000) which
makes the spec flaky; replace that call with a targeted wait such as waiting for
a specific element or network/state transition (e.g.,
page.waitForSelector('#selector'), page.waitForLoadState('networkidle'), or
page.waitForResponse(...) ) inside the same test function so the test proceeds
only when the required condition is met; locate and update the
page.waitForTimeout usage in tests/accessibility.spec.ts (search for
page.waitForTimeout) and swap each occurrence with an appropriate conditional
wait tied to the element or event that the subsequent assertions expect.
- Line 6: Add explicit TypeScript parameter types for the test helpers: annotate
the waitForPageReady(page) parameter with the Playwright Page type (e.g., page:
Page) and likewise annotate mockAuth's parameter(s) (the helper at line 28) with
their appropriate types; import the Page type from your test library (e.g.,
'@playwright/test' or 'playwright') at the top and update the function
signatures for waitForPageReady and mockAuth to use those types to restore type
safety.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d6a65d32-ef14-457c-bfbc-4ac1ecf2b53a
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (38)
.gitignoreapp/[locale]/activity/page.tsxapp/[locale]/auth/2fa/page.tsxapp/[locale]/auth/error.tsxapp/[locale]/auth/signin/page.tsxapp/[locale]/auth/signup/page.tsxapp/[locale]/auth/wallet-setup/page.tsxapp/[locale]/layout.tsxapp/[locale]/page.tsxapp/burn/page.tsxapp/business/page.tsxapp/error.tsxapp/globals.cssapp/help/page.tsxapp/layout.tsxapp/mint/page.tsxapp/savings/deposit/page.tsxapp/savings/withdraw/page.tsxapp/send/[id]/page.tsxapp/send/page.tsxcomponents/error-boundary.tsxcomponents/mobile-nav.tsxcontexts/auth-context.tsxglobal.d.tsi18n/messages/en-KE.jsoni18n/messages/en-NG.jsoni18n/messages/en.jsoni18n/request.tslib/logger.tslib/stellar-wallets-kit.tslib/stellar/trustlines.tsmiddleware.tsnext.config.mjspackage.jsonplaywright-report/index.htmlplaywright.config.tstest-results/.last-run.jsontests/accessibility.spec.ts
|
|
||
| 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' }, |
There was a problem hiding this comment.
Restore the SME route to /business/sme.
The SME page in this PR context lives at app/business/sme/page.tsx, so pointing the card at /sme sends users to a non-existent top-level route instead of the actual business page.
🛠️ Proposed fix
- { id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/sme' },
+ { id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/business/sme' },📝 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.
| { id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/sme' }, | |
| { id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/business/sme' }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/business/page.tsx` at line 16, The card entry with id 'sme' currently
points to the wrong route; update its href value from '/sme' to the correct
nested path '/business/sme' in the array/object that contains { id: 'sme',
title: 'SME Services', ... } inside app/business/page.tsx so the card links to
the existing page at app/business/sme/page.tsx.
| useEffect(() => { | ||
| errorReporter.reportError(error, { | ||
| level: 'page', | ||
| context: { | ||
| digest: error.digest, | ||
| type: 'route-error' | ||
| } | ||
| }); | ||
| logger.error('Application error:', error); | ||
| }, [error]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all error.tsx files and check which error handling pattern they use
echo "=== Files using errorReporter ==="
rg -l 'errorReporter' --glob '**/error.tsx'
echo ""
echo "=== Files using logger ==="
rg -l "logger\.(error|warn)" --glob '**/error.tsx'
echo ""
echo "=== Error handling patterns in error.tsx files ==="
rg -n '(errorReporter\.reportError|logger\.error)' --glob '**/error.tsx' -A 5Repository: Pi-Defi-world/acbu-frontend
Length of output: 2728
Inconsistent error handling pattern across error boundaries.
This file uses logger.error() while 7 other error boundary files (app/[locale]/auth/error.tsx, app/send/error.tsx, app/transactions/error.tsx, app/wallet/error.tsx, app/me/error.tsx, app/bills/error.tsx, app/savings/error.tsx) use errorReporter.reportError() with structured context including page and digest. This inconsistency complicates error tracking and monitoring.
Align with the existing pattern by either:
- Updating this file to use
errorReporterwith appropriate metadata, or - Refactoring all other error pages to use
loggerif this represents a new pattern.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/error.tsx` around lines 14 - 16, The current useEffect uses
logger.error('Application error:', error) which is inconsistent with other error
boundaries; replace this call with errorReporter.reportError and pass structured
metadata similar to the other pages (include page: 'app' or an appropriate page
identifier and a digest computed from the error/stack), i.e., locate the
useEffect that references logger.error and swap it to call
errorReporter.reportError(error, { page: 'app', digest: <computedDigest> });
ensure you import/use the same errorReporter utility and compute the digest the
same way other files do so the format matches existing reports.
| --status-warning-border: oklch(0.88 0.06 95); | ||
| --status-neutral: var(--secondary); | ||
| --status-neutral-foreground: var(--secondary-foreground); | ||
| --status-neutral-border: color-mix(in oklab, var(--secondary) 70%, var(--border)); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
color-mix CSS browser support 2026
💡 Result:
As of April 2026, the CSS color-mix function has excellent browser support across all modern browsers and versions in current use. It has been widely available since mid-2023. Key support details: - Chrome/Edge: 111+ (current: 146+) - Firefox: 113+ (current: 151+) - Safari/iOS Safari: 16.2+ (current: 26+) - Mobile browsers (Chrome/Firefox/Samsung Internet for Android): Fully supported in latest versions No support in IE or very old browsers (pre-2020). Global usage is near 98%+ for browsers released after 2021. MDN confirms it is "baseline widely available" across devices.
Citations:
- 1: https://caniuse.com/wf-color-mix
- 2: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix
- 3: https://developer.chrome.com/docs/css-ui/css-color-mix
- 4: https://caniuse.com/mdn-css_types_color_color-mix
- 5: https://developer.mozilla.org/blog/color-palettes-css-color-mix/
Verify color-mix() browser support for your target audience.
color-mix() is used for the neutral border (lines 48, 92). Browser support includes Chrome 111+, Firefox 113+, and Safari 16.2+. As of April 2026, color-mix() is baseline widely available with 98%+ coverage across browsers released after 2021. Confirm this aligns with your application's browser support requirements.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/globals.css` at line 48, The CSS uses color-mix() for
--status-neutral-border which may not be available for all target browsers; add
a safe fallback and a feature check: declare a fallback value for
--status-neutral-border (e.g., a precomputed hex or rgba blend of --secondary
and --border) before the color-mix() line, and wrap the color-mix() assignment
in an `@supports` (color-mix(in oklab, 0%)) { --status-neutral-border:
color-mix(in oklab, var(--secondary) 70%, var(--border)); } so browsers without
color-mix() use the fallback while capable browsers get the mixed color. Ensure
you update both instances where --status-neutral-border is defined (the two
occurrences using --secondary and --border).
| export default async function RootLayout({ | ||
| children, | ||
| }: Readonly<{ | ||
| children: React.ReactNode | ||
| }>) { | ||
| 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 /> | ||
| <Analytics nonce={nonce} /> | ||
| </AuthProvider> | ||
| </ErrorBoundary> | ||
| </body> | ||
| </html> | ||
| ) | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return children; | ||
| } No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files app/layout.tsxRepository: Pi-Defi-world/acbu-frontend
Length of output: 86
🏁 Script executed:
fd -type f -name "layout.tsx" app/Repository: Pi-Defi-world/acbu-frontend
Length of output: 242
🏁 Script executed:
fd . app/ --max-depth 2 -type f -name "*.tsx" | head -20Repository: Pi-Defi-world/acbu-frontend
Length of output: 242
🏁 Script executed:
cat -n app/layout.tsxRepository: Pi-Defi-world/acbu-frontend
Length of output: 2805
🏁 Script executed:
find app -name "layout.tsx" -o -name "layout.ts"Repository: Pi-Defi-world/acbu-frontend
Length of output: 110
🏁 Script executed:
ls -la app/Repository: Pi-Defi-world/acbu-frontend
Length of output: 1522
🏁 Script executed:
cat -n app/[locale]/layout.tsxRepository: Pi-Defi-world/acbu-frontend
Length of output: 2404
🌐 Web query:
Next.js App Router root layout html body tags requirement where to define
💡 Result:
In Next.js App Router, the root layout (app/layout.tsx or app/layout.js) is required and must define the <html> and <body> tags. These tags wrap the shared UI and children (pages or nested layouts). Nested layouts do not include <html> or <body> tags; they only add wrapping elements. Example from official docs: export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) } Do not manually add <head> tags like <title> or <meta>; use the Metadata API instead for handling title, meta, etc., which supports streaming and deduplication. The root layout is the top-most layout in the app directory and applies to all routes.
Citations:
- 1: https://en.nextjs.im/docs/app/api-reference/file-conventions/layout
- 2: Missing required html tags vercel/next.js#64544
- 3: https://nextjs.org/docs/app/getting-started/layouts-and-pages
Move the <html> and <body> tags to app/layout.tsx.
Next.js requires the root layout to define these tags. The current implementation violates this contract—nested layouts must not include them. The structure currently in app/[locale]/layout.tsx needs to move back to the root layout.
Suggested fix
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
- return children;
+ return (
+ <html>
+ <body>{children}</body>
+ </html>
+ );
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/layout.tsx` around lines 70 - 76, RootLayout currently returns children
without the required top-level <html> and <body> elements; move the document
root markup from the nested locale layout into RootLayout by wrapping the
returned children inside the <html lang={...}> and <body> tags (preserve async
RootLayout and the children prop), and remove those tags from
app/[locale]/layout.tsx so nested layouts only render content; ensure any
metadata, classNames, or providers that were in the locale layout are
transferred into RootLayout and language selection is still passed down to
children.
| rateRows.map((r: { currency?: string; rate?: number }) => ( | ||
| <Card key={r.currency ?? r.rate} className="border-border p-4"> | ||
| <div className="flex justify-between"> | ||
| <p className="font-semibold text-foreground">ACBU/{r.currency}</p> | ||
| <p className="text-lg font-bold text-primary">{formatRate(r.rate)}</p> | ||
| <p className="font-semibold text-foreground">ACBU/{r.currency ?? 'Rate'}</p> | ||
| <p className="text-lg font-bold text-primary">{r.rate != null ? String(r.rate) : '—'}</p> | ||
| </div> | ||
| </Card> | ||
| )) |
There was a problem hiding this comment.
Non-unique React keys when currency is missing.
If multiple rate rows have missing currency values, they'll share the same key (r.rate), potentially causing React reconciliation issues.
🐛 Use index as fallback key
- rateRows.map((r: { currency?: string; rate?: number }) => (
- <Card key={r.currency ?? r.rate} className="border-border p-4">
+ rateRows.map((r: { currency?: string; rate?: number }, idx: number) => (
+ <Card key={r.currency ?? `rate-${idx}`} className="border-border p-4">📝 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.
| rateRows.map((r: { currency?: string; rate?: number }) => ( | |
| <Card key={r.currency ?? r.rate} className="border-border p-4"> | |
| <div className="flex justify-between"> | |
| <p className="font-semibold text-foreground">ACBU/{r.currency}</p> | |
| <p className="text-lg font-bold text-primary">{formatRate(r.rate)}</p> | |
| <p className="font-semibold text-foreground">ACBU/{r.currency ?? 'Rate'}</p> | |
| <p className="text-lg font-bold text-primary">{r.rate != null ? String(r.rate) : '—'}</p> | |
| </div> | |
| </Card> | |
| )) | |
| rateRows.map((r: { currency?: string; rate?: number }, idx: number) => ( | |
| <Card key={r.currency ?? `rate-${idx}`} className="border-border p-4"> | |
| <div className="flex justify-between"> | |
| <p className="font-semibold text-foreground">ACBU/{r.currency ?? 'Rate'}</p> | |
| <p className="text-lg font-bold text-primary">{r.rate != null ? String(r.rate) : '—'}</p> | |
| </div> | |
| </Card> | |
| )) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/mint/page.tsx` around lines 582 - 589, The JSX mapping over rateRows
(rateRows.map) uses key={r.currency ?? r.rate}, which can produce non-unique
keys when currency is missing; change the key to include a stable fallback
(e.g., key={`${r.currency ?? r.rate ?? index}`}) by using the map index as the
final fallback so each Card (Card component rendering AC BU/{r.currency ??
'Rate'}) has a unique key; update the map callback signature to (r, index) =>
... and use that composite key to avoid React reconciliation issues.
| login: (apiKey: string, userId: string, stellarAddress?: string | null) => void; | ||
| logout: () => Promise<void>; | ||
| setAuth: (userId: string | null, stellarAddress?: string | null) => void; | ||
| setAuth: (apiKey: string | null, userId: string | null, stellarAddress?: string | null) => void; |
There was a problem hiding this comment.
Update the known login callers in the same change.
This public API now expects (apiKey, userId, stellarAddress), but the provided sign-in and 2FA call sites still pass (userId, stellarAddress). That means setAuth will persist the user id as acbu_api_key and the stellar address as acbu_user_id, which breaks auth immediately after login.
🛠️ Required caller shape
login(result.api_key, result.user_id, result.stellar_address);Also applies to: 111-114
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@contexts/auth-context.tsx` around lines 22 - 24, The public auth API
signature for login changed to login(apiKey, userId, stellarAddress) but callers
still pass (userId, stellarAddress), causing setAuth to persist swapped values;
update every call site that invokes login (notably the sign-in and
2FA/verification handlers) to pass result.api_key first, then result.user_id,
then result.stellar_address (i.e., login(result.api_key, result.user_id,
result.stellar_address)); verify any helper wrappers that forward parameters to
login and update them similarly so setAuth receives correct values.
| "home": { | ||
| "acbu": "ACBU", | ||
| "wallet_balance": "Wallet balance", | ||
| "fiat": "Fiat", | ||
| "simulated_usd_equivalent": "Simulated · USD equivalent", | ||
| "add_demo_funds": "Add demo funds", | ||
| "recent_activity": "Recent Activity", | ||
| "view_all": "View all", | ||
| "no_recent_activity": "No recent activity", | ||
| "send_money": "Send money", | ||
| "some_currencies_missing_rate": "Some currencies missing a rate", | ||
| "usd": "USD" | ||
| }, |
There was a problem hiding this comment.
Keep en-KE in sync with the base English bundle.
home.approx_usd exists in i18n/messages/en.json but is missing here. If the localized home page requests that key under en-KE, you'll get a missing-message fallback or runtime error depending on the next-intl config.
🌍 Proposed fix
"home": {
"acbu": "ACBU",
"wallet_balance": "Wallet balance",
"fiat": "Fiat",
"simulated_usd_equivalent": "Simulated · USD equivalent",
"add_demo_funds": "Add demo funds",
"recent_activity": "Recent Activity",
"view_all": "View all",
"no_recent_activity": "No recent activity",
"send_money": "Send money",
"some_currencies_missing_rate": "Some currencies missing a rate",
- "usd": "USD"
+ "usd": "USD",
+ "approx_usd": "≈ USD"
},📝 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.
| "home": { | |
| "acbu": "ACBU", | |
| "wallet_balance": "Wallet balance", | |
| "fiat": "Fiat", | |
| "simulated_usd_equivalent": "Simulated · USD equivalent", | |
| "add_demo_funds": "Add demo funds", | |
| "recent_activity": "Recent Activity", | |
| "view_all": "View all", | |
| "no_recent_activity": "No recent activity", | |
| "send_money": "Send money", | |
| "some_currencies_missing_rate": "Some currencies missing a rate", | |
| "usd": "USD" | |
| }, | |
| "home": { | |
| "acbu": "ACBU", | |
| "wallet_balance": "Wallet balance", | |
| "fiat": "Fiat", | |
| "simulated_usd_equivalent": "Simulated · USD equivalent", | |
| "add_demo_funds": "Add demo funds", | |
| "recent_activity": "Recent Activity", | |
| "view_all": "View all", | |
| "no_recent_activity": "No recent activity", | |
| "send_money": "Send money", | |
| "some_currencies_missing_rate": "Some currencies missing a rate", | |
| "usd": "USD", | |
| "approx_usd": "≈ USD" | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@i18n/messages/en-KE.json` around lines 2 - 14, The en-KE messages are missing
the home.approx_usd key present in the base English bundle; add a "approx_usd"
entry under the "home" object (matching the base English meaning, e.g. "Approx.
USD") so that the home.approx_usd lookup succeeds for the en-KE locale; update
the "home" object in the en-KE JSON to include "approx_usd": "<appropriate
English text>" (use the exact string from the base i18n if available).
| function logMessage(level: LogLevel, message: string, data?: any) { | ||
| if (!isDebug && level !== 'error') return; | ||
|
|
||
| const logEntry = { | ||
| timestamp: new Date().toISOString(), | ||
| level, | ||
| message, | ||
| ...(data !== undefined && { data }) | ||
| }; | ||
|
|
||
| // In debug mode, or for errors, output structured logs to console | ||
| if (level === 'error') { | ||
| console.error(JSON.stringify(logEntry)); | ||
| } else if (level === 'warn') { | ||
| console.warn(JSON.stringify(logEntry)); | ||
| } else if (level === 'info') { | ||
| console.info(JSON.stringify(logEntry)); | ||
| } else { | ||
| console.log(JSON.stringify(logEntry)); | ||
| } | ||
| } | ||
|
|
||
| export const logger = { | ||
| info: (message: string, data?: any) => logMessage('info', message, data), | ||
| warn: (message: string, data?: any) => logMessage('warn', message, data), | ||
| error: (message: string, data?: any) => logMessage('error', message, data), | ||
| debug: (message: string, data?: any) => logMessage('debug', message, data), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check if the file exists and its contents
git ls-files lib/logger.tsRepository: Pi-Defi-world/acbu-frontend
Length of output: 85
🏁 Script executed:
# Search for ESLint configuration files
fd -t f '\.eslintrc.*|eslint\.config\.(js|mjs|cjs)' .Repository: Pi-Defi-world/acbu-frontend
Length of output: 90
🏁 Script executed:
# Check for TypeScript configuration
git ls-files tsconfig.jsonRepository: Pi-Defi-world/acbu-frontend
Length of output: 85
🏁 Script executed:
# Read the logger file to verify its contents
cat -n lib/logger.tsRepository: Pi-Defi-world/acbu-frontend
Length of output: 1386
🏁 Script executed:
# Read ESLint configuration
cat -n eslint.config.jsRepository: Pi-Defi-world/acbu-frontend
Length of output: 527
🏁 Script executed:
# Read TypeScript configuration
cat -n tsconfig.jsonRepository: Pi-Defi-world/acbu-frontend
Length of output: 1053
Replace any in the logger API.
The file uses any annotations that violate the configured @typescript-eslint/no-explicit-any rule. Replace with unknown type and create a LogData type alias for consistency:
Suggested fix
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
+type LogData = unknown;
-function logMessage(level: LogLevel, message: string, data?: any) {
+function logMessage(level: LogLevel, message: string, data?: LogData) {
@@
export const logger = {
- info: (message: string, data?: any) => logMessage('info', message, data),
- warn: (message: string, data?: any) => logMessage('warn', message, data),
- error: (message: string, data?: any) => logMessage('error', message, data),
- debug: (message: string, data?: any) => logMessage('debug', message, data),
+ info: (message: string, data?: LogData) => logMessage('info', message, data),
+ warn: (message: string, data?: LogData) => logMessage('warn', message, data),
+ error: (message: string, data?: LogData) => logMessage('error', message, data),
+ debug: (message: string, data?: LogData) => logMessage('debug', message, data),
};🧰 Tools
🪛 ESLint
[error] 5-5: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 28-28: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 29-29: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 30-30: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 31-31: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/logger.ts` around lines 5 - 31, The logger currently uses the forbidden
any type; introduce a LogData type alias (e.g., type LogData = unknown) and
replace all occurrences of any with LogData: update the logMessage signature to
logMessage(level: LogLevel, message: string, data?: LogData), update the logger
API methods (info/warn/error/debug) to accept data?: LogData, and keep the
existing logEntry construction and console branches unchanged so the runtime
behavior is identical while satisfying `@typescript-eslint/no-explicit-any`.
| function logMessage(level: LogLevel, message: string, data?: any) { | ||
| if (!isDebug && level !== 'error') return; | ||
|
|
||
| const logEntry = { | ||
| timestamp: new Date().toISOString(), | ||
| level, | ||
| message, | ||
| ...(data !== undefined && { data }) | ||
| }; | ||
|
|
||
| // In debug mode, or for errors, output structured logs to console | ||
| if (level === 'error') { | ||
| console.error(JSON.stringify(logEntry)); | ||
| } else if (level === 'warn') { | ||
| console.warn(JSON.stringify(logEntry)); | ||
| } else if (level === 'info') { | ||
| console.info(JSON.stringify(logEntry)); | ||
| } else { | ||
| console.log(JSON.stringify(logEntry)); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "logger.ts" --full-pathRepository: Pi-Defi-world/acbu-frontend
Length of output: 85
🏁 Script executed:
cat -n lib/logger.tsRepository: Pi-Defi-world/acbu-frontend
Length of output: 1386
🏁 Script executed:
fd -t f -e json -e js -e ts "eslint" --full-path | head -20Repository: Pi-Defi-world/acbu-frontend
Length of output: 88
🏁 Script executed:
fd -t f "eslintrc*" --full-pathRepository: Pi-Defi-world/acbu-frontend
Length of output: 53
🏁 Script executed:
fd -t f "eslint.config.*" --full-pathRepository: Pi-Defi-world/acbu-frontend
Length of output: 88
🏁 Script executed:
cat -n eslint.config.jsRepository: Pi-Defi-world/acbu-frontend
Length of output: 527
🏁 Script executed:
rg -t ts -t js "serialize|JSON\.stringify" --max-count=20Repository: Pi-Defi-world/acbu-frontend
Length of output: 941
🏁 Script executed:
cat -n lib/error-reporting.ts | head -50Repository: Pi-Defi-world/acbu-frontend
Length of output: 1722
🏁 Script executed:
node <<'EOF'
const tseslint = require('typescript-eslint');
const config = tseslint.configs.recommended;
// Check if no-explicit-any is in the recommended rules
const rules = config[config.length - 1]?.rules || {};
console.log('Rules with "any":', Object.keys(rules).filter(k => k.includes('any')));
console.log('@typescript-eslint/no-explicit-any:', rules['@typescript-eslint/no-explicit-any']);
EOFRepository: Pi-Defi-world/acbu-frontend
Length of output: 172
🏁 Script executed:
node <<'EOF'
// Test JSON.stringify with Error objects
const testError = new Error('Test error');
console.log('Error serialized:', JSON.stringify({ error: testError }));
// Test with circular reference
const circular = { a: 1 };
circular.self = circular;
try {
console.log('Circular:', JSON.stringify(circular));
} catch (e) {
console.log('Circular throws:', e.message);
}
// Test with circular in nested object
const obj = { data: {} };
obj.data.parent = obj;
try {
console.log('Nested circular:', JSON.stringify(obj));
} catch (e) {
console.log('Nested circular throws:', e.message);
}
EOFRepository: Pi-Defi-world/acbu-frontend
Length of output: 473
Make log serialization fail-safe.
JSON.stringify(logEntry) throws on circular payloads (e.g., Converting circular structure to JSON) and collapses Error instances to {}, stripping stack traces. This causes logging calls to fail inside error handlers and lose the diagnostic information you are trying to capture.
The any type annotations also violate the @typescript-eslint/no-explicit-any rule (enabled as error in your ESLint config).
Suggested fix
+function safeSerialize(value: unknown) {
+ const seen = new WeakSet<object>();
+
+ return JSON.stringify(value, (_key, current) => {
+ if (current instanceof Error) {
+ return {
+ name: current.name,
+ message: current.message,
+ stack: current.stack,
+ };
+ }
+
+ if (typeof current === 'object' && current !== null) {
+ if (seen.has(current)) return '[Circular]';
+ seen.add(current);
+ }
+
+ return current;
+ });
+}
+
function logMessage(level: LogLevel, message: string, data?: any) {
if (!isDebug && level !== 'error') return;
@@
// In debug mode, or for errors, output structured logs to console
if (level === 'error') {
- console.error(JSON.stringify(logEntry));
+ console.error(safeSerialize(logEntry));
} else if (level === 'warn') {
- console.warn(JSON.stringify(logEntry));
+ console.warn(safeSerialize(logEntry));
} else if (level === 'info') {
- console.info(JSON.stringify(logEntry));
+ console.info(safeSerialize(logEntry));
} else {
- console.log(JSON.stringify(logEntry));
+ console.log(safeSerialize(logEntry));
}
}🧰 Tools
🪛 ESLint
[error] 5-5: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/logger.ts` around lines 5 - 24, logMessage currently uses JSON.stringify
directly and accepts data: any, which fails on circular structures and strips
Error details; change the signature to use data?: unknown and implement a
fail-safe serializer (e.g., safeStringify) that (1) converts Error instances to
plain objects preserving name, message, stack and enumerable properties, (2)
guards against circular refs using a seen Set (or use util.inspect as fallback)
and returns a stable JSON string, and (3) never throws; then replace all
JSON.stringify(logEntry) calls in logMessage with safeStringify(logEntry) so
logging never fails and error stacks are preserved.
| "devDependencies": { | ||
| "@axe-core/playwright": "^4.11.2", | ||
| "@eslint/js": "^10.0.1", | ||
| "@playwright/test": "^1.59.1", | ||
| "@tailwindcss/postcss": "^4.1.9", | ||
| "@types/node": "^22", | ||
| "@types/react": "^19", | ||
| "@types/react-dom": "^19", | ||
| "eslint": "^9.7.0", | ||
| "eslint-plugin-react": "^7.37.5", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
ESLint @eslint/js version compatibility eslint v9 v10
💡 Result:
ESLint v9 is compatible with @eslint/js ^9.x versions. ESLint v10 is compatible with @eslint/js ^10.x versions. @eslint/js v10.x explicitly supports ESLint ^10.0.0 as an optional peer dependency, as added in the ESLint v10 release process. The official migration guide for v10 mentions using @eslint/js v10.x, particularly noting restored 'name' properties in core configs and compatibility notes with @eslint/eslintrc FlatCompat. For v9, official docs and examples use @eslint/js ^9.x (e.g., v9.35.0+ shown in npm and migration guides). npm data shows @eslint/js latest is v10.0.1 (for ESLint v10), with v9.39.4 as the latest 9.x maintenance version (still actively downloaded). No cross-version compatibility issues are noted in official docs; each major ESLint version aligns with corresponding @eslint/js major versions. Ecosystem plugins may lag v10 support, but @eslint/js itself is designed for version matching.
Citations:
- 1: https://eslint.org/docs/latest/use/migrate-to-10.0.0
- 2: https://github.com/eslint/eslint/releases/tag/v10.0.0
- 3: https://eslint.org/docs/latest/use/migrate-to-9.0.0
- 4: https://registry.npmjs.org/eslint
- 5: https://www.npmjs.com/package/@eslint/js?activeTab=versions
- 6: chore: add
eslinttopeerDependenciesof@eslint/jseslint/eslint#20467 - 7: eslint/eslint@1f66734
🌐 Web query:
ESLint v10 migration guide breaking changes @eslint/js``
💡 Result:
ESLint v10.0.0 migration guide and breaking changes: Official migration guide: https://eslint.org/docs/latest/use/migrate-to-10.0.0 Key breaking changes for users: - Node.js versions below v20.19.0, v21.x, v23.x no longer supported. Requires Node.js ^20.19.0 || ^22.13.0 || >=24.0.0. Upgrade Node.js if necessary. - Old eslintrc configuration format (.eslintrc.*, package.json eslintConfig) completely removed. Must use flat config (eslint.config.js). Migrate using npx @eslint/migrate-config .eslintrc.json. Deprecated APIs FlatESLint and LegacyESLint removed; use ESLint class. - New default configuration file lookup: starts from linted file directory upward (previously cwd-based). Remove v10_config_lookup_from_file flag if used. - eslint:recommended updated with new rules. Re-run lint after migration. - eslint-env comments now reported as errors. Remove them from code. - Jiti < v2.2.0 no longer supported for TS configs. - JSX references now tracked by default, may cause new warnings (e.g., no-unused-vars). - stylish formatter uses native styleText instead of chalk. - Rule changes: radix rule deprecated "always"/"as-needed" options; no-shadow-restricted-names reports globalThis; func-names schema stricter; no-invalid-regexp allowConstructorFlags requires unique items. - name property added back to core configs from @eslint/js (v10 compatible). If using FlatCompat, upgrade @eslint/eslintrc. For @eslint/js in flat config (eslint.config.js): Install: npm install --save-dev @eslint/js Example: import js from "@eslint/js"; import { defineConfig } from "eslint/config"; // or "eslint/config" if available export default defineConfig([ js.configs.recommended, // or { plugins: { js }, extends: ["js/recommended"] }, { rules: { // overrides } } ]); Other changes for plugin/integration devs: removal of deprecated APIs/methods (SourceCode methods, context members, nodeType in LintMessage, etc.), Program AST range spans full text, stricter RuleTester, etc. ESLint v10 released Feb 2026. Most users can upgrade directly if on flat config from v9; otherwise migrate config first. Migration steps: 1. Upgrade Node.js. 2. npm install eslint@latest @eslint/js@latest 3. Migrate config if needed. 4. Remove eslint-env comments, legacy files. 5. Test lint run. 6. Update editor integrations (e.g., VSCode eslint.useFlatConfig default true).
Citations:
- 1: https://eslint.org/docs/latest/use/migrate-to-10.0.0
- 2: https://hi.eslint.org/docs/latest/use/migrate-to-10.0.0
- 3: https://eslint.org/docs/head/use/configure/configuration-files
- 4: https://eslint.org/docs/head/use/getting-started
🌐 Web query:
@eslint/js version 10 eslint version 9 compatibility
💡 Result:
@eslint/js version 10 is not compatible with ESLint version 9. Use @eslint/js version 9 instead. @eslint/js v10.0.0 was initially released by accident and marked as deprecated/avoid (Sep 2024), but a proper v10.0.1 was later released (as of 2026-04). However, @eslint/js v10 has a peer dependency on eslint ^10.0.0 (added intentionally after ESLint v10 release in Feb 2026), which prevents installation with ESLint v9 due to strict peer dep checking in npm. The official ESLint v10 migration guide mentions using @eslint/js v10.x only in the context of ESLint v10, and notes potential issues with legacy compat tools that require updating @eslint/eslintrc. No sources indicate backward compatibility of @eslint/js v10 with ESLint v9. For ESLint v9, stick to @eslint/js ^9.x (latest around 9.39.x as of early 2026). ESLint v9 remains in maintenance.
Citations:
- 1: why @eslint/js 10 is depreciated and 9 is latest? eslint/eslint#18887
- 2: https://eslint.org/docs/latest/use/migrate-to-10.0.0
- 3: https://github.com/eslint/eslint/releases/tag/v10.0.0
- 4: https://eslint.org/blog/2026/02/eslint-v10.0.0-released/
- 5: https://www.npmjs.com/package/@eslint/js
- 6: chore: add
eslinttopeerDependenciesof@eslint/jseslint/eslint#20467 - 7: eslint/eslint@1f66734
Align @eslint/js major version with eslint.
@eslint/js@^10.0.1 requires eslint@^10.0.0, but the current setup pairs it with eslint@^9.7.0. This version mismatch will cause peer dependency conflicts. Either upgrade eslint to v10 and use @eslint/js@^10, or downgrade @eslint/js to ^9.x to match eslint@^9.7.0.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` around lines 73 - 82, The devDependency versions in
package.json are mismatched: "@eslint/js" is ^10.0.1 while "eslint" is ^9.7.0;
update one to align majors. Either bump "eslint" to a v10 release (e.g., change
the "eslint" entry to a ^10.x version) to match "@eslint/js@^10", or downgrade
"@eslint/js" to a ^9.x release so it matches the existing "eslint@^9.7.0";
update the package.json devDependencies accordingly and run your package manager
to refresh lockfiles.
closes #218
Summary by CodeRabbit
New Features
Bug Fixes
Tests