Conversation
* Fix: Iban country detection and incorrect bank acc details * Fix: update IBAN country validation to use correct locale string comparison * add validations for US and mexican bank accounts * fix typo * fix claim flow and create a reusable function for getting 3 letter code * fix country code mismatch * fix: show error below input field * remove unnecessary checks * remove unnecessary CLABE check
* feat: lpv2.1 * fix: gigaclouds, font and exchange widget * fixes and improvements * remove duplicate export * remove unused component
* fix: hero section responsiveness issue * fix: stars position * fix height on desktop * remove unused code
* feat: handle send link claims to bank account for peanut users (#1078) * reafactor: create reusable country list component and use it for all the flows * feat: reusable user accounts components * feat: handle different cases based on kyc status for bank claim * fix: account creation * chore: add docstring to hooks * chore: better comments for bank flow manager * fix: kyc modal closing after tos acceptance issue * fix: remove bank acc caching from withdraw flow * fix: update confirm claim modal copy * fix: remove bank acc caching from claim flow * fix: navheader title * remove duplicate debounce code and use `useDebounce` hook instead (#1079) * Landing page v2.1 (#1089) * lpv2.1 part 1 * Add exchange widget * add and integrate exchange API * add yourMoney component bg * update landing countries svg * integrate frankfurter API * fixes and improvements * decrease hero section height * allow max 2 decimal places * Add `/exchange` route * fix: overlay * make destination amount editable and bugg fixes * some fixes & currency improvements * crucial commit * fix checkmark, font size and weight --------- Co-authored-by: Hugo Montenegro <h@hugo0.com> * [TASK-13186] refactor: use networkName instead of axelarChainName (#1095) * refactor: use networkName instead of axelarChainName * fix: types * fix: onramp currency (#1096) * fix: stretched favicon (#1099) * [TASK-13971] fix: scientific notation in eip681 parsing (#1097) * fix: scientific notation in eip681 parsing * fix: qr handling tests * fix: peanut sdk mock * pull iban hotfix (#1100) * fix: claim flow bugs (#1102) * fix: cross chain claim * fix: full name issue on confirm bank claim view * fix: back navigation on desktop views * Fix back button not working on /profile (#1101) * Fix back button not working * fix public profile page * extract internal navigation logic to utility function * fix: send link claims to us bank accounts (#1108) * fix: usa bank account claims * fix: show bank account details in confirm claim view * Sync Landing page changes (#1111) * reduce clouds size and update font * fix: hero section responsiveness issue * fix: formatting errors * add currency animation * fix: us bank claims after kyc for logged in users (#1112) * fix: trim account form inputs for spaces (#1114) * [TASK-14107] fix: don't allow claiming on xChain if route is not found (#1115) * fix: don't allow claiming on xChain if route is not found * fix(claim): use correct decimals for min receive amount * feat: handle redirect uri when on unsupported browsers (#1117) * feat: handle redirect uri when on unsupported browsers * fix: confirm bank claim ui rows for iban guest claim * remove animation (#1118) * fix: formatting --------- Co-authored-by: Kushagra Sarathe <76868364+kushagrasarathe@users.noreply.github.com> Co-authored-by: Mohd Zishan <72738005+Zishan-7@users.noreply.github.com> Co-authored-by: Hugo Montenegro <h@hugo0.com>
* fix: bank claim flow runtime error * fix: dont have iban as optional * fix: merge conflicts
* fix: add missing countries * remove duplicate comment
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughLarge refactor introducing a bank-claim flow (ClaimBankFlow), updating types and contexts, adding new country/currency selection UI, implementing an exchange-rate API and hook, migrating chain display names to networkName, revising landing page composition, expanding IBridgeAccount, and multiple supporting hook/utility additions, deletions, and favicon/icon/asset updates. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Suggested labels
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 59
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (24)
public/game/peanut-game.html (1)
1004-1106: Avoid comma-wrapped grouped assignment; revert to simple statements.The added extra parenthesis turns two top-level assignments into a grouped comma expression. It’s valid JS but reduces readability and can confuse tooling/minifiers. Use plain statements.
-;((Obstacle.MAX_OBSTACLE_LENGTH = 3), -(Obstacle.prototype = { +Obstacle.MAX_OBSTACLE_LENGTH = 3 +Obstacle.prototype = { /** * Initialise the DOM for the obstacle. * @param {number} speed */ init: function (speed) { … }, … -})) +}src/components/Global/ValidatedInput/index.tsx (1)
95-99: Spinner can get stuck when clearing inputIf a validation is in-flight and debouncedValue becomes '', this effect returns early and the previous effect’s finally won’t clear isValidating (guarded by isStale). Result: spinner may remain true until next keystroke.
Apply:
useEffect(() => { - if (debouncedValue === '') { - return - } + if (debouncedValue === '') { + // reset state when cleared + setIsValid(false) + setIsValidating(false) + return + }src/hooks/useUserSearch.ts (1)
22-26: Avoid stale searches toggling isSearchingA slow earlier request can flip isSearching to false after a newer one starts. Also, clearing the box should stop the spinner immediately.
- if (!debouncedValue) { - setSearchResults([]) - return - } + if (!debouncedValue) { + setSearchResults([]) + setIsSearching(false) + return + } ... - } finally { - setIsSearching(false) - } + } finally { + if (currentValueRef.current === debouncedValue) { + setIsSearching(false) + } + }Also applies to: 44-46
src/components/Global/UnsupportedBrowserModal/index.tsx (1)
114-131: Hard-coded visible={true} makes the modal unclosable when props.visible is true.
If a parent forces visible=true and allowClose=true, the close button appears but the modal never closes. Derive visibility from state/prop.Apply this diff:
- return ( - <ActionModal - visible={true} + const isOpen = visible || showInAppBrowserModalViaDetection + return ( + <ActionModal + visible={isOpen} onClose={handleModalClose} - title="Open this link in your browser" + title="Unsupported browser" description={ - "Peanut doesn't work inside this app because of browser limitations. To continue, please open this link in your default browser." + "Peanut doesn't work inside this app because of browser limitations. To continue, please open this link in your default browser." }Note: The title change aligns with the previously agreed copy for unsupported scenarios.
src/components/LandingPage/imageAssets.tsx (4)
24-29: Fix initial/animate mismatch and include startXOffset in travel distance to avoid jumpy starts and speed drift.Currently,
initial.xand the first keyframe disagree, causing a visible teleport on start, andtotalDistanceignoresstartXOffset, making speed inconsistent across instances.Apply:
- // Total travel distance is screen width + image width + offset - const totalDistance = vpWidth + imageWidth + // Compute start/end so first keyframe equals initial; include offset in distance + const startX = side === 'left' ? vpWidth + startXOffset : -imageWidth - startXOffset + const endX = side === 'left' ? -imageWidth - startXOffset : vpWidth + startXOffset + const distance = Math.abs(endX - startX) ... - initial={{ - x: side === 'left' ? -imageWidth : vpWidth + startXOffset, - }} + initial={{ x: startX }} ... - animate={{ - x: side === 'left' ? [vpWidth, -imageWidth] : [-imageWidth, vpWidth], - }} + animate={{ x: [startX, endX] }} ... - transition={{ - ease: 'linear', - duration: totalDistance / speed, - repeat: Infinity, - }} + transition={{ + ease: 'linear', + duration: distance / speed, + repeat: Infinity, + }}Also applies to: 36-45
17-17: Remove unuseddurationprop from the type and call sites.
durationis required by the prop type but unused, forcing callers to pass dead data.- duration: number + // duration removed — duration is derived from distance/speed- duration={10}- duration={12}- duration={15}- duration={18}Also applies to: 58-58, 67-67, 88-88, 98-98
32-35: Decorative cloud images should be hidden from AT and optimized for animation.Use empty alt text and hide from assistive tech; add
willChangeto hint the compositor.- alt={`Floating Cloud ${side}`} + alt="" + aria-hidden="true" ... - style={{ top, width: imageWidth }} + style={{ top, width: imageWidth, willChange: 'transform' }}
121-134: Add alt="" and aria-hidden to decorative star images.Missing alt attributes cause noisy announcements in screen readers.
<motion.img initial={{ opacity: 0, translateY: 20, translateX: 5 }} whileInView={{ opacity: 1, translateY: 0, translateX: 0 }} transition={{ type: 'spring', damping: 5 }} src={Star.src} + alt="" + aria-hidden="true" className="absolute bottom-[-4%] left-[1%] w-8 sm:bottom-[11%] sm:left-[12%] md:bottom-[18%] md:left-[5%] md:w-12" /> <motion.img initial={{ opacity: 0, translateY: 28, translateX: -5 }} whileInView={{ opacity: 1, translateY: 0, translateX: 0 }} transition={{ type: 'spring', damping: 5 }} src={Star.src} + alt="" + aria-hidden="true" className="absolute right-[1.5%] top-[-12%] w-8 sm:right-[6%] sm:top-[8%] md:right-[5%] md:top-[8%] md:w-12 lg:right-[10%]" />src/components/0_Bruddle/Button.tsx (2)
209-218: Tailwind class bug: dynamic translate-y can purge and also emits “undefinedpx”.
active:translate-y-[${shadowSize}px]won’t be seen by Tailwind JIT and yields invalid class whenshadowSizeis undefined. Use a static safelisted map.Apply:
@@ - const buttonClasses = twMerge( - `btn w-full flex items-center gap-2 transition-all duration-100 active:translate-x-[3px] active:translate-y-[${shadowSize}px] active:shadow-none notranslate`, + const activeTranslateByShadow: Record<ShadowSize, string> = { + '4': 'active:translate-y-[4px]', + '6': 'active:translate-y-[6px]', + '8': 'active:translate-y-[8px]', + } + + const buttonClasses = twMerge( + `btn w-full flex items-center gap-2 transition-all duration-100 active:translate-x-[3px] active:shadow-none notranslate`, buttonVariants[variant], variant === 'transparent' && props.disabled && 'disabled:bg-transparent disabled:border-transparent', size && buttonSizes[size], shape === 'square' && 'btn-square', shadowSize && buttonShadows[shadowType || 'primary'][shadowSize], + shadowSize && activeTranslateByShadow[shadowSize], className )
98-101: Ref handling can break with function refs.
CastingreftoRefObjectis unsafe; useuseImperativeHandle.-import React, { forwardRef, useEffect, useRef, useState, useCallback } from 'react' +import React, { forwardRef, useEffect, useRef, useState, useCallback, useImperativeHandle } from 'react' @@ - const localRef = useRef<HTMLButtonElement>(null) - const buttonRef = (ref as React.RefObject<HTMLButtonElement>) || localRef + const localRef = useRef<HTMLButtonElement>(null) + useImperativeHandle(ref, () => localRef.current) + const buttonRef = localRefsrc/utils/general.utils.ts (2)
195-196: Fix early return/breaks that prematurely stop iterating localStorage.These control-flow statements exit the whole function or loop early, skipping valid entries.
Use continue instead:
- if (key === `${address} - created links` || key === `${address} - claimed links`) return + if (key === `${address} - created links` || key === `${address} - claimed links`) continue- if (startIndex === -1) { - return - } + if (startIndex === -1) { + continue + }- if (!item) break + if (!item) continue- if (!item) break + if (!item) continue- if (!item) break + if (!item) continueAlso applies to: 218-224, 534-536, 600-601, 666-667
855-858: Sorting by timestamp strings yields NaN; parse to Date (or epoch) before comparing.b.mostRecentInteraction - a.mostRecentInteraction subtracts strings.
Apply:
- .sort((a, b) => b.mostRecentInteraction - a.mostRecentInteraction) + .sort( + (a, b) => + new Date(b.mostRecentInteraction).getTime() - + new Date(a.mostRecentInteraction).getTime() + )src/utils/bridge.utils.ts (1)
14-18: Normalize countryId once to handle case/variants ('us', 'Usa', etc.).Prevents subtle bugs and aligns with prior learning that maps use uppercase keys.
-export const getCurrencyConfig = (countryId: string, operationType: BridgeOperationType): CurrencyConfig => { - if (countryId === 'US' || countryId === 'USA') { +export const getCurrencyConfig = (countryId: string, operationType: BridgeOperationType): CurrencyConfig => { + const id = countryId.toUpperCase() + if (id === 'US' || id === 'USA') { return { currency: 'usd', paymentRail: operationType === 'onramp' ? 'ach_push' : 'ach', } } - if (countryId === 'MX') { + if (id === 'MX') { return { currency: 'mxn', paymentRail: 'spei', // SPEI works for both onramp and offramp in Mexico } }Also update minimums:
-export const getMinimumAmount = (countryId: string): number => { - if (countryId === 'MX') { +export const getMinimumAmount = (countryId: string): number => { + const id = countryId.toUpperCase() + if (id === 'MX') { return 50 } return 1 }src/components/AddMoney/components/DepositMethodList.tsx (1)
76-85: Include flagcdn.com in next.config.js images.remotePatterns
Add an entry under images.remotePatterns in next.config.js to allow loading flags from https://flagcdn.com (e.g.{ protocol: 'https', hostname: 'flagcdn.com', port: '', pathname: '/w160/**' }) to prevent runtime image failures.src/interfaces/interfaces.ts (1)
72-79: Duplicate interface name IToken with conflicting shape.You declare IToken twice; later declaration shadows the earlier (and drops logoURI), causing type conflicts.
export interface IToken { address: string name: string symbol: string decimals: number logoURI: string } - -export interface IToken { - chainId: string - address: string - name: string - symbol: string - decimals: number -} +// If needed, introduce a distinct type instead of shadowing: +// export interface BasicToken { chainId: string; address: string; name: string; symbol: string; decimals: number }Also applies to: 113-118
src/components/Claim/Claim.utils.ts (1)
42-51: Populate token decimals from source; 0 breaks formatting/amount math.combinedObject.tokens.push({ address: token.address || '', name: token.name || '', symbol: token.symbol || '', - decimals: 0, + decimals: typeof token.decimals === 'number' ? token.decimals : 18, logoURI: token.logoURI || '', chainId: chain.chainId || '', })src/components/AddMoney/views/NetworkSelection.view.tsx (1)
46-47: Fix “Coming Soon” logic to reflect the configured listCurrent logic marks every network except the Peanut Wallet chain as “coming soon,” ignoring TOKEN_SELECTOR_COMING_SOON_NETWORKS. Use the configured list to determine coming soon status.
- const isComingSoon = network.chainId !== PEANUT_WALLET_CHAIN.id.toString() + const comingSoonIds = new Set( + TOKEN_SELECTOR_COMING_SOON_NETWORKS.map((n) => String(n.chainId)) + ) + const isComingSoon = comingSoonIds.has(String(network.chainId))src/app/actions/external-accounts.ts (2)
13-31: Don’t return unvalidated API responses; parse to IBridgeAccount (and surface coherent errors).Returning raw
datarisks shape drift. Parse minimally and fail closed.export async function createBridgeExternalAccountForGuest( customerId: string, accountDetails: AddBankAccountPayload -): Promise<IBridgeAccount | { error: string }> { +): Promise<IBridgeAccount | { error: string }> { try { const response = await fetchWithSentry(`${API_URL}/bridge/customers/${customerId}/external-accounts`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'api-key': API_KEY, }, body: JSON.stringify({ ...accountDetails, reuseOnError: true }), // note: reuseOnError is used to avoid showing errors for duplicate accounts on guest flow }) - const data = await response.json() + const data = await response.json() if (!response.ok) { - return { error: data.error || 'Failed to create external account.' } + return { error: (data && (data.error || data.message)) || 'Failed to create external account.' } } - return data + // Narrow the response to the expected shape + const parsed = BridgeAccountSchema.safeParse(data) + if (!parsed.success) { + return { error: 'Invalid response from server.' } + } + return parsed.dataAdd this lightweight schema near the top of the file (outside the diffed range):
import { z } from 'zod' const BridgeAccountSchema = z .object({ id: z.string(), customer_id: z.string(), account_owner_name: z.string(), account_type: z.enum(['iban', 'us', 'clabe']), beneficiary_address_valid: z.boolean(), }) .passthrough()
21-22: Add an idempotency key for the POST to prevent duplicate external-account creation.Use a stable hash of inputs to guard retries/timeouts.
headers: { 'Content-Type': 'application/json', 'api-key': API_KEY, + 'Idempotency-Key': `${customerId}:${hashAccount(accountDetails)}`, },Outside this range, define:
import crypto from 'crypto' function hashAccount(a: AddBankAccountPayload): string { return crypto.createHash('sha256').update(JSON.stringify(a)).digest('hex') }src/components/Claim/Link/views/Confirm.bank-claim.view.tsx (1)
67-69: formatUnits requires decimals; add a safe fallback to avoid runtime throw.- amount={formatUnits(claimLinkData?.amount ?? 0, claimLinkData?.tokenDecimals) || '0.00'} + amount={formatUnits( + (claimLinkData?.amount as any) ?? 0, + (claimLinkData?.tokenDecimals as number | undefined) ?? 6 + ) || '0.00'}If decimals are always present upstream, assert that and remove the fallback; otherwise this prevents crashes.
src/hooks/useKycFlow.ts (2)
56-66: Fix unsafe optional chaining on username.
user?.user.usernamewill throw ifuser.useris undefined. Useuser?.user?.username.Apply:
- username: user?.user.username ?? undefined, + username: user?.user?.username ?? undefined,
101-110: Fix missing bracket infields[iqt_token]query parameter
Insrc/utils/bridge-accounts.utils.ts, theconvertPersonaUrlreturn string is missing the closing bracket forfields[iqt_token], resulting in an invalid Persona widget URL. Apply this patch:@@ src/utils/bridge-accounts.utils.ts - return `https://bridge.withpersona.com/widget?environment=production&inquiry-template-id=${templateId}&fields[iqt_token=${iqtToken}&iframe-origin=${origin}&redirect-uri=${origin}&fields[developer_id]=${developerId}&reference-id=${referenceId}` + return `https://bridge.withpersona.com/widget?environment=production&inquiry-template-id=${templateId}&fields[iqt_token]=${iqtToken}&iframe-origin=${origin}&redirect-uri=${origin}&fields[developer_id]=${developerId}&reference-id=${referenceId}`src/components/Claim/Link/Onchain/Confirm.view.tsx (1)
87-95: Use route-derived destination chain/token to avoid mismatches.When executing x-chain claim, use the selected route’s toChain/toToken instead of the selector’s current values.
- claimTxHash = await claimLinkXchain({ - address: recipient ? recipient.address : (address ?? ''), - link: claimLinkData.link, - destinationChainId: selectedChainID, - destinationToken: selectedTokenAddress, - }) + claimTxHash = await claimLinkXchain({ + address: recipient ? recipient.address : (address ?? ''), + link: claimLinkData.link, + destinationChainId: selectedRoute.rawResponse.route.params.toChain, + destinationToken: selectedRoute.rawResponse.route.estimate.toToken.address, + })src/components/AddWithdraw/DynamicBankAccountForm.tsx (1)
123-131: Country detection bug for MX/US codes.Support both 2- and 3-letter forms. Also pass normalized code to isIBANCountry.
- const isUs = country.toUpperCase() === 'USA' - const isMx = country.toUpperCase() === 'MX' - const isIban = isUs || isMx ? false : isIBANCountry(country) + const countryCode = country.toUpperCase() + const isUs = countryCode === 'USA' || countryCode === 'US' + const isMx = countryCode === 'MEX' || countryCode === 'MX' + const isIban = isUs || isMx ? false : isIBANCountry(countryCode)
🧹 Nitpick comments (61)
src/components/Global/ValidatedInput/index.tsx (2)
48-48: Remove unused previousValueRefpreviousValueRef is written but never read; safe to delete.
- const previousValueRef = useRef(value) ... - previousValueRef.current = debouncedValueAlso applies to: 101-101
50-50: Drop unused listId reflistId is never referenced.
- const listId = useRef(`datalist-${Math.random().toString(36).substr(2, 9)}`)src/hooks/useUserSearch.ts (2)
27-49: Optional: add cancellation to save bandwidthIf usersApi.search supports AbortController/Signal, cancel previous requests when debouncedValue changes or on unmount.
39-43: Brittle error checkMatching on '3 characters' couples to server message text. Prefer a typed error shape or status code (e.g., 400 with code 'MIN_LENGTH').
src/utils/__mocks__/peanut-sdk.ts (1)
31-31: Nit: make the mock readonly to catch accidental mutationsFreezing the literal helps TS flag inadvertent writes in tests.
Apply this diff:
-] +] as constsrc/components/0_Bruddle/Button.tsx (2)
103-106: Timer types: prefer env-agnostic ReturnType.
Prevents TS mismatches between Node and DOM.- const [pressTimer, setPressTimer] = useState<NodeJS.Timeout | null>(null) + const [pressTimer, setPressTimer] = useState<ReturnType<typeof setTimeout> | null>(null) @@ - const [progressInterval, setProgressInterval] = useState<NodeJS.Timeout | null>(null) + const [progressInterval, setProgressInterval] = useState<ReturnType<typeof setInterval> | null>(null)
242-247: Unify input events with Pointer Events to avoid duplicate mouse/touch flows.
Simplifies logic and avoids double timers on some devices.- onMouseDown={longPress ? handlePressStart : undefined} - onMouseUp={longPress ? handlePressEnd : undefined} - onMouseLeave={longPress ? handlePressCancel : undefined} - onTouchStart={longPress ? handlePressStart : undefined} - onTouchEnd={longPress ? handlePressEnd : undefined} - onTouchCancel={longPress ? handlePressCancel : undefined} + onPointerDown={longPress ? handlePressStart : undefined} + onPointerUp={longPress ? handlePressEnd : undefined} + onPointerCancel={longPress ? handlePressCancel : undefined}src/components/Global/PeanutActionDetailsCard/index.tsx (1)
159-165: Ensure flagcdn URL uses lowercase ISO alpha-2.flagcdn expects lowercase codes; uppercased inputs will 404.
- src={`https://flagcdn.com/w320/${countryCodeForFlag}.png`} + src={`https://flagcdn.com/w320/${countryCodeForFlag.toLowerCase()}.png`}src/components/Global/Icons/chevron-down.tsx (1)
3-10: Optional a11y: default to decorative.Most chevrons are decorative; set aria-hidden by default (overridable via props).
-export const ChevronDownIcon: FC<SVGProps<SVGSVGElement>> = (props) => ( - <svg width="13" height="8" viewBox="0 0 13 8" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> +export const ChevronDownIcon: FC<SVGProps<SVGSVGElement>> = ({ 'aria-hidden': ariaHidden = true, ...props }) => ( + <svg width="13" height="8" viewBox="0 0 13 8" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden={ariaHidden} {...props}>src/components/AddMoney/components/DepositMethodList.tsx (1)
59-60: Minor: remove redundant nullish coalescing
twoLetterCountryCodeis always a string;toLowerCase()never returns null/undefined. Drop?? ''.-const countryCodeForFlag = twoLetterCountryCode.toLowerCase() ?? '' +const countryCodeForFlag = twoLetterCountryCode.toLowerCase()src/hooks/useGeoLocaion.ts (1)
1-7: Optional: expose a reload functionReturning
reload()can help callers retry without remounting.const reload = () => { /* call fetchCountry() */ } return { countryCode, isLoading, error, reload }Consider feature-flagging geolocation (env toggle) and adding a fallback (e.g., server-provided country) to reduce dependency on a third-party endpoint.
src/constants/countryCurrencyMapping.ts (1)
9-35: Make the list immutable and type-safePrevent accidental mutation and ensure entries satisfy the interface.
-export const countryCurrencyMappings: CountryCurrencyMapping[] = [ +export const countryCurrencyMappings = [ // ... -] -export default countryCurrencyMappings +] as const satisfies ReadonlyArray<CountryCurrencyMapping> +export default countryCurrencyMappingssrc/components/LandingPage/RegulatedRails.tsx (1)
47-60: Remove unused parameter in createCloudAnimation.The
topparameter is unused; drop it and update call sites.- const createCloudAnimation = (side: 'left' | 'right', top: string, width: number, speed: number) => { + const createCloudAnimation = (side: 'left' | 'right', width: number, speed: number) => { ... - {...createCloudAnimation('left', '20%', 200, 35)} + {...createCloudAnimation('left', 200, 35)} ... - {...createCloudAnimation('right', '60%', 220, 40)} + {...createCloudAnimation('right', 220, 40)}Also applies to: 70-71, 77-78
src/hooks/useExchangeRate.ts (1)
131-135: Reset edit state on currency change to avoid stale destination input.When currencies change, resetting edit state prevents confusing displays.
- useEffect(() => { - setSourceAmount(initialSourceAmount) - }, [initialSourceAmount]) + useEffect(() => { + setSourceAmount(initialSourceAmount) + setLastEditedField('source') + clearDestinationFields() + }, [initialSourceAmount, sourceCurrency, destinationCurrency])scripts/compare-rates.mjs (2)
57-61: Support --arg=value style flags.Small QoL: accept both “--pairs value” and “--pairs=value”.
function getArg(name) { - const i = params.indexOf(name) - if (i === -1) return null - return params[i + 1] || null + const i = params.indexOf(name) + if (i !== -1) return params[i + 1] || null + const withEq = params.find((p) => p.startsWith(name + '=')) + return withEq ? withEq.slice(name.length + 1) : null }
133-139: Be forgiving with Local API rate type.If local API returns a numeric string, it currently shows “-”.
- const localRate = typeof local?.rate === 'number' ? local.rate : undefined + const localRate = typeof local?.rate === 'number' ? local.rate : (local?.rate != null ? Number(local.rate) : undefined)src/components/LandingPage/hero.tsx (4)
47-49: Questionable positioning on secondary CTA.Using right:[calc(...)] on a relatively positioned flex item is brittle. Prefer consistent centering via flex utilities.
-const getButtonContainerClasses = (variant: 'primary' | 'secondary') => - `relative z-20 mt-8 md:mt-12 flex flex-col items-center justify-center ${variant === 'primary' ? 'mx-auto w-fit' : 'right-[calc(50%-120px)]'}` +const getButtonContainerClasses = (variant: 'primary' | 'secondary') => + `relative z-20 mt-8 md:mt-12 flex flex-col items-center justify-center ${variant === 'primary' ? 'mx-auto w-fit' : 'mx-auto w-fit'}`
30-37: Simplify redundant translateX logic.- translateX: buttonVisible ? (variant === 'primary' ? 0 : 0) : 20, + translateX: buttonVisible ? 0 : 20,
50-52: Remove dead code or use it.getButtonClasses isn’t used; either wire it into or drop it.
-const getButtonClasses = (variant: 'primary' | 'secondary') => - `${variant === 'primary' ? 'btn bg-white fill-n-1 text-n-1 hover:bg-white/90' : 'btn-yellow'} px-7 md:px-9 py-3 md:py-8 text-base md:text-xl btn-shadow-primary-4` +// removed unused helper
161-163: Duplicate decorative stars (HeroImages) rendered twice.Confirm if this duplication is intentional; otherwise remove one to avoid visual clutter and extra paints.
- <HeroImages /> + {/* <HeroImages /> */} @@ - <HeroImages /> + <HeroImages />Also applies to: 181-181
src/interfaces/interfaces.ts (1)
314-341: Potential PII exposure risk.Account numbers/routing numbers are sensitive. Ensure logging/analytics never include these fields.
src/components/Claim/Claim.utils.ts (3)
4-7: Parameter is non-optional; remove redundant null-guard.The early return is now dead code.
export function mapToIPeanutChainDetailsArray( data: (_interfaces.SquidChainWithTokens & { networkName: string })[] ): _interfaces.CombinedType[] { - if (!data) return []
10-35: Prefer actual chain attributes over placeholders.Hardcoding mainnet: true and empty nativeCurrency may mislead consumers.
- nativeCurrency: { - name: '', - symbol: '', - decimals: 0, - }, + nativeCurrency: { + name: chain.nativeCurrency?.name || '', + symbol: chain.nativeCurrency?.symbol || '', + decimals: typeof chain.nativeCurrency?.decimals === 'number' ? chain.nativeCurrency.decimals : 18, + }, @@ - mainnet: true, + mainnet: chain.isTestnet ? false : true,
61-74: Type sortCrossChainDetails to reduce any[].-export const sortCrossChainDetails = (details: any[], order: any[], sourceChainId: string) => { +export const sortCrossChainDetails = <T extends { chainId: string }>( + details: T[], + order: Array<{ chainId: string }>, + sourceChainId: string +) => {src/hooks/useTokenChainIcons.ts (1)
47-48: Strengthen fallback chain name to prevent generic 'CHAIN' label.If
networkNameis undefined, fall back to other known fields (e.g.,axelarChainName,name) before defaulting.Apply:
-// use networkName if available, otherwise fallback to simple CHAIN -const resolvedChainName = chainInfo.networkName ? chainInfo.networkName : `CHAIN` +// Prefer networkName; fallback to other known fields before generic label +const resolvedChainName = + chainInfo.networkName ?? + (chainInfo as any).axelarChainName ?? + (chainInfo as any).name ?? + 'CHAIN'src/components/AddMoney/views/NetworkSelection.view.tsx (2)
51-53: Normalize chainId to a consistent type (string) to avoid subtle mismatchesIf supportedSquidChainsAndTokens keys are strings but squidChainDetails.chainId is a number, props/callbacks can drift. Normalize once.
- chainId={squidChainDetails?.chainId ?? network.chainId} + chainId={String(squidChainDetails?.chainId ?? network.chainId)} @@ - chainId: squidChainDetails?.chainId ?? network.chainId, + chainId: String(squidChainDetails?.chainId ?? network.chainId),Also applies to: 62-65
60-66: Prevent clicks on “Coming Soon” itemsGuard the onClick to no-op when isComingSoon, unless NetworkListItem itself disables interaction.
- onClick={() => - onNetworkSelect({ - chainId: squidChainDetails?.chainId ?? network.chainId, - name: squidChainDetails?.networkName ?? network.name, - iconUrl: squidChainDetails?.chainIconURI ?? network.iconUrl, - }) - } + onClick={() => { + if (isComingSoon) return + onNetworkSelect({ + chainId: String(squidChainDetails?.chainId ?? network.chainId), + name: squidChainDetails?.networkName ?? network.name, + iconUrl: squidChainDetails?.chainIconURI ?? network.iconUrl, + }) + }}If NetworkListItem already disables interaction when isComingSoon, ignore this.
src/app/exchange/page.tsx (2)
1-1: Avoid page-level "use client" unless necessaryLet this page be a Server Component and render Client children (Layout/NoFees/Footer) as needed. This improves SSR/streaming and reduces bundle size.
-'use client' +
6-13: Optional: Add page metadata for SEOConsider exporting generateMetadata or a static metadata export for title/description of the Exchange page.
src/hooks/useSavedAccounts.tsx (1)
13-20: Tighten filter with includes to reduce branching and ease future extensionCleaner and less error-prone than multiple ORs.
- const savedAccounts = useMemo(() => { - return ( - user?.accounts.filter( - (acc) => acc.type === AccountType.IBAN || acc.type === AccountType.US || acc.type === AccountType.CLABE - ) ?? [] - ) - }, [user]) + const savedAccounts = useMemo(() => { + const allowed = [AccountType.IBAN, AccountType.US, AccountType.CLABE] as const + return user?.accounts?.filter((acc) => allowed.includes(acc.type)) ?? [] + }, [user])src/components/Common/CountryListSkeleton.tsx (1)
14-21: Disable interaction on skeleton itemsPrevents hover/click affordances on loading placeholders.
<SearchResultCard key={index} title={<div className="h-4 w-24 animate-pulse rounded bg-gray-200" />} position={position} - onClick={() => {}} + onClick={() => {}} + isDisabled leftIcon={<div className="h-8 w-8 animate-pulse rounded-full bg-gray-200" />} />src/components/Withdraw/views/Initial.withdraw.view.tsx (1)
51-66: Consider capturing review-prep failures in telemetry.Replace
console.errorwith your centralized logger (Sentry is used elsewhere) so missing details in withdraw review attempts are observable.- console.error('Token, chain, or address not selected/entered') + Sentry.captureMessage('Withdraw review missing data', { + level: 'warning', + extra: { selectedTokenData: !!selectedTokenData, selectedChainData: !!selectedChainData, hasRecipient: !!recipient.address } + })src/components/Withdraw/views/Confirm.withdraw.view.tsx (1)
141-141: Harden chain name fallback to avoid blank labels.If
resolvedChainNameandnetworkNameare unexpectedly missing, fall back tochainIdto prevent empty UI.- <span className="capitalize">{resolvedChainName || chain.networkName}</span> + <span className="capitalize">{resolvedChainName || chain.networkName || String(chain.chainId)}</span>src/context/WithdrawFlowContext.tsx (1)
119-135: Trim unnecessary setter from useMemo deps.State setters from
useStateare stable; including them in deps is noise.], - [ + [ amountToWithdraw, currentView, withdrawData, showCompatibilityModal, isPreparingReview, paymentError, usdAmount, isValidRecipient, inputChanging, recipient, error, selectedBankAccount, showAllWithdrawMethods, - setShowAllWithdrawMethods, resetWithdrawFlow, ]src/context/tokenSelector.context.tsx (2)
165-166: Use strict equality for chainId checks.Avoid implicit coercion; normalize both sides to strings.
- if (!supportedMobulaChains.some((chain) => chain.chainId == chainId)) { + if (!supportedMobulaChains.some((chain) => String(chain.chainId) === String(chainId))) {
175-183: Optional: Consider price=0 fallback on fetch miss (parity with Pay flow).If desired to keep UI flows active when price APIs miss, set
selectedTokenPriceto0instead ofundefined. Confirm this won’t enable flows that should be blocked without a price.src/app/(mobile-ui)/home/page.tsx (1)
137-159: Deduplicate the two balance-warning effects.They’re nearly identical but with slightly different conditions; keeping one source reduces drift.
- // effect for showing balance warning modal - useEffect(() => { - if (isFetchingBalance || balance === undefined) return - ... - }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal]) - - // effect for showing balance warning modal - useEffect(() => { - if (isFetchingBalance || balance === undefined) return - ... - }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal]) + // effect for showing balance warning modal + useEffect(() => { + if (isFetchingBalance || balance === undefined) return + if (typeof window === 'undefined') return + const hasSeenBalanceWarning = getFromLocalStorage(`${user!.user.userId}-hasSeenBalanceWarning`) + const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) + if ( + balanceInUsd > BALANCE_WARNING_THRESHOLD && + !hasSeenBalanceWarning && + !showIOSPWAInstallModal && + !showAddMoneyPromptModal && + !isPostSignupActionModalVisible + ) { + setShowBalanceWarningModal(true) + } + }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal, isPostSignupActionModalVisible])Also applies to: 160-182
src/app/actions/external-accounts.ts (3)
5-5: Use a type-only import to avoid unintentionally bundling types at runtime.-import { IBridgeAccount } from '@/interfaces' +import type { IBridgeAccount } from '@/interfaces'
7-9: Fail fast if required env vars are missing (avoid non-null assertions).-const API_KEY = process.env.PEANUT_API_KEY! -const API_URL = process.env.PEANUT_API_URL! +const API_KEY = process.env.PEANUT_API_KEY +const API_URL = process.env.PEANUT_API_URL +if (!API_KEY || !API_URL) { + throw new Error('PEANUT_API_KEY and PEANUT_API_URL must be set on the server.') +}
32-36: Avoid logging PII in server logs. Mask customer identifiers.- console.error(`Error creating external account for ${customerId}:`, error) + console.error(`Error creating external account for ****${customerId.slice(-4)}:`, error)src/components/Global/TokenSelector/TokenSelector.tsx (2)
575-577: Handle Drawer open/close correctly and only clear search on close.Current handler always calls close logic. Respect the next open state.
- <Drawer open={isDrawerOpen} onOpenChange={closeDrawer}> + <Drawer + open={isDrawerOpen} + onOpenChange={(open) => { + setIsDrawerOpen(open) + if (!open) setTimeout(() => setSearchValue(''), 200) + }} + >
525-536: Reset token image error when the logo URI changes.Without reset, a prior error masks new valid icons.
Add this effect nearby:
useEffect(() => { setButtonImageError(false) }, [buttonLogoURI])Also applies to: 548-548
src/components/Claim/Link/views/Confirm.bank-claim.view.tsx (1)
74-82: Enable copy-to-clipboard for bank identifiers (IBAN/CLABE/account/routing).Improves UX and reduces retyping errors.
- {bankDetails.iban && <PaymentInfoRow label="IBAN" value={bankDetails.iban.toUpperCase()} />} - {bankDetails.bic && <PaymentInfoRow label="BIC" value={bankDetails.bic.toUpperCase()} />} - {bankDetails.clabe && <PaymentInfoRow label="CLABE" value={bankDetails.clabe.toUpperCase()} />} + {bankDetails.iban && ( + <PaymentInfoRow label="IBAN" value={bankDetails.iban.toUpperCase()} allowCopy /> + )} + {bankDetails.bic && ( + <PaymentInfoRow label="BIC" value={bankDetails.bic.toUpperCase()} allowCopy /> + )} + {bankDetails.clabe && ( + <PaymentInfoRow label="CLABE" value={bankDetails.clabe.toUpperCase()} allowCopy /> + )} {bankDetails.accountNumber && ( - <PaymentInfoRow label="Account Number" value={bankDetails.accountNumber.toUpperCase()} /> + <PaymentInfoRow + label="Account Number" + value={bankDetails.accountNumber.toUpperCase()} + allowCopy + /> )} {bankDetails.routingNumber && ( - <PaymentInfoRow label="Routing Number" value={bankDetails.routingNumber.toUpperCase()} /> + <PaymentInfoRow + label="Routing Number" + value={bankDetails.routingNumber.toUpperCase()} + allowCopy + /> )}src/components/Profile/index.tsx (1)
9-11: Remove unused import after the back-logic change.-import { checkIfInternalNavigation } from '@/utils'src/hooks/useKycFlow.ts (2)
129-141: Tighten ToS-accepted -> KYC transition per known invariant.Given our invariant that a KYC link is always present after ToS acceptance (see Learnings), we can drop the defensive
apiResponse?.kycLinkcheck and assert non-null, keeping the “only when ToS is currently shown” guard.Apply:
- if (source === 'tos_accepted') { - if (wasShowingTos && apiResponse?.kycLink) { - const kycUrl = convertPersonaUrl(apiResponse.kycLink) + if (source === 'tos_accepted') { + if (wasShowingTos) { + const kycUrl = convertPersonaUrl(apiResponse!.kycLink) setIframeOptions({ src: kycUrl, visible: true, closeConfirmMessage: 'Are you sure? Your KYC progress will be lost.', }) } // ignore late ToS events when KYC is already open return }
150-157: Manual close behavior: confirm intended redirect only for add-flow.Behavior is sound. If other flows later need custom handling, consider passing an enum for source+context to avoid sprinkling flow checks.
src/components/LandingPage/Footer.tsx (1)
105-115: Use the imported SVG object with next/image (avoid .src).Passing the
StaticImportdirectly lets Next optimize consistently.Apply:
- <Image src={handThumbsUp.src} alt="Hand thumbs up" width={20} height={20} /> + <Image src={handThumbsUp} alt="Hand thumbs up" width={20} height={20} /> @@ - <Image src={handMiddleFinger.src} alt="Hand Middle finger" width={20} height={20} /> + <Image src={handMiddleFinger} alt="Hand Middle finger" width={20} height={20} /> @@ - <Image src={handWaving.src} alt="Hand waving" width={25} height={25} /> + <Image src={handWaving} alt="Hand waving" width={25} height={25} />src/components/LandingPage/securityBuiltIn.tsx (1)
12-16: Remove unused titleSvg from Feature.
titleSvgis defined but unused; drop it to reduce noise and prevent dead imports.Apply:
interface Feature { id: number title: string - titleSvg: any description: string iconSrc: any iconAlt: string } @@ - titleSvg: biometricProtection, @@ - titleSvg: selfCustodialDesign, @@ - titleSvg: kycOnlyWhenRequired,And remove the now-unused imports:
-import biometricProtection from '@/assets/illustrations/biometric-protection.svg' -import selfCustodialDesign from '@/assets/illustrations/self-custodial-design.svg' -import kycOnlyWhenRequired from '@/assets/illustrations/kyc-only-when-required.svg'Also applies to: 20-46
src/components/LandingPage/yourMoney.tsx (2)
71-71: Improve image semantics and LCP.Add a descriptive alt and mark this as priority; include responsive sizes.
- <Image src={LandingCountries} alt="countries" /> + <Image src={LandingCountries} alt="Global availability map" priority sizes="(min-width: 768px) 50vw, 100vw" />
20-48: Remove unused features data (dead code).The features array isn’t rendered anymore; delete or move to a constants file to avoid drift.
src/components/Common/CountryList.tsx (1)
105-106: Optional: avoid invalid flag URLs when no mapping exists.If no 2-letter mapping, skip the flag image instead of falling back to
country.id.toLowerCase().Also ensure
flagcdn.comis in next.config.js images.domains.src/utils/withdraw.utils.ts (2)
8-27: Add basic IBAN structure guard.Avoid false positives by ensuring IBAN starts with two letters; return null otherwise.
export const getCountryFromIban = (iban: string): string | null => { // Remove spaces and convert to uppercase const cleanIban = iban.replace(/\s/g, '').toUpperCase() + // Require at least 2 leading letters + if (!/^[A-Z]{2}/.test(cleanIban)) return null + // Extract the first 2 characters as country code const countryCode = cleanIban.substring(0, 2)
82-134: Nit: correct function name casing.Consider renaming to validateMXClabeAccount or validateMXCLABEAccount for consistency with CLABE acronym.
src/components/Claim/Link/views/BankFlowManager.view.tsx (1)
411-420: Name parsing is brittle.Splitting fullName by space fails for middle/compound names. Consider storing first/last separately in account details or using a safer split with a fallback.
src/components/LandingPage/noFees.tsx (2)
241-251: Optional: flag image fallback.If a currency has no flag mapping, the remote image 404s. Render the flag only when code exists (or add an onError fallback) to avoid layout shifts.
- <Image - src={`https://flagcdn.com/w320/${sourceCurrencyFlag}.png`} - alt={`${sourceCurrencyFlag} flag`} - width={160} - height={160} - className="size-4 rounded-full object-cover" - /> + {sourceCurrencyFlag && ( + <Image + src={`https://flagcdn.com/w320/${sourceCurrencyFlag}.png`} + alt={`${sourceCurrencyFlag} flag`} + width={160} + height={160} + className="size-4 rounded-full object-cover" + /> + )}
285-296: Apply same optional flag fallback for destination.- <Image - src={`https://flagcdn.com/w320/${destinationCurrencyFlag}.png`} - alt={`${destinationCurrencyFlag} flag`} - width={160} - height={160} - className="size-4 rounded-full object-cover" - /> + {destinationCurrencyFlag && ( + <Image + src={`https://flagcdn.com/w320/${destinationCurrencyFlag}.png`} + alt={`${destinationCurrencyFlag} flag`} + width={160} + height={160} + className="size-4 rounded-full object-cover" + /> + )}src/components/Common/ActionList.tsx (3)
100-105: Remove dead switch branch.'crypto' is not present in ACTION_METHODS; the case can be dropped.
- case 'crypto': case 'exchange-or-wallet': setClaimToExternalWallet(true) break
24-30: Type the icons field precisely.Avoid any[]. Use a union to match actual usage.
-interface Method { +interface Method { id: string title: string description: string - icons: any[] + icons: (string | StaticImageData)[] soon: boolean }Add (outside this hunk):
import type { StaticImageData } from 'next/image'
32-58: Move ACTION_METHODS to a constants module to prevent circular deps.Follow our prior learning for ActionList to centralize constants.
-const ACTION_METHODS: Method[] = [ - /* ... */ -]New file suggestion (outside this hunk):
// src/constants/actionlist.consts.ts import mercadoPagoIcon from '@/assets/payment-apps/mercado-pago.svg' import binanceIcon from '@/assets/exchanges/binance.svg' import { METAMASK_LOGO, TRUST_WALLET_SMALL_LOGO } from '@/assets/wallets' export interface ActionMethod { id: string title: string description: string icons: (string | StaticImageData)[] soon: boolean } export const ACTION_METHODS: ActionMethod[] = [ { id: 'bank', title: 'Bank', description: 'EUR, USD, ARS (more coming soon)', icons: ['https://flagcdn.com/w160/ar.png','https://flagcdn.com/w160/de.png','https://flagcdn.com/w160/us.png'], soon: false, }, { id: 'mercadopago', title: 'Mercado Pago', description: 'Instant transfers', icons: [mercadoPagoIcon], soon: true }, { id: 'exchange-or-wallet', title: 'Exchange or Wallet', description: 'Binance, Coinbase, Metamask and more', icons: [binanceIcon, TRUST_WALLET_SMALL_LOGO, METAMASK_LOGO], soon: false }, ]Then import it here:
import { ACTION_METHODS } from '@/constants/actionlist.consts'src/components/Claim/Link/Initial.view.tsx (2)
93-94: Remove unused resetClaimBankFlow destructure.It’s not used; drop to avoid confusion.
- resetFlow: resetClaimBankFlow,
773-784: DRY: centralize setup redirect logic.Same pattern exists here and in ActionList; extract to a helper.
Example helper:
// utils/navigation.ts export function pushSetupWithRedirect(router: ReturnType<typeof useRouter>) { const redirectUri = encodeURIComponent(`${window.location.pathname}${window.location.search}${window.location.hash}`) saveRedirectUrl() router.push(`/setup?redirect_uri=${redirectUri}`) }Then replace the inline block with:
pushSetupWithRedirect(router)
| const BRIDGE_API_KEY = process.env.BRIDGE_API_KEY | ||
|
|
||
| function getArg(name) { | ||
| const i = params.indexOf(name) | ||
| if (i === -1) return null | ||
| return params[i + 1] || null | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add a global timeout for network calls to avoid hanging CLI runs.
Long-running fetches can stall the script indefinitely. Introduce a configurable timeout.
const BRIDGE_API_KEY = process.env.BRIDGE_API_KEY
+const TIMEOUT_MS = Number(process.env.RATE_FETCH_TIMEOUT_MS) || 8000
+
+// helper
+async function fetchWithTimeout(input, init = {}, timeoutMs = TIMEOUT_MS) {
+ const controller = new AbortController()
+ const id = setTimeout(() => controller.abort(new Error('Timeout')), timeoutMs)
+ try {
+ return await fetch(input, { ...init, signal: controller.signal })
+ } finally {
+ clearTimeout(id)
+ }
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const BRIDGE_API_KEY = process.env.BRIDGE_API_KEY | |
| function getArg(name) { | |
| const i = params.indexOf(name) | |
| if (i === -1) return null | |
| return params[i + 1] || null | |
| } | |
| const BRIDGE_API_KEY = process.env.BRIDGE_API_KEY | |
| const TIMEOUT_MS = Number(process.env.RATE_FETCH_TIMEOUT_MS) || 8000 | |
| // helper | |
| async function fetchWithTimeout(input, init = {}, timeoutMs = TIMEOUT_MS) { | |
| const controller = new AbortController() | |
| const id = setTimeout(() => controller.abort(new Error('Timeout')), timeoutMs) | |
| try { | |
| return await fetch(input, { ...init, signal: controller.signal }) | |
| } finally { | |
| clearTimeout(id) | |
| } | |
| } | |
| function getArg(name) { | |
| const i = params.indexOf(name) | |
| if (i === -1) return null | |
| return params[i + 1] || null | |
| } |
🤖 Prompt for AI Agents
In scripts/compare-rates.mjs around lines 55 to 61, the script currently
performs network fetches without any timeout which can hang the CLI; add a
configurable global timeout mechanism: read a timeout value from an env var
(e.g., FETCH_TIMEOUT_MS) with a sensible default (e.g., 15000 ms), implement a
helper that wraps fetch using AbortController and a setTimeout to abort the
request when the timeout elapses (clear the timer on success/failure), and
replace direct fetch calls with this fetchWithTimeout helper so all network
calls respect the timeout and the process won't hang indefinitely.
| async function fetchBridge(from, to) { | ||
| if (!BRIDGE_API_KEY) { | ||
| return { error: 'Missing BRIDGE_API_KEY' } | ||
| } | ||
| const url = `https://api.bridge.xyz/v0/exchange_rates?from=${from.toLowerCase()}&to=${to.toLowerCase()}` | ||
| const res = await fetch(url, { | ||
| method: 'GET', | ||
| headers: { 'Api-Key': BRIDGE_API_KEY }, | ||
| }) | ||
| if (!res.ok) { | ||
| return { error: `${res.status} ${res.statusText}` } | ||
| } | ||
| const data = await res.json() | ||
| const { midmarket_rate, buy_rate, sell_rate } = data || {} | ||
| return { midmarket_rate, buy_rate, sell_rate } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use the timeout helper and tighten error handling for Bridge calls.
Also send Accept to be explicit.
- const res = await fetch(url, {
+ const res = await fetchWithTimeout(url, {
method: 'GET',
- headers: { 'Api-Key': BRIDGE_API_KEY },
+ headers: { 'Api-Key': BRIDGE_API_KEY, 'Accept': 'application/json' },
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function fetchBridge(from, to) { | |
| if (!BRIDGE_API_KEY) { | |
| return { error: 'Missing BRIDGE_API_KEY' } | |
| } | |
| const url = `https://api.bridge.xyz/v0/exchange_rates?from=${from.toLowerCase()}&to=${to.toLowerCase()}` | |
| const res = await fetch(url, { | |
| method: 'GET', | |
| headers: { 'Api-Key': BRIDGE_API_KEY }, | |
| }) | |
| if (!res.ok) { | |
| return { error: `${res.status} ${res.statusText}` } | |
| } | |
| const data = await res.json() | |
| const { midmarket_rate, buy_rate, sell_rate } = data || {} | |
| return { midmarket_rate, buy_rate, sell_rate } | |
| } | |
| async function fetchBridge(from, to) { | |
| if (!BRIDGE_API_KEY) { | |
| return { error: 'Missing BRIDGE_API_KEY' } | |
| } | |
| const url = `https://api.bridge.xyz/v0/exchange_rates?from=${from.toLowerCase()}&to=${to.toLowerCase()}` | |
| const res = await fetchWithTimeout(url, { | |
| method: 'GET', | |
| headers: { 'Api-Key': BRIDGE_API_KEY, 'Accept': 'application/json' }, | |
| }) | |
| if (!res.ok) { | |
| return { error: `${res.status} ${res.statusText}` } | |
| } | |
| const data = await res.json() | |
| const { midmarket_rate, buy_rate, sell_rate } = data || {} | |
| return { midmarket_rate, buy_rate, sell_rate } | |
| } |
🤖 Prompt for AI Agents
In scripts/compare-rates.mjs around lines 63 to 78, the Bridge fetch lacks a
timeout wrapper, explicit Accept header, and robust error handling; update
fetchBridge to use the project timeout helper (wrap the fetch call so it rejects
after the configured timeout), add headers 'Api-Key' and 'Accept:
application/json', and wrap the whole operation in try/catch so network or
timeout errors produce a clear { error: ... } return; additionally validate the
parsed JSON before destructuring and return an error if expected fields are
missing.
| async function fetchFrankfurter(from, to) { | ||
| const url = `https://api.frankfurter.app/latest?from=${from}&to=${to}` | ||
| const res = await fetch(url, { method: 'GET' }) | ||
| if (!res.ok) { | ||
| return { error: `${res.status} ${res.statusText}` } | ||
| } | ||
| const data = await res.json() | ||
| const rate = data?.rates?.[to] | ||
| return { rate, rate_995: typeof rate === 'number' ? rate * 0.995 : undefined } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Apply timeout and explicit Accept for Frankfurter.
- const res = await fetch(url, { method: 'GET' })
+ const res = await fetchWithTimeout(url, { method: 'GET', headers: { 'Accept': 'application/json' } })Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In scripts/compare-rates.mjs around lines 80 to 89, the Frankfurter fetch call
needs a request timeout and an explicit Accept header; update fetch to use an
AbortController with a short timeout (e.g., 3–5s) so the request is aborted on
timeout, include headers: { Accept: 'application/json' }, and handle
abort/errors by returning a descriptive { error: 'timeout' } or the fetch error
message; ensure the timeout timer is cleared after response to avoid leaks.
| async function fetchLocalApi(from, to) { | ||
| if (!apiArg) return {} | ||
| try { | ||
| const url = `${apiArg}?from=${from}&to=${to}` | ||
| const res = await fetch(url, { method: 'GET' }) | ||
| if (!res.ok) { | ||
| return { error: `${res.status} ${res.statusText}` } | ||
| } | ||
| const data = await res.json() | ||
| return { rate: data?.rate } | ||
| } catch (error) { | ||
| return { error: `Connection failed: ${error.message}` } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Apply timeout and coerce local API rate to a number.
Prevent silent undefined when rate comes back as a string.
- const res = await fetch(url, { method: 'GET' })
+ const res = await fetchWithTimeout(url, { method: 'GET', headers: { 'Accept': 'application/json' } })
@@
- const data = await res.json()
- return { rate: data?.rate }
+ const data = await res.json()
+ const n = typeof data?.rate === 'number' ? data.rate : Number(data?.rate)
+ return { rate: Number.isFinite(n) ? n : undefined }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function fetchLocalApi(from, to) { | |
| if (!apiArg) return {} | |
| try { | |
| const url = `${apiArg}?from=${from}&to=${to}` | |
| const res = await fetch(url, { method: 'GET' }) | |
| if (!res.ok) { | |
| return { error: `${res.status} ${res.statusText}` } | |
| } | |
| const data = await res.json() | |
| return { rate: data?.rate } | |
| } catch (error) { | |
| return { error: `Connection failed: ${error.message}` } | |
| } | |
| } | |
| async function fetchLocalApi(from, to) { | |
| if (!apiArg) return {} | |
| try { | |
| const url = `${apiArg}?from=${from}&to=${to}` | |
| const res = await fetchWithTimeout(url, { | |
| method: 'GET', | |
| headers: { 'Accept': 'application/json' } | |
| }) | |
| if (!res.ok) { | |
| return { error: `${res.status} ${res.statusText}` } | |
| } | |
| const data = await res.json() | |
| const n = typeof data?.rate === 'number' | |
| ? data.rate | |
| : Number(data?.rate) | |
| return { rate: Number.isFinite(n) ? n : undefined } | |
| } catch (error) { | |
| return { error: `Connection failed: ${error.message}` } | |
| } | |
| } |
🤖 Prompt for AI Agents
In scripts/compare-rates.mjs around lines 91 to 104, the local API call needs a
request timeout and must coerce the returned rate to a number to avoid silently
returning undefined when the API sends a string; add an AbortController with a
short timeout (e.g. 3-5s) and pass its signal to fetch, clear the timeout on
success, await res.json(), then coerce data.rate using Number(data.rate) (or
parseFloat) and return { rate: Number(...) } only if it is a finite number
otherwise return an { error: 'Invalid rate' } (preserve non-OK HTTP handling and
include error details on catch).
| ...(siteUrl ? { metadataBase: new URL(siteUrl) } : {}), | ||
| icons: { | ||
| icon: '/logo-favicon.png', | ||
| icon: '/favicon.ico', |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Switched favicon to /favicon.ico—confirm file exists and cross-page consistency.
Change is fine; ensure public/favicon.ico exists and update other pages still referencing legacy /logo-favicon.png.
🏁 Script executed:
#!/bin/bash
fd -a 'favicon.ico' public
rg -n "/logo-favicon.png" -g '!node_modules'Length of output: 214
Update QR sheet script to reference new favicon
- In scripts/generate-qr-sheet.ts line 10, replace
path.join(projectRoot, 'public/logo-favicon.png')withpath.join(projectRoot, 'public/favicon.ico'). Verifiedpublic/favicon.icoexists.
🤖 Prompt for AI Agents
In src/app/(mobile-ui)/claim/page.tsx around line 100 the QR sheet script
reference must be updated: modify scripts/generate-qr-sheet.ts (line 10) to
replace path.join(projectRoot, 'public/logo-favicon.png') with
path.join(projectRoot, 'public/favicon.ico') so the script points to the
verified existing favicon; ensure the import/path variable names remain correct
and run the script to confirm the new path resolves.
| export const useGeoLocaion = () => { | ||
| const [countryCode, setCountryCode] = useState<string | null>(null) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Typo in hook name: rename to useGeoLocation (file and export)
This will avoid import confusion and align with standard spelling.
- Rename file to
useGeoLocation.ts. - Update all imports project-wide:
Apply within this file:
-export const useGeoLocaion = () => {
+export const useGeoLocation = () => {🏁 Script executed:
#!/bin/bash
rg -n "useGeoLocaion" -g "!node_modules"Length of output: 338
Rename hook and file to correct spelling
- Rename file
src/hooks/useGeoLocaion.ts→src/hooks/useGeoLocation.tsand update its export to:- export const useGeoLocaion = () => { + export const useGeoLocation = () => {
- Update all usages in
src/components/Common/CountryList.tsx:- Line 9: change
import { useGeoLocaion } from '@/hooks/useGeoLocaion'→import { useGeoLocation } from '@/hooks/useGeoLocation' - Line 35: change
useGeoLocaion()→useGeoLocation()
- Line 9: change
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const useGeoLocaion = () => { | |
| const [countryCode, setCountryCode] = useState<string | null>(null) | |
| // File: src/hooks/useGeoLocation.ts | |
| import { useState } from 'react' | |
| export const useGeoLocation = () => { | |
| const [countryCode, setCountryCode] = useState<string | null>(null) | |
| // …rest of hook… | |
| } |
🤖 Prompt for AI Agents
In src/hooks/useGeoLocaion.ts around lines 8-9, the hook and filename are
misspelled as "useGeoLocaion"; rename the file to src/hooks/useGeoLocation.ts,
update the exported function name to useGeoLocation, and update all
imports/usages accordingly—specifically change the import in
src/components/Common/CountryList.tsx line 9 from "@/hooks/useGeoLocaion" to
"@/hooks/useGeoLocation" and replace the call at line 35 from useGeoLocaion() to
useGeoLocation().
| const fetchCountry = async () => { | ||
| try { | ||
| const response = await fetch('https://ipapi.co/country') | ||
| if (!response.ok) { | ||
| throw new Error('Failed to fetch country') | ||
| } | ||
| const countryCode = await response.text() | ||
| setCountryCode(countryCode) | ||
| } catch (err: any) { | ||
| setError(err.message) | ||
| } finally { | ||
| setIsLoading(false) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Harden fetch with timeout, abort, and normalization
Prevent hanging requests, normalize the code, and improve error safety.
- const fetchCountry = async () => {
+ const fetchCountry = async () => {
try {
- const response = await fetch('https://ipapi.co/country')
+ const controller = new AbortController()
+ const timeout = setTimeout(() => controller.abort(), 5000)
+ const response = await fetch('https://ipapi.co/country', {
+ signal: controller.signal,
+ headers: { Accept: 'text/plain' },
+ cache: 'no-store',
+ })
if (!response.ok) {
throw new Error('Failed to fetch country')
}
- const countryCode = await response.text()
- setCountryCode(countryCode)
- } catch (err: any) {
- setError(err.message)
+ const text = await response.text()
+ const code = text.trim().toUpperCase()
+ setCountryCode(code)
+ } catch (err: unknown) {
+ setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
+ // @ts-ignore - clear even if controller aborted
+ if (typeof timeout !== 'undefined') clearTimeout(timeout)
setIsLoading(false)
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/hooks/useGeoLocaion.ts around lines 15 to 27, the fetch call should be
hardened: wrap fetch in an AbortController with a timeout (e.g., 3–5s) that
calls controller.abort(), pass controller.signal to fetch, clear the timeout on
success, and handle AbortError separately in the catch; also normalize the
returned country code by trimming and converting to uppercase before calling
setCountryCode, and ensure error messages are safe (use a default string if
err.message is missing) while retaining the existing finally block to
setIsLoading(false).
| export interface IBridgeAccount { | ||
| id: string | ||
| customer_id: string | ||
| created_at: string | ||
| updated_at: string | ||
| bank_name: string | null | ||
| account_name: string | null | ||
| account_owner_name: string | ||
| active: boolean | ||
| currency: string | ||
| account_owner_type: string | null | ||
| account_type: 'iban' | 'us' | ||
| first_name: string | null | ||
| last_name: string | null | ||
| business_name: string | null | ||
| beneficiary_address_valid?: boolean // Optional, only present in US account | ||
| last_4: string | ||
|
|
||
| // Use a union type for the account-specific details | ||
| account_details: IBridgeIbanDetails | IBridgeUsAccountDetails | ||
| } | ||
|
|
||
| export interface IBridgeIbanDetails { | ||
| type: 'iban' | ||
| last_4: string | ||
| bic: string | ||
| country: string | ||
| } | ||
|
|
||
| interface IBridgeUsAccountDetails { | ||
| type: 'us' | ||
| last_4: string | ||
| routing_number: string | ||
| } | ||
|
|
||
| interface IBridgeDepositInstructions { | ||
| payment_rail: string | ||
| amount: string | ||
| currency: string | ||
| from_address: string | ||
| to_address: string | ||
| } | ||
|
|
||
| interface IBridgeSource { | ||
| payment_rail: string | ||
| currency: string | ||
| from_address: string | ||
| } | ||
|
|
||
| interface IBridgeDestination { | ||
| payment_rail: string | ||
| currency: string | ||
| external_account_id: string | ||
| } | ||
|
|
||
| interface IBridgeReceipt { | ||
| initial_amount: string | ||
| developer_fee: string | ||
| exchange_fee: string | ||
| subtotal_amount: string | ||
| gas_fee: string | ||
| final_amount: string | ||
| exchange_rate?: string | ||
| } | ||
|
|
||
| interface IBridgeTransaction { | ||
| id: string | ||
| client_reference_id: string | null | ||
| state: string | ||
| on_behalf_of: string | ||
| source_deposit_instructions: IBridgeDepositInstructions | ||
| currency: string | ||
| amount: string | ||
| developer_fee: string | ||
| source: IBridgeSource | ||
| destination: IBridgeDestination | ||
| receipt: IBridgeReceipt | ||
| created_at: string | ||
| updated_at: string | ||
| currency?: 'usd' | 'eur' | 'mxn' | ||
| bank_name?: string | ||
| account_owner_name: string | ||
| account_number?: string | ||
| routing_number?: string | ||
| account_type: 'iban' | 'us' | 'clabe' | ||
| iban?: { | ||
| account_number: string | ||
| bic?: string | ||
| country: string | ||
| } | ||
| clabe?: { | ||
| account_number: string | ||
| } | ||
| account?: { | ||
| account_number: string | ||
| routing_number: string | ||
| checking_or_savings?: string | ||
| } | ||
| account_owner_type: 'individual' | 'business' | ||
| first_name?: string | ||
| last_name?: string | ||
| business_name?: string | ||
| address?: { | ||
| street_line_1: string | ||
| street_line_2?: string | ||
| city: string | ||
| country: string | ||
| state?: string | ||
| postal_code?: string | ||
| } | ||
| beneficiary_address_valid: boolean | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Model Bridge accounts as a discriminated union and relax currency.
- Top-level account_number/routing_number duplicate nested fields; make the shape mutually exclusive via account_type.
- Bridge may add currencies beyond 'usd' | 'eur' | 'mxn'; use a string or a broader union to avoid future breaks.
- Consider making last_4 optional (varies by account type).
export interface IBridgeAccount {
id: string
customer_id: string
- last_4: string
- currency?: 'usd' | 'eur' | 'mxn'
+ last_4?: string
+ currency?: string
- bank_name?: string
- account_owner_name: string
- account_number?: string
- routing_number?: string
- account_type: 'iban' | 'us' | 'clabe'
- iban?: {
+ bank_name?: string
+ account_owner_name: string
+ account_type: 'iban' | 'us' | 'clabe'
+ // IBAN details
+ iban?: {
account_number: string
bic?: string
country: string
}
- clabe?: {
+ // Mexico CLABE
+ clabe?: {
account_number: string
}
- account?: {
+ // US account details
+ account?: {
account_number: string
routing_number: string
- checking_or_savings?: string
+ checking_or_savings?: 'checking' | 'savings'
}
account_owner_type: 'individual' | 'business'
@@
beneficiary_address_valid: boolean
}To confirm optionality and currency list, search call sites:
🏁 Script executed:
#!/bin/bash
rg -n "IBridgeAccount" -C2
rg -n "\blast_4\b" -C2Length of output: 1406
🏁 Script executed:
#!/bin/bash
# Search for usage of account_number outside the interface definition
rg -n "account_number" -C2 | grep -v 'src/interfaces/interfaces.ts'
# Search for usage of routing_number outside the interface definition
rg -n "routing_number" -C2 | grep -v 'src/interfaces/interfaces.ts'Length of output: 6834
Update call sites or preserve top-level fields
Removing account_number/routing_number from the top level breaks existing consumers. In
src/components/Claim/Link/views/BankFlowManager.view.tsx (around lines 332 and 334), change:
accountNumber: externalAccountResponse.account_number
routingNumber: externalAccountResponse.account?.routing_numberto use the nested properties (externalAccountResponse.account.account_number and .account.routing_number), or keep the top-level aliases on IBridgeAccount. Also verify any other direct uses of account_number/routing_number.
🤖 Prompt for AI Agents
In src/interfaces/interfaces.ts around lines 182 to 218, the interface removed
top-level account_number and routing_number which breaks consumers expecting
those fields; restore compatibility by either reintroducing top-level
account_number?: string and routing_number?: string as aliases that mirror
account?.account_number and account?.routing_number, or update all call sites
(e.g., src/components/Claim/Link/views/BankFlowManager.view.tsx lines ~332-334)
to read nested values externalAccountResponse.account.account_number and
externalAccountResponse.account.routing_number; ensure both patterns are handled
or add a migration note and update tests.
| }, | ||
| export const TOKEN_DETAILS = [ | ||
| { | ||
| chainId: '1', |
There was a problem hiding this comment.
Blocker: chainId should be a number, not a string
Downstream lookups typically use numeric chainIds (and CHAIN_DETAILS here uses numbers). Using a string risks mismatches (e.g., strict equality or map keys).
Apply this diff:
- chainId: '1',
+ chainId: 1,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| chainId: '1', | |
| chainId: 1, |
🤖 Prompt for AI Agents
In src/utils/__mocks__/peanut-sdk.ts around line 19, the mock sets chainId as
the string '1' which must be a number to match downstream lookups and
CHAIN_DETAILS; change chainId from the string '1' to the numeric literal 1
(ensure any related tests or mock consumers expect a number and update them if
they rely on a string).
| export function checkIfInternalNavigation(): boolean { | ||
| return !!document.referrer && new URL(document.referrer).origin === window.location.origin | ||
| } |
There was a problem hiding this comment.
Guard against SSR and invalid referrers in checkIfInternalNavigation.
Accessing window/document will crash on the server; also URL() can throw on malformed referrers.
Apply:
-export function checkIfInternalNavigation(): boolean {
- return !!document.referrer && new URL(document.referrer).origin === window.location.origin
-}
+export function checkIfInternalNavigation(): boolean {
+ if (typeof window === 'undefined' || typeof document === 'undefined') return false
+ const ref = document.referrer
+ if (!ref) return false
+ try {
+ return new URL(ref).origin === window.location.origin
+ } catch {
+ return false
+ }
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function checkIfInternalNavigation(): boolean { | |
| return !!document.referrer && new URL(document.referrer).origin === window.location.origin | |
| } | |
| export function checkIfInternalNavigation(): boolean { | |
| if (typeof window === 'undefined' || typeof document === 'undefined') return false | |
| const ref = document.referrer | |
| if (!ref) return false | |
| try { | |
| return new URL(ref).origin === window.location.origin | |
| } catch { | |
| return false | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/utils/general.utils.ts around lines 1221 to 1223, the function accesses
document/window (breaking SSR) and calls new URL() directly (which can throw for
malformed referrers); update the function to first guard for SSR by checking
typeof window !== 'undefined' and typeof document !== 'undefined', ensure
document.referrer is a non-empty string, then try to construct a URL in a
try/catch (or otherwise safely parse) and return false on error; finally compare
the parsed referrer.origin to window.location.origin and return the boolean
result.
| error: string | null | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Prop contract change: ensure all call sites pass error.
error is now required; verify usages.
🏁 Script executed:
#!/bin/bash
rg -nP "(<|\\()\s*DynamicBankAccountForm\\b" -C3 --type=ts --type=tsxLength of output: 97
🏁 Script executed:
#!/bin/bash
rg -nP '(<|\()\s*DynamicBankAccountForm\b' -C3 -g '*.tsx' -g '*.ts'Length of output: 1502
🏁 Script executed:
#!/bin/bash
find . -type f -name 'AddWithdrawCountriesList.tsx'
find . -type f -name 'BankFlowManager.view.tsx'Length of output: 216
🏁 Script executed:
sed -n '1,200p' src/components/AddWithdraw/DynamicBankAccountForm.tsxLength of output: 8171
Pass the error prop to all DynamicBankAccountForm instances
DynamicBankAccountFormProps now requires error: string | null; add error={…} to each call site (e.g. in src/components/AddWithdraw/AddWithdrawCountriesList.tsx and src/components/Claim/Link/views/BankFlowManager.view.tsx).
🤖 Prompt for AI Agents
In src/components/AddWithdraw/DynamicBankAccountForm.tsx around lines 46-47, the
prop type now requires error: string | null but several call sites don’t pass
it; update every DynamicBankAccountForm usage (at minimum in
src/components/AddWithdraw/AddWithdrawCountriesList.tsx and
src/components/Claim/Link/views/BankFlowManager.view.tsx) to include an
error={someError} prop — where possible forward the existing error state/prop
from the parent, or pass error={null} if none is available, ensuring the value
is string|null and matches the expected prop type.
| const { country: countryNameParams } = useParams() | ||
| const { amountToWithdraw } = useWithdrawFlow() | ||
| const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ') | ||
| const lastName = lastNameParts.join(' ') | ||
|
|
||
| let selectedCountry = (countryNameFromProps ?? (countryNameParams as string)).toLowerCase() | ||
|
|
There was a problem hiding this comment.
Handle catch-all route param and preserve title-casing.
useParams for [...country] can be string[]. Also avoid lowercasing when persisting countryName.
- const { country: countryNameParams } = useParams()
+ const { country: countryNameParams } = useParams()
@@
- let selectedCountry = (countryNameFromProps ?? (countryNameParams as string)).toLowerCase()
+ const rawCountryParam = Array.isArray(countryNameParams)
+ ? countryNameParams[0]
+ : (countryNameParams as string | undefined)
+ const selectedCountryNameLC = (countryNameFromProps ?? rawCountryParam ?? '').toLowerCase()🤖 Prompt for AI Agents
In src/components/AddWithdraw/DynamicBankAccountForm.tsx around lines 66 to 72,
handle the catch‑all route param which may be a string[] and stop lowercasing
the country name when persisting it: read useParams() as { country?: string |
string[] }, normalize countryNameParams by selecting the first element when it's
an array (e.g. Array.isArray(countryNameParams) ? countryNameParams[0] :
countryNameParams) and then compute selectedCountry from countryNameFromProps ??
normalizedCountryName without calling toLowerCase() so the original title‑casing
is preserved.
| countryName: selectedCountry, | ||
| accountOwnerType: BridgeAccountOwnerType.INDIVIDUAL, |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Don’t lowercase countryName in payload.
Send a proper title-cased name for downstream UX/logging.
- countryName: selectedCountry,
+ countryName: countryNameFromProps ?? (rawCountryParam || ''),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| countryName: selectedCountry, | |
| accountOwnerType: BridgeAccountOwnerType.INDIVIDUAL, | |
| countryName: countryNameFromProps ?? (rawCountryParam || ''), | |
| accountOwnerType: BridgeAccountOwnerType.INDIVIDUAL, |
🤖 Prompt for AI Agents
In src/components/AddWithdraw/DynamicBankAccountForm.tsx around lines 175-176,
the payload currently sends countryName in lowercase; change it to a title-cased
country name instead. Before building the payload, derive a titleCasedCountry by
trimming selectedCountry and capitalizing the first letter of each word (or
reuse an existing util if available), handle empty/null safely, and assign
countryName: titleCasedCountry in the payload while leaving accountOwnerType as
BridgeAccountOwnerType.INDIVIDUAL.
| const isMx = country.toUpperCase() === 'MX' | ||
| const isUs = country.toUpperCase() === 'USA' | ||
| const isIban = isUs || isMx ? false : isIBANCountry(country) | ||
|
|
There was a problem hiding this comment.
Mirror MX/US detection outside submit.
Keep logic consistent across render and submit.
- const isMx = country.toUpperCase() === 'MX'
- const isUs = country.toUpperCase() === 'USA'
- const isIban = isUs || isMx ? false : isIBANCountry(country)
+ const cc = country.toUpperCase()
+ const isMx = cc === 'MEX' || cc === 'MX'
+ const isUs = cc === 'USA' || cc === 'US'
+ const isIban = isUs || isMx ? false : isIBANCountry(cc)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const isMx = country.toUpperCase() === 'MX' | |
| const isUs = country.toUpperCase() === 'USA' | |
| const isIban = isUs || isMx ? false : isIBANCountry(country) | |
| const cc = country.toUpperCase() | |
| const isMx = cc === 'MEX' || cc === 'MX' | |
| const isUs = cc === 'USA' || cc === 'US' | |
| const isIban = isUs || isMx ? false : isIBANCountry(cc) |
🤖 Prompt for AI Agents
In src/components/AddWithdraw/DynamicBankAccountForm.tsx around lines 215 to
218, the MX/US/IBAN detection logic is only defined in render which causes
inconsistent behavior when submitting; refactor by extracting the country
normalization and detection into a single helper (or a shared variable at
component scope) used both in render and in the submit handler so isMx, isUs and
isIban are computed consistently (e.g., normalize country.toUpperCase() once,
then set isMx = === 'MX', isUs = === 'USA', isIban = !(isMx || isUs) &&
isIBANCountry(country) and use that same logic in submit).
| if (getCountryFromIban(val)?.toLowerCase() !== selectedCountry) { | ||
| return 'IBAN does not match the selected country' | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use normalized country-name var in IBAN-country check.
- if (getCountryFromIban(val)?.toLowerCase() !== selectedCountry) {
+ if (getCountryFromIban(val)?.toLowerCase() !== selectedCountryNameLC) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (getCountryFromIban(val)?.toLowerCase() !== selectedCountry) { | |
| return 'IBAN does not match the selected country' | |
| } | |
| @@ src/components/AddWithdraw/DynamicBankAccountForm.tsx:328 | |
| if (getCountryFromIban(val)?.toLowerCase() !== selectedCountryNameLC) { | |
| return 'IBAN does not match the selected country' | |
| } |
🤖 Prompt for AI Agents
In src/components/AddWithdraw/DynamicBankAccountForm.tsx around lines 328 to
331, the IBAN country check uses selectedCountry directly; change it to use the
normalized country-name variable used elsewhere (e.g. normalizedCountryName) so
comparison is consistent: call getCountryFromIban(val)?.toLowerCase() and
compare it to the normalized country variable (already lowercased) instead of
selectedCountry, keeping the same error message when they differ.
| import countryCurrencyMappings from '@/constants/countryCurrencyMapping' | ||
|
|
There was a problem hiding this comment.
Fix incorrect import (named vs default).
countryCurrencyMappings is a named export.
-import countryCurrencyMappings from '@/constants/countryCurrencyMapping'
+import { countryCurrencyMappings } from '@/constants/countryCurrencyMapping'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import countryCurrencyMappings from '@/constants/countryCurrencyMapping' | |
| import { countryCurrencyMappings } from '@/constants/countryCurrencyMapping' |
🤖 Prompt for AI Agents
In src/components/LandingPage/noFees.tsx around lines 15-16 the module is
imported as a default export but countryCurrencyMappings is a named export;
change the import to a named import (e.g. import { countryCurrencyMappings }
from '@/constants/countryCurrencyMapping') and update any local references if
necessary to match the named identifier.
| if (typeof debouncedSourceAmount === 'number' && debouncedSourceAmount !== urlSourceAmount) { | ||
| updateUrlParams({ amount: debouncedSourceAmount }) | ||
| } | ||
| }, [debouncedSourceAmount, urlSourceAmount, updateUrlParams]) | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Guard against NaN in URL sync.
Avoid writing “amount=NaN” when the input is invalid.
- if (typeof debouncedSourceAmount === 'number' && debouncedSourceAmount !== urlSourceAmount) {
+ if (
+ typeof debouncedSourceAmount === 'number' &&
+ Number.isFinite(debouncedSourceAmount) &&
+ debouncedSourceAmount !== urlSourceAmount
+ ) {
updateUrlParams({ amount: debouncedSourceAmount })
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (typeof debouncedSourceAmount === 'number' && debouncedSourceAmount !== urlSourceAmount) { | |
| updateUrlParams({ amount: debouncedSourceAmount }) | |
| } | |
| }, [debouncedSourceAmount, urlSourceAmount, updateUrlParams]) | |
| useEffect(() => { | |
| if ( | |
| typeof debouncedSourceAmount === 'number' && | |
| Number.isFinite(debouncedSourceAmount) && | |
| debouncedSourceAmount !== urlSourceAmount | |
| ) { | |
| updateUrlParams({ amount: debouncedSourceAmount }) | |
| } | |
| }, [debouncedSourceAmount, urlSourceAmount, updateUrlParams]) |
🤖 Prompt for AI Agents
In src/components/LandingPage/noFees.tsx around lines 76 to 81, the effect may
write amount=NaN into the URL when the input is invalid; guard the update by
ensuring debouncedSourceAmount is a finite number (e.g.
Number.isFinite(debouncedSourceAmount)) before comparing to urlSourceAmount and
calling updateUrlParams, so only valid numeric amounts are written to the URL.
| if (user?.user.userId) { | ||
| setClaimType(BankClaimType.ReceiverKycNeeded) | ||
| } else { | ||
| setClaimType(BankClaimType.GuestKycNeeded) | ||
| } |
There was a problem hiding this comment.
Fix optional chaining to avoid runtime errors when user.user is undefined.
Use user?.user?.userId consistently.
- if (user?.user.userId) {
+ if (user?.user?.userId) {
...
- if (user?.user.userId) {
+ if (user?.user?.userId) {
...
- if (user?.user.userId) {
+ if (user?.user?.userId) {Also applies to: 57-61, 64-68
🤖 Prompt for AI Agents
In src/hooks/useDetermineBankClaimType.ts around lines 39-43 (and similarly at
57-61 and 64-68), the code uses user?.user.userId which will throw if user.user
is undefined; change these checks to use optional chaining for the nested
property (user?.user?.userId) and update the corresponding condition branches to
rely on that safe check so the setClaimType calls only run when the nested
userId exists.
| // Validate CLABE check digit using the official algorithm | ||
| const digits = cleanAccountNumber.split('').map(Number) | ||
| const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7] | ||
|
|
||
| let sum = 0 | ||
| for (let i = 0; i < 17; i++) { | ||
| sum += digits[i] * weights[i] | ||
| } | ||
|
|
||
| const remainder = sum % 10 | ||
| const calculatedCheckDigit = remainder === 0 ? 0 : 10 - remainder | ||
| const providedCheckDigit = digits[17] | ||
|
|
||
| if (calculatedCheckDigit !== providedCheckDigit) { | ||
| return { | ||
| isValid: false, | ||
| error: 'CLABE check digit is invalid', | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Fix CLABE check-digit algorithm (per-digit mod 10 is required).
CLABE validation must sum (digit × weight) % 10 for the first 17 digits, then compute check = (10 - (sum % 10)) % 10. Current logic misses the per-digit modulo, causing false negatives/positives.
Apply:
- const digits = cleanAccountNumber.split('').map(Number)
- const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7]
-
- let sum = 0
- for (let i = 0; i < 17; i++) {
- sum += digits[i] * weights[i]
- }
-
- const remainder = sum % 10
- const calculatedCheckDigit = remainder === 0 ? 0 : 10 - remainder
+ const digits = cleanAccountNumber.split('').map(Number)
+ const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7]
+
+ let sum = 0
+ for (let i = 0; i < 17; i++) {
+ sum += (digits[i] * weights[i]) % 10
+ }
+
+ const calculatedCheckDigit = (10 - (sum % 10)) % 10📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Validate CLABE check digit using the official algorithm | |
| const digits = cleanAccountNumber.split('').map(Number) | |
| const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7] | |
| let sum = 0 | |
| for (let i = 0; i < 17; i++) { | |
| sum += digits[i] * weights[i] | |
| } | |
| const remainder = sum % 10 | |
| const calculatedCheckDigit = remainder === 0 ? 0 : 10 - remainder | |
| const providedCheckDigit = digits[17] | |
| if (calculatedCheckDigit !== providedCheckDigit) { | |
| return { | |
| isValid: false, | |
| error: 'CLABE check digit is invalid', | |
| } | |
| } | |
| // Validate CLABE check digit using the official algorithm | |
| const digits = cleanAccountNumber.split('').map(Number) | |
| const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7] | |
| let sum = 0 | |
| for (let i = 0; i < 17; i++) { | |
| sum += (digits[i] * weights[i]) % 10 | |
| } | |
| const calculatedCheckDigit = (10 - (sum % 10)) % 10 | |
| const providedCheckDigit = digits[17] | |
| if (calculatedCheckDigit !== providedCheckDigit) { | |
| return { | |
| isValid: false, | |
| error: 'CLABE check digit is invalid', | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/utils/withdraw.utils.ts around lines 110 to 129, the CLABE check-digit
computation incorrectly sums raw digit×weight values instead of applying
per-digit modulo 10 before accumulating, and then computes the check digit;
update the algorithm to for each of the first 17 digits compute (digit * weight)
% 10 and add that to the running sum, then compute the check digit as (10 - (sum
% 10)) % 10 and compare against the provided 18th digit, returning the same
error object if they differ.
| // Returns the 3-letter country code for the given country code | ||
| export const getCountryCodeForWithdraw = (country: string) => { | ||
| // If the input is already a 3-digit code and exists in the map, return it | ||
| if (countryCodeMap[country]) { | ||
| return country | ||
| } | ||
|
|
||
| // If the input is a 2-digit code, find the corresponding 3-digit code | ||
| const threeDigitCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === country) | ||
|
|
||
| return threeDigitCode || country | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Normalize input to uppercase and fix doc wording (“3-letter” not “3-digit”).
countryCodeMap keys are uppercase 3-letter codes; normalize before lookup (matches prior learning).
-// Returns the 3-letter country code for the given country code
-export const getCountryCodeForWithdraw = (country: string) => {
- // If the input is already a 3-digit code and exists in the map, return it
- if (countryCodeMap[country]) {
- return country
- }
-
- // If the input is a 2-digit code, find the corresponding 3-digit code
- const threeDigitCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === country)
-
- return threeDigitCode || country
-}
+// Returns the 3-letter country code (e.g., 'DEU') for the given country code
+export const getCountryCodeForWithdraw = (country: string) => {
+ const code = country.toUpperCase()
+ // If input is already a 3-letter code and exists in the map, return it
+ if (countryCodeMap[code]) return code
+ // If input is a 2-letter code, find the corresponding 3-letter code
+ const threeLetter = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === code)
+ return (threeLetter ?? code)
+}Used learning: “countryCodeMap uses uppercase 3-letter codes as keys; normalize inputs to uppercase.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Returns the 3-letter country code for the given country code | |
| export const getCountryCodeForWithdraw = (country: string) => { | |
| // If the input is already a 3-digit code and exists in the map, return it | |
| if (countryCodeMap[country]) { | |
| return country | |
| } | |
| // If the input is a 2-digit code, find the corresponding 3-digit code | |
| const threeDigitCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === country) | |
| return threeDigitCode || country | |
| } | |
| // Returns the 3-letter country code (e.g., 'DEU') for the given country code | |
| export const getCountryCodeForWithdraw = (country: string) => { | |
| const code = country.toUpperCase() | |
| // If input is already a 3-letter code and exists in the map, return it | |
| if (countryCodeMap[code]) return code | |
| // If input is a 2-letter code, find the corresponding 3-letter code | |
| const threeLetter = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === code) | |
| return (threeLetter ?? code) | |
| } |
🤖 Prompt for AI Agents
In src/utils/withdraw.utils.ts around lines 136 to 147, update the doc comment
to say "3-letter country code" and normalize the input to uppercase before
lookups; specifically, convert the incoming country to country =
country.toUpperCase() (or a new const) before checking countryCodeMap keys and
before searching Object.keys(countryCodeMap).find(...), so that existing
uppercase 3-letter keys are matched correctly and 2-letter inputs are compared
in uppercase as well; finally return the found 3-letter code or the original
input.
No description provided.