Stage manteca-integration#1269
Conversation
…eat/manteca-add-flow
… step state in MercadoPago component
[TASK-12885] Feat/manteca add flow
…positCard for improved account details display
…ange-rate [TASK-14780] Feat/add manteca currencies to exchange rate widget
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedFailed to post review comments WalkthroughAdds Mantéca regional payments (QR Pay, deposits, withdrawals) across app routes, components, hooks, and contexts. Refactors KYC to bridge/mantéca flows, updates request-fulfillment logic, country-code mappings, currency pricing API (buy/sell), exchange-rate route, and transaction receipt surfaces. Introduces new pages, modals, UI components, and renames several APIs/props. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches and finishing touches❌ Failed checks (2 warnings, 1 inconclusive)
✨ Finishing touches
🧪 Generate unit tests
✨ 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 37
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (14)
src/hooks/useDetermineBankClaimType.ts (1)
75-75: Missing dependency in useEffect array.The effect uses
isUserBridgeKycApproved(line 31) but the dependency array doesn't include it. React's exhaustive-deps rule will warn about this omission, and the effect may not re-run when KYC status changes independently of theuserobject reference.Apply this diff to add the missing dependency:
- }, [user, senderUserId, setSenderDetails]) + }, [user, senderUserId, setSenderDetails, isUserBridgeKycApproved])src/components/Refund/index.tsx (1)
152-154: Critical: Fix property access in onChange callback.The callback accesses
chainId.chainId, but the Select component now passes aSelectItemobject with onlyidandtitleproperties. This will cause a runtime error.Apply this diff to fix the property access:
- onChange={(chainId: any) => { - refundForm.setValue('chainId', chainId.chainId) + onChange={(item) => { + refundForm.setValue('chainId', item.id) }}src/hooks/useCurrency.ts (2)
27-57: Prevent stale updates from in‑flight requests (race condition).If code changes while getCurrencyPrice is pending, the older promise may overwrite newer state. Guard with a cancellation flag and reset state at the start of a fetch.
Apply this diff:
useEffect(() => { - if (!code) { - setIsLoading(false) - return - } + let cancelled = false + if (!code) { + setSymbol(null) + setPrice(null) + setIsLoading(false) + return () => { + cancelled = true + } + } if (code === 'USD') { setSymbol(SYMBOLS_BY_CURRENCY_CODE[code]) setPrice({ buy: 1, sell: 1 }) setIsLoading(false) - return + return () => { + cancelled = true + } } - if (!Object.keys(SYMBOLS_BY_CURRENCY_CODE).includes(code)) { - setCode(null) - setIsLoading(false) - return - } + if (!(code in SYMBOLS_BY_CURRENCY_CODE)) { + setCode(null) + setSymbol(null) + setPrice(null) + setIsLoading(false) + return () => { + cancelled = true + } + } setIsLoading(true) + setSymbol(null) + setPrice(null) getCurrencyPrice(code) .then((price) => { - setSymbol(SYMBOLS_BY_CURRENCY_CODE[code]) - setPrice(price) - setIsLoading(false) + if (cancelled) return + if (!Number.isFinite(price.buy) || !Number.isFinite(price.sell)) { + console.error('Invalid price payload', price) + setIsLoading(false) + return + } + setSymbol(SYMBOLS_BY_CURRENCY_CODE[code]) + setPrice(price) + setIsLoading(false) }) .catch((err) => { console.error(err) - setIsLoading(false) + if (cancelled) return + setSymbol(null) + setPrice(null) + setIsLoading(false) }) - }, [code]) + return () => { + cancelled = true + } + }, [code])
40-44: Use O(1) membership check and clear stale state on invalid codes.Object.keys(...).includes(...) allocates and you leave previous symbol/price around.
Use the diff in the race-condition comment above; it already swaps to
(code in SYMBOLS_BY_CURRENCY_CODE)and clears state.src/app/api/exchange-rate/route.ts (1)
17-21: Validate currency codes format (A‑Z, length 3).Avoids hitting providers with invalid inputs and reduces noise.
Apply this diff:
// Validate required parameters if (!from || !to) { return NextResponse.json({ error: 'Missing required parameters: from and to' }, { status: 400 }) } const fromUc = from.toUpperCase() const toUc = to.toUpperCase() + if (!/^[A-Z]{3}$/.test(fromUc) || !/^[A-Z]{3}$/.test(toUc)) { + return NextResponse.json({ error: 'Invalid currency code format' }, { status: 400 }) + }src/components/Claim/useClaimLink.tsx (1)
99-106: Add missing handler for the ‘regional-claim’ stepVerified that the new
'regional-req-fulfill'step is correctly consumed inPaymentForm/index.tsx, but there is no branch handling the'regional-claim'value anywhere. You must implement routing or state logic for'regional-claim'(e.g. in the Claim or ClaimLink components) to complete the regional payment flow.src/components/AddMoney/UserDetailsForm.tsx (1)
14-14: Remove unusedisSubmittingprop from interface.The
isSubmittingprop is declared in the interface but not used in the component implementation (line 20). This creates an unnecessary prop that callers must provide but has no effect.Apply this diff to remove the unused prop:
interface UserDetailsFormProps { onSubmit: (data: UserDetailsFormData) => Promise<{ error?: string }> - isSubmitting: boolean onValidChange?: (isValid: boolean) => void initialData?: Partial<UserDetailsFormData> }Also applies to: 20-20
src/app/(mobile-ui)/home/page.tsx (1)
140-185: Remove duplicate balance warning effectLines 140-162 and 164-185 contain identical logic for showing the balance warning modal. This duplication will cause the effect to run twice and may lead to unexpected behavior.
Remove one of the duplicate effects:
- // effect for showing balance warning modal - useEffect(() => { - if (isFetchingBalance || balance === undefined || !user) return - - if (typeof window !== 'undefined') { - const hasSeenBalanceWarning = getFromLocalStorage(`${user!.user.userId}-hasSeenBalanceWarning`) - const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) - - // show if: - // 1. balance is above the threshold - // 2. user hasn't seen this warning in the current session - // 3. no other modals are currently active - if ( - balanceInUsd > BALANCE_WARNING_THRESHOLD && - !hasSeenBalanceWarning && - !showIOSPWAInstallModal && - !showAddMoneyPromptModal - ) { - setShowBalanceWarningModal(true) - } - } - }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal, user])src/components/TransactionDetails/transactionTransformer.ts (1)
328-334: Add MANTECA flows to bridge status mappingThe new Mantéca on/off-ramp entries still hit the generic status branch, so statuses like
PAYMENT_SUBMITTED/PAYMENT_PROCESSEDcollapse to “pending”. Please include the Mantéca entry types in the bridge-style status block so they keep their processing/completed states.- if ( - entry.type === EHistoryEntryType.BRIDGE_OFFRAMP || - entry.type === EHistoryEntryType.BRIDGE_ONRAMP || - entry.type === EHistoryEntryType.BANK_SEND_LINK_CLAIM || - entry.extraData?.fulfillmentType === 'bridge' - ) { + if ( + entry.type === EHistoryEntryType.BRIDGE_OFFRAMP || + entry.type === EHistoryEntryType.BRIDGE_ONRAMP || + entry.type === EHistoryEntryType.MANTECA_OFFRAMP || + entry.type === EHistoryEntryType.MANTECA_ONRAMP || + entry.type === EHistoryEntryType.BANK_SEND_LINK_CLAIM || + entry.extraData?.fulfillmentType === 'bridge' + ) {src/components/Common/CountryListRouter.tsx (1)
63-86: Reset Mantéca flags when selecting non-supported countriesAfter the first Mantéca selection,
setClaimToMercadoPago(true)/setFulfillUsingManteca(true)stay latched. Picking a different country later still drives the Mantéca UI. Please explicitly clear the flags in the non-Mantéca branches (and restore any UI toggles you suppressed) so the flow reverts to the standard bank experience.if (flow === 'claim') { setSelectedCountry(country) if (isMantecaSupportedCountry) { setClaimBankFlowStep(null) // reset the flow step to initial view first setClaimToMercadoPago(true) } else { + setClaimToMercadoPago(false) setClaimBankFlowStep(ClaimBankFlowStep.BankDetailsForm) } } else if (flow === 'request') { if (isMantecaSupportedCountry) { setShowRequestFulfilmentBankFlowManager(false) setFulfillUsingManteca(true) + } else { + setFulfillUsingManteca(false) + setShowRequestFulfilmentBankFlowManager(true) } setSelectedCountryForRequest(country)src/hooks/useBridgeKycFlow.ts (1)
118-157: Include onManualClose in useCallback deps to avoid stale closureonManualClose is used inside handleIframeClose but omitted from the dependency list, risking no-op callbacks after prop changes.
- }, [iframeOptions.src, apiResponse, flow, router]) + }, [iframeOptions.src, apiResponse, flow, router, onManualClose])src/components/Kyc/KycStatusItem.tsx (1)
79-79: Tailwind class typo breaks subtitle colorUse text-gray-1 (consistent with the codebase), not text-grey-1.
-<p className="text-sm text-grey-1">{subtitle}</p> +<p className="text-sm text-gray-1">{subtitle}</p>src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)
100-161: IncludeisPublicin the memo dependencies.
rowVisibilityConfignow branches onisPublic, but the memo only depends on[transaction, isPendingBankRequest]. IfisPublicflips (drawer reused for a public receipt), this memo will keep the stale private value and expose bank/deposit fields that should stay hidden. AddisPublicto the dependency array so the memo recomputes safely.src/components/Common/ActionList.tsx (1)
168-177: Ensure geo-filtered methods react to location changes.
Line 168 memoizessortedActionMethodsonly onrequiresVerification, so whenuserGeoLocationCountryCodefinishes loading thegeolocatedMethodsarray changes but this memo stays frozen. Result: Brazilian users still see Mercado Pago, and everyone else still sees Pix, because the geo-specific filtering never applies. Include the geo-dependent array (or the underlying country code) in the dependency list so the list updates once location data arrives.- const sortedActionMethods = useMemo(() => { - return [...geolocatedMethods].sort((a, b) => { + const sortedActionMethods = useMemo(() => { + return [...geolocatedMethods].sort((a, b) => { const aIsUnavailable = a.soon || (a.id === 'bank' && requiresVerification) const bIsUnavailable = b.soon || (b.id === 'bank' && requiresVerification) if (aIsUnavailable === bIsUnavailable) { return 0 } return aIsUnavailable ? 1 : -1 }) - }, [requiresVerification]) + }, [geolocatedMethods, requiresVerification])
🧹 Nitpick comments (56)
src/components/Kyc/KycVerificationInProgressModal.tsx (1)
52-59: Consider addingaria-hiddento the decorative icon.The component implementation is clean and well-structured. For a minor accessibility improvement, consider adding
aria-hidden="true"to the Icon since it's decorative (the text conveys the full message).Apply this diff to improve accessibility:
- <Icon name="info" className="h-3 w-3" /> + <Icon name="info" className="h-3 w-3" aria-hidden="true" />src/context/authContext.tsx (1)
162-162: Consider removing debug console.log.The
console.log({ user })statement appears to be leftover debug code. Consider removing it to avoid cluttering production console logs and potentially exposing user data in browser DevTools.Apply this diff to remove the debug statement:
- console.log({ user }) - return (src/components/Refund/index.tsx (1)
147-151: Optimize: Compute mapped items once.The array is mapped twice—once for
itemsand again forvalue. This is inefficient and error-prone if the mappings diverge.Refactor to compute the mapped items once:
+ const mappedChains = consts.supportedPeanutChains.map((chain) => ({ + id: chain.chainId, + title: chain.name, + })) <Select className="h-8 border border-n-1 p-1 outline-none" classButton="h-auto px-0 border-none bg-trasparent text-sm !font-normal" classOptions="-left-4 -right-3 w-auto py-1 overflow-auto max-h-36" classArrow="ml-1" - items={consts.supportedPeanutChains.map((chain) => ({ - id: chain.chainId, - title: chain.name, - }))} + items={mappedChains} value={ - consts.supportedPeanutChains - .map((c) => ({ id: c.chainId, title: c.name })) - .find((i) => i.id === refundFormWatch.chainId) ?? null + mappedChains.find((i) => i.id === refundFormWatch.chainId) ?? null }src/context/tokenSelector.context.tsx (1)
56-70: Consider moving constant token data objects outside the component.
peanutWalletTokenDataandemptyTokenDataare recreated on every render. Since they are constant values, moving them outside the component body would:
- Avoid unnecessary object creation on each render.
- Prevent potential stale closure issues in
resetTokenContextProvider(lines 104-118), where these objects are referenced inside auseCallback.Apply this diff to move the constants outside:
+'use client' +import React, { createContext, useEffect, useState, useCallback } from 'react' + +import { getSquidChainsAndTokens } from '@/app/actions/squid' +// ... other imports + +const emptyTokenData = { + address: '', + chainId: '', + decimals: undefined, +} + +const peanutWalletTokenData = { + price: 1, + decimals: PEANUT_WALLET_TOKEN_DECIMALS, + symbol: PEANUT_WALLET_TOKEN_SYMBOL, + name: PEANUT_WALLET_TOKEN_NAME, + address: PEANUT_WALLET_TOKEN, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + logoURI: PEANUT_WALLET_TOKEN_IMG_URL, +} as ITokenPriceData + export const TokenContextProvider = ({ children }: { children: React.ReactNode }) => { const { isConnected: isPeanutWallet } = useWallet() - const { user } = useAuth() - - const peanutWalletTokenData = { - price: 1, - decimals: PEANUT_WALLET_TOKEN_DECIMALS, - symbol: PEANUT_WALLET_TOKEN_SYMBOL, - name: PEANUT_WALLET_TOKEN_NAME, - address: PEANUT_WALLET_TOKEN, - chainId: PEANUT_WALLET_CHAIN.id.toString(), - logoURI: PEANUT_WALLET_TOKEN_IMG_URL, - } as ITokenPriceData - - const emptyTokenData = { - address: '', - chainId: '', - decimals: undefined, - }src/components/Profile/views/IdentityVerification.view.tsx (2)
110-123: Consider extracting helper functions outside the component.
isBridgeSupportedCountryandisMantecaSupportedCountryare recreated on every render. Since they only depend on external constants, consider moving them outside the component for better performance.Apply this diff:
+'use client' +import { updateUserById } from '@/app/actions/users' +// ... other imports ... + +// Helper functions (add before component) +const isBridgeSupportedCountry = (code: string) => { + const upper = code.toUpperCase() + return ( + upper === 'US' || + upper === 'MX' || + Object.keys(BRIDGE_ALPHA3_TO_ALPHA2).includes(upper) || + Object.values(BRIDGE_ALPHA3_TO_ALPHA2).includes(upper) + ) +} + +const isMantecaSupportedCountry = (code: string) => { + const upper = code.toUpperCase() + return Object.prototype.hasOwnProperty.call(MantecaSupportedExchanges, upper) +} + const IdentityVerificationView = () => { const router = useRouter() // ... rest of component ... - - // country validation helpers - const isBridgeSupportedCountry = (code: string) => { - const upper = code.toUpperCase() - return ( - upper === 'US' || - upper === 'MX' || - Object.keys(BRIDGE_ALPHA3_TO_ALPHA2).includes(upper) || - Object.values(BRIDGE_ALPHA3_TO_ALPHA2).includes(upper) - ) - } - - const isMantecaSupportedCountry = (code: string) => { - const upper = code.toUpperCase() - return Object.prototype.hasOwnProperty.call(MantecaSupportedExchanges, upper) - }
39-47: Consider memoizing handleRedirect.The
handleRedirectfunction is called from multipleuseCallbackhooks (handleBridgeKycSuccess,handleBack) but is not itself memoized. This could lead to stale closures if those callbacks are not properly updated.Apply this diff:
- const handleRedirect = () => { + const handleRedirect = useCallback(() => { const redirectUrl = getRedirectUrl() if (redirectUrl) { clearRedirectUrl() router.push(redirectUrl) } else { router.replace('/profile') } - } + }, [router])Then update the dependent callbacks to include
handleRedirectin their dependency arrays instead ofrouter.src/components/TransactionDetails/transaction-details.utils.ts (1)
21-21: Fix typo in comment.The comment has a typo: "rder" should be "order".
Apply this diff:
-// rder of the rows in the receipt +// order of the rows in the receiptsrc/app/actions/onramp.ts (1)
77-81: Confirm rate orientation and harden parsing for amount calculation
- Ensure price.buy is the correct direction for USD → source currency here; inconsistent usage will misquote users.
- Guard against invalid strings (commas, empty, NaN) before Number(); fail fast with a clear error.
Suggested patch:
- const amount = (Number(params.amount) * price.buy).toFixed(2) + const usd = Number(String(params.amount).replace(/,/g, '')) + if (!Number.isFinite(usd) || usd <= 0) { + return { error: 'Invalid amount.' } + } + const amount = (usd * price.buy).toFixed(2)src/hooks/useCreateOnramp.ts (1)
45-49: Avoid param reassignment; sanitize USD input and validate before computingReassigning amount obscures types and risks NaN if usdAmount has commas/invalid chars.
Apply:
- amount = (Number(usdAmount) * price.buy).toFixed(2) + const usd = Number(String(usdAmount).replace(/,/g, '')) + if (!Number.isFinite(usd) || usd <= 0) throw new Error('Invalid USD amount') + const calculatedAmount = (usd * price.buy).toFixed(2)Then use calculatedAmount below when sending the request:
- amount, + amount: calculatedAmount ?? amount,src/components/AddMoney/components/MantecaAddMoney.tsx (1)
25-27: Define deposit limit constants as numbers or document string intentUsing strings works with parseUnits, but for readability consider numbers and convert locally, or add a comment that strings are intentional for parseUnits compatibility.
src/hooks/useCurrency.ts (1)
4-19: Tighten typing of symbol map and expose a CurrencyCode type.Improves safety and autocompletion across the app.
Apply this diff:
-export const SYMBOLS_BY_CURRENCY_CODE: Record<string, string> = { +export const SYMBOLS_BY_CURRENCY_CODE = { ARS: 'ARS', USD: '$', EUR: '€', MXN: 'MX$', BRL: 'R$', COP: 'Col$', CRC: '₡', BOB: '$b', PUSD: 'PUSD', GTQ: 'Q', PHP: '₱', GBP: '£', JPY: '¥', CAD: 'CA$', -} +} as const +export type CurrencyCode = keyof typeof SYMBOLS_BY_CURRENCY_CODEsrc/app/api/exchange-rate/route.ts (6)
78-96: Parallelize intermediate USD lookups for non‑USD pairs.Two independent calls; run them concurrently.
Apply this diff:
- const fromToUsdRate = await getExchangeRate(fromUc, 'USD') - const usdToToRate = await getExchangeRate('USD', toUc) + const [fromToUsdRate, usdToToRate] = await Promise.all([ + getExchangeRate(fromUc, 'USD'), + getExchangeRate('USD', toUc), + ])
102-120: Name shadowing: getExchangeRate here vs actions/exchange-rate.getExchangeRate.To reduce confusion during debugging, consider renaming the local helper to resolveExchangeRate.
146-158: Parallelize price fetches for cross currency via USD.Saves one RTT and lowers latency.
Apply this diff:
- const fromPrices = await getCurrencyPrice(from) - const toPrices = await getCurrencyPrice(to) + const [fromPrices, toPrices] = await Promise.all([getCurrencyPrice(from), getCurrencyPrice(to)])
176-185: Parallelize Frankfurter direct hops.Same rationale: independent requests.
Apply this diff:
- const fromToUsdRate = await fetchDirectFromFrankfurter(from, 'USD') - const usdToToRate = await fetchDirectFromFrankfurter('USD', to) + const [fromToUsdRate, usdToToRate] = await Promise.all([ + fetchDirectFromFrankfurter(from, 'USD'), + fetchDirectFromFrankfurter('USD', to), + ])
8-10: Factor provider spread into a named constant for clarity.Improves readability and makes future tweaks safer.
Apply this diff:
-// LATAM currencies that should use Manteca API -const MANTECA_CURRENCIES = new Set(['ARS', 'BRL', 'COP', 'CRC', 'PUSD', 'GTQ', 'PHP', 'BOB']) +// LATAM currencies that should use Manteca API +const MANTECA_CURRENCIES = new Set(['ARS', 'BRL', 'COP', 'CRC', 'PUSD', 'GTQ', 'PHP', 'BOB']) +const FRANKFURTER_MARKDOWN = 0.005 // 50 bps ... - return data.rates[to] * 0.995 // Subtract 50bps + return data.rates[to] * (1 - FRANKFURTER_MARKDOWN)Also applies to: 191-208
123-131: Reduce log noise and avoid leaking parameters at info level.Swap console.log to debug (or remove) and keep errors; parameters can remain but avoid verbose logs in hot paths.
- console.log('Fetching from getCurrencyPrice') + // debug: using getCurrencyPrice pathAlso applies to: 164-166
src/components/Kyc/InitiateMantecaKYCModal.tsx (2)
55-63: Handle openMantecaKyc result for better UX.openMantecaKyc returns success/error; consider awaiting and surfacing errors (toast) to avoid silent failures.
- onClick: () => openMantecaKyc(country), + onClick: async () => { + const res = await openMantecaKyc(country) + if (!res.success) { + // TODO: surface error via toast/inline message + console.error(res.error) + } + },
72-136: Minor: ensure selectedCountry.title is safe for ReactNode injection.If title can come from external sources, sanitize or constrain to string to avoid surprises; otherwise ignore.
src/components/AddMoney/components/MantecaDepositShareDetails.tsx (2)
54-67: Consider refactoring data mutation for clarity.The code mutates
usdAmountafter extraction (line 62), and usesletdeclarations that are reassigned. While functional, this approach can be harder to trace. Consider computing the final values directly.Apply this diff for a more declarative approach:
const depositAddress = depositDetails.details.depositAddress const shortenedAddress = depositAddress.length > 30 ? shortenStringLong(depositAddress, 10) : depositAddress const depositAlias = depositDetails.details.depositAlias const depositAmount = currencyAmount ?? depositDetails.stages['1'].thresholdAmount - let usdAmount = depositDetails.stages['3'].amount const currencySymbol = depositDetails.stages['1'].asset const exchangeRate = depositDetails.details.price - let networkFees = depositDetails.details.withdrawCostInAsset - usdAmount = (Number(usdAmount) - Number(networkFees)).toString() - if (Number(networkFees) < 0.01) { - networkFees = '< 0.01 USD' - } else { - networkFees = `${formatCurrency(networkFees)} USD` - } + const rawNetworkFees = depositDetails.details.withdrawCostInAsset + const networkFees = Number(rawNetworkFees) < 0.01 + ? '< 0.01 USD' + : `${formatCurrency(rawNetworkFees)} USD` + const usdAmount = (Number(depositDetails.stages['3'].amount) - Number(rawNetworkFees)).toString()
85-114: Improve image accessibility and error handling.Lines 94-99: The Image component uses a generic alt text "flag" and relies on an external CDN without error handling. Consider more descriptive alt text and adding error boundaries.
Apply this diff:
<Image src={`https://flagcdn.com/w160/${countryCodeForFlag}.png`} - alt={`flag`} + alt={`${currentCountryDetails?.title || 'Country'} flag`} width={48} height={48} className="h-12 w-12 rounded-full object-cover" + onError={(e) => { + e.currentTarget.style.display = 'none' + }} />src/components/Profile/views/ProfileEdit.view.tsx (1)
126-127: Suggest consistent hook usage for KYC checks.Lines 126 and 134 still use direct
user?.user.bridgeKycStatus === 'approved'checks instead of theisUserBridgeKycApprovedvalue from theuseKycStatushook.Apply this diff for consistency:
disabled={user?.user.bridgeKycStatus === 'approved'} + disabled={isUserBridgeKycApproved}And:
disabled={user?.user.bridgeKycStatus === 'approved'} + disabled={isUserBridgeKycApproved}Also applies to: 134-135
src/components/Claim/Link/Initial.view.tsx (1)
637-651: Consider removing the deadisRewardbranch.The
isRewardconstant is hardcoded tofalseat line 399, so the ternary at lines 643-647 will always take the else branch. You can simplify this to always compute the USD amount.Apply this diff to simplify the amount calculation:
if (claimToMercadoPago) { return ( <MantecaFlowManager claimLinkData={claimLinkData} attachment={attachment} amount={ - isReward - ? formatTokenAmount(Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)))! - : (formatTokenAmount( - Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) * tokenPrice - ) ?? '') + formatTokenAmount( + Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) * (tokenPrice ?? 0) + ) ?? '' } /> ) }src/components/Global/MantecaDetailsCard/index.tsx (1)
5-11: PreferidorrowIdinstead ofkeyin the data model.Using a
keyfield in the row type can be confusing since React treatskeyspecially in JSX. Consider renaming toid/rowIdand mapping it to the JSXkeyprop.src/context/ClaimBankFlowContext.tsx (2)
45-48: Centralize regional method type.Instead of inline string literals ('mercadopago' | 'pix'), define a shared
type/enum(e.g.,RegionalMethodType) in a consts/types module to avoid drift across files.
137-140: Clean upuseMemodeps (setter inconsistency).You include
setClaimToMercadoPagobut notsetRegionalMethodType. Setters fromuseStateare stable and can be omitted. Recommend removing the setter from deps for consistency.Apply this diff:
[ claimToExternalWallet, flowStep, selectedCountry, resetFlow, offrampDetails, claimError, claimType, senderDetails, showVerificationModal, bankDetails, savedAccounts, selectedBankAccount, senderKycStatus, justCompletedKyc, claimToMercadoPago, - setClaimToMercadoPago, regionalMethodType, ]src/components/Profile/AvatarWithBadge.tsx (2)
7-7: Broadenlogotype to support remote URLs.If callers may pass CDN/remote URLs, widen to
string | StaticImport(Next 13+). Update import and prop type.Apply this diff:
-import Image, { type StaticImageData } from 'next/image' +import Image, { type StaticImport } from 'next/image' @@ - logo?: StaticImageData + logo?: string | StaticImportAlso applies to: 24-25
104-112: Tailwind JIT cannot see dynamicsize-[${…}]; usefillorw-full h-full.The class
size-[${iconSizeMap[size]}]won't generate CSS. Preferfillto fully cover the rounded container.Apply this diff:
- {logo && ( - <Image - src={logo} - alt={''} - width={160} - height={160} - className={`size-[${iconSizeMap[size]}] rounded-full object-cover`} - /> - )} + {logo && ( + <div className="relative h-full w-full overflow-hidden rounded-full"> + <Image + src={logo} + alt={name ? `${name} logo` : 'logo'} + fill + sizes="(max-width: 768px) 64px, 96px" + className="object-cover" + priority={size !== 'large'} + /> + </div> + )}src/components/Global/DirectSendQR/utils.ts (1)
68-73: Avoid relying on object key iteration order for precedence.Since order matters (MP before QR3), use an ordered tuple list for matching to make intent explicit and resilient to refactors.
You can add:
const ORDERED_REGEXES: ReadonlyArray<[QrType, RegExp]> = [ [EQrType.MERCADO_PAGO, MP_AR_REGEX], [EQrType.ARGENTINA_QR3, ARGENTINA_QR3_REGEX], [EQrType.BITCOIN_ONCHAIN, /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/], [EQrType.BITCOIN_INVOICE, /^ln(bc|tb|bcrt)([0-9]{1,}[a-z0-9]+){1}$/], [EQrType.PIX, PIX_REGEX], [EQrType.XRP_ADDRESS, /^r[0-9a-zA-Z]{24,34}$/], [EQrType.TRON_ADDRESS, /^T[1-9A-HJ-NP-Za-km-z]{33}$/], [EQrType.SOLANA_ADDRESS, /^[1-9A-HJ-NP-Za-km-z]{32,44}$/], [EQrType.URL, /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/], ]Then in
recognizeQr:for (const [type, regex] of ORDERED_REGEXES) { if (regex.test(data)) return type }src/app/(mobile-ui)/history/page.tsx (2)
70-85: Ensure KYC item UUIDs are unique.
'bridge-kyc-status-item'is fine for a single status row, but verification UUID fallback (provider-mantecaGeo) could collide across multiple verifications. Append a timestamp or index to guarantee uniqueness.Example:
- uuid: verification.providerUserId ?? `${verification.provider}-${verification.mantecaGeo}`, + uuid: + verification.providerUserId ?? + `${verification.provider}-${verification.mantecaGeo}-${verification.approvedAt ?? verification.updatedAt ?? verification.createdAt ?? 'na'}`
162-169: Pass a consistent date to KycStatusItem.Subtitle uses
bridgeKycStartedAt, while grouping usesitem.timestamp. To avoid mismatches, passitem.timestampas a fallback so the visible date aligns with grouping.Apply this diff:
- bridgeKycStartedAt={ - item.bridgeKycStatus ? user?.user.bridgeKycStartedAt : undefined - } + bridgeKycStartedAt={ + item.bridgeKycStatus ? (user?.user.bridgeKycStartedAt ?? item.timestamp) : undefined + }src/components/Claim/Link/views/MantecaDetailsStep.view.tsx (1)
20-22: Remove unnecessaryasynckeyword.The
handleOnClickfunction is markedasyncbut performs no asynchronous operations. This adds unnecessary overhead and can be misleading.Apply this diff:
- const handleOnClick = async () => { + const handleOnClick = () => { setCurrentStep(MercadoPagoStep.REVIEW) }src/app/actions/bridge/get-customer.ts (1)
55-59: Remove the debugconsole.log.Shipping with
console.log('normalized kushagra', …)is noisy and leaks into server logs. Please drop it before merging.Apply this diff:
- const normalized = normalizeCountry(raw) - console.log('normalized kushagra', normalized) - return { countryCode: normalized, rawCountry: raw } + const normalized = normalizeCountry(raw) + return { countryCode: normalized, rawCountry: raw }src/components/Claim/Link/views/MantecaReviewStep.tsx (5)
9-9: Consider renamingMercadoPagoSteptoMantecaStepfor consistency.The enum name
MercadoPagoStepappears inconsistent with the component nameMantecaReviewStepand the broader Manteca integration context. If this enum is specifically for Manteca flows, consider renaming it toMantecaSteporMantecaFlowStepfor clarity.Also applies to: 13-13
51-51: Redundant null check fordestinationAddress.The check
if (destinationAddress)on line 51 is redundant sincedestinationAddressis a required prop (non-optional in the interface). Consider removing this check or making the prop optional if it can legitimately be empty.
55-60: Add validation forclaimResponse.The code extracts
txHashfromclaimResponsewithout first checking ifclaimResponseitself is valid. IfsendLinksApi.claimreturnsnullorundefined, the optional chaining on line 56 will result intxHashbeingundefined, which is then caught by the check on line 57. However, it would be clearer to validateclaimResponseexplicitly before extractingtxHash.Consider this approach:
const claimResponse = await sendLinksApi.claim(MANTECA_DEPOSIT_ADDRESS, claimLink) + if (!claimResponse) { + setError('Claim failed: no response from server.') + return + } const txHash = claimResponse?.claim?.txHash if (!txHash) { setError('Claim failed: missing transaction hash.') return }
61-66: Validate the cleaned amount before sending to API.While removing commas from
amountis correct, there's no validation that the resulting string is a valid number. Ifamountcontains unexpected characters (beyond commas), the API call may fail with an unclear error.Consider adding validation:
+ const cleanedAmount = amount.replace(/,/g, '') + if (isNaN(Number(cleanedAmount)) || Number(cleanedAmount) <= 0) { + setError('Invalid amount format.') + return + } const { data, error: withdrawError } = await mantecaApi.withdraw({ - amount: amount.replace(/,/g, ''), + amount: cleanedAmount, destinationAddress, txHash, currency, })
81-83: ReviewcoverFullScreenusage in a multi-step flow.Using
coverFullScreenfor the loading state may not be ideal if this component is part of a modal or a multi-step flow, as it will cover the entire screen rather than just the step container. Consider whethercoverFullScreen={false}would provide a better user experience.src/app/[...recipient]/client.tsx (1)
395-398: Verify comment placement and logic clarity.The comment on line 398 ("Send to bank step if its mentioned in the URL and guest KYC is not needed") appears to describe the
useEffectbelow (lines 399-411) rather than theshowActionListcalculation. Consider moving the comment immediately above theuseEffectfor clarity.Additionally, the
showActionListlogic is correct but could benefit from a brief inline comment explaining the conditions.const showActionList = (flow !== 'direct_pay' || (flow === 'direct_pay' && !user)) && // Show for direct-pay when user is not logged in !fulfillUsingManteca // Show when not fulfilling using Manteca - // Send to bank step if its mentioned in the URL and guest KYC is not needed + + // Send to bank step if its mentioned in the URL and guest KYC is not needed useEffect(() => {src/components/Kyc/CountryFlagAndName.tsx (1)
25-32: Add error handling for flag image loading.The
Imagecomponent doesn't have anonErrorhandler or fallback. If the flag image fails to load (e.g., invalid country code), the component will show a broken image. Consider adding a fallback icon or placeholder.) : ( <Image src={`https://flagcdn.com/w160/${countryCode?.toLowerCase()}.png`} alt={`${countryName} flag`} width={80} height={80} className="h-5 w-5 rounded-full object-cover object-center shadow-sm" loading="lazy" + onError={(e) => { + e.currentTarget.style.display = 'none' + }} /> )}src/components/Kyc/states/KycFailed.tsx (1)
50-59: Consider using theloadingprop for better UX.The
Buttoncomponent supports aloadingprop (as shown in the relevant snippets fromButton.tsx) that renders a loading indicator. Currently, the button is onlydisabledwhenisLoadingis true, and the loading state is indicated by changing the text to "Loading...". Using theloadingprop would provide a more consistent loading experience with a spinner.Apply this diff:
<Button icon="retry" variant="purple" className="w-full" shadowSize="4" onClick={onRetry} disabled={isLoading} + loading={isLoading} > - {isLoading ? 'Loading...' : 'Retry verification'} + Retry verification </Button>src/components/AddMoney/components/InputAmountStep.tsx (1)
50-58: Non-null assertions are safe here but could be more explicit.The non-null assertions on lines 53-55 (
currencyData.code!,currencyData.symbol!,currencyData.price!.buy) are safe because they're inside a conditional that checkscurrencyDatais truthy. However, the assertions rely on the assumption that whencurrencyDataexists,code,symbol, andpriceare also defined. Consider making this more explicit with a type guard or additional checks if theuseCurrencyhook can return partial data.currency={ currencyData ? { - code: currencyData.code!, - symbol: currencyData.symbol!, - price: currencyData.price!.buy, + code: currencyData.code ?? '', + symbol: currencyData.symbol ?? '', + price: currencyData.price?.buy ?? 1, } : undefined }src/components/TransactionDetails/TransactionCard.tsx (2)
105-112: Fix Next/Image sizing to avoid layout shift and distortionWidth/height (30px) conflicts with Tailwind size-12 (48px). Use fill within the sized parent or align dimensions.
- <Image - src={avatarUrl} - alt="Icon" - className="size-12 object-contain" - width={30} - height={30} - /> + <Image + src={avatarUrl} + alt="Transaction avatar" + fill + className="object-contain" + sizes="48px" + />The wrapping div is already
relative h-12 w-12, sofillis appropriate.
77-81: Make name-shortening intent clearerPassing
chars=0relies on internal defaults and can confuse readers. Prefer explicit first/last counts.-} else if (type === 'pay' && displayName.length > 19) { - displayName = shortenStringLong(displayName, 0, 16) +} else if (type === 'pay' && displayName.length > 19) { + displayName = shortenStringLong(displayName, undefined, 16, 0) }src/hooks/useBridgeKycFlow.ts (1)
67-76: Remove duplicate prevStatusRef assignmentprevStatusRef.current is set twice each run. Keep a single update at the end of the effect.
- const prevStatus = prevStatusRef.current - prevStatusRef.current = liveKycStatus + const prevStatus = prevStatusRef.current if (prevStatus !== 'approved' && liveKycStatus === 'approved') { setIsVerificationProgressModalOpen(false) onKycSuccess?.() } else if (prevStatus !== 'rejected' && liveKycStatus === 'rejected') { setIsVerificationProgressModalOpen(false) } - prevStatusRef.current = liveKycStatus + prevStatusRef.current = liveKycStatussrc/components/Kyc/KycStatusItem.tsx (1)
47-61: Label subtitle by status (Approved/Rejected/Submitted) for clarityCurrently always "Submitted on". Switch label based on status and include kycStatus in deps.
- const subtitle = useMemo(() => { - const date = verification - ? (verification.approvedAt ?? verification.updatedAt ?? verification.createdAt) - : bridgeKycStartedAt - if (!date) { - return 'Verification in progress' - } - try { - return `Submitted on ${formatDate(new Date(date)).split(' - ')[0]}` - } catch (error) { - console.error('Failed to parse date:', error) - return 'Verification in progress' - } - }, [bridgeKycStartedAt, verification]) + const subtitle = useMemo(() => { + const date = + verification + ? (verification.approvedAt ?? verification.updatedAt ?? verification.createdAt) + : bridgeKycStartedAt + if (!date) return 'Verification in progress' + try { + const day = formatDate(new Date(date)).split(' - ')[0] + const label = + (verification?.approvedAt || kycStatus === 'approved') ? 'Approved on' : + (kycStatus === 'rejected') ? 'Rejected on' : 'Submitted on' + return `${label} ${day}` + } catch (error) { + console.error('Failed to parse date:', error) + return 'Verification in progress' + } + }, [bridgeKycStartedAt, verification, kycStatus])src/app/(mobile-ui)/withdraw/manteca/page.tsx (1)
200-209: Useerror.codeto detect user-rejection
In the catch block ofsrc/app/(mobile-ui)/withdraw/manteca/page.tsx, replace or augment the string check with a guard forerror.code === 4001(EIP-1193 “user rejected request”) instead of relying ontoString().includes('not allowed').src/components/Payment/Views/MantecaFulfillment.view.tsx (2)
35-43: Avoid hardcoding Argentina; import canonical CountryData.Define and reuse AR data from the central country list to prevent drift (title/currency/path/ISO). If a shared constant already exists, import that instead of duplicating here.
64-69: Title nit: “Send” may confuse on a deposit/fulfillment screen.Consider “Fulfill request” or “Deposit instructions” to better match the view’s purpose.
src/app/(mobile-ui)/withdraw/page.tsx (5)
34-36: Update comment to reflect behavior.Code shows any selected method moves to 'inputAmount' (not crypto-only). Adjust the comment to avoid confusion.
69-84: Simplify step effect and avoid dependency onstepto prevent redundant runs.Set step solely from
selectedMethodand reset amount when transitioning back. Example:-useEffect(() => { - if (selectedMethod) { - setStep('inputAmount') - if (amountFromContext && !rawTokenAmount) { - setRawTokenAmount(amountFromContext) - } - } else { - setStep('selectMethod') - // clear the raw token amount when switching back to method selection - if (step !== 'selectMethod') { - setRawTokenAmount('') - setTokenInputKey((k) => k + 1) - } - } -}, [selectedMethod, amountFromContext, step, rawTokenAmount]) +useEffect(() => { + if (selectedMethod) { + setStep('inputAmount') + if (amountFromContext && !rawTokenAmount) { + setRawTokenAmount(amountFromContext) + } + } else { + setStep('selectMethod') + setRawTokenAmount('') + setTokenInputKey((k) => k + 1) + } + // eslint-disable-next-line react-hooks/exhaustive-deps +}, [selectedMethod, amountFromContext])
117-119: Clarify error copy.Use “Minimum withdrawal is $1.” (or “1 USD”) for clarity.
- message = 'Minimum withdrawal is 1.' + message = 'Minimum withdrawal is $1.'
212-214: Remove unusedmethodTitle.This constant isn’t used. Delete to satisfy linters and reduce noise.
- const methodTitle = selectedMethod?.title || selectedMethod?.countryPath || 'Selected method'
92-129: Consider reusing shared amount validation.There’s a common validator at src/lib/validation/amount.ts. Reuse or align behavior to avoid divergent rules between flows.
src/components/Payment/PaymentForm/index.tsx (1)
482-490: Stabilize URL-driven gating; don’t depend onuser.Depending on
usercan briefly unset the flag before auth hydrates. Rely on the URL step only and include the setter in deps.-useEffect(() => { - const stepFromURL = searchParams.get('step') - if (user && stepFromURL === 'regional-req-fulfill') { - setFulfillUsingManteca(true) - } else { - setFulfillUsingManteca(false) - } -}, [user, searchParams]) +useEffect(() => { + const stepFromURL = searchParams.get('step') + setFulfillUsingManteca(stepFromURL === 'regional-req-fulfill') +}, [searchParams, setFulfillUsingManteca])src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)
208-222: GuardisPublicin the dependencies as well.
shouldShowShareReceiptshort-circuits onisPublic, yet the memo ignores that prop. If the component toggles between private/public modes, the share button will keep the old state. AddisPublicto the dependency array to avoid stale UI.
No description provided.