From 62c5bcc92bb21cb896742401a5f6b7eb6b00ddf2 Mon Sep 17 00:00:00 2001
From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 23:19:57 +0530
Subject: [PATCH 1/3] fix(add-money): choose deposit method once, not twice
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The add-money flow asked "Bank or Crypto" twice — once on a standalone
method screen, then again on the per-country screen after picking a
country. Customers reported this as buggy/confusing (TASK-20033).
Collapse the redundant first screen: /add-money now lands directly on the
country list (with a crypto shortcut + the offramp-migrate entry), and the
country-aware per-country screen remains the single place the method is
chosen. The per-country screen already knows each country's real methods
and coming-soon states, so no country regresses.
---
.../__tests__/add-money-states.test.tsx | 40 ++++------
src/app/(mobile-ui)/add-money/page.tsx | 64 ++++++++++-----
.../views/AddMoneyMethodSelection.view.tsx | 80 -------------------
src/components/Invites/campaign-maps.ts | 4 +-
4 files changed, 60 insertions(+), 128 deletions(-)
delete mode 100644 src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx
diff --git a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
index b0aeec54ac..4255cecb76 100644
--- a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
+++ b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
@@ -598,6 +598,11 @@ jest.mock('@/components/Common/CountryList', () => ({
CountryList: (props: any) => (
{props.inputTitle}
+ {props.onCryptoClick && (
+
props.onCryptoClick('add')}>
+ Crypto Deposit
+
+ )}
props.onCountryClick({ path: 'argentina', id: 'AR' })}
@@ -982,14 +987,18 @@ beforeEach(() => {
})
// ============================================================
-// GROUP 1: Landing / Method Selection
+// GROUP 1: Landing / Country + Crypto selection
// ============================================================
-describe('GROUP 1: Landing / Method Selection', () => {
- test('default view shows Crypto and Bank Transfer options', () => {
+// The standalone "Bank or Crypto" method screen was removed (TASK-20033): the
+// root now lands directly on the country list with a crypto shortcut, so the
+// method is chosen once — on the per-country screen — not twice.
+describe('GROUP 1: Landing / Country + Crypto selection', () => {
+ test('default view shows the country list with a crypto shortcut', () => {
renderWithProviders( )
- expect(screen.getByText('Crypto')).toBeInTheDocument()
- expect(screen.getByText('Bank Transfer')).toBeInTheDocument()
+ expect(screen.getByTestId('country-list')).toBeInTheDocument()
+ expect(screen.getByText('Select your country')).toBeInTheDocument()
+ expect(screen.getByTestId('action-card-crypto')).toBeInTheDocument()
expect(screen.getByText('Add Money')).toBeInTheDocument()
})
@@ -1011,33 +1020,14 @@ describe('GROUP 1: Landing / Method Selection', () => {
expect(mockRouterPush).toHaveBeenCalledWith('/add-money/crypto?network=EVM')
})
- test('clicking Bank Transfer switches to country list', () => {
- renderWithProviders( )
-
- fireEvent.click(screen.getByTestId('action-card-bank-transfer'))
-
- // The mock for nuqs useQueryState will be called via setMethod('bank')
- // and then the component should render the country list
- expect(mockSetQueryState).toHaveBeenCalled()
- })
-
- test('method=bank shows country list', () => {
- resetQueryState({ method: 'bank' })
- renderWithProviders( )
-
- expect(screen.getByTestId('country-list')).toBeInTheDocument()
- expect(screen.getByText('Select your country')).toBeInTheDocument()
- })
-
test('selecting a country from list navigates to country page', () => {
- resetQueryState({ method: 'bank' })
renderWithProviders( )
fireEvent.click(screen.getByTestId('country-argentina'))
expect(mockRouterPush).toHaveBeenCalledWith('/add-money/argentina')
})
- test('back from method selection navigates to /home', () => {
+ test('back from the country list navigates to /home', () => {
renderWithProviders( )
fireEvent.click(screen.getByTestId('nav-header'))
diff --git a/src/app/(mobile-ui)/add-money/page.tsx b/src/app/(mobile-ui)/add-money/page.tsx
index 495b65140b..89e945ace5 100644
--- a/src/app/(mobile-ui)/add-money/page.tsx
+++ b/src/app/(mobile-ui)/add-money/page.tsx
@@ -1,6 +1,5 @@
'use client'
-import AddMoneyMethodSelection from '@/components/AddMoney/views/AddMoneyMethodSelection.view'
import AddWithdrawCountriesList from '@/components/AddWithdraw/AddWithdrawCountriesList'
import dynamic from 'next/dynamic'
@@ -9,12 +8,19 @@ const OnrampBankPage = dynamic(() => import('./_onramp-bank'), { ssr: false })
const OnrampMantecaPage = dynamic(() => import('./_onramp-manteca'), { ssr: false })
import { CountryList } from '@/components/Common/CountryList'
import type { CountryData } from '@/components/AddMoney/consts'
+import { ActionListCard } from '@/components/ActionListCard'
+import AvatarWithBadge from '@/components/Profile/AvatarWithBadge'
+import ChooseNetworkDrawer from '@/components/AddMoney/components/ChooseNetworkDrawer'
+// offramp.xyz migrants get this link-granted badge at signup (peanut-api-ts
+// invite/badge routes, code `offramp` / utm `offramp`).
+import { OFFRAMP_BADGE_CODE } from '@/components/Invites/campaign-maps'
+import type { RhinoChainType } from '@/services/services.types'
import NavHeader from '@/components/Global/NavHeader'
+import { useAuth } from '@/context/authContext'
import { useOnrampFlow } from '@/context/OnrampFlowContext'
import { useRouter, useSearchParams } from 'next/navigation'
-import { useEffect } from 'react'
-import { useQueryState, parseAsStringEnum } from 'nuqs'
-import { checkIfInternalNavigation, getRedirectUrl, clearRedirectUrl, getFromLocalStorage } from '@/utils/general.utils'
+import { useEffect, useState } from 'react'
+import { getRedirectUrl, clearRedirectUrl, getFromLocalStorage } from '@/utils/general.utils'
import posthog from 'posthog-js'
import { ANALYTICS_EVENTS } from '@/constants/analytics.consts'
import { addMoneyCountryUrl } from '@/utils/native-routes'
@@ -23,25 +29,23 @@ export default function AddMoneyPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { resetOnrampFlow } = useOnrampFlow()
- const [method, setMethod] = useQueryState('method', parseAsStringEnum(['bank']))
+ const { user } = useAuth()
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false)
// native app passes country as query param instead of path segment
const countryFromQuery = searchParams.get('country')
+ // offramp migrants get a tailored arbitrum deposit entry above the country list
+ const hasOfframpBadge = user?.user?.badges?.some((b) => b.code === OFFRAMP_BADGE_CODE) ?? false
+
useEffect(() => {
if (!countryFromQuery) resetOnrampFlow()
}, [])
const handleBack = () => {
- // if viewing country-specific form, go back to country list
+ // native country sub-view → back to the country list root
if (countryFromQuery) {
- router.push('/add-money?method=bank')
- return
- }
-
- // if on country list view, go back to method selection
- if (method === 'bank') {
- setMethod(null)
+ router.push('/add-money')
return
}
@@ -71,6 +75,11 @@ export default function AddMoneyPage() {
router.push(addMoneyCountryUrl(country.path))
}
+ const handleNetworkSelect = (network: RhinoChainType) => {
+ setIsDrawerOpen(false)
+ router.push(`/add-money/crypto?network=${network}`)
+ }
+
// native app: render sub-views based on query params
const viewFromQuery = searchParams.get('view')
if (countryFromQuery && viewFromQuery === 'bank') {
@@ -88,16 +97,29 @@ export default function AddMoneyPage() {
- {method === 'bank' ? (
-
}
+ onClick={() => router.push('/add-money/crypto?network=EVM&source=offramp')}
/>
- ) : (
-
setMethod('bank')} />
)}
+
+ setIsDrawerOpen(true)}
+ />
+
+ setIsDrawerOpen(false)}
+ onSelect={handleNetworkSelect}
+ />
)
}
diff --git a/src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx b/src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx
deleted file mode 100644
index d1c663fd70..0000000000
--- a/src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client'
-
-import { ActionListCard } from '@/components/ActionListCard'
-import AvatarWithBadge from '@/components/Profile/AvatarWithBadge'
-import ChooseNetworkDrawer from '../components/ChooseNetworkDrawer'
-// offramp.xyz migrants get this link-granted badge at signup (peanut-api-ts
-// invite/badge routes, code `offramp` / utm `offramp`).
-import { OFFRAMP_BADGE_CODE } from '@/components/Invites/campaign-maps'
-import type { RhinoChainType } from '@/services/services.types'
-import { useAuth } from '@/context/authContext'
-import { useRouter } from 'next/navigation'
-import { useState } from 'react'
-
-interface AddMoneyMethodSelectionProps {
- onBankTransferClick: () => void
-}
-
-const AddMoneyMethodSelection = ({ onBankTransferClick }: AddMoneyMethodSelectionProps) => {
- const router = useRouter()
- const { user } = useAuth()
- const [isDrawerOpen, setIsDrawerOpen] = useState(false)
-
- // offramp migrants get a tailored, de-cluttered arbitrum deposit entry
- const hasOfframpBadge = user?.user?.badges?.some((b) => b.code === OFFRAMP_BADGE_CODE) ?? false
-
- const handleNetworkSelect = (network: RhinoChainType) => {
- setIsDrawerOpen(false)
- router.push(`/add-money/crypto?network=${network}`)
- }
-
- return (
- <>
-
-
How would you like to add money?
-
- {hasOfframpBadge && (
-
- }
- onClick={() => router.push('/add-money/crypto?network=EVM&source=offramp')}
- />
- )}
-
}
- onClick={() => setIsDrawerOpen(true)}
- />
-
- }
- onClick={onBankTransferClick}
- />
-
-
-
- setIsDrawerOpen(false)}
- onSelect={handleNetworkSelect}
- />
- >
- )
-}
-
-export default AddMoneyMethodSelection
diff --git a/src/components/Invites/campaign-maps.ts b/src/components/Invites/campaign-maps.ts
index 3faef6afc8..a89bc28125 100644
--- a/src/components/Invites/campaign-maps.ts
+++ b/src/components/Invites/campaign-maps.ts
@@ -5,8 +5,8 @@
// parallel-maps→single-record regression). campaign-maps.test.ts guards that.
// offramp.xyz → Peanut migration badge. Single FE source of truth for the code —
-// the add-money entry gate (AddMoneyMethodSelection) and both maps below key on
-// it. Mirrors peanut-api-ts BADGE_CODES.OFFRAMP_USER.
+// the add-money entry (AddMoneyPage) and both maps below key on it.
+// Mirrors peanut-api-ts BADGE_CODES.OFFRAMP_USER.
export const OFFRAMP_BADGE_CODE = 'OFFRAMP_USER'
// mapping of special invite codes to their campaign tags
From 7fdf0c36efbe5622fbe4dbfd9891d1660d31beb4 Mon Sep 17 00:00:00 2001
From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 23:40:00 +0530
Subject: [PATCH 2/3] =?UTF-8?q?fix(add-money):=20address=20review=20?=
=?UTF-8?q?=E2=80=94=20effect=20deps,=20dead=20branch,=20analytics=20parit?=
=?UTF-8?q?y?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- useEffect now depends on countryFromQuery + (stable) resetOnrampFlow, so
stale onramp state is cleared on native back-nav to the root, not only on
mount (CodeRabbit + code-review). Also clears the exhaustive-deps warning.
- drop the unreachable countryFromQuery branch in handleBack (the ?country
sub-views early-return their own NavHeader).
- fire DEPOSIT_METHOD_SELECTED{method_type:'crypto'} on network select so
root crypto deposits match the bank event in the deposit-method funnel.
- AddWithdrawCountriesList add-flow back-nav → '/add-money' (drop the now-dead
?method=bank param).
- cover the offramp-badge landing card (present with badge, absent without).
---
.../__tests__/add-money-states.test.tsx | 19 +++++++++++++++++++
src/app/(mobile-ui)/add-money/page.tsx | 19 +++++++++++++------
.../AddWithdraw/AddWithdrawCountriesList.tsx | 4 +++-
3 files changed, 35 insertions(+), 7 deletions(-)
diff --git a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
index 4255cecb76..733d061559 100644
--- a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
+++ b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
@@ -1033,6 +1033,25 @@ describe('GROUP 1: Landing / Country + Crypto selection', () => {
fireEvent.click(screen.getByTestId('nav-header'))
expect(mockRouterPush).toHaveBeenCalledWith('/home')
})
+
+ test('offramp-badge users see the Migrate from Offramp card', () => {
+ mockUseAuth.mockReturnValue({
+ user: { user: { username: 'test-user', userId: 'user-123', badges: [{ code: 'OFFRAMP_USER' }] } },
+ isFetchingUser: false,
+ fetchUser: jest.fn(),
+ })
+ renderWithProviders( )
+
+ const card = screen.getByTestId('action-card-migrate-from-offramp')
+ expect(card).toBeInTheDocument()
+ fireEvent.click(card)
+ expect(mockRouterPush).toHaveBeenCalledWith('/add-money/crypto?network=EVM&source=offramp')
+ })
+
+ test('users without the offramp badge do not see the Migrate card', () => {
+ renderWithProviders( )
+ expect(screen.queryByTestId('action-card-migrate-from-offramp')).not.toBeInTheDocument()
+ })
})
// ============================================================
diff --git a/src/app/(mobile-ui)/add-money/page.tsx b/src/app/(mobile-ui)/add-money/page.tsx
index 89e945ace5..26378dad82 100644
--- a/src/app/(mobile-ui)/add-money/page.tsx
+++ b/src/app/(mobile-ui)/add-money/page.tsx
@@ -38,16 +38,17 @@ export default function AddMoneyPage() {
// offramp migrants get a tailored arbitrum deposit entry above the country list
const hasOfframpBadge = user?.user?.badges?.some((b) => b.code === OFFRAMP_BADGE_CODE) ?? false
+ // clear stale onramp state whenever we land on the root country list (no
+ // country in the URL) — reruns on back-nav from a ?country=… sub-view, not
+ // just on mount. resetOnrampFlow is a stable useCallback.
useEffect(() => {
if (!countryFromQuery) resetOnrampFlow()
- }, [])
+ }, [countryFromQuery, resetOnrampFlow])
const handleBack = () => {
- // native country sub-view → back to the country list root
- if (countryFromQuery) {
- router.push('/add-money')
- return
- }
+ // note: this only runs from the root list's NavHeader — the ?country=…
+ // sub-views early-return their own NavHeader below, so countryFromQuery
+ // is always absent here.
// check if we have a saved redirect url (from request fulfillment or similar flows)
const redirectUrl = getRedirectUrl()
@@ -77,6 +78,12 @@ export default function AddMoneyPage() {
const handleNetworkSelect = (network: RhinoChainType) => {
setIsDrawerOpen(false)
+ // mirror the bank event in handleCountryClick so crypto deposits from the
+ // root aren't undercounted in the deposit-method funnel.
+ posthog.capture(ANALYTICS_EVENTS.DEPOSIT_METHOD_SELECTED, {
+ method_type: 'crypto',
+ country: 'crypto',
+ })
router.push(`/add-money/crypto?network=${network}`)
}
diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
index b33e55bfc0..674c0fa9af 100644
--- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
+++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
@@ -532,7 +532,9 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
onPrev={() => {
setAmountToWithdraw('')
if (flow === 'add') {
- router.push('/add-money?method=bank')
+ // root add-money is the country list now (the ?method=bank
+ // method screen was removed) — no stale param.
+ router.push('/add-money')
} else if (isBankFromSend) {
// if coming from bank send flow: set method and go to amount input view
setSelectedMethod({
From 3e0519f6b28602cefeddbf82e13bd3b751a6aa2d Mon Sep 17 00:00:00 2001
From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com>
Date: Thu, 23 Jul 2026 12:46:16 +0530
Subject: [PATCH 3/3] fix(add-money): keep the method screen, skip the
redundant second one
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Switch from Option A (which deleted the Bank/Crypto method screen and left an
ambiguous country list — users couldn't tell bank was an option) to Option B:
- Keep the up-front Bank/Crypto method screen (bank is clearly visible).
- After the user picks Bank + a country, go STRAIGHT to that country's deposit
screen instead of showing a second "From Bank / From Crypto" list:
- AR/BR -> /add-money//manteca (Manteca surfaces Pix/Mercado Pago)
- other bank-supported countries -> /add-money//bank
Every add method for a given country converges on one of these two screens
anyway, so the per-country list was pure repetition where bank is live.
- Coming-soon countries (bank not enabled) keep the per-country screen — there
it's still useful: it shows the "soon" bank state and the crypto fallback.
Net effect: the user clicks "bank" once, up front; the duplicate second
selection is gone. This reverts the Option A page/view/test churn — the diff is
now just handleCountryClick routing (+ effect-deps cleanup) and its tests.
---
.../__tests__/add-money-states.test.tsx | 75 +++++++++------
src/app/(mobile-ui)/add-money/page.tsx | 95 ++++++++-----------
.../views/AddMoneyMethodSelection.view.tsx | 80 ++++++++++++++++
.../AddWithdraw/AddWithdrawCountriesList.tsx | 4 +-
src/components/Invites/campaign-maps.ts | 4 +-
5 files changed, 169 insertions(+), 89 deletions(-)
create mode 100644 src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx
diff --git a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
index 733d061559..07140bf537 100644
--- a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
+++ b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
@@ -598,11 +598,6 @@ jest.mock('@/components/Common/CountryList', () => ({
CountryList: (props: any) => (
{props.inputTitle}
- {props.onCryptoClick && (
- props.onCryptoClick('add')}>
- Crypto Deposit
-
- )}
props.onCountryClick({ path: 'argentina', id: 'AR' })}
@@ -612,6 +607,9 @@ jest.mock('@/components/Common/CountryList', () => ({
props.onCountryClick({ path: 'germany', id: 'DE' })}>
Germany
+ props.onCountryClick({ path: 'chad', id: 'TD' })}>
+ Chad
+
),
}))
@@ -987,18 +985,14 @@ beforeEach(() => {
})
// ============================================================
-// GROUP 1: Landing / Country + Crypto selection
+// GROUP 1: Landing / Method Selection
// ============================================================
-// The standalone "Bank or Crypto" method screen was removed (TASK-20033): the
-// root now lands directly on the country list with a crypto shortcut, so the
-// method is chosen once — on the per-country screen — not twice.
-describe('GROUP 1: Landing / Country + Crypto selection', () => {
- test('default view shows the country list with a crypto shortcut', () => {
+describe('GROUP 1: Landing / Method Selection', () => {
+ test('default view shows Crypto and Bank Transfer options', () => {
renderWithProviders( )
- expect(screen.getByTestId('country-list')).toBeInTheDocument()
- expect(screen.getByText('Select your country')).toBeInTheDocument()
- expect(screen.getByTestId('action-card-crypto')).toBeInTheDocument()
+ expect(screen.getByText('Crypto')).toBeInTheDocument()
+ expect(screen.getByText('Bank Transfer')).toBeInTheDocument()
expect(screen.getByText('Add Money')).toBeInTheDocument()
})
@@ -1020,37 +1014,56 @@ describe('GROUP 1: Landing / Country + Crypto selection', () => {
expect(mockRouterPush).toHaveBeenCalledWith('/add-money/crypto?network=EVM')
})
- test('selecting a country from list navigates to country page', () => {
+ test('clicking Bank Transfer switches to country list', () => {
+ renderWithProviders( )
+
+ fireEvent.click(screen.getByTestId('action-card-bank-transfer'))
+
+ // The mock for nuqs useQueryState will be called via setMethod('bank')
+ // and then the component should render the country list
+ expect(mockSetQueryState).toHaveBeenCalled()
+ })
+
+ test('method=bank shows country list', () => {
+ resetQueryState({ method: 'bank' })
+ renderWithProviders( )
+
+ expect(screen.getByTestId('country-list')).toBeInTheDocument()
+ expect(screen.getByText('Select your country')).toBeInTheDocument()
+ })
+
+ // TASK-20033: picking a bank-supported country skips the redundant per-country
+ // method list and goes straight to the deposit screen (Manteca for AR/BR,
+ // Bridge bank otherwise). Coming-soon countries keep the per-country screen.
+ test('selecting a Manteca country (AR/BR) goes straight to the manteca deposit', () => {
+ resetQueryState({ method: 'bank' })
renderWithProviders( )
fireEvent.click(screen.getByTestId('country-argentina'))
- expect(mockRouterPush).toHaveBeenCalledWith('/add-money/argentina')
+ expect(mockRouterPush).toHaveBeenCalledWith('/add-money/argentina/manteca')
})
- test('back from the country list navigates to /home', () => {
+ test('selecting a Bridge-supported country goes straight to the bank deposit', () => {
+ resetQueryState({ method: 'bank' })
renderWithProviders( )
- fireEvent.click(screen.getByTestId('nav-header'))
- expect(mockRouterPush).toHaveBeenCalledWith('/home')
+ fireEvent.click(screen.getByTestId('country-germany'))
+ expect(mockRouterPush).toHaveBeenCalledWith('/add-money/germany/bank')
})
- test('offramp-badge users see the Migrate from Offramp card', () => {
- mockUseAuth.mockReturnValue({
- user: { user: { username: 'test-user', userId: 'user-123', badges: [{ code: 'OFFRAMP_USER' }] } },
- isFetchingUser: false,
- fetchUser: jest.fn(),
- })
+ test('selecting a coming-soon country keeps the per-country method screen', () => {
+ resetQueryState({ method: 'bank' })
renderWithProviders( )
- const card = screen.getByTestId('action-card-migrate-from-offramp')
- expect(card).toBeInTheDocument()
- fireEvent.click(card)
- expect(mockRouterPush).toHaveBeenCalledWith('/add-money/crypto?network=EVM&source=offramp')
+ fireEvent.click(screen.getByTestId('country-chad'))
+ expect(mockRouterPush).toHaveBeenCalledWith('/add-money/chad')
})
- test('users without the offramp badge do not see the Migrate card', () => {
+ test('back from method selection navigates to /home', () => {
renderWithProviders( )
- expect(screen.queryByTestId('action-card-migrate-from-offramp')).not.toBeInTheDocument()
+
+ fireEvent.click(screen.getByTestId('nav-header'))
+ expect(mockRouterPush).toHaveBeenCalledWith('/home')
})
})
diff --git a/src/app/(mobile-ui)/add-money/page.tsx b/src/app/(mobile-ui)/add-money/page.tsx
index 26378dad82..d2ecdf0a6a 100644
--- a/src/app/(mobile-ui)/add-money/page.tsx
+++ b/src/app/(mobile-ui)/add-money/page.tsx
@@ -1,5 +1,6 @@
'use client'
+import AddMoneyMethodSelection from '@/components/AddMoney/views/AddMoneyMethodSelection.view'
import AddWithdrawCountriesList from '@/components/AddWithdraw/AddWithdrawCountriesList'
import dynamic from 'next/dynamic'
@@ -8,47 +9,46 @@ const OnrampBankPage = dynamic(() => import('./_onramp-bank'), { ssr: false })
const OnrampMantecaPage = dynamic(() => import('./_onramp-manteca'), { ssr: false })
import { CountryList } from '@/components/Common/CountryList'
import type { CountryData } from '@/components/AddMoney/consts'
-import { ActionListCard } from '@/components/ActionListCard'
-import AvatarWithBadge from '@/components/Profile/AvatarWithBadge'
-import ChooseNetworkDrawer from '@/components/AddMoney/components/ChooseNetworkDrawer'
-// offramp.xyz migrants get this link-granted badge at signup (peanut-api-ts
-// invite/badge routes, code `offramp` / utm `offramp`).
-import { OFFRAMP_BADGE_CODE } from '@/components/Invites/campaign-maps'
-import type { RhinoChainType } from '@/services/services.types'
import NavHeader from '@/components/Global/NavHeader'
-import { useAuth } from '@/context/authContext'
import { useOnrampFlow } from '@/context/OnrampFlowContext'
import { useRouter, useSearchParams } from 'next/navigation'
-import { useEffect, useState } from 'react'
+import { useEffect } from 'react'
+import { useQueryState, parseAsStringEnum } from 'nuqs'
import { getRedirectUrl, clearRedirectUrl, getFromLocalStorage } from '@/utils/general.utils'
+import { isBridgeSupportedCountry } from '@/utils/regions.utils'
+import { isMantecaSupportedCountryCode } from '@/constants/manteca.consts'
import posthog from 'posthog-js'
import { ANALYTICS_EVENTS } from '@/constants/analytics.consts'
-import { addMoneyCountryUrl } from '@/utils/native-routes'
+import { addMoneyCountryUrl, rewriteMethodPath } from '@/utils/native-routes'
export default function AddMoneyPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { resetOnrampFlow } = useOnrampFlow()
- const { user } = useAuth()
- const [isDrawerOpen, setIsDrawerOpen] = useState(false)
+ const [method, setMethod] = useQueryState('method', parseAsStringEnum(['bank']))
// native app passes country as query param instead of path segment
const countryFromQuery = searchParams.get('country')
- // offramp migrants get a tailored arbitrum deposit entry above the country list
- const hasOfframpBadge = user?.user?.badges?.some((b) => b.code === OFFRAMP_BADGE_CODE) ?? false
-
- // clear stale onramp state whenever we land on the root country list (no
- // country in the URL) — reruns on back-nav from a ?country=… sub-view, not
- // just on mount. resetOnrampFlow is a stable useCallback.
+ // clear stale onramp state on the root list (no country in the URL); reruns
+ // on back-nav from a ?country=… sub-view, not just on mount. resetOnrampFlow
+ // is a stable useCallback.
useEffect(() => {
if (!countryFromQuery) resetOnrampFlow()
}, [countryFromQuery, resetOnrampFlow])
const handleBack = () => {
- // note: this only runs from the root list's NavHeader — the ?country=…
- // sub-views early-return their own NavHeader below, so countryFromQuery
- // is always absent here.
+ // if viewing country-specific form, go back to country list
+ if (countryFromQuery) {
+ router.push('/add-money?method=bank')
+ return
+ }
+
+ // if on country list view, go back to method selection
+ if (method === 'bank') {
+ setMethod(null)
+ return
+ }
// check if we have a saved redirect url (from request fulfillment or similar flows)
const redirectUrl = getRedirectUrl()
@@ -73,18 +73,20 @@ export default function AddMoneyPage() {
method_type: 'bank',
country: country.path,
})
- router.push(addMoneyCountryUrl(country.path))
- }
- const handleNetworkSelect = (network: RhinoChainType) => {
- setIsDrawerOpen(false)
- // mirror the bank event in handleCountryClick so crypto deposits from the
- // root aren't undercounted in the deposit-method funnel.
- posthog.capture(ANALYTICS_EVENTS.DEPOSIT_METHOD_SELECTED, {
- method_type: 'crypto',
- country: 'crypto',
- })
- router.push(`/add-money/crypto?network=${network}`)
+ // The user already chose "Bank" — skip the redundant per-country method
+ // list and go straight to the deposit screen. AR/BR deposit via Manteca
+ // (which surfaces Pix / Mercado Pago itself); every other bank-supported
+ // country goes to the Bridge bank flow. Countries where bank isn't live
+ // yet keep the per-country screen, which is still useful there: it shows
+ // the "coming soon" bank state and the crypto fallback.
+ if (isMantecaSupportedCountryCode(country.id)) {
+ router.push(rewriteMethodPath(`/add-money/${country.path}/manteca`))
+ } else if (isBridgeSupportedCountry(country.id)) {
+ router.push(rewriteMethodPath(`/add-money/${country.path}/bank`))
+ } else {
+ router.push(addMoneyCountryUrl(country.path))
+ }
}
// native app: render sub-views based on query params
@@ -104,29 +106,16 @@ export default function AddMoneyPage() {
- {hasOfframpBadge && (
-
}
- onClick={() => router.push('/add-money/crypto?network=EVM&source=offramp')}
+ {method === 'bank' ? (
+
+ ) : (
+
setMethod('bank')} />
)}
-
- setIsDrawerOpen(true)}
- />
-
- setIsDrawerOpen(false)}
- onSelect={handleNetworkSelect}
- />
)
}
diff --git a/src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx b/src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx
new file mode 100644
index 0000000000..d1c663fd70
--- /dev/null
+++ b/src/components/AddMoney/views/AddMoneyMethodSelection.view.tsx
@@ -0,0 +1,80 @@
+'use client'
+
+import { ActionListCard } from '@/components/ActionListCard'
+import AvatarWithBadge from '@/components/Profile/AvatarWithBadge'
+import ChooseNetworkDrawer from '../components/ChooseNetworkDrawer'
+// offramp.xyz migrants get this link-granted badge at signup (peanut-api-ts
+// invite/badge routes, code `offramp` / utm `offramp`).
+import { OFFRAMP_BADGE_CODE } from '@/components/Invites/campaign-maps'
+import type { RhinoChainType } from '@/services/services.types'
+import { useAuth } from '@/context/authContext'
+import { useRouter } from 'next/navigation'
+import { useState } from 'react'
+
+interface AddMoneyMethodSelectionProps {
+ onBankTransferClick: () => void
+}
+
+const AddMoneyMethodSelection = ({ onBankTransferClick }: AddMoneyMethodSelectionProps) => {
+ const router = useRouter()
+ const { user } = useAuth()
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false)
+
+ // offramp migrants get a tailored, de-cluttered arbitrum deposit entry
+ const hasOfframpBadge = user?.user?.badges?.some((b) => b.code === OFFRAMP_BADGE_CODE) ?? false
+
+ const handleNetworkSelect = (network: RhinoChainType) => {
+ setIsDrawerOpen(false)
+ router.push(`/add-money/crypto?network=${network}`)
+ }
+
+ return (
+ <>
+
+
How would you like to add money?
+
+ {hasOfframpBadge && (
+
+ }
+ onClick={() => router.push('/add-money/crypto?network=EVM&source=offramp')}
+ />
+ )}
+
}
+ onClick={() => setIsDrawerOpen(true)}
+ />
+
+ }
+ onClick={onBankTransferClick}
+ />
+
+
+
+ setIsDrawerOpen(false)}
+ onSelect={handleNetworkSelect}
+ />
+ >
+ )
+}
+
+export default AddMoneyMethodSelection
diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
index 674c0fa9af..b33e55bfc0 100644
--- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
+++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
@@ -532,9 +532,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
onPrev={() => {
setAmountToWithdraw('')
if (flow === 'add') {
- // root add-money is the country list now (the ?method=bank
- // method screen was removed) — no stale param.
- router.push('/add-money')
+ router.push('/add-money?method=bank')
} else if (isBankFromSend) {
// if coming from bank send flow: set method and go to amount input view
setSelectedMethod({
diff --git a/src/components/Invites/campaign-maps.ts b/src/components/Invites/campaign-maps.ts
index a89bc28125..3faef6afc8 100644
--- a/src/components/Invites/campaign-maps.ts
+++ b/src/components/Invites/campaign-maps.ts
@@ -5,8 +5,8 @@
// parallel-maps→single-record regression). campaign-maps.test.ts guards that.
// offramp.xyz → Peanut migration badge. Single FE source of truth for the code —
-// the add-money entry (AddMoneyPage) and both maps below key on it.
-// Mirrors peanut-api-ts BADGE_CODES.OFFRAMP_USER.
+// the add-money entry gate (AddMoneyMethodSelection) and both maps below key on
+// it. Mirrors peanut-api-ts BADGE_CODES.OFFRAMP_USER.
export const OFFRAMP_BADGE_CODE = 'OFFRAMP_USER'
// mapping of special invite codes to their campaign tags