+ )
+ }),
}))
jest.mock('@/components/0_Bruddle/Button', () => ({
diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx
index 3af4d8d932..1b504bc110 100644
--- a/src/app/(mobile-ui)/qr-pay/page.tsx
+++ b/src/app/(mobile-ui)/qr-pay/page.tsx
@@ -32,7 +32,7 @@ import { useCardMarkupRate } from '@/hooks/useCardMarkupRate'
import ErrorAlert from '@/components/Global/ErrorAlert'
import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts'
import { PERK_HOLD_DURATION_MS } from '@/constants/general.consts'
-import { MANTECA_DEPOSIT_ADDRESS } from '@/constants/manteca.consts'
+import { MANTECA_QR_DEPOSIT_ADDRESS_AR, MANTECA_QR_DEPOSIT_ADDRESS_NON_AR } from '@/constants/manteca.consts'
import { MIN_MANTECA_QR_PAYMENT_AMOUNT, MIN_PIX_AMOUNT_BRL } from '@/constants/payment.consts'
import { isPixRecurringCode } from '@/utils/withdraw.utils'
import { formatUnits, parseUnits } from 'viem'
@@ -273,6 +273,10 @@ export default function QRPayPage() {
if (sumsubFlow.showWrapper || sumsubFlow.isModalOpen) {
sumsubFlow.completeFlow()
}
+ // Field-level deps are complete for this body. useMultiPhaseKycFlow returns a fresh
+ // object each render, so depending on sumsubFlow itself would re-fire every render
+ // and call completeFlow() repeatedly.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [kycGateState, sumsubFlow.showWrapper, sumsubFlow.isModalOpen, sumsubFlow.completeFlow])
const queryClient = useQueryClient()
@@ -384,7 +388,7 @@ export default function QRPayPage() {
if (isSuccess || !!errorMessage) {
setLoadingState('Idle')
}
- }, [isSuccess, errorMessage])
+ }, [isSuccess, errorMessage, setLoadingState])
// First fetch for qrcode info — only after KYC gating allows proceeding
useEffect(() => {
@@ -403,6 +407,11 @@ export default function QRPayPage() {
}
setIsFirstLoad(false)
+ // Keyed on the scan (timestamp/processor/qrCode) on purpose: this resets payment
+ // state for each new QR. resetState is a render-body function and t/
+ // pixRecurringErrorMessage derive from it, so including them would re-run the
+ // reset on every render and wipe the amount mid-entry.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [timestamp, paymentProcessor, qrCode])
// Get amount from payment lock (Manteca)
@@ -418,7 +427,7 @@ export default function QRPayPage() {
setAmount(paymentLock.paymentAgainstAmount)
setCurrencyAmount(paymentLock.paymentAssetAmount)
}
- }, [paymentLock?.code, paymentProcessor])
+ }, [paymentLock, paymentProcessor])
// Get currency object from payment lock (Manteca)
useEffect(() => {
@@ -440,7 +449,7 @@ export default function QRPayPage() {
}
}
getCurrencyObject().then(setCurrency)
- }, [paymentLock?.code, paymentProcessor])
+ }, [paymentLock, paymentProcessor])
const isBlockingError = useMemo(() => {
// The settling failure says "try again in a few seconds" — keep the Pay
@@ -458,7 +467,7 @@ export default function QRPayPage() {
// For dynamic QR codes, backend provides the USD amount
return paymentLock.paymentAgainstAmount
}
- }, [paymentLock?.code, paymentLock?.paymentAgainstAmount, amount])
+ }, [paymentLock, amount])
// Live card-vs-local-rail markup, driven by Manteca's rate + (for ARS)
// BCRA's official rate. Used by both the confirm-screen "Save vs card"
@@ -541,7 +550,7 @@ export default function QRPayPage() {
!isPixRecurringCode(qrCode) &&
!paymentLock &&
!shouldBlockPay,
- retry: (failureCount, error: any) => {
+ retry: (failureCount, error) => {
// Don't retry provider-specific errors
if (NON_RETRYABLE_QR_PAY_ERRORS.some((code) => error?.message?.includes(code))) {
return false
@@ -569,8 +578,8 @@ export default function QRPayPage() {
setLoadingState('Idle')
}
- if (paymentLockError || paymentLockFailureReason) {
- const error = paymentLockError ?? paymentLockFailureReason
+ const error = paymentLockError ?? paymentLockFailureReason
+ if (error) {
setLoadingState('Idle')
// Provider-specific errors: show appropriate message
@@ -665,7 +674,9 @@ export default function QRPayPage() {
const requiredUsdcAmount = parseUnits(finalPaymentLock.paymentAgainstAmount, PEANUT_WALLET_TOKEN_DECIMALS)
signedArtifact = await signSpend({
requiredUsdcAmount,
- recipient: MANTECA_DEPOSIT_ADDRESS,
+ // Per-rail Manteca QR funding wallet: Pix → non-AR, everything else → AR
+ // (same binary heuristic as the backend's getQrReceiveAddress).
+ recipient: qrType === EQrType.PIX ? MANTECA_QR_DEPOSIT_ADDRESS_NON_AR : MANTECA_QR_DEPOSIT_ADDRESS_AR,
rainSpendingPower: rainCentsToUsdcUnits(rainCardOverview?.balance?.spendingPower),
kind: 'QR_PAY',
})
@@ -770,7 +781,6 @@ export default function QRPayPage() {
}, [
paymentLock,
signSpend,
- balance,
rainCardOverview,
qrCode,
currencyAmount,
diff --git a/src/app/(mobile-ui)/qr/[code]/page.tsx b/src/app/(mobile-ui)/qr/[code]/page.tsx
index f4da917cc1..e4ce42452e 100644
--- a/src/app/(mobile-ui)/qr/[code]/page.tsx
+++ b/src/app/(mobile-ui)/qr/[code]/page.tsx
@@ -110,7 +110,7 @@ export default function RedirectQrClaimPage() {
// Success! Show success page, then redirect to invite (which goes to profile for logged-in users)
router.push(qrSuccessUrl(code))
- } catch (err: any) {
+ } catch (err) {
console.error('Error claiming QR:', err)
// Always show generic error message (don't expose backend details)
setError(t('claim.claimFailed'))
diff --git a/src/app/(mobile-ui)/qr/[code]/success/page.tsx b/src/app/(mobile-ui)/qr/[code]/success/page.tsx
index 255cc53e44..4699b96a8e 100644
--- a/src/app/(mobile-ui)/qr/[code]/success/page.tsx
+++ b/src/app/(mobile-ui)/qr/[code]/success/page.tsx
@@ -102,9 +102,9 @@ export default function RedirectQrSuccessPage() {
url: qrUrl,
})
}
- } catch (error: any) {
+ } catch (error) {
// Ignore user cancellation
- if (error.name !== 'AbortError') {
+ if (!(error instanceof Error) || error.name !== 'AbortError') {
console.error('Share error:', error)
}
}
diff --git a/src/app/(mobile-ui)/receipt/page.tsx b/src/app/(mobile-ui)/receipt/page.tsx
new file mode 100644
index 0000000000..4c64cccea0
--- /dev/null
+++ b/src/app/(mobile-ui)/receipt/page.tsx
@@ -0,0 +1,70 @@
+'use client'
+
+import { useEffect } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useQuery } from '@tanstack/react-query'
+import PageContainer from '@/components/0_Bruddle/PageContainer'
+import NavHeader from '@/components/Global/NavHeader'
+import PeanutLoading from '@/components/Global/PeanutLoading'
+import { TransactionDetailsReceipt } from '@/components/TransactionDetails/TransactionDetailsReceipt'
+import { mapTransactionDataForDrawer } from '@/components/TransactionDetails/transactionTransformer'
+import { resolveReceiptKind } from '@/components/TransactionDetails/strategies/registry'
+import { TRANSACTIONS } from '@/constants/query.consts'
+import { apiFetch } from '@/utils/api-fetch'
+import { completeHistoryEntry, isFinalState, type HistoryEntry } from '@/utils/history.utils'
+
+// Client twin of the web /receipt/[entryId] page. That one is a server component
+// and is stripped from the static export (scripts/native-build.js), so a receipt
+// deep link — the most common push destination — had nothing to render natively.
+// deepLinkToNativePath maps /receipt/?kind=X onto this route's query params.
+export default function NativeReceiptPage() {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const entryId = searchParams.get('id')
+ const kind = resolveReceiptKind(searchParams.get('kind') ?? undefined, searchParams.get('t') ?? undefined)
+
+ const {
+ data: entry,
+ isLoading,
+ isError,
+ } = useQuery({
+ queryKey: [TRANSACTIONS, 'entry', entryId, kind],
+ enabled: Boolean(entryId && kind),
+ queryFn: async (): Promise => {
+ const response = await apiFetch(`/history/${encodeURIComponent(entryId!)}?kind=${kind}`)
+ if (!response.ok) throw new Error(`Failed to fetch history entry: ${response.status}`)
+ return completeHistoryEntry(await response.json())
+ },
+ // A pending transaction can still settle while the receipt is open.
+ refetchInterval: (query) => (query.state.data && !isFinalState(query.state.data) ? 15_000 : false),
+ })
+
+ // `isError` also trips on a failed background poll, so gate the bail-out on
+ // having no data at all — a flaky refetch must not yank the user off a
+ // receipt they're watching settle.
+ const isUnrecoverable = !entryId || !kind || (isError && !entry)
+
+ useEffect(() => {
+ if (isUnrecoverable) router.replace('/home')
+ }, [isUnrecoverable, router])
+
+ if (isUnrecoverable) return null
+
+ return (
+
+
+
+
+
+ {isLoading || !entry ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/src/app/(mobile-ui)/recover-funds/page.tsx b/src/app/(mobile-ui)/recover-funds/page.tsx
index efcefcbaef..194cb0bfd5 100644
--- a/src/app/(mobile-ui)/recover-funds/page.tsx
+++ b/src/app/(mobile-ui)/recover-funds/page.tsx
@@ -3,7 +3,7 @@
import NavHeader from '@/components/Global/NavHeader'
import ScrollableList from '@/components/Global/TokenSelector/Components/ScrollableList'
import TokenListItem from '@/components/Global/TokenSelector/Components/TokenListItem'
-import { type IUserBalance } from '@/interfaces'
+import { type IUserBalance } from '@/interfaces/interfaces'
import { useState, useEffect, useCallback, useContext } from 'react'
import { useWallet } from '@/hooks/wallet/useWallet'
import { fetchWalletBalances } from '@/services/tokens-price'
@@ -21,7 +21,7 @@ import PeanutLoading from '@/components/Global/PeanutLoading'
import { erc20Abi, parseUnits, encodeFunctionData, formatUnits } from 'viem'
import type { Address, Hash, TransactionReceipt } from 'viem'
import { useRouter } from 'next/navigation'
-import { loadingStateContext } from '@/context'
+import { loadingStateContext } from '@/context/loadingStates.context'
import { captureException } from '@sentry/nextjs'
import { mainnet, base, linea } from 'viem/chains'
import { getPublicClient, type ChainId } from '@/app/actions/clients'
diff --git a/src/app/(mobile-ui)/rewards/invites/page.tsx b/src/app/(mobile-ui)/rewards/invites/page.tsx
index 254a64f852..8a950af447 100644
--- a/src/app/(mobile-ui)/rewards/invites/page.tsx
+++ b/src/app/(mobile-ui)/rewards/invites/page.tsx
@@ -12,7 +12,7 @@ import { invitesApi } from '@/services/invites'
import { useQuery } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useSafeBack } from '@/hooks/useSafeBack'
-import { STAR_STRAIGHT_ICON } from '@/assets'
+import STAR_STRAIGHT_ICON from '@/assets/icons/starStraight.svg'
import Image from 'next/image'
import EmptyState from '@/components/Global/EmptyStates/EmptyState'
import { getInitialsFromName } from '@/utils/general.utils'
diff --git a/src/app/(mobile-ui)/rewards/page.tsx b/src/app/(mobile-ui)/rewards/page.tsx
index e65c3f121b..8f2d00a795 100644
--- a/src/app/(mobile-ui)/rewards/page.tsx
+++ b/src/app/(mobile-ui)/rewards/page.tsx
@@ -15,7 +15,11 @@ import { getInitialsFromName } from '@/utils/general.utils'
import { useQuery } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useSafeBack } from '@/hooks/useSafeBack'
-import { STAR_STRAIGHT_ICON, TIER_0_BADGE, TIER_1_BADGE, TIER_2_BADGE, TIER_3_BADGE } from '@/assets'
+import STAR_STRAIGHT_ICON from '@/assets/icons/starStraight.svg'
+import TIER_0_BADGE from '@/assets/badges/tier0.svg'
+import TIER_1_BADGE from '@/assets/badges/tier1.svg'
+import TIER_2_BADGE from '@/assets/badges/tier2.svg'
+import TIER_3_BADGE from '@/assets/badges/tier3.svg'
import Image from 'next/image'
import { pointsApi } from '@/services/points'
import EmptyState from '@/components/Global/EmptyStates/EmptyState'
diff --git a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
index 43f0870652..23d17f7310 100644
--- a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
+++ b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
@@ -12,7 +12,7 @@ import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants/zer
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { useWallet } from '@/hooks/wallet/useWallet'
import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions'
-import { AccountType, type Account } from '@/interfaces'
+import { AccountType, type Account } from '@/interfaces/interfaces'
import { formatIban, shortenStringLong, isTxReverted } from '@/utils/general.utils'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import { useEffect, useMemo, useState } from 'react'
@@ -37,7 +37,7 @@ import { useAdvisoryPreempt } from '@/hooks/useAdvisoryPreempt'
import { useEeaUpliftFunnel } from '@/hooks/useEeaUpliftFunnel'
import { upliftTriggerFromGate, upliftTriggerFromAdvisory } from '@/utils/eea-uplift.utils'
import { useCapabilities } from '@/hooks/useCapabilities'
-import { getKycModalVariant, getGateUserMessage } from '@/utils/capability-gate'
+import { resolveKycModalVariant, getGateUserMessage } from '@/utils/capability-gate'
import { useModalsContext } from '@/context/ModalsContext'
import ExchangeRate from '@/components/ExchangeRate'
import countryCurrencyMappings, { isNonEuroSepaCountry } from '@/constants/countryCurrencyMapping'
@@ -150,7 +150,7 @@ export default function WithdrawBankPage() {
router.replace('/withdraw')
}
}
- }, [country, isBridgeSupportedCountry, router])
+ }, [country, router])
// check if we came from send flow - using method param to detect (only bank goes through this page)
const methodParam = searchParams.get('method')
@@ -349,14 +349,14 @@ export default function WithdrawBankPage() {
method_type: 'bridge',
country,
})
- } catch (e: any) {
+ } catch (e) {
const error = toFriendlyError(e)
posthog.capture(ANALYTICS_EVENTS.WITHDRAW_FAILED, {
method_type: 'bridge',
error_message: error,
})
if (error.includes('Something failed. Please try again.')) {
- setError({ showError: true, errorMessage: e.message })
+ setError({ showError: true, errorMessage: e instanceof Error ? e.message : String(e) })
} else {
setError({ showError: true, errorMessage: error })
}
@@ -598,7 +598,7 @@ export default function WithdrawBankPage() {
}}
isLoading={sumsubFlow.isLoading}
error={sumsubFlow.error}
- variant={getKycModalVariant(gate.kind)}
+ variant={resolveKycModalVariant(gate)}
providerMessage={getGateUserMessage(gate)}
regionName={getCountryFromPath(country)?.title}
/>
diff --git a/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx b/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
index 25b77a4ee5..289a25f3e9 100644
--- a/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
+++ b/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
@@ -114,15 +114,6 @@ jest.mock('@/hooks/useGetExchangeRate', () => ({
default: () => mockUseGetExchangeRate(),
}))
-jest.mock('@/interfaces', () => ({
- AccountType: {
- IBAN: 'iban',
- US: 'us',
- GB: 'gb',
- CLABE: 'clabe',
- },
-}))
-
const mockUseLimitsValidation = jest.fn()
jest.mock('@/features/limits/hooks/useLimitsValidation', () => ({
useLimitsValidation: (...args: any[]) => mockUseLimitsValidation(...args),
@@ -399,12 +390,14 @@ describe('GROUP 3: Amount Validation', () => {
expect(screen.getByTestId('limits-warning-card')).toBeInTheDocument()
})
- test('Crypto withdrawal allows sub-$1 amounts (no fiat-rail minimum)', () => {
+ test('Crypto withdrawal has no amount-step minimum (parity with send-via-link)', () => {
// Regression: the shared amount step applied the bank $1 minimum to
// crypto (getMinimumAmount('') → 1), blocking sub-$1 on-chain sends
- // that send-via-link already allows.
+ // that send-via-link already allows. Same-chain Arbitrum withdrawals
+ // have no minimum at all; Rhino's per-network bridge minimums are
+ // enforced at review time, once the destination is known.
mockWithdrawFlow.selectedMethod = { type: 'crypto' }
- mockWithdrawFlow.amountToWithdraw = '0.5'
+ mockWithdrawFlow.amountToWithdraw = '0.4'
renderWithdraw()
diff --git a/src/app/(mobile-ui)/withdraw/crypto/page.tsx b/src/app/(mobile-ui)/withdraw/crypto/page.tsx
index ab623c0863..3350dc9ee8 100644
--- a/src/app/(mobile-ui)/withdraw/crypto/page.tsx
+++ b/src/app/(mobile-ui)/withdraw/crypto/page.tsx
@@ -17,7 +17,7 @@ import type {
TRequestResponse,
} from '@/services/services.types'
import { NATIVE_TOKEN_ADDRESS } from '@/utils/token.utils'
-import { isWithdrawFeeDisproportionate } from '@/utils/cross-chain-fee.utils'
+import { isWithdrawFeeDisproportionate, getMinWithdrawUsdForChain } from '@/utils/cross-chain-fee.utils'
import { isAmountWithinBalance } from '@/utils/balance.utils'
import { isBelowRhinoMinDeposit } from '@/utils/withdraw.utils'
import * as peanutInterfaces from '@/interfaces/peanut-sdk-types'
@@ -27,7 +27,7 @@ import { useSafeBack } from '@/hooks/useSafeBack'
import type { Address, Hex, TransactionReceipt } from 'viem'
import { parseUnits } from 'viem'
import { Slider } from '@/components/Slider'
-import { tokenSelectorContext } from '@/context'
+import { tokenSelectorContext } from '@/context/tokenSelector.context'
import { useHaptic } from 'use-haptic'
import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts'
import { useCrossChainTransfer } from '@/features/payments/shared/hooks/useCrossChainTransfer'
@@ -172,6 +172,26 @@ export default function WithdrawCryptoPage() {
return
}
+ // Same-chain USDC is a direct transfer — no Rhino, no minimum
+ // (parity with send-via-link). Every other destination/token rides
+ // Rhino, which parks (doesn't auto-refund) deposits below the route
+ // minimum — block those before any request/charge is created.
+ // amountToWithdraw is USD.
+ const isSameChainUsdc =
+ data.chain.chainId.toString() === PEANUT_WALLET_CHAIN.id.toString() &&
+ data.token.address.toLowerCase() === PEANUT_WALLET_TOKEN.toLowerCase()
+ if (!isSameChainUsdc) {
+ const usdToWithdraw = parseFloat(amountToWithdraw)
+ const minUsd = getMinWithdrawUsdForChain(data.chain.chainId)
+ if (!Number.isFinite(usdToWithdraw) || usdToWithdraw < minUsd) {
+ const minDisplay = minUsd % 1 === 0 ? `$${minUsd}` : `$${minUsd.toFixed(2)}`
+ setError(
+ `Withdrawals to ${data.chain.networkName} need at least ${minDisplay}. Increase the amount or pick a different network.`
+ )
+ return
+ }
+ }
+
clearErrors()
setChargeDetails(null)
setIsPreparingReview(true)
@@ -237,9 +257,9 @@ export default function WithdrawCryptoPage() {
setChargeDetails(fullChargeDetails)
setShowCompatibilityModal(true)
- } catch (err: any) {
+ } catch (err) {
console.error('Error during setup review (request/charge creation):', err)
- const errorMessage = err.message || t('errors.prepareFailed')
+ const errorMessage = err instanceof Error && err.message ? err.message : t('errors.prepareFailed')
setError(errorMessage)
} finally {
setIsPreparingReview(false)
diff --git a/src/app/(mobile-ui)/withdraw/page.tsx b/src/app/(mobile-ui)/withdraw/page.tsx
index 502f27d656..6f8dfee691 100644
--- a/src/app/(mobile-ui)/withdraw/page.tsx
+++ b/src/app/(mobile-ui)/withdraw/page.tsx
@@ -8,12 +8,11 @@ import AmountInput from '@/components/Global/AmountInput'
import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { useWallet } from '@/hooks/wallet/useWallet'
-import { tokenSelectorContext } from '@/context/tokenSelector.context'
import { getCountryFromAccount, getCountryFromPath, getMinimumAmount } from '@/utils/bridge.utils'
import useGetExchangeRate from '@/hooks/useGetExchangeRate'
-import { AccountType } from '@/interfaces'
+import { AccountType } from '@/interfaces/interfaces'
import { useRouter, useSearchParams } from 'next/navigation'
-import React, { useCallback, useEffect, useMemo, useState, useRef, useContext } from 'react'
+import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
import { formatUnits } from 'viem'
import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation'
import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard'
@@ -32,7 +31,6 @@ export default function WithdrawPage() {
const tNav = useTranslations('navigation')
const tCommon = useTranslations('common')
const tErrors = useTranslations('errors')
- const { selectedTokenData } = useContext(tokenSelectorContext)
// check if coming from send flow based on method query param
const methodParam = searchParams.get('method')
@@ -133,7 +131,12 @@ export default function WithdrawPage() {
// compute minimum withdrawal in USD using the exchange rate
const minUsdAmount = useMemo(() => {
- if (isCryptoWithdraw) return 0 // any amount > 0 is valid, same as send-via-link
+ // no amount-step minimum for crypto: same-chain (Arbitrum) withdrawals
+ // are direct transfers with no floor, matching send-via-link. Rhino's
+ // per-network bridge minimums ($0.50, ETH $5, Tron $10) are enforced
+ // chain-aware at review time (see withdraw/crypto), once the
+ // destination is known.
+ if (isCryptoWithdraw) return 0
const localMin = getMinimumAmount(countryIso2)
// for US or unknown, minimum is already in USD
if (!countryIso2 || countryIso2 === 'US') return localMin
@@ -199,9 +202,10 @@ export default function WithdrawPage() {
return false
}
- // convert the entered token amount to USD
- const price = selectedTokenData?.price ?? 0 // 0 for safety; will fail below
- const usdEquivalent = price ? amount * price : amount // if no price assume token pegged 1 USD
+ // AmountInput is USD-pinned on this page (price: 1), so the typed
+ // value IS the USD value — scaling by the app-wide token price let
+ // a stale non-USD price loosen or false-trip the minimums.
+ const usdEquivalent = amount
// While the balance is still loading, maxDecimalAmount is 0 — skip the
// balance check so a pre-filled amount isn't false-blocked; the effect
@@ -227,7 +231,7 @@ export default function WithdrawPage() {
setError({ showError: true, errorMessage: message })
return false
},
- [balance, maxDecimalAmount, setError, selectedTokenData?.price, isFromSendFlow, minUsdAmount, t, tErrors]
+ [balance, maxDecimalAmount, setError, isFromSendFlow, minUsdAmount, t, tErrors]
)
const handleTokenAmountChange = useCallback(
@@ -278,7 +282,7 @@ export default function WithdrawPage() {
const handleAmountContinue = () => {
if (validateAmount(rawTokenAmount) && selectedMethod) {
setAmountToWithdraw(rawTokenAmount)
- const usdVal = (selectedTokenData?.price ?? 1) * parseFloat(rawTokenAmount)
+ const usdVal = parseFloat(rawTokenAmount)
setUsdAmount(usdVal.toString())
posthog.capture(ANALYTICS_EVENTS.WITHDRAW_AMOUNT_ENTERED, {
amount_usd: usdVal,
@@ -356,13 +360,12 @@ export default function WithdrawPage() {
const numericAmount = parseFloat(rawTokenAmount)
if (!Number.isFinite(numericAmount) || numericAmount <= 0) return true
- const usdEq = (selectedTokenData?.price ?? 1) * numericAmount
- if (usdEq < minUsdAmount) return true // below country-specific minimum
+ if (numericAmount < minUsdAmount) return true // below the method's USD minimum
// only apply the balance ceiling once it has loaded (maxDecimalAmount is 0
// while spendableBalance is undefined) — else Continue is disabled during load
return (balance !== undefined && numericAmount > maxDecimalAmount) || error.showError
- }, [rawTokenAmount, balance, maxDecimalAmount, error.showError, selectedTokenData?.price, minUsdAmount])
+ }, [rawTokenAmount, balance, maxDecimalAmount, error.showError, minUsdAmount])
// native app: render country-specific views when ?country= is present
const viewFromQuery = searchParams.get('view')
diff --git a/src/app/(setup)/layout.tsx b/src/app/(setup)/layout.tsx
index 29695bf7a5..49634e6e0a 100644
--- a/src/app/(setup)/layout.tsx
+++ b/src/app/(setup)/layout.tsx
@@ -64,7 +64,7 @@ function SetupLayoutContent({ children }: { children?: React.ReactNode }) {
} else {
dispatch(setupActions.setShowIosPwaInstallScreen(false))
}
- }, [isPWA, deviceType])
+ }, [isPWA, deviceType, dispatch])
usePullToRefresh()
diff --git a/src/app/ClientProviders.tsx b/src/app/ClientProviders.tsx
index 308fae7f90..a43875de8c 100644
--- a/src/app/ClientProviders.tsx
+++ b/src/app/ClientProviders.tsx
@@ -10,6 +10,7 @@ import { ConsoleGreeting } from '@/components/Global/ConsoleGreeting'
import RainCooldownIntroModal from '@/components/Global/RainCooldown/IntroModal'
import StaleCardApprovalReEnableModal from '@/components/Global/StaleCardApproval/ReEnableModal'
import BadgeEarnToast from '@/components/Badges/BadgeEarnToast'
+import { AppLockGate } from '@/components/Global/AppLock'
import { ScreenOrientationLocker } from '@/components/Global/ScreenOrientationLocker'
import { TranslationSafeWrapper } from '@/components/Global/TranslationSafeWrapper'
import { AppIntlProvider } from '@/i18n/app/AppIntlProvider'
@@ -65,7 +66,9 @@ export function ClientProviders({ children }: { children: React.ReactNode }) {
)}
- {children}
+ {/* Wraps rather than sits beside the page: while the
+ native app is locked, nothing protected renders. */}
+ {children}
diff --git a/src/app/[...recipient]/page.tsx b/src/app/[...recipient]/page.tsx
index af5c7de1a9..c67c992029 100644
--- a/src/app/[...recipient]/page.tsx
+++ b/src/app/[...recipient]/page.tsx
@@ -13,9 +13,10 @@ import { couldBeRecipient, isReservedRoute } from '@/constants/routes'
type PageProps = {
params: Promise<{ recipient?: string[] }>
+ searchParams: Promise<{ chargeId?: string }>
}
-export async function generateMetadata({ params, searchParams }: any) {
+export async function generateMetadata({ params, searchParams }: PageProps) {
const resolvedSearchParams = await searchParams
const resolvedParams = await params
diff --git a/src/app/[locale]/(marketing)/team/page.tsx b/src/app/[locale]/(marketing)/team/page.tsx
index 32b19267f8..ef693253de 100644
--- a/src/app/[locale]/(marketing)/team/page.tsx
+++ b/src/app/[locale]/(marketing)/team/page.tsx
@@ -1,5 +1,6 @@
import { notFound } from 'next/navigation'
import { type Metadata } from 'next'
+import Image from 'next/image'
import { generateMetadata as metadataHelper } from '@/app/metadata'
import { MarketingHero } from '@/components/Marketing/MarketingHero'
import { MarketingShell } from '@/components/Marketing/MarketingShell'
@@ -108,7 +109,7 @@ export default async function TeamPage({ params }: PageProps) {
return (
{member.image ? (
- = (() => {
const map: Record = {}
for (const c of countryData) {
- const iso2 = (c as any).iso2 || c.id
- const iso3 = (c as any).iso3
+ const iso2 = c.iso2 || c.id
+ const iso3 = c.iso3
if (iso2 && iso3) {
map[String(iso3).toUpperCase()] = String(iso2).toUpperCase()
}
diff --git a/src/app/actions/card.ts b/src/app/actions/card.ts
index 2fdffe2f13..70d256356f 100644
--- a/src/app/actions/card.ts
+++ b/src/app/actions/card.ts
@@ -51,8 +51,8 @@ export const getCardInfo = async (): Promise<{ data?: CardInfoResponse; error?:
const data = await response.json()
return { data }
- } catch (e: any) {
- return { error: e.message || 'An unexpected error occurred' }
+ } catch (e) {
+ return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}
@@ -76,7 +76,7 @@ export const purchaseCard = async (): Promise<{ data?: CardPurchaseResponse; err
const data = await response.json()
return { data }
- } catch (e: any) {
- return { error: e.message || 'An unexpected error occurred' }
+ } catch (e) {
+ return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}
diff --git a/src/app/actions/currency.ts b/src/app/actions/currency.ts
index c694b7a4ee..654fac5c14 100644
--- a/src/app/actions/currency.ts
+++ b/src/app/actions/currency.ts
@@ -1,5 +1,5 @@
import { getExchangeRate } from './exchange-rate'
-import { AccountType } from '@/interfaces'
+import { AccountType } from '@/interfaces/interfaces'
import { mantecaApi } from '@/services/manteca'
import { unstable_cache } from '@/utils/no-cache'
diff --git a/src/app/actions/exchange-rate.ts b/src/app/actions/exchange-rate.ts
index d7642d1ee5..2ee4eab8ae 100644
--- a/src/app/actions/exchange-rate.ts
+++ b/src/app/actions/exchange-rate.ts
@@ -1,4 +1,4 @@
-import { AccountType } from '@/interfaces'
+import { AccountType } from '@/interfaces/interfaces'
import { serverFetch } from '@/utils/api-fetch'
export interface ExchangeRateResponse {
diff --git a/src/app/actions/external-accounts.ts b/src/app/actions/external-accounts.ts
index 60445fd9f7..efad0fb477 100644
--- a/src/app/actions/external-accounts.ts
+++ b/src/app/actions/external-accounts.ts
@@ -1,5 +1,5 @@
import { type AddBankAccountPayload } from './types/users.types'
-import { type IBridgeAccount } from '@/interfaces'
+import { type IBridgeAccount } from '@/interfaces/interfaces'
import { serverFetch } from '@/utils/api-fetch'
export async function createBridgeExternalAccountForGuest(
diff --git a/src/app/actions/onramp-quote.ts b/src/app/actions/onramp-quote.ts
index 7ac69c6a7e..5692501ffe 100644
--- a/src/app/actions/onramp-quote.ts
+++ b/src/app/actions/onramp-quote.ts
@@ -1,5 +1,5 @@
import { fetchWithSentry } from '@/utils/sentry.utils'
-import { AccountType } from '@/interfaces'
+import { AccountType } from '@/interfaces/interfaces'
import { PEANUT_API_URL } from '@/constants/general.consts'
import { getAuthHeaders } from '@/utils/auth-token'
diff --git a/src/app/actions/sumsub.ts b/src/app/actions/sumsub.ts
index 519e8f43ca..017bde0d66 100644
--- a/src/app/actions/sumsub.ts
+++ b/src/app/actions/sumsub.ts
@@ -48,7 +48,7 @@ export interface SelfHealResubmissionResponse {
applicantId: string
actionId: string
externalActionId: string
- requiredAction: 'REUPLOAD_ID' | 'REUPLOAD_ADDRESS_PROOF' | 'CONTACT_SUPPORT'
+ requiredAction: 'REUPLOAD_ID' | 'REUPLOAD_ADDRESS_PROOF' | 'CONTACT_SUPPORT' | 'RAIN_DOCUMENT'
userMessage: string
attempt: number
maxAttempts: number
@@ -86,7 +86,7 @@ export const restartIdentityVerification = async (): Promise<{
// initiate self-heal document resubmission for a provider-rejected user
export const initiateSelfHealResubmission = async (
- provider: 'BRIDGE' | 'MANTECA',
+ provider: 'BRIDGE' | 'MANTECA' | 'RAIN',
// Optional — target a specific (e.g. future-dated advisory) Bridge requirement
// by key. Omitted for the legacy blocking flow (current nextAction).
requirementKey?: string
diff --git a/src/app/actions/users.ts b/src/app/actions/users.ts
index a57b505eca..0d2e99de9e 100644
--- a/src/app/actions/users.ts
+++ b/src/app/actions/users.ts
@@ -1,10 +1,11 @@
import { type ApiUser } from '@/services/users'
import { type AddBankAccountPayload, BridgeEndorsementType, type InitiateKycResponse } from './types/users.types'
-import { type CounterpartyUser } from '@/interfaces'
-import { type ContactsResponse } from '@/interfaces'
+import { type CounterpartyUser } from '@/interfaces/interfaces'
+import { type ContactsResponse } from '@/interfaces/interfaces'
import { serverFetch } from '@/utils/api-fetch'
+import { withStepUpHeader } from '@/services/step-up'
-export const updateUserById = async (payload: Record): Promise<{ data?: ApiUser; error?: string }> => {
+export const updateUserById = async (payload: Record): Promise<{ data?: ApiUser; error?: string }> => {
try {
const response = await serverFetch('/update-user', {
method: 'POST',
@@ -16,8 +17,8 @@ export const updateUserById = async (payload: Record): Promise<{ da
return { error: responseJson.message || responseJson.error || 'Failed to update user' }
}
return { data: responseJson }
- } catch (e: any) {
- return { error: e.message || 'An unexpected error occurred' }
+ } catch (e) {
+ return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}
@@ -41,16 +42,33 @@ export const getKycDetails = async (params?: {
}
}
return { data: responseJson }
- } catch (e: any) {
- return { error: e.message || 'An unexpected error occurred' }
+ } catch (e) {
+ return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}
-export const addBankAccount = async (payload: AddBankAccountPayload): Promise<{ data?: any; error?: string }> => {
+// shape of the account returned by POST /users/accounts, as consumed by callers
+export interface AddedBankAccount {
+ id: string
+ type: string
+ identifier?: string
+ bridgeAccountId?: string
+ bic?: string
+ routingNumber?: string
+ sortCode?: string
+ firstName?: string
+ lastName?: string
+ details: { accountOwnerName?: string; countryCode?: string }
+}
+
+export const addBankAccount = async (
+ payload: AddBankAccountPayload
+): Promise<{ data?: AddedBankAccount; error?: string }> => {
try {
const response = await serverFetch('/users/accounts', {
method: 'POST',
body: JSON.stringify(payload),
+ headers: await withStepUpHeader({}),
})
const responseJson = await response.json()
@@ -63,8 +81,8 @@ export const addBankAccount = async (payload: AddBankAccountPayload): Promise<{
}
}
return { data: responseJson }
- } catch (e: any) {
- return { error: e.message || 'An unexpected error occurred' }
+ } catch (e) {
+ return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}
diff --git a/src/app/api/health/justaname/route.ts b/src/app/api/health/justaname/route.ts
index c10f0ec78d..739fde0fb4 100644
--- a/src/app/api/health/justaname/route.ts
+++ b/src/app/api/health/justaname/route.ts
@@ -40,7 +40,13 @@ export async function GET() {
// Test a second endpoint - ENS name lookup (if available)
const lookupTestStart = Date.now()
- let lookupHealth: any = { status: 'not_tested', message: 'Lookup endpoint not tested' }
+ let lookupHealth: {
+ status: string
+ message?: string
+ responseTime?: number
+ httpStatus?: number
+ error?: string
+ } = { status: 'not_tested', message: 'Lookup endpoint not tested' }
try {
// Test reverse ENS lookup if the API supports it
diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts
index e2529b8e8e..1e9cfcb0e5 100644
--- a/src/app/api/health/route.ts
+++ b/src/app/api/health/route.ts
@@ -22,10 +22,29 @@ export const fetchCache = 'force-no-store'
let lastNotificationTime = 0
const NOTIFICATION_COOLDOWN_MS = 30 * 60 * 1000 // 30 minutes
+type ServiceHealth = {
+ status?: string
+ responseTime?: number
+ timestamp?: string
+ details?: Record
+ error?: string
+}
+
+type HealthSummary = { total: number; healthy: number; degraded: number; unhealthy: number }
+
+type HealthReport = {
+ status: string
+ healthScore: number
+ timestamp: string
+ summary: HealthSummary
+ services: Record
+ systemInfo?: { environment?: string; version?: string; region?: string }
+}
+
/**
* Send Discord notification when system is unhealthy (with cooldown).
*/
-async function sendDiscordNotification(healthData: any) {
+async function sendDiscordNotification(healthData: HealthReport) {
try {
const webhookUrl = process.env.DISCORD_WEBHOOK_URL
if (!webhookUrl) {
@@ -44,12 +63,12 @@ async function sendDiscordNotification(healthData: any) {
lastNotificationTime = now
const failedServices = Object.entries(healthData.services)
- .filter(([_, service]: [string, any]) => service.status === 'unhealthy')
- .map(([name, service]: [string, any]) => `• ${name}: ${service.error || 'unhealthy'}`)
+ .filter(([_, service]) => service.status === 'unhealthy')
+ .map(([name, service]) => `• ${name}: ${service.error || 'unhealthy'}`)
const degradedServices = Object.entries(healthData.services)
- .filter(([_, service]: [string, any]) => service.status === 'degraded')
- .map(([name, service]: [string, any]) => `• ${name}: ${service.error || 'degraded'}`)
+ .filter(([_, service]) => service.status === 'degraded')
+ .map(([name, service]) => `• ${name}: ${service.error || 'degraded'}`)
// Only @mention the role in production
const isProduction = process.env.NODE_ENV === 'production'
@@ -123,7 +142,7 @@ export async function GET() {
clearTimeout(timeoutId)
- const data = await response.json()
+ const data: ServiceHealth = await response.json()
// Pass through the sub-check's own status rather than only trusting HTTP status.
// Sub-checks now return degraded (HTTP 200) for non-critical partial failures.
@@ -135,7 +154,7 @@ export async function GET() {
})
)
- const results: any = {
+ const results: { services: Record; summary: HealthSummary } = {
services: {},
summary: {
total: services.length,
@@ -150,7 +169,7 @@ export async function GET() {
const serviceName = services[index]
if (result.status === 'fulfilled') {
- const serviceData = result.value as any
+ const serviceData = result.value
const serviceStatus = serviceData.status || 'unhealthy'
results.services[serviceName] = {
diff --git a/src/app/api/health/rpc/route.ts b/src/app/api/health/rpc/route.ts
index 59a57691c2..95749b9a15 100644
--- a/src/app/api/health/rpc/route.ts
+++ b/src/app/api/health/rpc/route.ts
@@ -17,6 +17,23 @@ const ALCHEMY_API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY
// If a critical chain has zero healthy providers, overall status = unhealthy.
const CRITICAL_CHAINS = new Set([1, 42161]) // Ethereum, Arbitrum
+type ProviderHealth = {
+ status: 'healthy' | 'degraded' | 'unhealthy'
+ responseTime: number
+ blockNumber?: number | null
+ httpStatus?: number
+ error?: string
+ url: string
+}
+
+type ChainHealth = {
+ chainId: number
+ critical: boolean
+ providers: Record
+ overallStatus: string
+ summary?: { total: number; healthy: number; degraded: number; unhealthy: number }
+}
+
export async function GET() {
const startTime = Date.now()
@@ -34,7 +51,7 @@ export async function GET() {
)
}
- const chainResults: any = {}
+ const chainResults: Record = {}
const chainsToTest = [
{ id: 1, name: 'ethereum' },
@@ -117,7 +134,7 @@ export async function GET() {
)
// Determine chain overall status
- const chainProviders = Object.values(chainResults[chain.name].providers) as any[]
+ const chainProviders = Object.values(chainResults[chain.name].providers)
const healthyCount = chainProviders.filter((p) => p.status === 'healthy').length
const degradedCount = chainProviders.filter((p) => p.status === 'degraded').length
const unhealthyCount = chainProviders.length - healthyCount - degradedCount
diff --git a/src/app/api/health/zerodev/route.ts b/src/app/api/health/zerodev/route.ts
index c360c8f1a2..6c28246337 100644
--- a/src/app/api/health/zerodev/route.ts
+++ b/src/app/api/health/zerodev/route.ts
@@ -34,7 +34,20 @@ export async function GET() {
)
}
- const results: any = {
+ type ProbeResult = {
+ status?: string
+ responseTime?: number
+ httpStatus?: number
+ entryPoints?: unknown
+ chainId?: unknown
+ message?: string
+ error?: string
+ }
+
+ const results: {
+ arbitrum: { bundler: ProbeResult; paymaster: ProbeResult }
+ configuration: Record
+ } = {
arbitrum: { bundler: {}, paymaster: {} },
configuration: {
projectId: 'configured',
diff --git a/src/app/api/og/route.tsx b/src/app/api/og/route.tsx
index 190cb7e334..1202ea41ac 100644
--- a/src/app/api/og/route.tsx
+++ b/src/app/api/og/route.tsx
@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og'
import { PaymentCardOG } from '@/components/og/PaymentCardOG'
import { NextResponse, type NextRequest } from 'next/server'
-import { type PaymentLink } from '@/interfaces'
+import { type PaymentLink } from '@/interfaces/interfaces'
import { promises as fs } from 'fs'
import path from 'path'
import getOrigin from '@/lib/hosting/get-origin'
diff --git a/src/app/api/recent-transactions/route.ts b/src/app/api/recent-transactions/route.ts
index 53ee649595..12cff58fbb 100644
--- a/src/app/api/recent-transactions/route.ts
+++ b/src/app/api/recent-transactions/route.ts
@@ -93,7 +93,7 @@ export async function POST(request: NextRequest, _context: { params: Promise
-
+
diff --git a/src/app/kyc/success/page.tsx b/src/app/kyc/success/page.tsx
index 7e050b722b..81c99a7cca 100644
--- a/src/app/kyc/success/page.tsx
+++ b/src/app/kyc/success/page.tsx
@@ -2,7 +2,7 @@
import { useEffect } from 'react'
import Image from 'next/image'
-import { HandThumbsUp } from '@/assets'
+import HandThumbsUp from '@/assets/illustrations/hand-thumbs-up.svg'
/*
This page is just to let users know that their KYC was successful. Incase there's some issue with webosckets closing the modal, ideally this should not happen but added this as fallback guide
diff --git a/src/app/lp/card/CardLandingPage.tsx b/src/app/lp/card/CardLandingPage.tsx
index 401b45398d..6444eefc27 100644
--- a/src/app/lp/card/CardLandingPage.tsx
+++ b/src/app/lp/card/CardLandingPage.tsx
@@ -8,7 +8,8 @@ import PioneerCard3D from '@/components/LandingPage/PioneerCard3D'
import { Marquee } from '@/components/LandingPage'
import { useAuth } from '@/context/authContext'
import { useRouter } from 'next/navigation'
-import { Star, HandThumbsUp } from '@/assets'
+import Star from '@/assets/illustrations/star.svg'
+import HandThumbsUp from '@/assets/illustrations/hand-thumbs-up.svg'
import { useEffect } from 'react'
import underMaintenanceConfig from '@/config/underMaintenance.config'
diff --git a/src/app/quests/[questId]/page.tsx b/src/app/quests/[questId]/page.tsx
index 88b8654e09..679f3eb689 100644
--- a/src/app/quests/[questId]/page.tsx
+++ b/src/app/quests/[questId]/page.tsx
@@ -6,7 +6,7 @@ import { useRouter, useSearchParams } from 'next/navigation'
import { motion } from 'framer-motion'
import Image from 'next/image'
import borderCloud from '@/assets/illustrations/border-cloud.svg'
-import { Star } from '@/assets'
+import Star from '@/assets/illustrations/star.svg'
import { useEffect, useState, useCallback } from 'react'
import { Button } from '@/components/0_Bruddle/Button'
import { QuestLeaderboard } from '../components/QuestLeaderboard'
diff --git a/src/app/quests/components/QuestsHero.tsx b/src/app/quests/components/QuestsHero.tsx
index 4fbc2434a5..644abc7af9 100644
--- a/src/app/quests/components/QuestsHero.tsx
+++ b/src/app/quests/components/QuestsHero.tsx
@@ -5,7 +5,7 @@ import { motion } from 'framer-motion'
import { QuestCard } from './QuestCard'
import borderCloud from '@/assets/illustrations/border-cloud.svg'
import handPointing from '@/assets/illustrations/got-it-hand.svg'
-import { Star } from '@/assets'
+import Star from '@/assets/illustrations/star.svg'
import { useRouter, useSearchParams } from 'next/navigation'
import Image from 'next/image'
import { Button } from '@/components/0_Bruddle/Button'
diff --git a/src/app/quests/explore/page.tsx b/src/app/quests/explore/page.tsx
index cfaa2e94ca..08ef266bbc 100644
--- a/src/app/quests/explore/page.tsx
+++ b/src/app/quests/explore/page.tsx
@@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from 'next/navigation'
import { motion } from 'framer-motion'
import Image from 'next/image'
import borderCloud from '@/assets/illustrations/border-cloud.svg'
-import { Star } from '@/assets'
+import Star from '@/assets/illustrations/star.svg'
import { useEffect, useState, useMemo, useCallback } from 'react'
import { Button } from '@/components/0_Bruddle/Button'
import { QuestLeaderboard } from '../components/QuestLeaderboard'
@@ -144,7 +144,13 @@ export default function QuestsExplorePage() {
-
+
QUESTS
diff --git a/src/components/AddMoney/components/AddMoneyBankDetails.tsx b/src/components/AddMoney/components/AddMoneyBankDetails.tsx
index ab6141b0d6..97e5f39e71 100644
--- a/src/components/AddMoney/components/AddMoneyBankDetails.tsx
+++ b/src/components/AddMoney/components/AddMoneyBankDetails.tsx
@@ -450,6 +450,12 @@ export default function AddMoneyBankDetails(props: AddMoneyBankDetailsProps) {
shortDepositReference(onrampData?.depositInstructions?.depositMessage) ||
tCommon('loading'),
}),
+ // name mismatch is the top cause of returned deposits (Bridge BE01). the
+ // own-name rule holds for ACH/SEPA/wire; MX SPEI supports paying from a
+ // third-party/client account, so this item is omitted there.
+ ...(currentCountryDetails?.id !== 'MX'
+ ? [t('bankDetails.doubleCheckSenderName'), t('bankDetails.doubleCheckRecipientName')]
+ : []),
]}
/>
diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx
index f34a7fef3a..696ee67c4c 100644
--- a/src/components/AddMoney/components/MantecaAddMoney.tsx
+++ b/src/components/AddMoney/components/MantecaAddMoney.tsx
@@ -263,8 +263,8 @@ const MantecaAddMoney: FC = () => {
onVerify={async () => {
if (mantecaRejection.state === 'blocked') {
// blocked users cannot self-heal — route to support
- if (typeof window !== 'undefined' && (window as any).$crisp) {
- ;(window as any).$crisp.push(['do', 'chat:open'])
+ if (typeof window !== 'undefined' && window.$crisp) {
+ window.$crisp.push(['do', 'chat:open'])
}
setShowKycModal(false)
return
diff --git a/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx b/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx
index bd06d013f6..0a80826169 100644
--- a/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx
+++ b/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx
@@ -41,7 +41,7 @@ jest.mock('@/components/0_Bruddle/Button', () => ({
),
}))
-// eslint-disable-next-line import/first -- must come after jest.mock
+// must come after the jest.mock calls above
import MantecaPixQrDeposit from '../MantecaPixQrDeposit'
const PIX_CODE = '00020126-COPIA-E-COLA'
diff --git a/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx b/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx
index f6ff2041d2..4bbf456ca8 100644
--- a/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx
+++ b/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx
@@ -12,7 +12,7 @@ jest.mock('@/services/manteca', () => ({
mantecaApi: { getDepositStatus: mockGetDepositStatus },
}))
-// eslint-disable-next-line import/first -- must come after jest.mock
+// must come after the jest.mock calls above
import { useMantecaDepositPolling } from '../useMantecaDepositPolling'
describe('useMantecaDepositPolling', () => {
diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
index 836f5eec18..6f406085b9 100644
--- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
+++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
@@ -18,7 +18,7 @@ import { DynamicBankAccountForm, type IBankAccountDetails } from './DynamicBankA
import { addBankAccount } from '@/app/actions/users'
import { type AddBankAccountPayload } from '@/app/actions/types/users.types'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
-import { type Account } from '@/interfaces'
+import { type Account } from '@/interfaces/interfaces'
import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType'
import { useAppDispatch } from '@/redux/hooks'
@@ -29,7 +29,7 @@ import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
import { InitiateKycModal } from '@/components/Kyc/InitiateKycModal'
import { useCapabilities } from '@/hooks/useCapabilities'
-import { getKycModalVariant, getGateUserMessage } from '@/utils/capability-gate'
+import { resolveKycModalVariant, getGateUserMessage } from '@/utils/capability-gate'
import { railJurisdictionForBank } from '@/utils/bridge.utils'
import { getRegionIntent } from '@/utils/regions.utils'
import { useTosGuard } from '@/hooks/useTosGuard'
@@ -382,7 +382,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
}}
isLoading={sumsubFlow.isLoading}
error={sumsubFlow.error}
- variant={getKycModalVariant(gate.kind)}
+ variant={resolveKycModalVariant(gate)}
providerMessage={getGateUserMessage(gate)}
regionName={currentCountry?.title}
/>
diff --git a/src/components/AddWithdraw/DynamicBankAccountForm.tsx b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
index 53cd38777d..3bfd39ee35 100644
--- a/src/components/AddWithdraw/DynamicBankAccountForm.tsx
+++ b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
@@ -1,11 +1,11 @@
'use client'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
-import { useForm, Controller } from 'react-hook-form'
+import { useForm, Controller, type ControllerRenderProps, type FieldPath, type RegisterOptions } from 'react-hook-form'
import { useAuth } from '@/context/authContext'
import { Button } from '@/components/0_Bruddle/Button'
import { type AddBankAccountPayload, BridgeAccountOwnerType, BridgeAccountType } from '@/app/actions/types/users.types'
import BaseInput from '@/components/0_Bruddle/BaseInput'
-import BaseSelect from '@/components/0_Bruddle/BaseSelect'
+import BaseSelect, { type BaseSelectOption } from '@/components/0_Bruddle/BaseSelect'
import { BRIDGE_ALPHA3_TO_ALPHA2, ALL_COUNTRIES_ALPHA3_TO_ALPHA2 } from '@/components/AddMoney/consts'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import {
@@ -304,8 +304,8 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
dispatch(bankFormActions.setFormData(formDataToSave))
setIsSubmitting(false)
}
- } catch (error: any) {
- setSubmissionError(error.message)
+ } catch (error) {
+ setSubmissionError(error instanceof Error ? error.message : String(error))
} finally {
setIsSubmitting(false)
}
@@ -328,13 +328,13 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
}
}
- const renderInput = (
- name: keyof IBankAccountDetails,
+ const renderInput = >(
+ name: TName,
placeholder: string,
- rules: any,
+ rules: RegisterOptions,
type: string = 'text',
rightAdornment?: React.ReactNode,
- onBlur?: (field: any) => Promise | void,
+ onBlur?: (field: ControllerRenderProps) => Promise | void,
showCharCount?: boolean,
maxLength?: number
) => {
@@ -391,7 +391,12 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
)
}
- const renderSelect = (name: keyof IBankAccountDetails, placeholder: string, options: any[], rules: any) => (
+ const renderSelect = (
+ name: keyof IBankAccountDetails,
+ placeholder: string,
+ options: BaseSelectOption[],
+ rules: RegisterOptions
+ ) => (