feat: limits in settings #1620
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 addingas constfor type safety.
BRIDGE_REGIONSandMANTECA_REGIONSare typed asstring[]rather than literal tuples. Addingas constwould enable type-safe checks when validating region values elsewhere in the codebase (similar to howLIMITS_PROVIDERSandQR_COUNTRIESare 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 constsrc/components/Limits/components/PeriodToggle.tsx (2)
5-5: Consider reusingLimitsPeriodfrom consts.The
Periodtype here duplicates theLimitsPeriodtype already exported fromconsts.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
Triggerelements 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 LimitsProvidercast on Line 15 doesn't add type safety since it's cast before validation. Theincludes()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 LimitsProvidersrc/components/Limits/components/LimitsProgressBar.tsx (1)
19-34: Consider using consistent percent value for color and width.The component uses
clampedPercentfor the bar width (line 30) butremainingPercent(unclamped) for the color class (line 28). Ifremaining > 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
formatLimitfunction only adds$for USD; other currencies like EUR will display asEUR1,000instead of€1,000. Consider using theLIMITS_CURRENCY_SYMBOLSconstant fromconsts.limits.tsfor consistency withMantecaLimitsPage.♻️ 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 potentialNaNfromparseFloat.If the API returns malformed strings for limit values,
parseFloatwill returnNaN, 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.
restOfWorldRegionis 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_REGIONinstead ofrestOfWorldRegionin the JSX.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
src/app/(mobile-ui)/limits/[provider]/page.tsxsrc/app/(mobile-ui)/limits/page.tsxsrc/app/not-found.tsxsrc/components/Global/Icons/Icon.tsxsrc/components/Limits/BridgeLimitsPage.view.tsxsrc/components/Limits/LimitsPage.view.tsxsrc/components/Limits/MantecaLimitsPage.view.tsxsrc/components/Limits/components/CryptoLimitsSection.tsxsrc/components/Limits/components/FiatLimitsLockedCard.tsxsrc/components/Limits/components/LimitsProgressBar.tsxsrc/components/Limits/components/PeriodToggle.tsxsrc/components/Limits/consts.limits.tssrc/components/Limits/utils.limits.tssrc/components/Profile/index.tsxsrc/constants/payment.consts.tssrc/constants/query.consts.tssrc/hooks/useLimits.tssrc/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.tssrc/constants/payment.consts.tssrc/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.tssrc/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
LIMITSquery 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
metericon is properly integrated:
- Import follows the established pattern for MUI icons
- IconName union includes the new name
- Component mapping uses the standard
MaterialIconWrapperpatternAlso 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
metericon correctly- Positioned logically after "Regions & Verification"
- Follows the established
ProfileMenuItempatternsrc/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
ARSandARGmap to the same flag.
21-29: LGTM!The
as constassertions 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/Cardis correct. The Card component is properly exported fromsrc/components/Global/Card/index.tsx. While a separate Card component exists atsrc/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
PageContainerwrapper maintains consistency with other route pages.src/components/Limits/MantecaLimitsPage.view.tsx (2)
27-31: LGTM!The
formatAmounthelper 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.assetas 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
stringtypes 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
UnlockedRegionsListcomponent correctly usesgetProviderRoutefor navigation and properly positions cards usinggetCardPosition.
154-209: LGTM!The
LockedRegionsListcomponent 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.
|
@coderabbitai review in depth. Evaluate:
Also, write a very concise testing guide for QA |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 29726 🏁 Script executed: Length of output: 164 🏁 Script executed: Length of output: 1538
🔴 Risk of Bugs & Breaking ChangesHIGH Priority Issues
MEDIUM Priority Issues
LOW Priority Issues
⚡ Performance & LatencyMEDIUM Impact
LOW Impact
🧹 Code Quality & DRY ViolationsMajor Violations
Minor Issues
✅ Positive Observations
🧪 Concise QA Testing GuideTest Environment Setup
Critical Test Cases1. Navigation & Access
2. Account A (No KYC) - Locked State
3. Account B (Bridge KYC) - US/Europe/Mexico
4. Account C (Manteca KYC) - LATAM
5. Account D (Both KYC) - Full Access
6. Account E (Pending Bridge KYC)
7. Edge Cases
8. Visual & Responsiveness
9. Accessibility
Performance Checks
Estimated Testing Time: 45-60 minutes with all account states prepared. Summary RecommendationBLOCK MERGE until:
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 |
There was a problem hiding this comment.
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: Emptyhref=""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: ImportLimitsPeriodtype fromsrc/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.comhave 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.
hasAnyKycis just an alias forisUserKycApprovedwith no additional logic. Consider usingisUserKycApproveddirectly for clarity.
114-117: Redundant length check.The
regions.length > 0check on Line 116 is redundant since the parent component already guards withunlockedRegions.length > 0at 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 checksfilteredLockedRegions.length > 0at Line 71.src/features/limits/views/MantecaLimitsView.tsx (1)
26-31: Consider reusing the existingformatAmountutility.There's a robust
formatAmountfunction insrc/utils/general.utils.tsthat 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 inLIMITS_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
📒 Files selected for processing (12)
src/app/(mobile-ui)/limits/[provider]/page.tsxsrc/app/(mobile-ui)/limits/page.tsxsrc/features/limits/components/CryptoLimitsSection.tsxsrc/features/limits/components/FiatLimitsLockedCard.tsxsrc/features/limits/components/IncreaseLimitsButton.tsxsrc/features/limits/components/LimitsProgressBar.tsxsrc/features/limits/components/PeriodToggle.tsxsrc/features/limits/consts.tssrc/features/limits/utils/limits.utils.tssrc/features/limits/views/BridgeLimitsView.tsxsrc/features/limits/views/LimitsPageView.tsxsrc/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.tsxsrc/features/limits/views/MantecaLimitsView.tsxsrc/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 usingremainingPercentfor color determination andclampedPercentfor width is sensible—color reflects actual limit status while width stays within bounds.However, per the PR objectives flagging
parseFloatNaN concerns elsewhere, consider whether parent components could pass NaN values here. Ifremainingortotalare 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
remainingPercentcalculation (Line 55-56), and appropriate use ofgetLimitColorClassfor 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 constfor 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.
There was a problem hiding this comment.
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 emptyhref=""placeholder (it navigates/reloads)Ship-safe options:
- Don’t render until you have a real URL, or
- Accept a
docsUrlprop 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 idsRight now
BRIDGE_REGIONSis “path segments” (incl.'north-america') whileBridgeRegionis “query ids” (incl.'us'). Consider encoding that in types and typingREGION_TO_BRIDGE_PARAMaccordingly 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: MakeBANK_TRANSFER_REGIONSreadonly to avoid accidental mutationMinor: 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: PreferonRetry/refetchoverrouter.refresh()for retry
router.refresh()will re-render the whole route; for limits fetching it’s usually cleaner to pass anonRetrycallback (e.g.,refetchfromuseLimits) 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
📒 Files selected for processing (5)
src/features/limits/components/LimitsDocsLink.tsxsrc/features/limits/components/LimitsError.tsxsrc/features/limits/consts.tssrc/features/limits/views/BridgeLimitsView.tsxsrc/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 tousfor invalid region param is intentional—no changes neededThe code explicitly documents this behavior (line 24:
?region=us|mexico|europe|argentina|brazil (persists source region)) and implements it safely.parseAsStringEnumwith 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.jshasremotePatternsconfigured withhostname: '*'for both HTTP and HTTPS, soflagcdn.comis 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.
Hugo0
left a comment
There was a problem hiding this comment.
Approved, but have multiple issues. Please consider them - if you dont think they are justified in tackling, please ping me and elaboratewhy
feat: limits cards in payment flows
There was a problem hiding this comment.
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
errorfrom a prior fetch attempt whileisLoadingis true during a refetch. With independent conditionals, bothPeanutLoadingandLimitsErrorcould 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''inexpandedCountry(typed asQrCountryId | 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
limitsCardPropsonce 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 makinglimitsCurrencyoptional to matchlimitsValidation.Since
limitsValidationis optional, callers without limits validation would still need to providelimitsCurrency. 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?: LimitCurrencyThen 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
InfoCardis now hidden whenlimitsValidation.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
indexas a key works but can cause issues if items are reordered. If items have unique identifiers (likehreffor 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
useMemohas an empty dependency array, meaningdaysUntilMonthlyResetis 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)) }, [])
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
other internal pages screenshots