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 && ( + + )} - )} +
), })) @@ -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