Skip to content

feat: limits in settings #1620

Merged
kushagrasarathe merged 20 commits intopeanut-wallet-devfrom
feat/limits
Jan 20, 2026
Merged

feat: limits in settings #1620
kushagrasarathe merged 20 commits intopeanut-wallet-devfrom
feat/limits

Conversation

@kushagrasarathe
Copy link
Contributor

@kushagrasarathe kushagrasarathe commented Jan 14, 2026

  • contributes to TASK-17857
  • also contributes to TASK-17856

  • when user has NO kyc
Screenshot 2026-01-14 at 2 59 10 PM
  • when user has only bridge kyc
image
  • when user has only manteca kyc
    dont have screenshot for this one, but it would basically be like above with latam unlocked and bridge entries in below

  • when user both manteca and bridge kyc

image

other internal pages screenshots

  • bridge limits
Screenshot 2026-01-14 at 3 14 54 PM
  • manteca limits
Screenshot 2026-01-14 at 3 15 47 PM

@vercel
Copy link

vercel bot commented Jan 14, 2026

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

Project Deployment Review Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jan 20, 2026 0:02am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

Walkthrough

This PR adds a comprehensive payment limits feature including new page routes for region-specific limit views, UI components for displaying transaction limits, validation hooks for enforcing limits across payment flows, new type definitions, and utility functions for currency handling and flag resolution.

Changes

Cohort / File(s) Summary
New Limits Routes & Pages
src/app/(mobile-ui)/limits/page.tsx, src/app/(mobile-ui)/limits/[provider]/page.tsx
Introduces main limits page and dynamic provider-specific limits page (bridge/manteca) with validation against LIMITS_PROVIDERS.
Limits Views (Core Components)
src/features/limits/views/BridgeLimitsView.tsx, src/features/limits/views/MantecaLimitsView.tsx, src/features/limits/views/LimitsPageView.tsx
Adds three view components: BridgeLimitsView renders per-transaction limits for bank transfers and QR payments with country expansion; MantecaLimitsView displays per-currency limits with monthly/yearly toggle; LimitsPageView composes regions, KYC state, and crypto limits.
Limits UI Components
src/features/limits/components/LimitsProgressBar.tsx, src/features/limits/components/PeriodToggle.tsx, src/features/limits/components/LimitsWarningCard.tsx, src/features/limits/components/CryptoLimitsSection.tsx, src/features/limits/components/FiatLimitsLockedCard.tsx, src/features/limits/components/IncreaseLimitsButton.tsx, src/features/limits/components/LimitsDocsLink.tsx, src/features/limits/components/LimitsError.tsx
Introduces reusable UI components: progress bars for remaining amounts, period toggles, warning/error cards with support links, crypto limits section, locked state card, increase limits button, and error handling.
Limits Hooks & Validation
src/hooks/useLimits.ts, src/features/limits/hooks/useLimitsValidation.ts
Adds useLimits hook for fetching user limits from API; useLimitsValidation hook validates transaction amounts against manteca/bridge/QR limits with blocking/warning states and per-flow-type logic.
Limits Configuration & Utilities
src/features/limits/consts.ts, src/features/limits/utils.ts
Defines LIMITS_PROVIDERS, QR country handling, and utility functions for region routing, currency mapping, flag URLs, limit data extraction, and warning card generation.
Type Definitions
src/interfaces/interfaces.ts
Adds BridgeLimits, MantecaLimit, and UserLimitsResponse interfaces for type-safe limit data.
Payment Constants
src/constants/payment.consts.ts, src/constants/query.consts.ts
Introduces MAX/MIN_MANTECA_WITHDRAW_AMOUNT, MAX_QR_PAYMENT_AMOUNT_FOREIGN constants and LIMITS query key.
Currency & Flag Utilities
src/constants/countryCurrencyMapping.ts
Adds public utility functions: getFlagUrl, getCurrencyFlagCode, getCurrencyFlagUrl, getCountryByCurrency for centralized flag and currency resolution.
Integration into Existing Flows
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/(mobile-ui)/qr-pay/page.tsx, src/app/(mobile-ui)/withdraw/page.tsx, src/app/(mobile-ui)/withdraw/manteca/page.tsx, src/components/AddMoney/components/InputAmountStep.tsx, src/components/AddMoney/components/MantecaAddMoney.tsx
Integrates limits validation and warning cards into add-money, qr-pay, and withdraw flows; updates button disabled states and error display logic to respect limits blocking.
UI & Profile Updates
src/components/Global/Icons/Icon.tsx, src/components/Profile/index.tsx, src/app/not-found.tsx
Adds meter and info-filled icons; inserts Payment limits menu item in profile; updates not-found page styling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • peanutprotocol/peanut-ui#1621: Directly related parallel work introducing the same limits validation, warning cards, and integration into payment flows.

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: limits in settings' is vague and does not clearly convey the primary changes. It omits key scope details like multi-provider support, KYC-state handling, and transaction limit visualization. Revise to a more specific title such as 'feat: add transaction limits UI for bridge and manteca providers' to better communicate the main feature scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is related to the changeset, providing task references and UI screenshots across KYC states and providers, though it lacks detailed technical explanation of implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/limits

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/components/Limits/BridgeLimitsPage.view.tsx`:
- Around line 138-146: The anchor with empty href in BridgeLimitsPage.view.tsx
causes a page reload; update the component to conditionally render the <a>
element (the "See more about limits" link with className "text-center text-sm
underline") only when a non-empty docsUrl is available (e.g., a prop or local
constant named docsUrl); if docsUrl is falsy, omit the anchor (or render a
non-clickable span) instead, and when rendering use href={docsUrl} plus target
and rel as before.

In `@src/components/Limits/LimitsPage.view.tsx`:
- Around line 63-73: The "Locked regions" header and LockedRegionsList are being
rendered when hasRestOfWorld is true even if filteredLockedRegions is empty,
producing an empty section; change the render condition to only show the "Locked
regions" block when filteredLockedRegions.length > 0 (remove hasRestOfWorld from
that condition) so the header and LockedRegionsList are only shown when there
are actual regions to list, and ensure Rest of World continues to be handled
separately in the "Other regions" area.

In `@src/components/Limits/MantecaLimitsPage.view.tsx`:
- Around line 100-113: Replace the placeholder anchor that uses href="#" and
target="_blank" (the <a> element with text "See more about limits") because it
opens the current page in a new tab; either hide/remove this anchor until a real
docs URL is provided, or replace it with a non-navigating disabled control
(e.g., a disabled Button component or an anchor rendered conditionally when
docsUrl is truthy). Update the UI logic around the Button and the anchor so the
Increase my limits Button remains actionable while the docs link is only
rendered/active when a valid URL (e.g., docsUrl) exists, and ensure any fallback
uses rel/noopener only when actually opening an external link.

In `@src/components/Limits/utils.limits.ts`:
- Around line 38-40: The comment for LIMIT_WARNING_THRESHOLD is inconsistent
with its value: update either the comment or the constant so they match; either
change the comment "<30% = red" to "<=20% = red" to reflect
LIMIT_WARNING_THRESHOLD = 20, or set LIMIT_WARNING_THRESHOLD to 30 if the
intended cutoff is 30; ensure any logic using LIMIT_WARNING_THRESHOLD and
LIMIT_HEALTHY_THRESHOLD remains consistent with the chosen threshold.

In `@src/hooks/useLimits.ts`:
- Around line 22-42: In fetchLimits, ensure you await response.json() so JSON
parse errors propagate (change the final return to await response.json()), and
guard the Authorization header built from Cookies.get('jwt-token') (in the
headers object) by checking for a missing token and either omitting the header
or throwing a clear error before calling fetchWithSentry; reference function
fetchLimits, fetchWithSentry, response.json, and Cookies.get('jwt-token') when
locating the changes.
🧹 Nitpick comments (8)
src/components/Limits/consts.limits.ts (1)

2-3: Consider adding as const for type safety.

BRIDGE_REGIONS and MANTECA_REGIONS are typed as string[] rather than literal tuples. Adding as const would enable type-safe checks when validating region values elsewhere in the codebase (similar to how LIMITS_PROVIDERS and QR_COUNTRIES are defined).

Suggested change
-export const BRIDGE_REGIONS = ['europe', 'north-america', 'mexico', 'argentina', 'brazil']
-export const MANTECA_REGIONS = ['latam']
+export const BRIDGE_REGIONS = ['europe', 'north-america', 'mexico', 'argentina', 'brazil'] as const
+export const MANTECA_REGIONS = ['latam'] as const
src/components/Limits/components/PeriodToggle.tsx (2)

5-5: Consider reusing LimitsPeriod from consts.

The Period type here duplicates the LimitsPeriod type already exported from consts.limits.ts. Reusing the existing type maintains a single source of truth.

Suggested change
 'use client'
 
 import { Root, List, Trigger } from '@radix-ui/react-tabs'
+import { type LimitsPeriod } from '../consts.limits'
 
-type Period = 'monthly' | 'yearly'
+type Period = LimitsPeriod

21-32: Extract duplicate className to reduce repetition.

Both Trigger elements share identical className strings. Extracting to a constant improves maintainability.

Suggested change
 export default function PeriodToggle({ value, onChange, className }: PeriodToggleProps) {
+    const triggerClassName =
+        'rounded-xl border border-transparent px-3 py-1 text-xs font-medium text-grey-1 transition-all data-[state=active]:border-primary-1 data-[state=active]:bg-primary-1/10 data-[state=active]:text-primary-1'
+
     return (
         <Root value={value} onValueChange={(v) => onChange(v as Period)} className={className}>
             <List className="flex items-center rounded-xl bg-grey-4 p-0" aria-label="Select period">
-                <Trigger
-                    value="monthly"
-                    className="rounded-xl border border-transparent px-3 py-1 text-xs font-medium text-grey-1 transition-all data-[state=active]:border-primary-1 data-[state=active]:bg-primary-1/10 data-[state=active]:text-primary-1"
-                >
+                <Trigger value="monthly" className={triggerClassName}>
                     Monthly
                 </Trigger>
-                <Trigger
-                    value="yearly"
-                    className="rounded-xl border border-transparent px-3 py-1 text-xs font-medium text-grey-1 transition-all data-[state=active]:border-primary-1 data-[state=active]:bg-primary-1/10 data-[state=active]:text-primary-1"
-                >
+                <Trigger value="yearly" className={triggerClassName}>
                     Yearly
                 </Trigger>
             </List>
         </Root>
     )
 }
src/app/(mobile-ui)/limits/[provider]/page.tsx (1)

14-17: Type assertion before validation is redundant.

The as LimitsProvider cast on Line 15 doesn't add type safety since it's cast before validation. The includes() method on a readonly tuple accepts broader types anyway. Consider removing the cast or using a type guard for cleaner code.

Suggested change with type guard
+function isValidProvider(provider: string): provider is LimitsProvider {
+    return LIMITS_PROVIDERS.includes(provider as LimitsProvider)
+}
+
 export default async function ProviderLimitsPage({ params }: ProviderLimitsPageProps) {
     const { provider } = await params
 
-    // validate provider - notFound() is safe in server components
-    if (!LIMITS_PROVIDERS.includes(provider as LimitsProvider)) {
+    if (!isValidProvider(provider)) {
         notFound()
     }
+    
+    // provider is now narrowed to LimitsProvider
src/components/Limits/components/LimitsProgressBar.tsx (1)

19-34: Consider using consistent percent value for color and width.

The component uses clampedPercent for the bar width (line 30) but remainingPercent (unclamped) for the color class (line 28). If remaining > total (e.g., due to data issues), the color could reflect >100% while width is capped at 100%.

♻️ Suggested fix for consistency
 const LimitsProgressBar = ({ total, remaining }: LimitsProgressBarProps) => {
     const remainingPercent = total > 0 ? (remaining / total) * 100 : 0
     const clampedPercent = Math.min(Math.max(remainingPercent, 0), 100)

     return (
         <div className="relative h-1.5 w-full overflow-hidden rounded-full bg-grey-2">
             <div
                 className={twMerge(
                     'absolute left-0 h-full rounded-full transition-all duration-300',
-                    getLimitColorClass(remainingPercent, 'bg')
+                    getLimitColorClass(clampedPercent, 'bg')
                 )}
                 style={{ width: `${clampedPercent}%` }}
             />
         </div>
     )
 }
src/components/Limits/BridgeLimitsPage.view.tsx (1)

40-43: Currency formatting only handles USD symbol.

The formatLimit function only adds $ for USD; other currencies like EUR will display as EUR1,000 instead of €1,000. Consider using the LIMITS_CURRENCY_SYMBOLS constant from consts.limits.ts for consistency with MantecaLimitsPage.

♻️ Suggested fix
+import { LIMITS_CURRENCY_SYMBOLS } from './consts.limits'

     // format limit amount with currency symbol
     const formatLimit = (amount: string, asset: string) => {
-        const symbol = asset === 'USD' ? '$' : asset
+        const symbol = LIMITS_CURRENCY_SYMBOLS[asset] || asset
         return `${symbol}${Number(amount).toLocaleString()}`
     }
src/components/Limits/utils.limits.ts (1)

25-36: Consider handling potential NaN from parseFloat.

If the API returns malformed strings for limit values, parseFloat will return NaN, which could cause display issues. Consider adding fallback to 0.

♻️ Suggested defensive fix
 export function getLimitData(limit: MantecaLimit, period: LimitsPeriod) {
     if (period === 'monthly') {
         return {
-            limit: parseFloat(limit.monthlyLimit),
-            remaining: parseFloat(limit.availableMonthlyLimit),
+            limit: parseFloat(limit.monthlyLimit) || 0,
+            remaining: parseFloat(limit.availableMonthlyLimit) || 0,
         }
     }
     return {
-        limit: parseFloat(limit.yearlyLimit),
-        remaining: parseFloat(limit.availableYearlyLimit),
+        limit: parseFloat(limit.yearlyLimit) || 0,
+        remaining: parseFloat(limit.availableYearlyLimit) || 0,
     }
 }
src/components/Limits/LimitsPage.view.tsx (1)

27-31: Consider moving static config outside the component.

restOfWorldRegion is a static object that doesn't depend on any props or state. Moving it outside the component would avoid recreating the object on every render.

Suggested refactor
+const REST_OF_WORLD_REGION: Region = {
+    path: 'rest-of-the-world',
+    name: 'Rest of the world',
+    icon: REST_OF_WORLD_GLOBE_ICON,
+}
+
 const LimitsPage = () => {
     const router = useRouter()
     const { unlockedRegions, lockedRegions } = useIdentityVerification()
     const { isUserKycApproved, isUserBridgeKycUnderReview, isUserMantecaKycApproved } = useKycStatus()

     // check if user has any kyc at all
     const hasAnyKyc = isUserKycApproved

-    // rest of world region config (static)
-    const restOfWorldRegion: Region = {
-        path: 'rest-of-the-world',
-        name: 'Rest of the world',
-        icon: REST_OF_WORLD_GLOBE_ICON,
-    }

Then reference REST_OF_WORLD_REGION instead of restOfWorldRegion in the JSX.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa4601c and a48b55f.

📒 Files selected for processing (18)
  • src/app/(mobile-ui)/limits/[provider]/page.tsx
  • src/app/(mobile-ui)/limits/page.tsx
  • src/app/not-found.tsx
  • src/components/Global/Icons/Icon.tsx
  • src/components/Limits/BridgeLimitsPage.view.tsx
  • src/components/Limits/LimitsPage.view.tsx
  • src/components/Limits/MantecaLimitsPage.view.tsx
  • src/components/Limits/components/CryptoLimitsSection.tsx
  • src/components/Limits/components/FiatLimitsLockedCard.tsx
  • src/components/Limits/components/LimitsProgressBar.tsx
  • src/components/Limits/components/PeriodToggle.tsx
  • src/components/Limits/consts.limits.ts
  • src/components/Limits/utils.limits.ts
  • src/components/Profile/index.tsx
  • src/constants/payment.consts.ts
  • src/constants/query.consts.ts
  • src/hooks/useLimits.ts
  • src/interfaces/interfaces.ts
🧰 Additional context used
🧠 Learnings (15)
📚 Learning: 2024-10-08T20:13:42.967Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 420
File: src/components/Offramp/Offramp.consts.ts:27-28
Timestamp: 2024-10-08T20:13:42.967Z
Learning: In `src/components/Offramp/Offramp.consts.ts`, the `MIN_CASHOUT_LIMIT` is set to $10 because smaller amounts are impractical due to approximately $1 fee per cashout.

Applied to files:

  • src/constants/query.consts.ts
  • src/constants/payment.consts.ts
  • src/components/Limits/consts.limits.ts
📚 Learning: 2025-10-08T17:13:13.155Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1299
File: src/app/(mobile-ui)/points/page.tsx:41-51
Timestamp: 2025-10-08T17:13:13.155Z
Learning: In `src/app/(mobile-ui)/points/page.tsx`, the icon name "invite-heart" is intentionally used (not "inviter-heart") when displaying who invited the current user, as this is a deliberate design choice despite semantic differences with UserHeader usage.

Applied to files:

  • src/components/Global/Icons/Icon.tsx
📚 Learning: 2024-10-25T11:33:46.776Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 484
File: src/components/Cashout/Components/Initial.view.tsx:273-274
Timestamp: 2024-10-25T11:33:46.776Z
Learning: In the `InitialCashoutView` component (`src/components/Cashout/Components/Initial.view.tsx`), linked bank accounts should not generate error states, and the `ValidatedInput` component will clear any error messages if needed. Therefore, it's unnecessary to manually clear the error state when selecting or clearing linked bank accounts.

Applied to files:

  • src/components/Limits/components/FiatLimitsLockedCard.tsx
📚 Learning: 2025-11-06T08:19:05.201Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1407
File: src/components/AddWithdraw/DynamicBankAccountForm.tsx:459-475
Timestamp: 2025-11-06T08:19:05.201Z
Learning: In src/components/AddWithdraw/DynamicBankAccountForm.tsx, the street address field has a 35-character maximum length limit because the backend provider enforces this constraint, applicable to both US and Mexican addresses.

Applied to files:

  • src/components/Limits/BridgeLimitsPage.view.tsx
📚 Learning: 2025-05-22T15:38:48.586Z
Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 869
File: src/app/(mobile-ui)/withdraw/page.tsx:82-88
Timestamp: 2025-05-22T15:38:48.586Z
Learning: The country-specific withdrawal route exists at src/app/(mobile-ui)/withdraw/[...country]/page.tsx and renders the AddWithdrawCountriesList component with flow="withdraw".

Applied to files:

  • src/app/(mobile-ui)/limits/page.tsx
📚 Learning: 2025-08-14T14:42:54.411Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1094
File: src/utils/withdraw.utils.ts:181-191
Timestamp: 2025-08-14T14:42:54.411Z
Learning: The countryCodeMap in src/components/AddMoney/consts/index.ts uses uppercase 3-letter country codes as keys (like 'AUT', 'BEL', 'CZE') that map to 2-letter country codes, requiring input normalization to uppercase for proper lookups.

Applied to files:

  • src/constants/payment.consts.ts
  • src/components/Limits/consts.limits.ts
📚 Learning: 2024-10-29T12:19:41.968Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 495
File: src/components/Global/TokenAmountInput/index.tsx:23-30
Timestamp: 2024-10-29T12:19:41.968Z
Learning: In the `TokenAmountInput` component (`src/components/Global/TokenAmountInput/index.tsx`), when the 'Max' button is clicked, we intentionally set the input denomination to 'TOKEN' because we are setting the value as token.

Applied to files:

  • src/constants/payment.consts.ts
📚 Learning: 2025-07-07T20:22:11.092Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 958
File: src/app/actions/tokens.ts:266-266
Timestamp: 2025-07-07T20:22:11.092Z
Learning: In `src/app/actions/tokens.ts`, within the `fetchWalletBalances` function, using the non-null assertion operator `!` on `process.env.MOBULA_API_KEY!` is intentional and correct, and should not be flagged for replacement with explicit validation.

Applied to files:

  • src/constants/payment.consts.ts
📚 Learning: 2024-10-07T15:28:25.280Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:28:25.280Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(estimatedGasCost, 3)` return strings, ensuring consistent return types for `calculatedFee`.

Applied to files:

  • src/constants/payment.consts.ts
📚 Learning: 2025-10-29T11:27:59.248Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1368
File: src/components/Common/ActionList.tsx:109-111
Timestamp: 2025-10-29T11:27:59.248Z
Learning: In `src/components/Common/ActionList.tsx`, the `balance` from `useWallet()` hook is always in USDC (as a formatted string), making it directly comparable to USD amounts without conversion. The comparison `Number(balance) >= amountInUsd` is intentional and correct.

Applied to files:

  • src/constants/payment.consts.ts
📚 Learning: 2024-10-07T15:25:45.170Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:25:45.170Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(...)` return strings, ensuring that `calculatedFee` consistently returns a string without the need for additional type conversion.

Applied to files:

  • src/constants/payment.consts.ts
📚 Learning: 2024-12-11T10:13:22.806Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 564
File: src/components/Request/Pay/Views/Initial.view.tsx:430-430
Timestamp: 2024-12-11T10:13:22.806Z
Learning: In the React TypeScript file `src/components/Request/Pay/Views/Initial.view.tsx`, when reviewing the `InitialView` component, do not flag potential issues with using non-null assertion `!` on the `slippagePercentage` variable, as handling undefined values in this context is considered out of scope.

Applied to files:

  • src/components/Limits/MantecaLimitsPage.view.tsx
📚 Learning: 2025-09-18T09:30:42.901Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1230
File: src/app/(mobile-ui)/withdraw/page.tsx:92-97
Timestamp: 2025-09-18T09:30:42.901Z
Learning: In src/app/(mobile-ui)/withdraw/page.tsx, the useEffect that calls setShowAllWithdrawMethods(true) when amountFromContext exists is intentionally designed to run only on component mount (empty dependency array), not when amountFromContext changes. This is the correct behavior for the withdraw flow where showing all methods should only happen on initial load when an amount is already present.

Applied to files:

  • src/components/Limits/MantecaLimitsPage.view.tsx
📚 Learning: 2025-08-12T17:47:28.362Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1089
File: src/app/api/bridge/exchange-rate/route.ts:4-19
Timestamp: 2025-08-12T17:47:28.362Z
Learning: In the Bridge exchange rate API route (src/app/api/bridge/exchange-rate/route.ts), the ExchangeRateResponse interface uses numeric types for rates because the route converts string values from the Bridge API to floats using parseFloat() before returning the response.

Applied to files:

  • src/interfaces/interfaces.ts
📚 Learning: 2025-07-24T13:26:10.290Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1014
File: src/components/Claim/Link/Initial.view.tsx:413-413
Timestamp: 2025-07-24T13:26:10.290Z
Learning: In the peanut-ui repository, the change from `${SQUID_API_URL}/route` to `${SQUID_API_URL}/v2/route` in src/components/Claim/Link/Initial.view.tsx was a typo fix, not an API migration, as the codebase was already using Squid API v2.

Applied to files:

  • src/app/not-found.tsx
🧬 Code graph analysis (8)
src/components/Limits/components/FiatLimitsLockedCard.tsx (3)
src/components/0_Bruddle/Card.tsx (1)
  • Card (72-72)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (289-298)
src/components/0_Bruddle/Button.tsx (1)
  • Button (80-283)
src/hooks/useLimits.ts (4)
src/interfaces/interfaces.ts (1)
  • UserLimitsResponse (377-380)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (79-83)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (36-150)
src/constants/query.consts.ts (1)
  • LIMITS (6-6)
src/components/Limits/BridgeLimitsPage.view.tsx (5)
src/hooks/useLimits.ts (1)
  • useLimits (19-65)
src/hooks/useKycStatus.tsx (1)
  • useKycStatus (12-36)
src/components/Limits/consts.limits.ts (4)
  • BridgeRegion (15-15)
  • QrCountryId (26-26)
  • BANK_TRANSFER_REGIONS (18-18)
  • QR_COUNTRIES (21-24)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (289-298)
src/constants/payment.consts.ts (1)
  • MAX_QR_PAYMENT_AMOUNT_FOREIGN (12-12)
src/components/Limits/components/LimitsProgressBar.tsx (1)
src/components/Limits/utils.limits.ts (1)
  • getLimitColorClass (46-54)
src/app/(mobile-ui)/limits/[provider]/page.tsx (1)
src/components/Limits/consts.limits.ts (2)
  • LIMITS_PROVIDERS (28-28)
  • LimitsProvider (29-29)
src/components/Limits/components/CryptoLimitsSection.tsx (3)
src/components/0_Bruddle/Card.tsx (1)
  • Card (72-72)
src/components/Tooltip/index.tsx (1)
  • Tooltip (18-106)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (289-298)
src/components/Limits/LimitsPage.view.tsx (3)
src/hooks/useIdentityVerification.tsx (2)
  • useIdentityVerification (97-272)
  • Region (11-16)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (15-20)
src/components/Limits/utils.limits.ts (1)
  • getProviderRoute (8-20)
src/components/Limits/utils.limits.ts (2)
src/components/Limits/consts.limits.ts (4)
  • MANTECA_REGIONS (3-3)
  • BRIDGE_REGIONS (2-2)
  • REGION_TO_BRIDGE_PARAM (6-12)
  • LimitsPeriod (46-46)
src/interfaces/interfaces.ts (1)
  • MantecaLimit (367-375)
🔇 Additional comments (20)
src/app/not-found.tsx (1)

10-14: LGTM!

These are minor styling updates that add visual consistency to the 404 page with shadows and borders. The changes align with the design system patterns (shadow-4, border-n-1, btn-medium) and don't affect functionality.

src/constants/query.consts.ts (1)

6-6: LGTM!

The new LIMITS query key constant follows the established naming conventions and is appropriately placed alongside other query key constants.

src/constants/payment.consts.ts (1)

10-12: LGTM!

The constant is well-documented and appropriately grouped with other QR payment limit constants. The value aligns with the existing MAX_MANTECA_DEPOSIT_AMOUNT.

src/components/Global/Icons/Icon.tsx (1)

65-65: LGTM!

The new meter icon is properly integrated:

  • Import follows the established pattern for MUI icons
  • IconName union includes the new name
  • Component mapping uses the standard MaterialIconWrapper pattern

Also applies to: 92-92, 279-279

src/components/Profile/index.tsx (1)

90-91: LGTM!

The new "Limits" menu item is properly integrated into the profile menu:

  • Uses the new meter icon correctly
  • Positioned logically after "Regions & Verification"
  • Follows the established ProfileMenuItem pattern
src/components/Limits/components/FiatLimitsLockedCard.tsx (1)

1-42: LGTM!

Clean, well-structured component:

  • Proper 'use client' directive for client-side navigation
  • Good JSDoc documentation explaining the component's purpose
  • Uses established UI components (Card, Icon, Button) correctly
  • Clear user messaging and appropriate CTA routing to verification flow
src/components/Limits/consts.limits.ts (2)

31-37: LGTM with a note on mixed key conventions.

The comment documents the dual-purpose mapping (currency and country codes). This flexibility is reasonable, but consumers should be aware that both ARS and ARG map to the same flag.


21-29: LGTM!

The as const assertions with derived types (QrCountryId, LimitsProvider) are a good pattern for type safety. The currency symbols mapping is clean and straightforward.

Also applies to: 39-46

src/app/(mobile-ui)/limits/page.tsx (1)

1-10: LGTM!

Clean route component following the established pattern of wrapping view components in PageContainer.

src/components/Limits/components/CryptoLimitsSection.tsx (2)

10-24: LGTM!

Clean presentational component with appropriate tooltip usage for explaining the "no limits" context. The 'use client' directive is correctly applied for the interactive tooltip.


3-3: The import path @/components/Global/Card is correct. The Card component is properly exported from src/components/Global/Card/index.tsx. While a separate Card component exists at src/components/0_Bruddle/Card.tsx, this does not affect the validity of the import in CryptoLimitsSection.tsx, which is using the correct path for the Global Card component.

Likely an incorrect or invalid review comment.

src/app/(mobile-ui)/limits/[provider]/page.tsx (1)

19-24: LGTM!

The conditional rendering pattern is appropriate for the current two-provider setup. The PageContainer wrapper maintains consistency with other route pages.

src/components/Limits/MantecaLimitsPage.view.tsx (2)

27-31: LGTM!

The formatAmount helper correctly formats amounts with currency symbols and locale-aware number formatting with 2 decimal places.


45-92: LGTM!

The limit cards implementation is well-structured with proper conditional rendering, flag images, period toggle, progress bars, and color-coded remaining amounts. The use of limit.asset as the key is appropriate since assets should be unique per user.

src/hooks/useLimits.ts (1)

44-64: LGTM!

The useQuery configuration with 5-minute stale time is appropriate for limits data that doesn't change frequently. The return object provides clean access to both raw data and derived convenience flags.

src/components/Limits/BridgeLimitsPage.view.tsx (1)

86-136: LGTM!

The QR payment limits accordion is well-implemented using Radix UI with proper accessibility patterns, smooth animations, and conditional rendering based on Manteca KYC status.

src/components/Limits/utils.limits.ts (1)

8-20: LGTM!

The routing logic correctly handles the three cases: Manteca regions with KYC, Bridge regions, and a sensible default fallback. The region query parameter pattern enables proper back-navigation context.

src/interfaces/interfaces.ts (1)

359-380: LGTM!

The interface definitions are well-structured. Using string types for monetary limit values avoids floating-point precision issues, and the nullable types (MantecaLimit[] | null, BridgeLimits | null) appropriately handle cases where users don't have KYC for a specific provider.

src/components/Limits/LimitsPage.view.tsx (2)

111-147: LGTM!

The UnlockedRegionsList component correctly uses getProviderRoute for navigation and properly positions cards using getCardPosition.


154-209: LGTM!

The LockedRegionsList component correctly handles the pending state for Bridge KYC regions (Europe/North America) and provides appropriate navigation for other locked regions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@kushagrasarathe
Copy link
Contributor Author

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@notion-workspace
Copy link

@notion-workspace
Copy link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the main hook and API integration
cat src/hooks/useLimits.ts

# Read the limit page components to understand state management
cat src/components/Limits/LimitsPage.view.tsx

# Read bridge and manteca pages
cat src/components/Limits/BridgeLimitsPage.view.tsx
cat src/components/Limits/MantecaLimitsPage.view.tsx

# Read utilities and constants
cat src/components/Limits/utils.limits.ts
cat src/components/Limits/consts.limits.ts

# Check interfaces
cat src/interfaces/interfaces.ts | grep -A 30 "BridgeLimits\|MantecaLimit\|UserLimitsResponse"

# Check the dynamic route implementation
cat src/app/\(mobile-ui\)/limits/\[provider\]/page.tsx

Length of output: 29726


🏁 Script executed:

#!/bin/bash
# Check for the missing IncreaseLimitsButton component
fd -t f "IncreaseLimitsButton" src/

# Also check if it's defined anywhere in the codebase
rg -l "IncreaseLimitsButton" src/

Length of output: 164


🏁 Script executed:

#!/bin/bash
# Read the IncreaseLimitsButton component
cat src/components/Limits/components/IncreaseLimitsButton.tsx

# Check if there are any other helper components we might have missed
fd -e tsx -e ts . src/components/Limits/components/

# Look for potential duplicate error handling patterns
rg -n "Failed to load limits" src/components/Limits/

# Check for parseFloat usage without NaN handling
rg -n "parseFloat" src/components/Limits/

Length of output: 1538


@kushagrasarathe, here's my in-depth review of PR #1620:

🔴 Risk of Bugs & Breaking Changes

HIGH Priority Issues

  1. Missing NaN Guards in utils.limits.ts (Lines 28-34)

    return {
        limit: parseFloat(limit.monthlyLimit),
        remaining: parseFloat(limit.availableMonthlyLimit),
    }

    Risk: If API returns non-numeric strings, parseFloat() returns NaN, causing broken UI (progress bar width, formatted amounts). This will crash calculations and display.

    Fix: Add validation:

    const parsed = parseFloat(limit.monthlyLimit);
    limit: isNaN(parsed) ? 0 : parsed,
  2. Empty href Links (BridgeLimitsPage:155, MantecaLimitsPage:113)

    href=""  // or href="#"

    Risk: Causes page navigation issues, accessibility problems. Users clicking will see broken behavior.

  3. URL State Type Mismatch

    • BridgeLimitsPage uses parseAsStringEnum with default 'us' but no validation for malformed URLs
    • If region param is garbage, defaults silently to 'us' - confusing UX

MEDIUM Priority Issues

  1. No Auth Guards on Dynamic Route

    • /limits/[provider]/page.tsx has no explicit auth check
    • Relies on upstream middleware, but hook returns { manteca: null, bridge: null } without token
    • Risk: Unauthenticated users see loading → empty state (poor UX)
  2. External Image Dependencies

    • flagcdn.com URLs hardcoded in constants
    • Risk: If CDN fails, broken images throughout UI. No fallback images.
  3. Provider Validation Edge Case

    if (!LIMITS_PROVIDERS.includes(provider as LimitsProvider)) {
        notFound()
    }

    Uses type assertion - TypeScript won't catch issues if LIMITS_PROVIDERS changes.

LOW Priority Issues

  1. Breaking Existing Functionality: ✅ Very low risk - only adds new routes, single menu item integration

⚡ Performance & Latency

MEDIUM Impact

  1. Excessive Refetching

    refetchOnMount: true,
    refetchOnWindowFocus: true,

    Impact: API called every window focus AND mount. For limits that "don't change often" (per comment), this is wasteful. Could cause rate limiting issues.

    Recommendation: Set refetchOnWindowFocus: false or increase staleTime to 10-15 minutes.

  2. No Image Optimization

    • External images from flagcdn.com use Image component but load w160 resolution
    • No lazy loading for accordion items (all QR countries render upfront)

LOW Impact

  1. Redundant Hook Calls

    • LimitsPage calls useIdentityVerification and useKycStatus
    • Child lists receive computed props, but parent could optimize with useMemo for region filtering
  2. Query Cache Strategy: ✅ Good - 5min stale time, 10min garbage collection


🧹 Code Quality & DRY Violations

Major Violations

  1. Duplicated Error Handling (3 instances)

    // BridgeLimitsPage.view.tsx:51-55
    // MantecaLimitsPage.view.tsx:39-43
    // Identical JSX

    Fix: Extract to shared component:

    const LimitsErrorCard = () => (
        <Card position="single" className="bg-error-1 text-error">
            <p className="text-sm">Failed to load limits. Please try again.</p>
        </Card>
    )
  2. Duplicated Loading State (3 instances)

    {isLoading && <PeanutLoading coverFullScreen />}

    Extract to LimitsLoadingState wrapper.

  3. Duplicated Fallback Message (2 instances)

    <Card position="single">
        <p className="text-sm text-grey-1">No limits data available.</p>
    </Card>
  4. Duplicated Link Section (2 instances)

    <a href="" target="_blank" ...>
        See more about limits
    </a>

    Extract to LimitsDocsLink component.

  5. Similar Formatting Functions

    • BridgeLimitsPage.formatLimit(amount, asset)
    • MantecaLimitsPage.formatAmount(amount, currency)

    Nearly identical logic. Move to utils.limits.ts:

    export function formatCurrency(amount: string | number, currency: string): string {
        const num = typeof amount === 'string' ? parseFloat(amount) : amount;
        if (isNaN(num)) return `${currency}0`;
        const symbol = LIMITS_CURRENCY_SYMBOLS[currency] || currency;
        return `${symbol}${num.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
    }

Minor Issues

  1. Magic Numbers/Strings

    • Hardcoded error messages not in constants
    • "text-xl md:text-2xl" duplicated 3 times
    • Animation classes "duration-300" not centralized
  2. Inconsistent Naming

    • BridgeLimits.onRampPerTransaction vs MantecaLimit.monthlyLimit
    • API contract feels inconsistent (string vs number representation)
  3. Missing PropTypes Documentation

    • Internal components (UnlockedRegionsList, LockedRegionsList) lack JSDoc

✅ Positive Observations

  • Clean separation: hooks, utils, components, constants
  • TypeScript types properly defined
  • React Query integration done correctly
  • Good use of URL state for deep linking
  • Accessible accordion implementation with Radix UI
  • Proper image optimization with Next.js Image component

🧪 Concise QA Testing Guide

Test Environment Setup

  1. Prepare 4 test accounts representing KYC states:
    • Account A: No KYC
    • Account B: Bridge KYC only (approved)
    • Account C: Manteca KYC only (approved)
    • Account D: Both Bridge + Manteca KYC (approved)
    • Account E: Bridge KYC pending

Critical Test Cases

1. Navigation & Access

  • Navigate to /profile → click "Limits" → verify redirects to /limits
  • Direct URL: /limits/bridge → verify page loads
  • Direct URL: /limits/manteca → verify page loads
  • Invalid URL: /limits/invalid → verify 404 page
  • Logout → try /limits → verify redirects to login

2. Account A (No KYC) - Locked State

  • /limits shows "Verify your identity to unlock fiat limits" card
  • All regions show as "Locked" with navigation to verification
  • No unlocked regions visible
  • Crypto section shows "No limits" with tooltip

3. Account B (Bridge KYC) - US/Europe/Mexico

  • /limits shows North America as "Unlocked"
  • Click North America → navigates to /limits/bridge?region=us
  • Verify "Add money" and "Withdrawing" limits display with correct amounts
  • QR payment accordion shows Argentina & Brazil
  • Expand Argentina → verify $2,000 limit displays
  • Back button → returns to /limits

4. Account C (Manteca KYC) - LATAM

  • /limits shows LATAM as "Unlocked"
  • Click LATAM → navigates to /limits/manteca
  • Verify currency cards display (ARS, BRL with correct flags)
  • Toggle Monthly/Yearly → verify limits recalculate
  • Progress bar color: >70% = green, 20-70% = yellow, <20% = red
  • "Increase my limits" button → opens support modal with prefilled message

5. Account D (Both KYC) - Full Access

  • /limits shows both NA and LATAM as "Unlocked"
  • Click NA → bridge limits page (no QR accordion)
  • Click LATAM → manteca limits page

6. Account E (Pending Bridge KYC)

  • Europe/North America show "Pending" badge
  • Clicking pending regions → no navigation (disabled)
  • Other locked regions still navigable

7. Edge Cases

  • API returns 400 → verify empty state "No limits data available"
  • API timeout → verify error message "Failed to load limits. Please try again."
  • Window focus → limits refresh (verify network call)
  • Invalid region param: /limits/bridge?region=xyz → defaults to us

8. Visual & Responsiveness

  • Mobile: all cards, accordions, toggles render correctly
  • Desktop: verify layout doesn't break
  • Images: all country flags load (Argentina, Brazil, etc.)
  • Progress bars animate smoothly
  • Accordion animations (expand/collapse) work

9. Accessibility

  • Keyboard navigation: Tab through all interactive elements
  • Screen reader: accordion announces state changes
  • Links have proper target="_blank" with rel="noopener noreferrer"

Performance Checks

  • Open DevTools Network → navigate to limits → verify single API call
  • Switch tabs away/back → verify refetch happens (expected behavior)
  • Check image loading: flagcdn.com requests succeed
  • No console errors or warnings

Estimated Testing Time: 45-60 minutes with all account states prepared.


Summary Recommendation

BLOCK MERGE until:

  1. ✅ Add NaN guards to parseFloat calls (critical bug risk)
  2. ✅ Fix empty href links or remove TODOs
  3. ⚠️ Consider extracting duplicate error/loading components (technical debt)
  4. ⚠️ Reduce refetch frequency to improve performance

After fixes, this is a solid feature addition with minimal breaking change risk. The architecture is sound; main concerns are around defensive programming for edge cases.


🧠 Learnings used
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 852
File: src/hooks/useTransactionHistory.ts:177-239
Timestamp: 2025-05-14T21:16:06.369Z
Learning: When reviewing code, pay attention to and resolve CodeRabbit comments as they can contain useful refactoring suggestions and insights.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 594
File: src/components/Request/Pay/Views/Initial.view.tsx:188-213
Timestamp: 2024-12-31T15:08:15.640Z
Learning: Always resolve coderabbit comments when reviewing code, since they can contain useful suggestions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/features/limits/utils/limits.utils.ts`:
- Around line 25-36: getLimitData currently calls parseFloat on fields like
monthlyLimit/availableMonthlyLimit (and yearly equivalents) without guarding
against non-numeric inputs, which can produce NaN and break the UI; update
getLimitData to defensively coerce inputs by checking for null/undefined/empty
strings and using parseFloat result only if Number.isFinite(value) (or
!Number.isNaN) otherwise fall back to 0 for both limit and remaining, ensuring
the function always returns numeric values for limit and remaining.

In `@src/features/limits/views/BridgeLimitsView.tsx`:
- Around line 40-43: formatLimit currently calls Number(amount).toLocaleString()
which will throw or render "NaN" for non-numeric API values; update formatLimit
to parse the amount (e.g., Number or parseFloat), check for NaN/isFinite, and
handle invalid values by returning a safe fallback (such as a dash or "0" with
the correct symbol) instead of calling toLocaleString on NaN; ensure the
function still prefixes USD with "$" and returns the localized string when the
parsed number is valid.

In `@src/features/limits/views/MantecaLimitsView.tsx`:
- Around line 102-110: The anchor in MantecaLimitsView currently uses a
placeholder href="#" which triggers scroll-to-top; change the render to avoid a
dummy href by conditionally rendering the link only when a real docs URL is
available (e.g., docsUrl prop or constant) or replace the anchor with a
non-navigating element (button/span) and an onClick that opens the docs URL in a
new tab; alternatively, if keeping an <a> element, remove href="#" and set href
to the real URL or omit href and add role="button" plus rel/target as needed to
preserve accessibility and behavior.
♻️ Duplicate comments (1)
src/features/limits/views/BridgeLimitsView.tsx (1)

138-146: Empty href="" will reload the current page when clicked.

This placeholder link causes a page reload on click. Consider hiding the link until the actual URL is available.

🛠️ Suggested fix
-                    <a
-                        // TODO: add link to docs
-                        href=""
-                        target="_blank"
-                        rel="noopener noreferrer"
-                        className="text-center text-sm underline"
-                    >
-                        See more about limits
-                    </a>
+                    {/* TODO: Uncomment when docs link is available
+                    <a
+                        href="https://docs.example.com/limits"
+                        target="_blank"
+                        rel="noopener noreferrer"
+                        className="text-center text-sm underline"
+                    >
+                        See more about limits
+                    </a>
+                    */}
🧹 Nitpick comments (8)
src/features/limits/components/PeriodToggle.tsx (2)

21-32: Extract duplicate Trigger className to a constant.

Both Trigger components share identical className strings. Consider extracting to reduce duplication.

♻️ Suggested refactor
+const triggerClassName =
+    'rounded-xl border border-transparent px-3 py-1 text-xs font-medium text-grey-1 transition-all data-[state=active]:border-primary-1 data-[state=active]:bg-primary-1/10 data-[state=active]:text-primary-1'
+
 export default function PeriodToggle({ value, onChange, className }: PeriodToggleProps) {
     return (
         <Root value={value} onValueChange={(v) => onChange(v as Period)} className={className}>
             <List className="flex items-center rounded-xl bg-grey-4 p-0" aria-label="Select period">
                 <Trigger
                     value="monthly"
-                    className="rounded-xl border border-transparent px-3 py-1 text-xs font-medium text-grey-1 transition-all data-[state=active]:border-primary-1 data-[state=active]:bg-primary-1/10 data-[state=active]:text-primary-1"
+                    className={triggerClassName}
                 >
                     Monthly
                 </Trigger>
                 <Trigger
                     value="yearly"
-                    className="rounded-xl border border-transparent px-3 py-1 text-xs font-medium text-grey-1 transition-all data-[state=active]:border-primary-1 data-[state=active]:bg-primary-1/10 data-[state=active]:text-primary-1"
+                    className={triggerClassName}
                 >
                     Yearly
                 </Trigger>
             </List>
         </Root>
     )
 }

5-5: Import LimitsPeriod type from src/features/limits/consts.ts.

The type is already exported in consts.ts as LimitsPeriod. Use it instead of redeclaring locally to maintain consistency across the limits feature.

src/features/limits/views/BridgeLimitsView.tsx (1)

106-112: Consider adding a fallback for external flag images.

The flag images from flagcdn.com have no fallback if the CDN fails or the image doesn't load, which could result in broken UI.

💡 Suggested improvement
                                                        <Image
                                                            src={country.flag}
                                                            alt={country.name}
                                                            width={24}
                                                            height={24}
                                                            className="size-5 rounded-full object-cover"
+                                                           onError={(e) => {
+                                                               e.currentTarget.style.display = 'none'
+                                                           }}
                                                        />

Or consider using a local fallback icon when the image fails to load.

src/features/limits/views/LimitsPageView.tsx (3)

24-25: Unnecessary variable aliasing.

hasAnyKyc is just an alias for isUserKycApproved with no additional logic. Consider using isUserKycApproved directly for clarity.


114-117: Redundant length check.

The regions.length > 0 check on Line 116 is redundant since the parent component already guards with unlockedRegions.length > 0 at Line 66 before rendering this component.

💡 Simplified version
     return (
         <div>
-            {regions.length > 0 && <h2 className="mb-2 font-bold">Unlocked regions</h2>}
+            <h2 className="mb-2 font-bold">Unlocked regions</h2>
             {regions.map((region, index) => (

165-167: Same redundant length check in LockedRegionsList.

Similar to UnlockedRegionsList, this guard is unnecessary since the parent already checks filteredLockedRegions.length > 0 at Line 71.

src/features/limits/views/MantecaLimitsView.tsx (1)

26-31: Consider reusing the existing formatAmount utility.

There's a robust formatAmount function in src/utils/general.utils.ts that handles NaN cases properly. The local version here could be replaced or extended from it to ensure consistent formatting and NaN handling across the codebase.

src/features/limits/consts.ts (1)

31-37: Clarify the dual-key pattern in LIMITS_CURRENCY_FLAGS.

The map contains both currency codes (ARS, BRL) and country codes (ARG, BRA) mapping to the same flags. Consider adding a comment explaining this is intentional to support both API response formats, or consolidate to one pattern with a lookup helper.

💡 Example with clarifying comment
 // currency/country to flag mapping
+// supports both currency codes (ARS, BRL) and country codes (ARG, BRA) from API responses
 export const LIMITS_CURRENCY_FLAGS: Record<string, string> = {
     ARS: 'https://flagcdn.com/w160/ar.png',
     BRL: 'https://flagcdn.com/w160/br.png',
     ARG: 'https://flagcdn.com/w160/ar.png',
     BRA: 'https://flagcdn.com/w160/br.png',
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55c8035 and 8df9bc1.

📒 Files selected for processing (12)
  • src/app/(mobile-ui)/limits/[provider]/page.tsx
  • src/app/(mobile-ui)/limits/page.tsx
  • src/features/limits/components/CryptoLimitsSection.tsx
  • src/features/limits/components/FiatLimitsLockedCard.tsx
  • src/features/limits/components/IncreaseLimitsButton.tsx
  • src/features/limits/components/LimitsProgressBar.tsx
  • src/features/limits/components/PeriodToggle.tsx
  • src/features/limits/consts.ts
  • src/features/limits/utils/limits.utils.ts
  • src/features/limits/views/BridgeLimitsView.tsx
  • src/features/limits/views/LimitsPageView.tsx
  • src/features/limits/views/MantecaLimitsView.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/(mobile-ui)/limits/[provider]/page.tsx
  • src/app/(mobile-ui)/limits/page.tsx
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2024-12-11T10:13:22.806Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 564
File: src/components/Request/Pay/Views/Initial.view.tsx:430-430
Timestamp: 2024-12-11T10:13:22.806Z
Learning: In the React TypeScript file `src/components/Request/Pay/Views/Initial.view.tsx`, when reviewing the `InitialView` component, do not flag potential issues with using non-null assertion `!` on the `slippagePercentage` variable, as handling undefined values in this context is considered out of scope.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
  • src/features/limits/views/MantecaLimitsView.tsx
  • src/features/limits/views/LimitsPageView.tsx
📚 Learning: 2025-07-24T10:57:15.315Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1000
File: src/components/og/ProfileCardOG.tsx:0-0
Timestamp: 2025-07-24T10:57:15.315Z
Learning: In `src/components/og/ProfileCardOG.tsx`, the scribble image should have an empty alt attribute (alt="") to prevent layout issues if the image fails to load. Since it's a decorative element positioned absolutely over the username text, showing alt text would interfere with the layout and username display.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-10-23T09:38:27.670Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-64
Timestamp: 2024-10-23T09:38:27.670Z
Learning: In `src/app/request/pay/page.tsx`, if `linkRes` is not OK in the `generateMetadata` function, the desired behavior is to use the standard title and preview image without throwing an error.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2025-08-14T14:42:54.411Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1094
File: src/utils/withdraw.utils.ts:181-191
Timestamp: 2025-08-14T14:42:54.411Z
Learning: The countryCodeMap in src/components/AddMoney/consts/index.ts uses uppercase 3-letter country codes as keys (like 'AUT', 'BEL', 'CZE') that map to 2-letter country codes, requiring input normalization to uppercase for proper lookups.

Applied to files:

  • src/features/limits/consts.ts
📚 Learning: 2024-10-29T12:19:41.968Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 495
File: src/components/Global/TokenAmountInput/index.tsx:23-30
Timestamp: 2024-10-29T12:19:41.968Z
Learning: In the `TokenAmountInput` component (`src/components/Global/TokenAmountInput/index.tsx`), when the 'Max' button is clicked, we intentionally set the input denomination to 'TOKEN' because we are setting the value as token.

Applied to files:

  • src/features/limits/components/IncreaseLimitsButton.tsx
📚 Learning: 2025-09-18T09:30:42.901Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1230
File: src/app/(mobile-ui)/withdraw/page.tsx:92-97
Timestamp: 2025-09-18T09:30:42.901Z
Learning: In src/app/(mobile-ui)/withdraw/page.tsx, the useEffect that calls setShowAllWithdrawMethods(true) when amountFromContext exists is intentionally designed to run only on component mount (empty dependency array), not when amountFromContext changes. This is the correct behavior for the withdraw flow where showing all methods should only happen on initial load when an amount is already present.

Applied to files:

  • src/features/limits/components/FiatLimitsLockedCard.tsx
🧬 Code graph analysis (5)
src/features/limits/components/LimitsProgressBar.tsx (1)
src/features/limits/utils/limits.utils.ts (1)
  • getLimitColorClass (46-54)
src/features/limits/views/MantecaLimitsView.tsx (9)
src/hooks/useLimits.ts (1)
  • useLimits (19-69)
src/features/limits/consts.ts (3)
  • LimitsPeriod (46-46)
  • LIMITS_CURRENCY_SYMBOLS (40-44)
  • LIMITS_CURRENCY_FLAGS (32-37)
src/utils/general.utils.ts (1)
  • formatAmount (322-362)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-28)
src/components/0_Bruddle/Card.tsx (1)
  • Card (72-72)
src/features/limits/utils/limits.utils.ts (2)
  • getLimitData (25-36)
  • getLimitColorClass (46-54)
src/features/limits/components/PeriodToggle.tsx (1)
  • PeriodToggle (17-36)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (289-298)
src/features/limits/components/IncreaseLimitsButton.tsx (1)
  • IncreaseLimitsButton (11-19)
src/features/limits/views/LimitsPageView.tsx (6)
src/hooks/useIdentityVerification.tsx (2)
  • useIdentityVerification (97-272)
  • Region (11-16)
src/hooks/useKycStatus.tsx (1)
  • useKycStatus (12-36)
src/features/limits/components/FiatLimitsLockedCard.tsx (1)
  • FiatLimitsLockedCard (12-42)
src/features/limits/components/CryptoLimitsSection.tsx (1)
  • CryptoLimitsSection (10-24)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (15-20)
src/features/limits/utils/limits.utils.ts (1)
  • getProviderRoute (8-20)
src/features/limits/utils/limits.utils.ts (2)
src/features/limits/consts.ts (4)
  • MANTECA_REGIONS (3-3)
  • BRIDGE_REGIONS (2-2)
  • REGION_TO_BRIDGE_PARAM (6-12)
  • LimitsPeriod (46-46)
src/interfaces/interfaces.ts (1)
  • MantecaLimit (367-375)
src/features/limits/components/IncreaseLimitsButton.tsx (2)
src/context/ModalsContext.tsx (1)
  • useModalsContext (81-87)
src/components/0_Bruddle/Button.tsx (1)
  • Button (80-283)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (9)
src/features/limits/components/CryptoLimitsSection.tsx (1)

1-24: LGTM!

Clean, simple presentational component. Good use of Tooltip for providing context about crypto limits, and the JSDoc comment clearly explains the component's purpose.

src/features/limits/components/FiatLimitsLockedCard.tsx (1)

1-42: LGTM!

Well-structured component with clear purpose. The JSDoc documentation is helpful, and the UI provides a clear call-to-action for users to complete identity verification.

src/features/limits/components/IncreaseLimitsButton.tsx (1)

1-19: LGTM!

Clean implementation with good separation of concerns. The prefilled message constant is well-placed, and the JSDoc clearly explains the component's purpose.

src/features/limits/components/LimitsProgressBar.tsx (1)

19-34: LGTM with a note on NaN edge cases.

Good defensive handling for total <= 0. The distinction between using remainingPercent for color determination and clampedPercent for width is sensible—color reflects actual limit status while width stays within bounds.

However, per the PR objectives flagging parseFloat NaN concerns elsewhere, consider whether parent components could pass NaN values here. If remaining or total are NaN, the arithmetic will propagate NaN, resulting in an empty progress bar (width: NaN% becomes 0) and potentially incorrect color. The upstream data flow should ensure valid numbers, but a guard here would add resilience.

src/features/limits/views/LimitsPageView.tsx (1)

48-100: Well-structured main view component.

Good separation of concerns with filtered regions via useMemo, proper conditional rendering for different KYC states, and clear section organization. The "Coming soon" badge for Rest of World is a nice UX touch.

src/features/limits/views/MantecaLimitsView.tsx (1)

45-92: Clean implementation of per-currency limit cards.

Good handling of the period toggle state, proper division-by-zero protection in remainingPercent calculation (Line 55-56), and appropriate use of getLimitColorClass for visual feedback.

src/features/limits/utils/limits.utils.ts (2)

8-20: Good defensive defaults in route resolution.

The fallback to 'us' region ensures the function always returns a valid route even for unexpected region paths. Clear logic flow with appropriate comments.


38-54: Well-designed threshold-based color utility.

Clean implementation with clearly documented threshold constants and a simple, readable branching structure that handles both background and text color variants.

src/features/limits/consts.ts (1)

1-46: Well-organized constants module.

Good use of TypeScript patterns: as const for deriving types from arrays, clear separation of region/provider/currency concerns, and self-documenting constant names. The type exports (BridgeRegion, QrCountryId, LimitsProvider, LimitsPeriod) provide strong typing for consumers.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@kushagrasarathe kushagrasarathe changed the title [wip] feat: limits feat: limits in settings Jan 14, 2026
@kushagrasarathe kushagrasarathe requested a review from Hugo0 January 14, 2026 10:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/features/limits/views/BridgeLimitsView.tsx`:
- Around line 91-96: The Accordion.Root onValueChange handler can receive an
empty string when the panel is collapsed, but currently it casts that value to
QrCountryId which allows `''` to be stored in expandedCountry; change the
handler in Accordion.Root (the onValueChange that calls setExpandedCountry) to
normalize the value by checking if value === '' (or falsy) and call
setExpandedCountry(undefined) in that case, otherwise cast and set the
QrCountryId; keep references to expandedCountry, setExpandedCountry and
QrCountryId when making this change to ensure the state remains type-safe.
- Around line 53-56: In BridgeLimitsView, the isLoading and error conditionals
can both render during a refetch; make rendering mutually exclusive by changing
the JSX to use an early return or a single conditional switch: inside the
BridgeLimitsView component, check isLoading first and return <PeanutLoading
coverFullScreen />; else if (error) return <LimitsError />; otherwise render the
normal content—this ensures only one of PeanutLoading or LimitsError is
displayed at a time.
♻️ Duplicate comments (1)
src/features/limits/components/LimitsDocsLink.tsx (1)

1-13: Remove empty href="" placeholder (it navigates/reloads)

Ship-safe options:

  1. Don’t render until you have a real URL, or
  2. Accept a docsUrl prop and render conditionally.
Proposed fix (conditional render)
-export default function LimitsDocsLink() {
+export default function LimitsDocsLink({ docsUrl }: { docsUrl?: string }) {
+    if (!docsUrl) return null
     return (
         <a
-            // TODO: add link to docs
-            href=""
+            href={docsUrl}
             target="_blank"
             rel="noopener noreferrer"
             className="text-center text-sm underline"
         >
             See more about limits
         </a>
     )
 }
🧹 Nitpick comments (3)
src/features/limits/consts.ts (2)

1-12: Tighten region typing to prevent drift between path segments and query param ids

Right now BRIDGE_REGIONS is “path segments” (incl. 'north-america') while BridgeRegion is “query ids” (incl. 'us'). Consider encoding that in types and typing REGION_TO_BRIDGE_PARAM accordingly so adding/removing a region fails fast at compile time.

Proposed refactor
 // region path to provider mapping for navigation
-export const BRIDGE_REGIONS = ['europe', 'north-america', 'mexico', 'argentina', 'brazil']
-export const MANTECA_REGIONS = ['latam']
+export const BRIDGE_REGIONS = ['europe', 'north-america', 'mexico', 'argentina', 'brazil'] as const
+export type BridgeRegionPath = (typeof BRIDGE_REGIONS)[number]
+
+export const MANTECA_REGIONS = ['latam'] as const
+export type MantecaRegionPath = (typeof MANTECA_REGIONS)[number]

 // map region paths to bridge page region query param
-export const REGION_TO_BRIDGE_PARAM: Record<string, string> = {
+export const REGION_TO_BRIDGE_PARAM: Record<BridgeRegionPath, BridgeRegion> = {
     europe: 'europe',
     'north-america': 'us',
     mexico: 'mexico',
     argentina: 'argentina',
     brazil: 'brazil',
 }

14-19: Make BANK_TRANSFER_REGIONS readonly to avoid accidental mutation

Minor: since this is configuration, keeping it immutable helps.

Proposed tweak
 export type BridgeRegion = 'us' | 'mexico' | 'europe' | 'argentina' | 'brazil'

 // regions that show main limits (bank transfers)
-export const BANK_TRANSFER_REGIONS: BridgeRegion[] = ['us', 'mexico', 'europe']
+export const BANK_TRANSFER_REGIONS = ['us', 'mexico', 'europe'] as const satisfies readonly BridgeRegion[]
src/features/limits/components/LimitsError.tsx (1)

1-22: Prefer onRetry/refetch over router.refresh() for retry

router.refresh() will re-render the whole route; for limits fetching it’s usually cleaner to pass an onRetry callback (e.g., refetch from useLimits) and invoke that.

Sketch
-export default function LimitsError() {
-    const router = useRouter()
+export default function LimitsError({ onRetry }: { onRetry?: () => void }) {
+    const router = useRouter()
     return (
         <div className="px-2">
             <EmptyState
                 title="Something went wrong"
                 description={'Failed to load limits. Please try again.'}
                 icon="alert"
             />
             <div className="mt-4 flex justify-center">
-                <Button icon="retry" shadowSize="4" onClick={() => router.refresh()}>
+                <Button icon="retry" shadowSize="4" onClick={() => (onRetry ? onRetry() : router.refresh())}>
                     Retry
                 </Button>
             </div>
         </div>
     )
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8df9bc1 and c666f41.

📒 Files selected for processing (5)
  • src/features/limits/components/LimitsDocsLink.tsx
  • src/features/limits/components/LimitsError.tsx
  • src/features/limits/consts.ts
  • src/features/limits/views/BridgeLimitsView.tsx
  • src/features/limits/views/MantecaLimitsView.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/limits/views/MantecaLimitsView.tsx
🧰 Additional context used
🧠 Learnings (15)
📚 Learning: 2024-11-18T21:36:11.486Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 535
File: src/components/Claim/Claim.tsx:142-146
Timestamp: 2024-11-18T21:36:11.486Z
Learning: In `src/components/Claim/Claim.tsx`, external calls like token price fetching and cross-chain details retrieval are already encapsulated within existing `try...catch` blocks, so additional error handling may be unnecessary.

Applied to files:

  • src/features/limits/components/LimitsError.tsx
📚 Learning: 2025-09-11T17:46:12.507Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1200
File: src/app/(mobile-ui)/recover-funds/page.tsx:9-9
Timestamp: 2025-09-11T17:46:12.507Z
Learning: Functions in Next.js that are not marked with "use server" and contain secrets are unsafe to import in client components, as they get bundled into the client JavaScript and can leak environment variables to the browser.

Applied to files:

  • src/features/limits/components/LimitsError.tsx
📚 Learning: 2024-12-11T10:13:22.806Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 564
File: src/components/Request/Pay/Views/Initial.view.tsx:430-430
Timestamp: 2024-12-11T10:13:22.806Z
Learning: In the React TypeScript file `src/components/Request/Pay/Views/Initial.view.tsx`, when reviewing the `InitialView` component, do not flag potential issues with using non-null assertion `!` on the `slippagePercentage` variable, as handling undefined values in this context is considered out of scope.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2025-07-24T10:57:15.315Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1000
File: src/components/og/ProfileCardOG.tsx:0-0
Timestamp: 2025-07-24T10:57:15.315Z
Learning: In `src/components/og/ProfileCardOG.tsx`, the scribble image should have an empty alt attribute (alt="") to prevent layout issues if the image fails to load. Since it's a decorative element positioned absolutely over the username text, showing alt text would interfere with the layout and username display.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-10-23T09:38:27.670Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-64
Timestamp: 2024-10-23T09:38:27.670Z
Learning: In `src/app/request/pay/page.tsx`, if `linkRes` is not OK in the `generateMetadata` function, the desired behavior is to use the standard title and preview image without throwing an error.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2025-08-22T07:25:59.304Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1104
File: src/components/Payment/PaymentForm/index.tsx:596-600
Timestamp: 2025-08-22T07:25:59.304Z
Learning: The `TokenAmountInput` component in `src/components/Global/TokenAmountInput/` always returns decimal strings (e.g., "1,234.56"), not base units. When passing these values to external APIs like Daimo's `toUnits` prop, simply stripping commas with `.replace(/,/g, '')` is sufficient.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2025-05-19T19:40:43.138Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 868
File: src/components/Payment/PaymentForm/index.tsx:284-293
Timestamp: 2025-05-19T19:40:43.138Z
Learning: When converting between USD and token amounts, always check if the token price (divisor) is valid and non-zero before performing the division to prevent Infinity, NaN, or errors. Implementing validation like `if (!tokenPrice || isNaN(tokenPrice) || tokenPrice === 0)` before division operations is crucial for handling cases where price data might be unavailable.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-10-29T12:20:47.207Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 495
File: src/components/Create/Link/Input.view.tsx:244-248
Timestamp: 2024-10-29T12:20:47.207Z
Learning: In the `TokenAmountInput` component within `src/components/Global/TokenAmountInput/index.tsx`, when `balance` is undefined, the `maxValue` prop should be set to an empty string `''`.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2025-10-29T11:27:59.248Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1368
File: src/components/Common/ActionList.tsx:109-111
Timestamp: 2025-10-29T11:27:59.248Z
Learning: In `src/components/Common/ActionList.tsx`, the `balance` from `useWallet()` hook is always in USDC (as a formatted string), making it directly comparable to USD amounts without conversion. The comparison `Number(balance) >= amountInUsd` is intentional and correct.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-12-02T17:21:45.515Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 551
File: src/context/walletContext/walletContext.tsx:87-88
Timestamp: 2024-12-02T17:21:45.515Z
Learning: When converting `totalBalance` (in USD) to a `BigInt` balance in `src/context/walletContext/walletContext.tsx`, multiplying by `1e6` is intentional to maintain compatibility with USDC's 6 decimal places. The application displays only 2 decimal places, so this level of precision is sufficient.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-10-07T15:28:25.280Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:28:25.280Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(estimatedGasCost, 3)` return strings, ensuring consistent return types for `calculatedFee`.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-10-07T15:25:45.170Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:25:45.170Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(...)` return strings, ensuring that `calculatedFee` consistently returns a string without the need for additional type conversion.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-10-29T12:19:41.968Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 495
File: src/components/Global/TokenAmountInput/index.tsx:23-30
Timestamp: 2024-10-29T12:19:41.968Z
Learning: In the `TokenAmountInput` component (`src/components/Global/TokenAmountInput/index.tsx`), when the 'Max' button is clicked, we intentionally set the input denomination to 'TOKEN' because we are setting the value as token.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2024-10-29T14:44:08.745Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 495
File: src/components/Cashout/Components/Initial.view.tsx:194-198
Timestamp: 2024-10-29T14:44:08.745Z
Learning: Using a fixed 6 decimal places for `floorFixed` is acceptable for token amounts in this codebase, even if tokens have varying decimal places.

Applied to files:

  • src/features/limits/views/BridgeLimitsView.tsx
📚 Learning: 2025-08-14T14:42:54.411Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1094
File: src/utils/withdraw.utils.ts:181-191
Timestamp: 2025-08-14T14:42:54.411Z
Learning: The countryCodeMap in src/components/AddMoney/consts/index.ts uses uppercase 3-letter country codes as keys (like 'AUT', 'BEL', 'CZE') that map to 2-letter country codes, requiring input normalization to uppercase for proper lookups.

Applied to files:

  • src/features/limits/consts.ts
🧬 Code graph analysis (2)
src/features/limits/components/LimitsError.tsx (2)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (15-30)
src/components/0_Bruddle/Button.tsx (1)
  • Button (80-283)
src/features/limits/views/BridgeLimitsView.tsx (8)
src/hooks/useLimits.ts (1)
  • useLimits (19-69)
src/hooks/useKycStatus.tsx (1)
  • useKycStatus (12-36)
src/features/limits/consts.ts (4)
  • BridgeRegion (15-15)
  • QrCountryId (26-26)
  • BANK_TRANSFER_REGIONS (18-18)
  • QR_COUNTRIES (21-24)
src/utils/general.utils.ts (1)
  • formatExtendedNumber (708-756)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-28)
src/constants/payment.consts.ts (1)
  • MAX_QR_PAYMENT_AMOUNT_FOREIGN (12-12)
src/features/limits/components/LimitsDocsLink.tsx (1)
  • LimitsDocsLink (1-13)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (15-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (3)
src/features/limits/views/BridgeLimitsView.tsx (2)

106-113: The remote flag images are already fully supported. The Next.js configuration uses a wildcard pattern (hostname: '*') that allows all remote images over both HTTP and HTTPS, so flagcdn.com requires no additional allowlisting.

Regarding fallback handling: per team preference, country flags should be displayed as-is without generic fallback images, prioritizing accurate country-specific imagery.

No changes needed.

Likely an incorrect or invalid review comment.


31-36: Silent defaulting to us for invalid region param is intentional—no changes needed

The code explicitly documents this behavior (line 24: ?region=us|mexico|europe|argentina|brazil (persists source region)) and implements it safely. parseAsStringEnum with the strict allowlist ensures only valid regions pass through; any invalid value defaults to 'us', a reasonable fallback for unknown users.

The region only controls whether bank transfer limits are shown (BANK_TRANSFER_REGIONS.includes(region)), and both paths—bank-transfer view (us/mexico/europe) and QR-only view (argentina/brazil)—are valid. No error UI or validation feedback is shown elsewhere in the component, confirming this silent default is the intended design.

src/features/limits/consts.ts (1)

20-27: Allowlisting is already configured; reconsider the fallback strategy suggestion.

The next.config.js has remotePatterns configured with hostname: '*' for both HTTP and HTTPS, so flagcdn.com is already allowlisted and will not fail at runtime due to missing configuration.

However, if the CDN goes down, images will not load. The current approach (storing full URLs with no fallback) is consistent with the team's preference to display accurate country flags without generic fallbacks, as established in prior work. If this trade-off is acceptable, no changes are needed.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@Hugo0 Hugo0 left a comment

Choose a reason for hiding this comment

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

Approved, but have multiple issues. Please consider them - if you dont think they are justified in tackling, please ping me and elaboratewhy

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@src/components/AddMoney/components/MantecaAddMoney.tsx`:
- Around line 217-218: The prop name is wrong and can be null: change the prop
passed to InputAmountStep from limitsCurrency={limitsValidation.currency} to use
the actual field limitsValidation.limitCurrency and provide a non-null fallback
(e.g., limitsValidation.limitCurrency ?? 'USD') so InputAmountStep receives a
valid LimitCurrency value; update the call site where limitsValidation and
limitsCurrency are referenced to use limitCurrency with the null-coalescing
fallback.

In `@src/constants/payment.consts.ts`:
- Around line 10-16: Update the minimum Manteca limits so they align with the
documented production minimum cashout threshold: change
MIN_MANTECA_WITHDRAW_AMOUNT and MIN_MANTECA_QR_PAYMENT_AMOUNT to the $10 minimum
(or add a justification comment if Manteca-specific fees are lower and you
intend to keep a different value); ensure the constants
MIN_MANTECA_WITHDRAW_AMOUNT and MIN_MANTECA_QR_PAYMENT_AMOUNT reflect the
established MIN_CASHOUT_LIMIT and add a short comment mentioning transaction
fees so future reviewers understand why the minimum is $10.

In `@src/features/limits/components/LimitsWarningCard.tsx`:
- Around line 34-35: The InfoCard is always using the "warning" variant even for
error states; update the variant prop on the InfoCard (where variant is set now
using type === 'error') so it selects 'error' when type === 'error' and
'warning' otherwise (i.e., set variant based on type to 'error' : 'warning') to
allow InfoCard's error styling to apply; locate the InfoCard component usage in
LimitsWarningCard.tsx and change the variant expression accordingly.

In `@src/features/limits/hooks/useLimitsValidation.ts`:
- Around line 96-97: The parsed monthly and available monthly values can be NaN
when the API returns non-numeric strings; update the parsing in
useLimitsValidation (where monthlyLimit and availableMonthly are derived from
relevantMantecaLimit.monthlyLimit and
relevantMantecaLimit.availableMonthlyLimit) to guard against NaN by using a safe
parser that returns a fallback (e.g., 0) when parseFloat yields NaN, or inline
checks with isNaN to coerce to 0 before any comparisons or message formation so
no NaN propagates.

In `@src/features/limits/utils.ts`:
- Around line 42-58: getProviderRoute currently sends any region whose
REGION_ROUTES entry has provider === 'manteca' to '/limits/manteca' regardless
of hasMantecaKyc; update the logic in getProviderRoute so that you only return
'/limits/manteca' when route.provider === 'manteca' AND hasMantecaKyc is true
(include the existing explicit latam check only if it still requires the same
KYC guard), otherwise fall back to the bridge route (`/limits/bridge` with the
same param handling) so users without Manteca KYC are routed to bridge; adjust
the early latam conditional or remove it if redundant and ensure use of
REGION_ROUTES, route.provider, hasMantecaKyc, and the
`/limits/bridge${route.param ? ...}` construction in your fix.
- Around line 102-112: The getLimitData function currently uses parseFloat on
MantecaLimit fields which can yield NaN; update getLimitData to safely convert
limit.monthlyLimit/availableMonthlyLimit and
limit.yearlyLimit/availableYearlyLimit by parsing (e.g., trim then parseFloat or
Number()), then guard the result with Number.isFinite (or isNaN) and fall back
to 0 (or a safe default) so UI consumers never receive NaN; reference the
getLimitData function and the MantecaLimit properties (monthlyLimit,
availableMonthlyLimit, yearlyLimit, availableYearlyLimit) when making this
change.

In `@src/features/limits/views/BridgeLimitsView.tsx`:
- Around line 69-85: The UI passes raw parseFloat results to
formatAmountWithCurrency which can become NaN for non-numeric API strings;
update the two usages (parseFloat(bridgeLimits.onRampPerTransaction) and
parseFloat(bridgeLimits.offRampPerTransaction) so you validate the parsed value
(e.g., const amt = parseFloat(...); if (!Number.isFinite(amt)) amt = 0 or null)
and pass a safe numeric fallback to formatAmountWithCurrency, or replace both
sites with a small utility like safeParseAmount(value, fallback = 0) that
returns a finite number before formatting.
♻️ Duplicate comments (2)
src/features/limits/views/BridgeLimitsView.tsx (2)

53-56: Consider making loading/error rendering mutually exclusive.

React Query can have a stale error from a prior fetch attempt while isLoading is true during a refetch. With independent conditionals, both PeanutLoading and LimitsError could render simultaneously.

♻️ Use early returns for cleaner state handling
 const BridgeLimitsView = () => {
     const router = useRouter()
     const { bridgeLimits, isLoading, error } = useLimits()
     // ... state declarations ...

+    if (isLoading) {
+        return (
+            <div className="flex min-h-[inherit] flex-col space-y-6">
+                <NavHeader title="Limits" onPrev={() => router.back()} titleClassName="text-xl md:text-2xl" />
+                <PeanutLoading coverFullScreen />
+            </div>
+        )
+    }
+
+    if (error) {
+        return (
+            <div className="flex min-h-[inherit] flex-col space-y-6">
+                <NavHeader title="Limits" onPrev={() => router.back()} titleClassName="text-xl md:text-2xl" />
+                <LimitsError />
+            </div>
+        )
+    }
+
     return (
         <div className="flex min-h-[inherit] flex-col space-y-6">
             <NavHeader title="Limits" onPrev={() => router.back()} titleClassName="text-xl md:text-2xl" />
-
-            {isLoading && <PeanutLoading coverFullScreen />}
-
-            {error && <LimitsError />}
-
-            {!isLoading && !error && bridgeLimits && (
+            {bridgeLimits && (

97-101: Normalize Accordion collapse value to avoid type inconsistency.

Radix Accordion emits an empty string '' when collapsing. The current cast stores '' in expandedCountry (typed as QrCountryId | undefined), which doesn't match the expected type.

🛠️ Proposed fix
 <Accordion.Root
     type="single"
     collapsible
     value={expandedCountry}
-    onValueChange={(value) => setExpandedCountry(value as QrCountryId | undefined)}
+    onValueChange={(value) => setExpandedCountry(value ? (value as QrCountryId) : undefined)}
 >
🧹 Nitpick comments (8)
src/app/(mobile-ui)/qr-pay/page.tsx (1)

1556-1564: Consider extracting the limits card props computation.

The IIFE pattern works but adds visual complexity. Consider computing limitsCardProps once before the return statement for cleaner JSX.

♻️ Suggested refactor
+    // compute limits card props once
+    const limitsCardProps = getLimitsWarningCardProps({
+        validation: limitsValidation,
+        flowType: 'qr-payment',
+        currency: limitsValidation.currency,
+    })
+
     return (
         <>
             ...
-                    {/* Limits Warning/Error Card */}
-                    {(() => {
-                        const limitsCardProps = getLimitsWarningCardProps({
-                            validation: limitsValidation,
-                            flowType: 'qr-payment',
-                            currency: limitsValidation.currency,
-                        })
-                        return limitsCardProps ? <LimitsWarningCard {...limitsCardProps} /> : null
-                    })()}
+                    {/* Limits Warning/Error Card */}
+                    {limitsCardProps && <LimitsWarningCard {...limitsCardProps} />}
src/components/AddMoney/components/InputAmountStep.tsx (1)

30-32: Consider making limitsCurrency optional to match limitsValidation.

Since limitsValidation is optional, callers without limits validation would still need to provide limitsCurrency. This creates an inconsistency where a caller must provide a currency even when not using limits.

♻️ Suggested fix
     limitsValidation?: LimitsValidationWithUser
-    // required - must be provided by caller based on the payment flow's currency (ARS, BRL, USD)
-    limitsCurrency: LimitCurrency
+    // optional - must be provided when limitsValidation is provided
+    limitsCurrency?: LimitCurrency

Then update the usage at line 59:

-              currency: limitsCurrency,
+              currency: limitsCurrency ?? 'USD',
src/app/(mobile-ui)/withdraw/page.tsx (1)

299-308: Same IIFE pattern as QR page - consider extracting to reduce repetition.

This pattern is duplicated across multiple pages. A helper component or hook could encapsulate the conditional rendering logic.

src/app/(mobile-ui)/withdraw/manteca/page.tsx (1)

439-448: Consider extracting the IIFE pattern for clarity.

The immediately-invoked function expression works but is slightly unusual for JSX rendering. A simpler approach could use a local variable before the return or leverage short-circuit evaluation.

♻️ Alternative using a variable
-                    {(() => {
-                        const limitsCardProps = getLimitsWarningCardProps({
-                            validation: limitsValidation,
-                            flowType: 'offramp',
-                            currency: limitsValidation.currency,
-                        })
-                        return limitsCardProps ? <LimitsWarningCard {...limitsCardProps} /> : null
-                    })()}
+                    {getLimitsWarningCardProps({
+                        validation: limitsValidation,
+                        flowType: 'offramp',
+                        currency: limitsValidation.currency,
+                    }) && (
+                        <LimitsWarningCard
+                            {...getLimitsWarningCardProps({
+                                validation: limitsValidation,
+                                flowType: 'offramp',
+                                currency: limitsValidation.currency,
+                            })!}
+                        />
+                    )}

Or extract to a memo/variable at the top of the component:

const limitsCardProps = useMemo(() => 
    getLimitsWarningCardProps({
        validation: limitsValidation,
        flowType: 'offramp',
        currency: limitsValidation.currency,
    }), [limitsValidation]
)
// then in JSX:
{limitsCardProps && <LimitsWarningCard {...limitsCardProps} />}
src/features/limits/views/MantecaLimitsView.tsx (1)

38-40: Same loading/error mutual exclusivity concern as BridgeLimitsView.

Consider using early returns to ensure mutually exclusive rendering of loading, error, and content states.

src/app/(mobile-ui)/add-money/[country]/bank/page.tsx (1)

409-426: Consider the same IIFE cleanup as suggested for Manteca page.

Same pattern as the Manteca withdraw page - the IIFE works but could be cleaner with a memoized variable.

Additionally, the InfoCard is now hidden when limitsValidation.isBlocking, which is good UX to avoid information overload.

src/features/limits/components/LimitsWarningCard.tsx (1)

44-57: Consider using stable keys instead of array index.

Using index as a key works but can cause issues if items are reordered. If items have unique identifiers (like href for links), prefer those.

♻️ Suggested improvement
-{items.map((item, index) => (
-    <li key={index}>
+{items.map((item) => (
+    <li key={item.href ?? item.text}>
src/features/limits/hooks/useLimitsValidation.ts (1)

74-80: Consider adding a dependency or documenting the empty deps intentionally.

The useMemo has an empty dependency array, meaning daysUntilMonthlyReset is calculated once per component mount. This is likely intentional for session stability (avoids recalculating mid-session), but worth a comment for clarity.

♻️ Add clarifying comment
     // calculate days until monthly reset (first of next month)
+    // note: intentionally no deps - calculate once per session for stability
     const daysUntilMonthlyReset = useMemo(() => {
         const now = new Date()
         const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
         const diffTime = nextMonth.getTime() - now.getTime()
         return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
     }, [])

@kushagrasarathe kushagrasarathe merged commit 91715b0 into peanut-wallet-dev Jan 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants