Skip to content

Localization (en / es-419 / pt-BR) + zero ESLint errors#2447

Open
innolope-dev wants to merge 46 commits into
devfrom
chore/eslint-cleanup
Open

Localization (en / es-419 / pt-BR) + zero ESLint errors#2447
innolope-dev wants to merge 46 commits into
devfrom
chore/eslint-cleanup

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

What this is

Two tightly-coupled efforts on one branch (the lint guard is what enforces the localization, so they ship together):

  1. App localization — all product-UI copy extracted to next-intl: 1,982 message keys per locale in src/i18n/app/messages/ for English, Latin-American Spanish (es-419), and Brazilian Portuguese (pt-BR), an AppIntlProvider mounted above the context tree, and an ESLint guard (react/jsx-no-literals, scoped to translated surfaces) that fails CI when someone hard-codes new UI copy.
  2. ESLint cleanup — the eslint CI job goes from 832 errors on main to 0 on this branch.

Synced with main as of 2026-07-17 (all 55 commits merged in, 9 conflicts resolved in favor of main's newer copy/logic with the i18n calls preserved).

Lint stats: 832 → 0 errors

Baseline on main (232 files) vs this branch:

Rule main branch How it was resolved
@typescript-eslint/no-explicit-any 307 0 112 production sites properly typed (see below); tests/mocks and dev-only tooling exempted via commented config overrides
@next/next/no-html-link-for-pages 198 0 <a>next/link across marketing/app pages
@typescript-eslint/no-unused-vars 120 0 dead bindings removed; intentional ones _-prefixed
no-restricted-imports (barrel ban) 119 0 deep imports of @/interfaces, @/assets, @/context, @/components, @/config; dead barrels deleted
@typescript-eslint/no-require-imports 72 0 converted to ES imports; the Jest require-after-jest.mock() idiom exempted for test files
react/no-unknown-property 6 0 fixed; styled-jsx's jsx/global attrs whitelisted
import/first 5 0 imports hoisted
misc (display-name, alt-text, no-empty-object-type, stale disable directives) 5 0 fixed individually
Total errors 832 0

Warnings: 183 → 61, almost all react-hooks/exhaustive-deps. The provably-safe deps were fixed on this branch; the remainder genuinely change behavior when "fixed" and are left for a dedicated pass rather than blind suppression.

The no-explicit-any sweep (307 → 0)

  • 112 production sites across 61 files fixed with real types — zero inline suppressions. Patterns: generated API/domain types where they exist (ChainWithTokens, IOnrampData, viem/zerodev exports, StaticImageData, react-hook-form generics); catch (e: any)catch (e) with instanceof narrowing; opaque payloads (websocket, health probes) → unknown or a minimal local interface covering only the fields actually read; vendor globals (navigator.brave, screen.orientation.lock, window.opera) → typed structural casts.
  • Typing surfaced real latent bugs fixed along the way: an undefined React key on badge history entries (b.id is optional), and an any that poisoned a TanStack query's TError inference in qr-pay.
  • Scoped, commented config exemptions (production keeps the ban): tests/mocks/fixtures; dev-only tooling (/dev pages, the window.debug console cheats, the InvitesGraph debug visualization).

New guards this branch adds (all clean at merge time)

  • react/jsx-no-literals on translated surfaces — hard-coded UI copy fails CI. Exempted: marketing (own i18n), /dev, shared primitives that take copy as props, and the hidden fix-card-signature support tool (support DMs the URL; English by design).
  • react/no-unknown-property at error level with styled-jsx exemptions.
  • Guard-rails inherited from earlier PRs kept intact (router.back() ban, nuqs history: 'push' ban, barrel-import ban, self-import ban).

Verification

  • pnpm eslint . → 0 errors, 61 warnings (warnings don't fail the job)
  • pnpm tsc --noEmit → clean
  • Jest: 150/150 suites, 2,045 tests passing
  • pnpm prettier --check . → clean

Notes for reviewers

  • 507 files changed (+15,014 / −4,581) over 45 commits — the bulk is mechanical string extraction and the three locale JSON files. The riskiest commits are the exhaustive-deps fixes and the any → typed conversions; both are covered by the test suite and were reviewed for behavior preservation (runtime semantics deliberately unchanged).
  • Supersedes feat/app-localization — every commit of that branch is contained here; it can be deleted after this merges to avoid a second divergent merge of the same work.
  • es-419 / pt-BR translations were machine-assisted and reviewed for the high-traffic flows; a native-speaker pass on the long tail is a good follow-up.

…zation header

CapacitorHttp's Android GET proxy (_capacitor_http_interceptor_) stalls under
load, timing out every in-flight request after 10s (PEANUT-UI-R44): ~400
timeouts/user on Android vs ~3 on web/iOS over 14d, GETs only, in correlated
bursts. Requests now go direct from the webview, same path as web.

- auth: token moves from the CapacitorHttp cookie jar to @capacitor/preferences
  (survives webview storage eviction, unlike localStorage — PEANUT-UI-QTQ),
  hydrated into an in-memory cache and sent as Authorization. authReady() gates
  callApi and direct fetchWithSentry call sites against cold-start races.
- login: the ZeroDev SDK consumes /passkeys/*/verify responses internally, so a
  window.fetch wrapper captures the body token on native. /users/me sliding
  refresh keeps it current via the existing setAuthToken path.
- old binaries keep working: server still accepts the jwt-token cookie and
  hasNativeSession falls back to the legacy jar. Existing native sessions are
  not migrated — testers log in once after updating.
- fetchWithSentry: idempotent requests (GET/HEAD) get one silent retry on
  timeout before surfacing.
- canary: one-shot startup probe reports direct-fetch viability to Sentry
  (message:"direct-fetch canary", tags outcome/transport) so the transport
  switch is validated fleet-wide via OTA before the binary rolls out.
@capacitor/device requires the next native binary release; JS falls back
to navigator.language on older binaries.
Hoist the lazy viem require in peanut-claim.utils (viem is already
statically imported there) and scope the rule off for Jest test files,
where require() after jest.mock()/resetModules() is intentional.
Remove dead imports/declarations, use bare catch where the binding was
unused, and _-prefix signature-bound params and kept hook results.
Also ignore ios/ and build/ (generated) in eslint.
Replace raw <a> internal-route anchors with <Link> (client-side
navigation) across LandingPage components and the dev debug page.
- src/i18n/app: locale config + resolveLocale normalizer, catalogs
  (en/es-419/pt-BR), deep-merge loader so missing keys render English,
  runtime locale store (Preferences/Device on native, cookie on web),
  AppIntlProvider with hydration-safe English-first state
- provider wired into ClientProviders; native splash gated on the
  startup locale being painted (2s timeout guard)
- loadingStates union converted to a const array + key mapping
- jest: transform ESM-only intl packages (pnpm-aware ignore pattern)

Marketing i18n (src/i18n/*.json) untouched.
/settings/language screen with the supported locales, reached from a
new Profile row showing the active language. Live re-render on switch,
persisted via Preferences (native) / cookie (web).
The OPEN-status badge fix was picked up by 1eb7c3c ("fix(lint): resolve
no-unused-vars") along with that commit's lint sweep. It is unrelated to
localization and is being reviewed on its own branch against main
(peanut-ui#2430), so remove it here to keep the two changes separable.

Pure removal — no behaviour change beyond reverting to main's badge logic.
Step titles/descriptions keyed by screenId (removed from ISetupStep),
all Setup views, wrapper, and (setup) pages on useTranslations.
es-419 + pt-BR drafts included.
Home screen, activation CTAs, carousel, perk/welcome modals, tab bar,
desktop sidebar and top navbar. TopNavbar maps pathname to typed
navigation.* keys, replacing the pathTitles util.
ExchangeRateWidget (marketing-shared) takes an optional labels prop with
English defaults; product callers pass translated labels.
Limits warning-card items now carry a kind discriminator so render
sites can map them to translated copy; qr-pay uses it.
The warning card rendered raw English item text for the withdraw and
add-money callers while qr-pay mapped the kind discriminator at its own
render site. Resolve copy inside the card instead, so every flow gets it,
and drop the duplicated mapping from qr-pay.

Passkey troubleshooting steps and warnings become ids resolved against
setup.passkey.help.*, so the modal's content is translated, not just its
chrome.
The min/max cashout branches assigned the placeholder slugs
'offramp_lt_minimum'/'offramp_mt_maximum', which ErrorAlert rendered
verbatim — users saw the raw slug. Assign real messages with the limit
formatted as currency.

Also return on the over-maximum branch: it set the error and then kept
going, fetching a route and letting the flow continue past the limit,
unlike the under-minimum branch.
validatePin returns reason codes instead of English copy so the util
stays copy-free; CardPinSetupFlow maps them to messages.
CardCountryConfirmScreen feeds the active locale into Intl.DisplayNames
(was hardcoded to en), so country names follow the UI language.
Drop the vestigial capitalize class on the feed's type label: it existed
to case raw enum values (getActionText returned the type verbatim), but
the catalog now supplies cased copy, and CSS title-casing mangles
multi-word labels in every locale.

cardDeclineReason and the bank-account label util return codes now, with
the copy resolved at the render site.
Drop the capitalize class on the badge-unlocked label for the same
reason as the transaction feed: it title-cased translated copy.

Remove invites.consts.ts — it held only display copy, and its last
consumer now reads the setup-flow waitlist key so the two gates can't
drift.
sumsub-reject-labels.consts.ts becomes a copy-free code registry; the
62 reject labels move to the kyc namespace and resolve at the render
site, with unknown codes collapsing to FALLBACK as before.

recover-funds no longer prints a raw loadingState or raw token amounts,
and the KYC screens format dates through the active locale instead of a
hardcoded en-US.
Marketing-shared components under Global/ keep their English and take
copy as props instead: they render inside the app intl context, but the
marketing site resolves its language from the URL, so a shared component
would show the app locale on a marketing page.

Fix UserCard.getTitle reading fullName/username while only depending on
type, which rendered a stale name after a rename.
Extraction silently rewrote ' to the typographic form in 32 places,
changing English copy that ships today (e.g. the balance-warning modal's
"you're the only one who can access your funds"). Straighten them all so
en matches source and reads consistently; the translated catalogs are
unaffected.

Also wrap the useSumsubKycFlow test in an intl provider — the hook now
calls useTranslations and the suite never had one.
friendly-error.utils.tsx becomes copy-free: ErrorHandler is replaced by
friendlyError(), returning a code or backend-provided text, resolved to
a message by the new useFriendlyError hook. Backend copy (rain collateral,
stale-card re-enable with its dynamic retry hint) passes through untranslated.

Three sites compared the rendered error string against a constant to gate
UI; those now compare the error CODE, so the gate survives translation.
src/features/payments was missed by every earlier pass — a lint probe
caught it. Extends the existing payment namespace (no payments dupe);
contributor and receipt counts use ICU plurals, the 'on <chain>' label
reuses the shared tokenSelector.onChain rich-text key. Also covers the
KYC status drawer, sumsub load-error, and amount-input balance label.
Scopes react/jsx-no-literals to the translated surface so new hardcoded
JSX strings fail lint. Excludes tests, the /dev catalog, and marketing-
shared Global components; allowlists non-copy glyphs (card masks, %,
decorative emoji). Extraction stragglers the guard surfaced are also
handled: the beta/demo banners and the transaction 'Enjoy Peanut!' title.
useSendMoney calls useTranslations, and it runs inside ContextProvider
(via TokenContextProvider → useWallet), which was mounted ABOVE the intl
provider — so every route 500'd with a missing-context error. Unit tests
each wrap their subject in a provider, so only a full-app render caught
it. Move AppIntlProvider to wrap ContextProvider (still inside
PeanutProvider, which needs no translations).
'Video element not available' was shown to users when the video element
lost the mount race. Say what it means to them instead.

Adds a ClientProviders provider-order test: it walks the real element
tree rather than rendering it, so the AppIntlProvider-wraps-ContextProvider
contract is checked without mocking the wallet and kernel stack.
Everything except no-explicit-any and no-restricted-imports. Most of these
turned out to be malformed eslint-disable directives rather than the code
defects the rule names suggested.

react-hooks/exhaustive-deps (PerkClaimModal, 2): the disable comments used an
em-dash before the description instead of ESLint's `--` separator, so ESLint
parsed the whole string as a rule name. The suppressions were not suppressing
anything and both exhaustive-deps warnings were firing. Both effects are
deliberately mount-only, so the separator is fixed and the reasoning kept.

react/no-unknown-property (6): `<style jsx global>` is styled-jsx, a Next
built-in, not an invalid DOM attribute. Taught the rule via `ignore` in the
config — two files under Card/share-asset had already been hand-disabling it,
so those disables are now redundant and removed.

import/first (5), jsx-a11y/alt-text (1): both rules are unregistered here (the
config uses import-x, and jsx-a11y is not installed), so these directives only
produced "rule not found" errors. Removed; the Jest-ordering rationale is kept
as a plain comment. The alt-text site is a next/image test mock, not a real
accessibility defect.

react/display-name (1): named the forwardRef render function in a Card mock.
no-empty-object-type (1): ILinkDetails.rawOnchainDepositInfo `{}` ->
Record<string, unknown>; the field is declared but read nowhere.

Also removed 7 dead disable directives for rules that are off in tests
(no-require-imports) or no longer exist (no-var-requires).

eslint: 441 -> 424 errors, 157 -> 148 warnings. typecheck, jest (142 suites /
1974 tests) and prettier all green.
Rewrites all 55 bare `from '@/interfaces'` imports to the file that actually
owns the symbol. The barrel only re-exported ./interfaces and
./wallet.interfaces, so this was uniform except useAccountSetup.ts, which is
the one consumer of WalletProviderType. src/interfaces/index.ts had no
remaining referrers and is deleted.

Also removed a jest.mock('@/interfaces') in withdraw-states.test.tsx. The deep
imports made it dead (it no longer intercepted anything), and it was redundant
regardless: it stubbed AccountType with four members whose values match the
real enum exactly, while shadowing the other three (EVM_ADDRESS,
PEANUT_WALLET, MANTECA). The suite passes against the real enum.

eslint: 424 -> 368 errors. typecheck 0, jest 142 suites / 1974 tests green.
Rewrites all 45 bare `from '@/assets'` imports. The root barrel re-exported ten
category sub-barrels, so a single named import pulled the whole asset graph in.

Most symbols resolve to a leaf file (the sub-barrels are pure
`export { default as X } from './x.svg'` re-exports), so those become direct
default imports — which is already the dominant convention in this codebase.
The exceptions are the computed consts in assets/mascot and assets/cards
(PeanutWhistling = pick(webp, gif), APPLE_WALLET_STEPS); those are defined in
their category index rather than re-exported, so the index is the owning file
and they keep a named import from '@/assets/mascot'.

eslint: 368 -> 323 errors. typecheck 0, jest 142 suites / 1974 tests green.
Clears the last 18 no-restricted-imports violations and deletes all three
barrels, which had no referrers left. no-restricted-imports is now at 0.

The mechanical part was small; the risk was in what the barrels were hiding.
Rewriting the imports silently orphaned four jest.mock() calls that targeted
barrel paths the source no longer imports. The suite stayed green either way,
so these would not have surfaced on their own:

- SendLinkActionList.test.tsx mocked '@/context' with stub devconnect values;
  the component imports '@/context/tokenSelector.context', so it was getting
  the real context. Repointed.
- chainRegistry/nonEvmLeak tests mock '@/config' because the wagmi networks
  config cannot construct under jest (appkit env). TokenSelector.consts now
  imports '@/config/wagmi.config', so both mocks were dead and nonEvmLeak was
  filtering on the real network list rather than the curated ids it asserts
  against. Repointed.
- claim-states.test.tsx had a redundant '@/context' mock already superseded by
  a '@/context/tokenSelector.context' mock. Removed.

request-states.test.tsx used the '@/context' barrel purely as a definition
holder for two deep re-mocks; inlined each context into its own mock so the
barrel could go.

scripts/native-build.js generates a claim page containing
`import { Claim } from '@/components'`. That is emitted at native-build time,
so neither typecheck, jest nor eslint would have caught the barrel deletion
breaking it. Pointed at '@/components/Claim/Claim', matching the deep import
its sibling entry already uses.

eslint: 323 -> 305 errors (no-restricted-imports 119 -> 0). typecheck 0,
jest 142 suites / 1974 tests green.
no-img-element: 59 -> 23 warnings. The 36 cleared here are all structurally
required to be raw <img>, so they are scoped off in the config rather than
suppressed one by one:

- src/components/og/**, src/app/api/og/** (23): rendered through Satori via
  next/og ImageResponse, which supports a subset of HTML/CSS and cannot render
  next/image. Raw <img> with explicit width/height is the required form.
- Card/share-asset/**, Global/ImageGeneration/** (6): rasterized to PNG by
  html-to-image (captureShareAsset.ts). next/image's lazy loading and wrapper
  markup break the capture — the same class of bug as the runtime <canvas>
  that file already documents.
- tests (7): these mock next/image down to a raw <img>.

Two inline disables became redundant under the new scoping and are removed.

Also reverts the @/assets/mascot leaf imports introduced in 17c723b.
assets/mascot/index.ts documents "Import from '@/assets/mascot' only; do not
reach for raw file paths" — the sweep missed it. Only PEANUTMAN/PEANUTMAN_PFP
(plain svg stills) were affected, so there was no behavior change; the animated
mascots go through pick(webp, gif) and were already on named imports. Restoring
the convention keeps the module the canonical home. mascot is the only asset
sub-barrel carrying such a rule — the other nine keep leaf imports.

The remaining 23 no-img-element are real UI and are left for a follow-up: they
are a mix of svg (which next/image does not optimize), animated pick() mascots
(next/image would freeze the animation), and prop/remote sources — each needs
a visual check rather than a blanket rewrite.

typecheck 0, jest 142 suites / 1974 tests green.
The IntersectionObserver cleanup read footerRef.current at teardown, by which
point the ref can be null or a different node — so unobserve could silently
skip, leaking the observer and leaving a callback able to setState after
unmount. Capture the node at setup and unobserve that.

Also clears three exhaustive-deps warnings that are provably inert:

- withdraw/[country]/bank and PaymentSuccessView listed module-level imports
  (isBridgeSupportedCountry, getInitialsFromName) as deps. Module bindings
  never change, so the rule is right that they are not valid dependencies;
  removing them cannot alter firing.
- PaymentSuccessView also listed `type` and `message`, neither of which the
  memo body references (verified over the memo's full range, not just the
  rule's word).

useFeatureFlag keeps `[version]` and gains a correctly-placed disable. `version`
is a deliberate cache-buster — the docblock records that a stable checker
identity froze gated UI at mount-time values (2026-07 chain-rollout review), so
removing it would reintroduce that bug. The existing directive was spread over
two comment lines, so eslint-disable-next-line pointed at the second comment
rather than the deps array and never applied — the same malformed-suppression
class as the PerkClaimModal em-dashes.

warnings: 112 -> 108. typecheck 0, jest 142 suites / 1974 tests green.
Audits which of the 79 missing-dep warnings can be satisfied without changing
when an effect fires, and fixes only those 12.

The audit: a missing dep is safe to add only if its identity is stable, and a
warning is safe only if EVERY dep it lists is stable.

- loadingStateContext and tokenSelectorContext expose their setters as raw
  useState setters, whose identity React guarantees. Adding setLoadingState,
  setSelectedChainID, setSelectedTokenAddress, setIsXChain or
  setRefetchXchainRoute to a dep array cannot re-fire an effect.
- react-redux's dispatch is likewise stable.

Both providers build their context value as a fresh object literal per render
rather than memoizing it. That costs every consumer a re-render whenever the
provider renders, but it does not affect the setters' identity, so it does not
change this conclusion. Worth a separate look.

The 67 left are not mechanical, and the naming actively misleads:

- tokenSelectorContext.updateSelectedChainID is a plain render-body function —
  a new identity every render. Adding it to deps would re-fire on every render.
  resetTokenContextProvider is useCallback([isPeanutWallet]), so it churns on
  connect/disconnect.
- setHasFetchedRoute, setSelectedRoute, setShowVerificationModal and
  setTransactionHash read like useState setters but are props; their stability
  is the parent's choice, not React's. setCurrentDenomination is an optional
  prop. setErrorMessage on the manteca page is a useCallback wrapper, not a
  setter.

So "it starts with set" is not evidence of anything here. Each of the rest is a
behavior decision needing its own read of the component.

warnings: 108 -> 97 (exhaustive-deps 85 -> 74). typecheck 0, jest 142 suites /
1974 tests green — a re-fire loop would surface as a timeout, and none did.
AmountInput built `denominations` as a plain object literal on every render,
then read prices out of it from three memos that did not depend on it. So a
price update could leave exchangeRate — and the converted amount shown to the
user — stale, because the memos only recomputed on displaySymbol /
secondaryDenomination changes.

Memoizing `denominations` fixes that and clears four of the file's seven
warnings honestly, rather than by adding a per-render object to three dep
arrays (which would have defeated the memos entirely). It is keyed on the
denomination fields, not the prop objects: primaryDenomination has an
object-literal default, so a prop-identity dep would rebuild it every render
and change nothing.

Also adds primaryDenomination.symbol to the publish effect. It decides which
consumer receives the display value and which the converted one, so a stale
read there reports the two amounts the wrong way round.

The remaining three are deliberate and now carry a reason: the initialAmount
sync is one-way by design (it writes displayValue, so depending on it would
re-run per keystroke), and the setCurrentDenomination /
setPrimary/Secondary/DisplayedAmount props are the parent's identity —
including them re-fires on every parent render.

Placement note: exhaustive-deps anchors on the dependency array, not the hook
call, so the directives sit immediately above `}, [...])`. A disable above
`useEffect(` silently does nothing — the same trap as the two suppressions
already fixed on this branch.

AmountInput: 7 -> 0 warnings. typecheck 0, jest 142 suites / 1974 tests green.
Six warnings, four different answers — none of them mechanical.

The three paymentLock hooks keyed on `paymentLock?.code` while reading
paymentAgainstAmount / paymentAssetAmount / paymentPrice out of the object.
paymentLock is set once (guarded by `!paymentLock`) and otherwise only reset to
null, so its identity changes exactly when these should re-run: depending on
the object is both safe and less fragile than the field key, which would miss a
lock whose amount changed without its code changing.

The other two must NOT take their missing deps:

- The scan effect would take `resetState`, a plain render-body function with a
  new identity every render. Including it re-runs the per-scan state reset on
  every render and wipes the amount mid-entry.
- The KYC watcher would take `sumsubFlow`, a fresh object from
  useMultiPhaseKycFlow each render, and call completeFlow() repeatedly. Its
  field-level deps already cover everything the body reads.

Both keep their current deps and now carry the reason.

`balance` was a genuinely unused dep on the payment-submit callback — the only
occurrences in the body are a comment and rainCardOverview?.balance, a property
on a different object. Removed.

qr-pay: 6 -> 0 warnings. typecheck 0, jest 142 suites / 1974 tests green.
Traced every setter Initial.view was missing back to its definition. The view
is reached only through Claim.tsx -> FlowManager (a component map that passes
`props` straight through), so the prop setters are Claim.tsx's own useState
setters, and setShowVerificationModal / setHideTokenSelector /
setClaimToMercadoPago / setRegionalMethodType are raw useState setters that
ClaimBankFlowContext puts on its value unwrapped. All stable, so adding them
cannot change when these hooks run. Applied to the three warnings whose missing
deps are all setters.

resetSelectedToken is the interesting one. The rule calls claimLinkData an
unnecessary dependency and it is indeed unused in the body — but it is what
gives the callback a new identity per link, and two effects depend on that
identity to reset the token selection when new link data arrives. Removing it
would have silently stopped those resets. Kept, with the reason recorded.

Initial.view: 11 -> 7 warnings. The rest mix stable setters with genuinely
reactive values (recipient.address, selectedRoute, inputChanging,
claimLinkData.status, handleClaimLink), so each changes firing and needs its
own read.

overall warnings: 84 -> 80. typecheck 0, jest 142 suites / 1974 tests green.
src/types/react-force-graph.d.ts hand-declared 'react-force-graph-2d' and
'd3-force'. It accounted for 28 of the 305 no-explicit-any errors, and it was
not just redundant — the ambient `declare module` shadowed the types
react-force-graph-2d already ships (dist/react-force-graph-2d.d.ts), replacing
properly generic GraphData<NodeType, LinkType> / NodeObject<NodeType> with
`any` plus a `[key: string]: any` index signature that disabled prop checking
on the component entirely.

react-force-graph-2d's own types are used now. d3-force ships none, so
@types/d3-force (the DefinitelyTyped package, 3.0.10) replaces the hand-rolled
block. Both are dynamically imported by InvitesGraph and typecheck clean
against the real definitions, so the shim was pure loss.

src/types: 29 -> 1 no-explicit-any. eslint: 305 -> 277 errors.

Worth recording: this does NOT cascade to consumers the way the cleanup plan
assumed. no-explicit-any counts `any` tokens written in a file, so fixing a
shared type never removes a consumer's own annotations — InvitesGraph still has
its 34. What changed is that real types now exist to write against; before,
there was nothing to migrate to.

typecheck 0, jest 142 suites / 1974 tests green.
Eight of the no-explicit-any errors lived in ILinkDetails (senderAddress,
tokenType, tokenAddress, tokenDecimals, tokenSymbol, tokenName, tokenURI,
metadata). None of them were worth typing: the interface is dead.

Its only referrers were two `claimLinkData?: ILinkDetails` slots on the Offramp
prop types, and nothing anywhere reads them. They were also mistyped — the
value Claim actually carries is ClaimLinkData, a different shape; FlowManager's
`as React.FC<ClaimPropsType>` cast is what kept the mismatch quiet.

A properly typed equivalent already exists and is the one in real use:
CountryListRouter reads senderAddress / tokenDecimals / tokenSymbol off
ClaimLinkData. ILinkDetails was the legacy duplicate, and it was where Phase 1's
`rawOnchainDepositInfo: {}` (also unread) lived.

Removed the interface and the two dead prop slots rather than inventing types
for fields no code consumes.

eslint: 277 -> 269 errors. typecheck 0, jest 142 suites / 1974 tests green.
Fifteen no-explicit-any errors were the same two untyped globals, cast at every
call site. global.d.ts already augments Window and Navigator, so both have a
real home:

- window.Capacitor: `(window as any).Capacitor` appeared twice in
  utils/capacitor.ts and eleven times in its test. The shape the code actually
  relies on is small and unambiguous — getPlatform(), plus an optional
  isNativePlatform() (capacitor.ts already reads it with `?.`, and several tests
  set the object without it).
- navigator.standalone: iOS Safari's non-standard installed-PWA flag, cast in
  utils/capacitor.ts and again in hooks/usePWAStatus.ts. Declaring it optional
  on Navigator cleared both.

These replace casts with types rather than moving the `any` somewhere quieter:
the declarations are what the runtime actually provides, and both files now
read the globals directly.

eslint: 269 -> 254 errors. typecheck 0, jest 142 suites / 1974 tests green.
Convert plain <img> to next/image across marketing, landing, and loading
components (static imports where possible; explicit width/height, priority/
sizes on the above-the-fold hero, unoptimized for the animated mascot).

Scope the rule off where <img> is required: Satori OG routes (plain HTML
only), share-asset components (html-to-image decode semantics), and test
files that mock next/image as <img> by design.
The web-safe test requires @/utils/demo → general.utils → app/actions/clients,
whose module-scope createPublicClient calls start recurring 60s fallback-rank
timers that ping real RPC endpoints and keep the Jest worker from exiting
(the 'worker failed to exit gracefully' warning). Mock the clients module —
nothing in this suite needs it.
Conflict resolutions keep the i18n t() calls while adopting main's newer
copy and logic:
- youreSending -> youreWithdrawing (manteca withdraw + PeanutActionDetailsCard),
  copy updated in all three locales
- SumsubKycWrapper exit modal: main's simplified single-variant structure
  with the existing wrapper.exitForNow* keys
- IframeWrapper: main's removal of StartVerificationView (dead
  global.startVerification i18n namespace dropped too), exit copy updated
- Bridge account holder rows: t() labels + main's resolveBridgeAccountHolderName
- AddMoneyBankDetails: t() strings + main's shortDepositReference helper
- Confirm.withdraw.view: tErrors insufficient-balance + main's belowMinimum block
- new main-side tests wrapped in NextIntlClientProvider (components now
  require intl context)
- src/content pinned to main's newer mirror sync (contains the legal change
  the branch had pinned in isolation)
…ssions

- i18n-extract the cancel-deposit confirm modal (ICU select keeps es/pt
  gender agreement for deposit vs request)
- exempt the hidden fix-card-signature support tool from the i18n guard
  (support DMs the URL; copy is English by design)
- allow explicit any in tests/mocks and dev-only tooling (/dev pages,
  window.debug cheats, InvitesGraph) — production keeps the ban
…ction code

112 sites across 61 files, no blanket suppressions:
- real types where they exist (generated API types, viem/zerodev exports,
  ChainWithTokens, StaticImageData, IconName, react-hook-form generics)
- catch (e: any) -> catch (e) with instanceof narrowing
- opaque payloads (websocket, health probes, vendor globals) -> unknown or
  a minimal local interface covering the fields actually read
- browser/vendor extensions (navigator.brave, screen.orientation.lock,
  window.opera, capgo shim flag) -> typed structural casts instead of any
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 17, 2026 10:13pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1ebc6312-17a4-469d-80d0-dd974c71c680

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/eslint-cleanup

Comment @coderabbitai help to get the list of available commands.

Keeps the i18n calls while adopting dev's newer work:
- AddMoneyBankDetails: dev's SEPA name-match checklist items extracted to
  bankDetails.doubleCheckSenderName/RecipientName in all three locales
- SumsubKycWrapper test: dev's watchdog-era rewrite taken wholesale, with
  the NextIntlClientProvider wrapper re-added (component requires intl)
@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6207.44 → 6548.72 (+341.28)
Findings: +3 net (+1374 new, -1371 resolved)

🆕 New findings (1374)

  • critical complexity — src/components/Global/InvitesGraph/index.tsx — CC 517, MI 53.14, SLOC 1718
  • critical complexity — src/app/(mobile-ui)/qr-pay/page.tsx — CC 303, MI 52.64, SLOC 1073
  • critical complexity — src/components/Claim/Link/Initial.view.tsx — CC 213, MI 50.68, SLOC 731
  • critical complexity — src/utils/general.utils.ts — CC 196, MI 57.4, SLOC 750
  • critical complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 161, MI 51.49, SLOC 421
  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 156, MI 51.57, SLOC 595
  • critical complexity — src/components/AddWithdraw/DynamicBankAccountForm.tsx — CC 150, MI 52.05, SLOC 461
  • critical complexity — src/components/Global/TokenSelector/TokenSelector.tsx — CC 126, MI 60.47, SLOC 353
  • critical complexity — src/app/(mobile-ui)/withdraw/page.tsx — CC 125, MI 52.93, SLOC 365
  • critical complexity — src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 123, MI 56.36, SLOC 369
  • critical complexity — src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 110, MI 46.94, SLOC 425
  • critical method-complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:89 — CC 108 SLOC 216
  • critical complexity — src/utils/demo-api.ts — CC 107, MI 59.53, SLOC 914
  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 106, MI 56.56, SLOC 432
  • critical complexity — src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 106, MI 49.11, SLOC 457
  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 102, MI 53.36, SLOC 386
  • critical complexity — src/components/Home/HomeHistory.tsx — CC 102, MI 57.43, SLOC 317
  • critical complexity — src/context/kernelClient.context.tsx — CC 101, MI 57.09, SLOC 477
  • critical complexity — src/components/Claim/Claim.tsx — CC 100, MI 53.54, SLOC 404
  • critical complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 96, MI 57.13, SLOC 365

…and 1354 more.

✅ Resolved (1371)

  • src/components/Global/InvitesGraph/index.tsx — CC 520, MI 53.13, SLOC 1714
  • src/app/(mobile-ui)/qr-pay/page.tsx — CC 304, MI 53.59, SLOC 971
  • src/components/Claim/Link/Initial.view.tsx — CC 212, MI 51.11, SLOC 686
  • src/utils/general.utils.ts — CC 199, MI 57.49, SLOC 755
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 160, MI 52.89, SLOC 352
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 155, MI 52.57, SLOC 528
  • src/components/AddWithdraw/DynamicBankAccountForm.tsx — CC 149, MI 53.51, SLOC 410
  • src/app/(mobile-ui)/withdraw/page.tsx — CC 125, MI 53.75, SLOC 339
  • src/components/Global/TokenSelector/TokenSelector.tsx — CC 125, MI 60.87, SLOC 331
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 123, MI 56.9, SLOC 351
  • src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 110, MI 47.15, SLOC 417
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:74 — CC 108 SLOC 170
  • src/utils/demo-api.ts — CC 107, MI 59.55, SLOC 914
  • src/app/(mobile-ui)/card/page.tsx — CC 106, MI 56.91, SLOC 419
  • src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 106, MI 49.34, SLOC 448
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 101, MI 54.47, SLOC 352
  • src/context/kernelClient.context.tsx — CC 101, MI 57.1, SLOC 477
  • src/components/Claim/Claim.tsx — CC 100, MI 53.93, SLOC 389
  • src/components/Home/HomeHistory.tsx — CC 97, MI 58.3, SLOC 296
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 96, MI 57.55, SLOC 347

…and 1351 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/native-auth-capture.ts 0.0 6.8 +6.8
src/i18n/app/AppIntlProvider.tsx 0.0 6.6 +6.6
src/utils/native-canary.ts 0.0 5.9 +5.9
src/components/TransactionDetails/useReceiptDateFormatter.ts 0.0 5.7 +5.7
src/i18n/app/config.ts 0.0 5.6 +5.6
src/components/Settings/LanguageView.tsx 0.0 5.4 +5.4
src/components/Global/FileUploadInput/index.tsx 5.4 10.7 +5.3
src/i18n/app/locale-store.ts 0.0 5.2 +5.2
src/i18n/app/messages.ts 0.0 5.2 +5.2
src/hooks/useFriendlyError.ts 0.0 5.0 +5.0
src/i18n/app/loading-states.ts 0.0 4.6 +4.6
src/components/Global/ConfirmInviteModal/index.tsx 3.0 7.5 +4.5
src/features/limits/components/LimitsDocsLink.tsx 1.2 4.9 +3.7
src/components/Badges/BadgeDetailModal.tsx 2.9 6.6 +3.6
src/components/Kyc/states/KycCompleted.tsx 6.4 10.0 +3.6
src/components/Global/PeanutActionCard/index.tsx 1.9 5.3 +3.4
src/components/Global/SuccessViewComponents/SuccessViewDetailsCard.tsx 2.1 5.4 +3.4
src/features/limits/components/LimitsWarningCard.tsx 4.2 7.5 +3.3
src/components/Global/RainCooldown/IntroModal.tsx 4.6 7.9 +3.3
src/components/Kyc/states/KycProcessing.tsx 5.2 8.5 +3.3

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2050 ran, 0 failed, 0 skipped, 33.8s

📊 Coverage (unit)

metric %
statements 58.8%
branches 42.3%
functions 47.6%
lines 59.2%
⏱ 10 slowest test cases
time test
3.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.2s src/utils/__tests__/auth-token.test.ts › returns Authorization on capacitor once a token is set
0.2s src/utils/__tests__/auth-token.test.ts › returns the token hydrated from Preferences after authReady
0.2s src/i18n/app/__tests__/messages.test.ts › every es-419 message compiles and formats
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

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.

1 participant