From 8ceba1c26efdfb52b44082f511e754164fbbf00b Mon Sep 17 00:00:00 2001 From: Amitabh Aggarwal Date: Fri, 8 May 2026 10:11:31 -0500 Subject: [PATCH 1/2] feat(ramp): add navigateToRampBuy helper and refactor useRampNavigation Co-authored-by: Cursor --- .../UI/Ramp/hooks/useRampNavigation.ts | 137 +---- app/components/UI/Ramp/navigation.ts | 13 + .../UI/Ramp/utils/navigateToRampBuy.test.ts | 478 ++++++++++++++++++ .../UI/Ramp/utils/navigateToRampBuy.ts | 162 ++++++ 4 files changed, 672 insertions(+), 118 deletions(-) create mode 100644 app/components/UI/Ramp/navigation.ts create mode 100644 app/components/UI/Ramp/utils/navigateToRampBuy.test.ts create mode 100644 app/components/UI/Ramp/utils/navigateToRampBuy.ts diff --git a/app/components/UI/Ramp/hooks/useRampNavigation.ts b/app/components/UI/Ramp/hooks/useRampNavigation.ts index e8386e716d7..87d92adb0c5 100644 --- a/app/components/UI/Ramp/hooks/useRampNavigation.ts +++ b/app/components/UI/Ramp/hooks/useRampNavigation.ts @@ -7,24 +7,15 @@ import { } from '../Aggregator/types'; import { createRampNavigationDetails } from '../Aggregator/routes/utils'; import { createDepositNavigationDetails } from '../Deposit/routes/utils'; -import { createTokenSelectionNavDetails } from '../Views/TokenSelection/TokenSelection'; -import { createBuildQuoteNavDetails } from '../Views/BuildQuote'; -import type { BuyFlowOrigin } from '../Views/BuildQuote/BuildQuote'; -import useRampsUnifiedV1Enabled from './useRampsUnifiedV1Enabled'; -import useRampsUnifiedV2Enabled from './useRampsUnifiedV2Enabled'; import { - getRampRoutingDecision, - UnifiedRampRoutingType, -} from '../../../../reducers/fiatOrders'; -import { createRampUnsupportedModalNavigationDetails } from '../components/RampUnsupportedModal/RampUnsupportedModal'; -import { createEligibilityFailedModalNavigationDetails } from '../components/EligibilityFailedModal/EligibilityFailedModal'; + navigateToRampBuy, + NavigateToRampBuyMode, + type NavigateToRampBuyOptions, +} from '../utils/navigateToRampBuy'; +import { getRampRoutingDecision } from '../../../../reducers/fiatOrders'; import { useRampsTokens } from './useRampsTokens'; -import { resolveRampControllerAssetId } from '../utils/resolveRampControllerAssetId'; - -enum RampMode { - AGGREGATOR = 'AGGREGATOR', - DEPOSIT = 'DEPOSIT', -} +import useRampsUnifiedV1Enabled from './useRampsUnifiedV1Enabled'; +import useRampsUnifiedV2Enabled from './useRampsUnifiedV2Enabled'; /** * Hook that returns functions to navigate to ramp flows. @@ -43,106 +34,14 @@ export const useRampNavigation = () => { const { setSelectedToken, tokens: rampsTokens } = useRampsTokens(); const goToBuy = useCallback( - ( - intent?: RampIntent, - options?: { - mode?: RampMode; - overrideUnifiedRouting?: boolean; - buyFlowOrigin?: BuyFlowOrigin; - }, - ) => { - const { mode = RampMode.AGGREGATOR, overrideUnifiedRouting = false } = - options || {}; - - const isUnifiedRoutingEnabled = - (isRampsUnifiedV1Enabled || isRampsUnifiedV2Enabled) && - !overrideUnifiedRouting; - - // Check error states first (applies to both V1 and V2) - if (isUnifiedRoutingEnabled) { - if (rampRoutingDecision === UnifiedRampRoutingType.ERROR) { - navigation.navigate( - ...createEligibilityFailedModalNavigationDetails(), - ); - return; - } - - if (rampRoutingDecision === UnifiedRampRoutingType.UNSUPPORTED) { - navigation.navigate(...createRampUnsupportedModalNavigationDetails()); - return; - } - } - - // V2: If assetId is provided and V2 is enabled, route to BuildQuote - // TODO: Check for provider support for the token and pass params to BuildQuote to show an error modal - if ( - isRampsUnifiedV2Enabled && - intent?.assetId && - !overrideUnifiedRouting - ) { - // Resolve to the controller's canonical assetId format (lowercase) - const controllerAssetId = resolveRampControllerAssetId( - intent.assetId, - rampsTokens?.allTokens ?? [], - ); - try { - setSelectedToken(controllerAssetId); - } catch { - // Token may not be in controller's list yet (still loading). - // Navigate anyway — BuildQuote will handle the missing token. - } - navigation.navigate( - ...createBuildQuoteNavDetails({ - assetId: controllerAssetId, - buyFlowOrigin: options?.buyFlowOrigin, - }), - ); - return; - } - - // V2: If no assetId and V2 is enabled, route to TokenSelection (matches handleRampUrl deeplink behavior) - if ( - isRampsUnifiedV2Enabled && - !intent?.assetId && - !overrideUnifiedRouting - ) { - navigation.navigate(...createTokenSelectionNavDetails()); - return; - } - - // V1 routing logic - if (isRampsUnifiedV1Enabled && !overrideUnifiedRouting) { - // If no assetId is provided, route to TokenSelection - if (!intent?.assetId) { - navigation.navigate(...createTokenSelectionNavDetails()); - return; - } - - // If routing decision hasn't been determined yet, route to TokenSelection - if (rampRoutingDecision === null) { - navigation.navigate(...createTokenSelectionNavDetails()); - return; - } - - // If assetId is provided, route based on rampRoutingDecision - if (rampRoutingDecision === UnifiedRampRoutingType.DEPOSIT) { - navigation.navigate(...createDepositNavigationDetails(intent)); - } else if (rampRoutingDecision === UnifiedRampRoutingType.AGGREGATOR) { - navigation.navigate( - ...createRampNavigationDetails(AggregatorRampType.BUY, intent), - ); - } - return; - } - - // When overriding unified routing or when v1 is disabled - if (mode === RampMode.DEPOSIT) { - navigation.navigate(...createDepositNavigationDetails(intent)); - } else { - navigation.navigate( - ...createRampNavigationDetails(AggregatorRampType.BUY, intent), - ); - } + (intent?: RampIntent, options?: NavigateToRampBuyOptions) => { + navigateToRampBuy(navigation, intent, options, { + isRampsUnifiedV1Enabled, + isRampsUnifiedV2Enabled, + rampRoutingDecision, + rampsTokensAll: rampsTokens?.allTokens ?? [], + setSelectedToken, + }); }, [ setSelectedToken, @@ -161,7 +60,7 @@ export const useRampNavigation = () => { const goToAggregator = useCallback( (intent?: RampIntent) => { goToBuy(intent, { - mode: RampMode.AGGREGATOR, + mode: NavigateToRampBuyMode.AGGREGATOR, overrideUnifiedRouting: true, }); }, @@ -184,12 +83,14 @@ export const useRampNavigation = () => { const goToDeposit = useCallback( (intent?: RampIntent) => { goToBuy(intent, { - mode: RampMode.DEPOSIT, + mode: NavigateToRampBuyMode.DEPOSIT, overrideUnifiedRouting: true, }); }, [goToBuy], ); + // Deprecated entries remain part of the public hook API for existing callers. + // eslint-disable-next-line @typescript-eslint/no-deprecated -- backward-compatible exports return { goToBuy, goToAggregator, goToSell, goToDeposit }; }; diff --git a/app/components/UI/Ramp/navigation.ts b/app/components/UI/Ramp/navigation.ts new file mode 100644 index 00000000000..a26ea79db16 --- /dev/null +++ b/app/components/UI/Ramp/navigation.ts @@ -0,0 +1,13 @@ +/** + * Stable, shallow imports for cross-team ramp **buy** navigation. + * Prefer importing from this module rather than deep Aggregator/Deposit paths. + */ +export { navigateToRampBuy } from './utils/navigateToRampBuy'; +export type { + NavigateToRampBuyNavigation, + NavigateToRampBuyDeps, + NavigateToRampBuyOptions, +} from './utils/navigateToRampBuy'; +export { NavigateToRampBuyMode } from './utils/navigateToRampBuy'; +export type { RampIntent } from './types'; +export type { BuyFlowOrigin } from './Views/BuildQuote/BuildQuote'; diff --git a/app/components/UI/Ramp/utils/navigateToRampBuy.test.ts b/app/components/UI/Ramp/utils/navigateToRampBuy.test.ts new file mode 100644 index 00000000000..a37b053fcab --- /dev/null +++ b/app/components/UI/Ramp/utils/navigateToRampBuy.test.ts @@ -0,0 +1,478 @@ +import Routes from '../../../../constants/navigation/Routes'; +import { navigateToRampBuy, NavigateToRampBuyMode } from './navigateToRampBuy'; +import { createRampNavigationDetails } from '../Aggregator/routes/utils'; +import { createDepositNavigationDetails } from '../Deposit/routes/utils'; +import { createTokenSelectionNavDetails } from '../Views/TokenSelection/TokenSelection'; +import { createBuildQuoteNavDetails } from '../Views/BuildQuote'; +import { RampType as AggregatorRampType } from '../Aggregator/types'; +import { UnifiedRampRoutingType } from '../../../../reducers/fiatOrders'; +import { createEligibilityFailedModalNavigationDetails } from '../components/EligibilityFailedModal/EligibilityFailedModal'; +import { createRampUnsupportedModalNavigationDetails } from '../components/RampUnsupportedModal/RampUnsupportedModal'; + +jest.mock('../Aggregator/routes/utils'); +jest.mock('../Deposit/routes/utils'); +jest.mock('../Views/TokenSelection/TokenSelection', () => { + const actual = jest.requireActual('../Views/TokenSelection/TokenSelection'); + const mockFn = jest.fn(); + return { + ...actual, + createTokenSelectionNavDetails: mockFn, + createTokenSelectionNavigationDetails: mockFn, + }; +}); +jest.mock('../Views/BuildQuote', () => { + const mockFn = jest.fn(); + return { + createBuildQuoteNavDetails: mockFn, + }; +}); + +const mockNavigate = jest.fn(); +const mockSetSelectedToken = jest.fn(); + +const mockCreateRampNavigationDetails = + createRampNavigationDetails as jest.MockedFunction< + typeof createRampNavigationDetails + >; +const mockCreateDepositNavigationDetails = + createDepositNavigationDetails as jest.MockedFunction< + typeof createDepositNavigationDetails + >; +const mockCreateTokenSelectionNavigationDetails = + createTokenSelectionNavDetails as jest.MockedFunction< + typeof createTokenSelectionNavDetails + >; +const mockCreateBuildQuoteNavDetails = + createBuildQuoteNavDetails as jest.MockedFunction< + typeof createBuildQuoteNavDetails + >; + +function navigate( + intent: Parameters[1], + options: Parameters[2], + deps: Partial[3]> & { + isRampsUnifiedV1Enabled: boolean; + isRampsUnifiedV2Enabled: boolean; + rampRoutingDecision: Parameters< + typeof navigateToRampBuy + >[3]['rampRoutingDecision']; + }, +) { + navigateToRampBuy({ navigate: mockNavigate }, intent, options, { + rampsTokensAll: [], + setSelectedToken: mockSetSelectedToken, + ...deps, + }); +} + +describe('navigateToRampBuy', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockSetSelectedToken.mockClear(); + + mockCreateRampNavigationDetails.mockReturnValue([ + Routes.RAMP.BUY, + ] as unknown as ReturnType); + + mockCreateDepositNavigationDetails.mockReturnValue([ + Routes.DEPOSIT.ID, + ] as unknown as ReturnType); + + mockCreateTokenSelectionNavigationDetails.mockReturnValue([ + Routes.RAMP.TOKEN_SELECTION, + ] as unknown as ReturnType); + + mockCreateBuildQuoteNavDetails.mockReturnValue([ + Routes.RAMP.TOKEN_SELECTION, + { + screen: Routes.RAMP.TOKEN_SELECTION, + params: { + screen: Routes.RAMP.AMOUNT_INPUT, + params: { assetId: 'eip155:1/erc20:0x123' }, + }, + }, + ] as unknown as ReturnType); + }); + + describe('when unified V2 is enabled', () => { + const v2Deps = { + isRampsUnifiedV1Enabled: false, + isRampsUnifiedV2Enabled: true, + rampRoutingDecision: null, + }; + + it('navigates to BuildQuote when assetId is provided', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const mockNavDetails = [ + Routes.RAMP.TOKEN_SELECTION, + { + screen: Routes.RAMP.TOKEN_SELECTION, + params: { + screen: Routes.RAMP.AMOUNT_INPUT, + params: { assetId: intent.assetId }, + }, + }, + ] as const; + mockCreateBuildQuoteNavDetails.mockReturnValue(mockNavDetails); + + navigate(intent, undefined, v2Deps); + + expect(mockSetSelectedToken).toHaveBeenCalledWith(intent.assetId); + expect(mockCreateBuildQuoteNavDetails).toHaveBeenCalledWith({ + assetId: intent.assetId, + buyFlowOrigin: undefined, + }); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + }); + + it('passes buyFlowOrigin through to BuildQuote params', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + + navigate(intent, { buyFlowOrigin: 'tokenInfo' }, v2Deps); + + expect(mockCreateBuildQuoteNavDetails).toHaveBeenCalledWith({ + assetId: intent.assetId, + buyFlowOrigin: 'tokenInfo', + }); + }); + + it('passes homeTokenList buyFlowOrigin through to BuildQuote params', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + + navigate(intent, { buyFlowOrigin: 'homeTokenList' }, v2Deps); + + expect(mockCreateBuildQuoteNavDetails).toHaveBeenCalledWith({ + assetId: intent.assetId, + buyFlowOrigin: 'homeTokenList', + }); + }); + + it('navigates to TokenSelection when no assetId and V1 is disabled', () => { + const mockNavDetails = [Routes.RAMP.TOKEN_SELECTION, undefined] as const; + mockCreateTokenSelectionNavigationDetails.mockReturnValue(mockNavDetails); + + navigate(undefined, undefined, v2Deps); + + expect(mockSetSelectedToken).not.toHaveBeenCalled(); + expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); + expect(mockCreateTokenSelectionNavigationDetails).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + }); + + it('navigates to TokenSelection when no assetId and V1 is also enabled', () => { + const mockNavDetails = [Routes.RAMP.TOKEN_SELECTION, undefined] as const; + mockCreateTokenSelectionNavigationDetails.mockReturnValue(mockNavDetails); + + navigate(undefined, undefined, { + ...v2Deps, + isRampsUnifiedV1Enabled: true, + }); + + expect(mockSetSelectedToken).not.toHaveBeenCalled(); + expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); + expect(mockCreateTokenSelectionNavigationDetails).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + }); + + it('does not navigate to BuildQuote when overrideUnifiedRouting is true', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const mockNavDetails = [Routes.RAMP.BUY] as const; + mockCreateRampNavigationDetails.mockReturnValue(mockNavDetails); + + navigate(intent, { overrideUnifiedRouting: true }, v2Deps); + + expect(mockSetSelectedToken).not.toHaveBeenCalled(); + expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); + expect(mockCreateRampNavigationDetails).toHaveBeenCalledWith( + AggregatorRampType.BUY, + intent, + ); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + }); + + it('takes precedence over V1 routing when V2 is enabled with assetId', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const mockNavDetails = [ + Routes.RAMP.TOKEN_SELECTION, + { + screen: Routes.RAMP.TOKEN_SELECTION, + params: { + screen: Routes.RAMP.AMOUNT_INPUT, + params: { assetId: intent.assetId }, + }, + }, + ] as const; + mockCreateBuildQuoteNavDetails.mockReturnValue(mockNavDetails); + + navigate(intent, undefined, { + isRampsUnifiedV1Enabled: true, + isRampsUnifiedV2Enabled: true, + rampRoutingDecision: UnifiedRampRoutingType.DEPOSIT, + }); + + expect(mockSetSelectedToken).toHaveBeenCalledWith(intent.assetId); + expect(mockCreateBuildQuoteNavDetails).toHaveBeenCalledWith({ + assetId: intent.assetId, + buyFlowOrigin: undefined, + }); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + }); + + describe('error and unsupported routing takes precedence over V2', () => { + it('navigates to eligibility failed modal when routing decision is ERROR', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const navDetails = createEligibilityFailedModalNavigationDetails(); + + navigate(intent, undefined, { + ...v2Deps, + rampRoutingDecision: UnifiedRampRoutingType.ERROR, + }); + + expect(mockSetSelectedToken).not.toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); + }); + + it('navigates to unsupported modal when routing decision is UNSUPPORTED', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const navDetails = createRampUnsupportedModalNavigationDetails(); + + navigate(intent, undefined, { + ...v2Deps, + rampRoutingDecision: UnifiedRampRoutingType.UNSUPPORTED, + }); + + expect(mockSetSelectedToken).not.toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when unified V1 is disabled', () => { + const legacyDeps = { + isRampsUnifiedV1Enabled: false, + isRampsUnifiedV2Enabled: false, + rampRoutingDecision: null, + }; + + it('navigates to aggregator BUY without intent', () => { + const mockNavDetails = [Routes.RAMP.BUY] as const; + mockCreateRampNavigationDetails.mockReturnValue(mockNavDetails); + + navigate(undefined, undefined, legacyDeps); + + expect(mockCreateRampNavigationDetails).toHaveBeenCalledWith( + AggregatorRampType.BUY, + undefined, + ); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + }); + + it('navigates to aggregator with intent', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const mockNavDetails = [Routes.RAMP.BUY] as const; + mockCreateRampNavigationDetails.mockReturnValue(mockNavDetails); + + navigate(intent, undefined, legacyDeps); + + expect(mockCreateRampNavigationDetails).toHaveBeenCalledWith( + AggregatorRampType.BUY, + intent, + ); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + }); + }); + + describe('when unified V1 is enabled', () => { + const v1Deps = { + isRampsUnifiedV1Enabled: true, + isRampsUnifiedV2Enabled: false, + rampRoutingDecision: null as UnifiedRampRoutingType | null, + }; + + describe('error and unsupported routing', () => { + it('navigates to eligibility failed modal when routing decision is ERROR', () => { + const navDetails = createEligibilityFailedModalNavigationDetails(); + + navigate(undefined, undefined, { + ...v1Deps, + rampRoutingDecision: UnifiedRampRoutingType.ERROR, + }); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + + it('navigates to eligibility failed modal when routing decision is ERROR with intent', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const navDetails = createEligibilityFailedModalNavigationDetails(); + + navigate(intent, undefined, { + ...v1Deps, + rampRoutingDecision: UnifiedRampRoutingType.ERROR, + }); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + + it('navigates to unsupported modal when routing decision is UNSUPPORTED', () => { + const navDetails = createRampUnsupportedModalNavigationDetails(); + + navigate(undefined, undefined, { + ...v1Deps, + rampRoutingDecision: UnifiedRampRoutingType.UNSUPPORTED, + }); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + + it('navigates to unsupported modal when routing decision is UNSUPPORTED with intent', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const navDetails = createRampUnsupportedModalNavigationDetails(); + + navigate(intent, undefined, { + ...v1Deps, + rampRoutingDecision: UnifiedRampRoutingType.UNSUPPORTED, + }); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + }); + + describe('token selection routing', () => { + it('navigates to TokenSelection when no assetId is provided', () => { + const mockNavDetails = [ + Routes.RAMP.TOKEN_SELECTION, + undefined, + ] as const; + mockCreateTokenSelectionNavigationDetails.mockReturnValue( + mockNavDetails, + ); + + navigate(undefined, undefined, v1Deps); + + expect(mockCreateTokenSelectionNavigationDetails).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + }); + + it('navigates to TokenSelection when intent is provided without assetId', () => { + const intent = { amount: '100', currency: 'USD' }; + const mockNavDetails = [ + Routes.RAMP.TOKEN_SELECTION, + undefined, + ] as const; + mockCreateTokenSelectionNavigationDetails.mockReturnValue( + mockNavDetails, + ); + + navigate(intent, undefined, v1Deps); + + expect(mockCreateTokenSelectionNavigationDetails).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + }); + }); + + describe('smart routing based on routing decision', () => { + const intent = { assetId: 'eip155:1/erc20:0x123' }; + + it('navigates to TokenSelection when routing decision is null', () => { + const mockNavDetails = [ + Routes.RAMP.TOKEN_SELECTION, + undefined, + ] as const; + mockCreateTokenSelectionNavigationDetails.mockReturnValue( + mockNavDetails, + ); + + navigate(intent, undefined, { ...v1Deps, rampRoutingDecision: null }); + + expect(mockCreateTokenSelectionNavigationDetails).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + }); + + it('navigates to deposit when routing decision is DEPOSIT', () => { + const mockNavDetails = [Routes.DEPOSIT.ID] as const; + mockCreateDepositNavigationDetails.mockReturnValue(mockNavDetails); + + navigate(intent, undefined, { + ...v1Deps, + rampRoutingDecision: UnifiedRampRoutingType.DEPOSIT, + }); + + expect(mockCreateDepositNavigationDetails).toHaveBeenCalledWith(intent); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + }); + + it('navigates to aggregator when routing decision is AGGREGATOR', () => { + const mockNavDetails = [Routes.RAMP.BUY] as const; + mockCreateRampNavigationDetails.mockReturnValue(mockNavDetails); + + navigate(intent, undefined, { + ...v1Deps, + rampRoutingDecision: UnifiedRampRoutingType.AGGREGATOR, + }); + + expect(mockCreateRampNavigationDetails).toHaveBeenCalledWith( + AggregatorRampType.BUY, + intent, + ); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + }); + }); + }); + + describe('override mode', () => { + it('navigates to deposit when mode is DEPOSIT and override unified routing', () => { + const mockNavDetails = [Routes.DEPOSIT.ID] as const; + mockCreateDepositNavigationDetails.mockReturnValue(mockNavDetails); + + navigate( + undefined, + { + mode: NavigateToRampBuyMode.DEPOSIT, + overrideUnifiedRouting: true, + }, + { + isRampsUnifiedV1Enabled: true, + isRampsUnifiedV2Enabled: false, + rampRoutingDecision: UnifiedRampRoutingType.AGGREGATOR, + }, + ); + + expect(mockCreateDepositNavigationDetails).toHaveBeenCalledWith( + undefined, + ); + expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); + }); + }); +}); diff --git a/app/components/UI/Ramp/utils/navigateToRampBuy.ts b/app/components/UI/Ramp/utils/navigateToRampBuy.ts new file mode 100644 index 00000000000..053394255f8 --- /dev/null +++ b/app/components/UI/Ramp/utils/navigateToRampBuy.ts @@ -0,0 +1,162 @@ +import type { NavigationProp, ParamListBase } from '@react-navigation/native'; +import { + RampIntent, + RampType as AggregatorRampType, +} from '../Aggregator/types'; +import { createRampNavigationDetails } from '../Aggregator/routes/utils'; +import { createDepositNavigationDetails } from '../Deposit/routes/utils'; +import { createTokenSelectionNavDetails } from '../Views/TokenSelection/TokenSelection'; +import { createBuildQuoteNavDetails } from '../Views/BuildQuote'; +import type { BuyFlowOrigin } from '../Views/BuildQuote/BuildQuote'; +import { UnifiedRampRoutingType } from '../../../../reducers/fiatOrders'; +import { createRampUnsupportedModalNavigationDetails } from '../components/RampUnsupportedModal/RampUnsupportedModal'; +import { createEligibilityFailedModalNavigationDetails } from '../components/EligibilityFailedModal/EligibilityFailedModal'; +import { + resolveRampControllerAssetId, + type TokenForResolve, +} from './resolveRampControllerAssetId'; + +/** + * How {@link navigateToRampBuy} should route when unified routing is overridden + * or legacy V1 is off (`mode` applies after eligibility checks and V2 branches). + */ +export enum NavigateToRampBuyMode { + AGGREGATOR = 'AGGREGATOR', + DEPOSIT = 'DEPOSIT', +} + +export interface NavigateToRampBuyOptions { + mode?: NavigateToRampBuyMode; + overrideUnifiedRouting?: boolean; + buyFlowOrigin?: BuyFlowOrigin; +} + +/** + * React Navigation handle used by ramp buy routing. Pass `useNavigation()` from + * a screen inside a navigator that hosts RAMP / DEPOSIT routes, or an equivalent + * object (e.g. `NavigationService.navigation` where typed compatibly). + */ +export type NavigateToRampBuyNavigation = Pick< + NavigationProp, + 'navigate' +>; + +/** + * Redux- and controller-derived inputs for buy routing. In React screens, wire + * these from hooks/selectors (`useRampsUnifiedV1Enabled`, `getRampRoutingDecision`, etc.). + */ +export interface NavigateToRampBuyDeps { + isRampsUnifiedV1Enabled: boolean; + isRampsUnifiedV2Enabled: boolean; + rampRoutingDecision: UnifiedRampRoutingType | null; + rampsTokensAll: TokenForResolve[]; + setSelectedToken: (assetId: string) => void; +} + +/** + * Imperatively starts the **buy** ramp experience with the same routing rules as + * {@link useRampNavigation}'s `goToBuy` (eligibility modals, Unified Buy 2 / BuildQuote, + * token selection, V1 deposit vs aggregator, override modes). + * + * Prefer `useRampNavigation` inside React components; use `navigateToRampBuy` when you + * have `navigation` and {@link NavigateToRampBuyDeps} outside that hook (e.g. tests). + * + * When `isRampsUnifiedV2Enabled` is true and `overrideUnifiedRouting` is false, an + * `intent.assetId` routes to BuildQuote (after resolving against `rampsTokensAll`); + * missing `assetId` routes to token selection. UB2 takes precedence over V1 when both + * apply and `assetId` is present. + * + * @param navigation - Navigation object with `navigate` (e.g. from `useNavigation()`). + * @param intent - Optional ramp intent (e.g. `assetId` for pre-selected token). + * @param options - `buyFlowOrigin` for BuildQuote, `overrideUnifiedRouting` to force legacy paths. + * @param deps - Feature flags, routing decision, token list, and `setSelectedToken`. + */ +export function navigateToRampBuy( + navigation: NavigateToRampBuyNavigation, + intent: RampIntent | undefined, + options: NavigateToRampBuyOptions | undefined, + deps: NavigateToRampBuyDeps, +): void { + const { + mode = NavigateToRampBuyMode.AGGREGATOR, + overrideUnifiedRouting = false, + } = options ?? {}; + + const { + isRampsUnifiedV1Enabled, + isRampsUnifiedV2Enabled, + rampRoutingDecision, + rampsTokensAll, + setSelectedToken, + } = deps; + + const isUnifiedRoutingEnabled = + (isRampsUnifiedV1Enabled || isRampsUnifiedV2Enabled) && + !overrideUnifiedRouting; + + if (isUnifiedRoutingEnabled) { + if (rampRoutingDecision === UnifiedRampRoutingType.ERROR) { + navigation.navigate(...createEligibilityFailedModalNavigationDetails()); + return; + } + + if (rampRoutingDecision === UnifiedRampRoutingType.UNSUPPORTED) { + navigation.navigate(...createRampUnsupportedModalNavigationDetails()); + return; + } + } + + if (isRampsUnifiedV2Enabled && intent?.assetId && !overrideUnifiedRouting) { + const controllerAssetId = resolveRampControllerAssetId( + intent.assetId, + rampsTokensAll, + ); + try { + setSelectedToken(controllerAssetId); + } catch { + // Token may not be in controller's list yet (still loading). + // Navigate anyway — BuildQuote will handle the missing token. + } + navigation.navigate( + ...createBuildQuoteNavDetails({ + assetId: controllerAssetId, + buyFlowOrigin: options?.buyFlowOrigin, + }), + ); + return; + } + + if (isRampsUnifiedV2Enabled && !intent?.assetId && !overrideUnifiedRouting) { + navigation.navigate(...createTokenSelectionNavDetails()); + return; + } + + if (isRampsUnifiedV1Enabled && !overrideUnifiedRouting) { + if (!intent?.assetId) { + navigation.navigate(...createTokenSelectionNavDetails()); + return; + } + + if (rampRoutingDecision === null) { + navigation.navigate(...createTokenSelectionNavDetails()); + return; + } + + if (rampRoutingDecision === UnifiedRampRoutingType.DEPOSIT) { + navigation.navigate(...createDepositNavigationDetails(intent)); + } else if (rampRoutingDecision === UnifiedRampRoutingType.AGGREGATOR) { + navigation.navigate( + ...createRampNavigationDetails(AggregatorRampType.BUY, intent), + ); + } + return; + } + + if (mode === NavigateToRampBuyMode.DEPOSIT) { + navigation.navigate(...createDepositNavigationDetails(intent)); + } else { + navigation.navigate( + ...createRampNavigationDetails(AggregatorRampType.BUY, intent), + ); + } +} From bf2b51bbdb6d080c97b968cafe54952fdeb2a507 Mon Sep 17 00:00:00 2001 From: Amitabh Aggarwal Date: Fri, 8 May 2026 10:12:01 -0500 Subject: [PATCH 2/2] feat(card): wire Add Funds to unified ramp buy from card home Co-authored-by: Cursor --- .../AddFundsBottomSheet.test.tsx | 43 ++++++++++--------- .../AddFundsBottomSheet.tsx | 25 ++++++----- .../util/cardFundingTokenToRampIntent.test.ts | 36 ++++++++++++++++ .../Card/util/cardFundingTokenToRampIntent.ts | 18 ++++++++ .../UI/Ramp/Views/BuildQuote/BuildQuote.tsx | 2 +- .../TokenNotAvailableModal.tsx | 13 +++++- 6 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 app/components/UI/Card/util/cardFundingTokenToRampIntent.test.ts create mode 100644 app/components/UI/Card/util/cardFundingTokenToRampIntent.ts diff --git a/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.test.tsx b/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.test.tsx index 23f9aa13673..d7cc0782709 100644 --- a/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.test.tsx +++ b/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react-native'; import AddFundsBottomSheet from './AddFundsBottomSheet'; import { useOpenSwaps } from '../../hooks/useOpenSwaps'; -import useDepositEnabled from '../../../Ramp/Deposit/hooks/useDepositEnabled'; +import useRampNetwork from '../../../Ramp/Aggregator/hooks/useRampNetwork'; import { isBridgeAllowed } from '../../../Bridge/utils'; import { MetaMetricsEvents } from '../../../../../core/Analytics'; import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; @@ -12,6 +12,7 @@ import { CardFundingToken, FundingStatus } from '../../types'; import { renderScreen } from '../../../../../util/test/renderWithProvider'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import { useRampNavigation } from '../../../Ramp/hooks/useRampNavigation'; +import useRampsUnifiedV2Enabled from '../../../Ramp/hooks/useRampsUnifiedV2Enabled'; import { CardHomeSelectors } from '../../Views/CardHome/CardHome.testIds'; import { RampsButtonClickData } from '../../../Ramp/hooks/useRampsButtonClickData'; @@ -19,7 +20,7 @@ import { RampsButtonClickData } from '../../../Ramp/hooks/useRampsButtonClickDat const mockUseParams = jest.fn(); const mockGoBack = jest.fn(); const mockNavigate = jest.fn(); -const mockGoToDeposit = jest.fn(); +const mockGoToBuy = jest.fn(); // Mock dependencies jest.mock('../../../Ramp/hooks/useRampNavigation'); @@ -27,7 +28,7 @@ jest.mock('../../hooks/useOpenSwaps', () => ({ useOpenSwaps: jest.fn(), })); -jest.mock('../../../Ramp/Deposit/hooks/useDepositEnabled', () => ({ +jest.mock('../../../Ramp/Aggregator/hooks/useRampNetwork', () => ({ __esModule: true, default: jest.fn(), })); @@ -47,7 +48,7 @@ jest.mock('../../../../../util/networks', () => ({ jest.mock('../../../../../util/trace', () => ({ trace: jest.fn(), TraceName: { - LoadDepositExperience: 'LoadDepositExperience', + LoadRampExperience: 'LoadRampExperience', }, })); @@ -148,12 +149,12 @@ describe('AddFundsBottomSheet', () => { }); (useRampNavigation as jest.Mock).mockReturnValue({ - goToDeposit: mockGoToDeposit, + goToBuy: mockGoToBuy, }); - (useDepositEnabled as jest.Mock).mockReturnValue({ - isDepositEnabled: true, - }); + (useRampNetwork as jest.Mock).mockReturnValue([true, true]); + + (useRampsUnifiedV2Enabled as jest.Mock).mockReturnValue(true); (isBridgeAllowed as jest.Mock).mockReturnValue(true); @@ -175,10 +176,8 @@ describe('AddFundsBottomSheet', () => { expect(getByText('Fund with crypto')).toBeOnTheScreen(); }); - it('renders with only swap option when deposit is disabled', () => { - (useDepositEnabled as jest.Mock).mockReturnValue({ - isDepositEnabled: false, - }); + it('renders with only swap option when unified buy v2 is disabled', () => { + (useRampsUnifiedV2Enabled as jest.Mock).mockReturnValue(false); const { getByText, queryByText } = setupComponent(); @@ -186,7 +185,7 @@ describe('AddFundsBottomSheet', () => { expect(queryByText('Fund with cash')).not.toBeOnTheScreen(); }); - it('renders with only deposit option when swaps are not allowed', () => { + it('renders with only cash option when swaps are not allowed', () => { (isBridgeAllowed as jest.Mock).mockReturnValue(false); const { getByText, queryByText } = setupComponent(); @@ -196,9 +195,8 @@ describe('AddFundsBottomSheet', () => { }); it('renders with no options when both are disabled', () => { - (useDepositEnabled as jest.Mock).mockReturnValue({ - isDepositEnabled: false, - }); + (useRampsUnifiedV2Enabled as jest.Mock).mockReturnValue(false); + (useRampNetwork as jest.Mock).mockReturnValue([false, false]); (isBridgeAllowed as jest.Mock).mockReturnValue(false); const { getByText, queryByText } = setupComponent(); @@ -223,7 +221,7 @@ describe('AddFundsBottomSheet', () => { expect(getByText('Swap tokens into USDC on Linea')).toBeTruthy(); }); - it('handles deposit option press correctly', () => { + it('handles fund with cash option press correctly', () => { const { getByText } = setupComponent(); fireEvent.press(getByText('Fund with cash')); @@ -239,7 +237,7 @@ describe('AddFundsBottomSheet', () => { button_text: 'Fund with cash', location: 'CardHome', chain_id_destination: '59144', - ramp_type: 'DEPOSIT', + ramp_type: 'UNIFIED_BUY_2', ramp_routing: undefined, is_authenticated: false, preferred_provider: undefined, @@ -248,7 +246,7 @@ describe('AddFundsBottomSheet', () => { ); expect(mockTrackEvent).toHaveBeenCalled(); expect(trace).toHaveBeenCalledWith({ - name: TraceName.LoadDepositExperience, + name: TraceName.LoadRampExperience, }); }); @@ -321,12 +319,15 @@ describe('AddFundsBottomSheet', () => { expect(getByText('Swap tokens into ETH on Linea')).toBeTruthy(); }); - it('navigates to deposit route when deposit callback is executed', () => { + it('navigates to unified buy when fund with cash callback is executed', () => { const { getByText } = setupComponent(); fireEvent.press(getByText('Fund with cash')); - expect(mockGoToDeposit).toHaveBeenCalled(); + expect(mockGoToBuy).toHaveBeenCalledWith( + { assetId: 'eip155:59144/erc20:0x456' }, + { buyFlowOrigin: 'cardHome' }, + ); }); it('renders component correctly', () => { diff --git a/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.tsx b/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.tsx index a37954cdb23..13d13c392c5 100644 --- a/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.tsx +++ b/app/components/UI/Card/components/AddFundsBottomSheet/AddFundsBottomSheet.tsx @@ -23,7 +23,7 @@ import { View } from 'react-native'; import { CardFundingToken } from '../../types'; import AppConstants from '../../../../../core/AppConstants'; import { isBridgeAllowed } from '../../../Bridge/utils'; -import useDepositEnabled from '../../../Ramp/Deposit/hooks/useDepositEnabled'; +import useRampNetwork from '../../../Ramp/Aggregator/hooks/useRampNetwork'; import { getDecimalChainId } from '../../../../../util/networks'; import { trace, TraceName } from '../../../../../util/trace'; import { useOpenSwaps } from '../../hooks/useOpenSwaps'; @@ -32,6 +32,7 @@ import { MetaMetricsEvents } from '../../../../../core/Analytics'; import { strings } from '../../../../../../locales/i18n'; import { CardHomeSelectors } from '../../Views/CardHome/CardHome.testIds'; import { useRampNavigation } from '../../../Ramp/hooks/useRampNavigation'; +import { cardFundingTokenToRampIntent } from '../../util/cardFundingTokenToRampIntent'; import { safeFormatChainIdToHex } from '../../util/safeFormatChainIdToHex'; import { getDetectedGeolocation } from '../../../../../reducers/fiatOrders'; import { useRampsButtonClickData } from '../../../Ramp/hooks/useRampsButtonClickData'; @@ -57,7 +58,7 @@ const AddFundsBottomSheet: React.FC = () => { const sheetRef = useRef(null); const { priorityToken } = useParams(); - const { isDepositEnabled } = useDepositEnabled(); + const [isNetworkRampSupported] = useRampNetwork(); const theme = useTheme(); const styles = createStyles(theme); const { openSwaps } = useOpenSwaps({ @@ -65,9 +66,10 @@ const AddFundsBottomSheet: React.FC = () => { }); const { trackEvent, createEventBuilder } = useAnalytics(); const rampGeodetectedRegion = useSelector(getDetectedGeolocation); - const { goToDeposit } = useRampNavigation(); + const { goToBuy } = useRampNavigation(); const buttonClickData = useRampsButtonClickData(); const isV2UnifiedEnabled = useRampsUnifiedV2Enabled(); + const isCashFundingEnabled = isV2UnifiedEnabled && isNetworkRampSupported; const closeBottomSheetAndNavigate = useCallback( (navigateFunc: () => void) => { @@ -83,9 +85,11 @@ const AddFundsBottomSheet: React.FC = () => { }); }, [priorityToken, openSwaps, closeBottomSheetAndNavigate]); - const openDeposit = useCallback(() => { + const openUnifiedBuy = useCallback(() => { closeBottomSheetAndNavigate(() => { - goToDeposit(); + goToBuy(cardFundingTokenToRampIntent(priorityToken), { + buyFlowOrigin: 'cardHome', + }); }); trackEvent( createEventBuilder( @@ -99,7 +103,7 @@ const AddFundsBottomSheet: React.FC = () => { button_text: 'Fund with cash', location: 'CardHome', chain_id_destination: getDecimalChainId(priorityToken?.caipChainId), - ramp_type: isV2UnifiedEnabled ? 'UNIFIED_BUY_2' : 'DEPOSIT', + ramp_type: 'UNIFIED_BUY_2', region: rampGeodetectedRegion, ramp_routing: buttonClickData.ramp_routing, is_authenticated: buttonClickData.is_authenticated, @@ -110,17 +114,16 @@ const AddFundsBottomSheet: React.FC = () => { ); trace({ - name: TraceName.LoadDepositExperience, + name: TraceName.LoadRampExperience, }); }, [ rampGeodetectedRegion, closeBottomSheetAndNavigate, - goToDeposit, + goToBuy, trackEvent, createEventBuilder, priorityToken, buttonClickData, - isV2UnifiedEnabled, ]); const options = [ @@ -128,9 +131,9 @@ const AddFundsBottomSheet: React.FC = () => { label: strings('card.add_funds_bottomsheet.deposit'), description: strings('card.add_funds_bottomsheet.deposit_description'), icon: IconName.Bank, - onPress: openDeposit, + onPress: openUnifiedBuy, testID: CardHomeSelectors.ADD_FUNDS_BOTTOM_SHEET_DEPOSIT_OPTION, - enabled: isDepositEnabled, + enabled: isCashFundingEnabled, }, { label: strings('card.add_funds_bottomsheet.swap'), diff --git a/app/components/UI/Card/util/cardFundingTokenToRampIntent.test.ts b/app/components/UI/Card/util/cardFundingTokenToRampIntent.test.ts new file mode 100644 index 00000000000..bac89660ba5 --- /dev/null +++ b/app/components/UI/Card/util/cardFundingTokenToRampIntent.test.ts @@ -0,0 +1,36 @@ +import { FundingStatus } from '../types'; +import { cardFundingTokenToRampIntent } from './cardFundingTokenToRampIntent'; + +describe('cardFundingTokenToRampIntent', () => { + it('returns erc20 assetId when token has an address', () => { + const intent = cardFundingTokenToRampIntent({ + address: '0xABC', + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + caipChainId: 'eip155:1', + fundingStatus: FundingStatus.Enabled, + spendableBalance: '0', + }); + + expect(intent).toEqual({ assetId: 'eip155:1/erc20:0xabc' }); + }); + + it('returns empty intent when token has no address', () => { + const intent = cardFundingTokenToRampIntent({ + address: null, + symbol: 'ETH', + decimals: 18, + name: 'Ether', + caipChainId: 'eip155:1', + fundingStatus: FundingStatus.Enabled, + spendableBalance: '0', + }); + + expect(intent).toEqual({}); + }); + + it('returns empty intent when token is undefined', () => { + expect(cardFundingTokenToRampIntent(undefined)).toEqual({}); + }); +}); diff --git a/app/components/UI/Card/util/cardFundingTokenToRampIntent.ts b/app/components/UI/Card/util/cardFundingTokenToRampIntent.ts new file mode 100644 index 00000000000..98103255343 --- /dev/null +++ b/app/components/UI/Card/util/cardFundingTokenToRampIntent.ts @@ -0,0 +1,18 @@ +import type { CardFundingToken } from '../types'; +import type { RampIntent } from '../../Ramp/types'; + +/** + * Maps a MetaMask Card priority funding token to a ramps {@link RampIntent} + * for unified buy (UB2). ERC-20 tokens use `eip155:…/erc20:0x…`. When + * `address` is missing, returns an empty intent so `goToBuy` opens token selection. + */ +export function cardFundingTokenToRampIntent( + token: CardFundingToken | undefined, +): RampIntent { + if (!token?.address) { + return {}; + } + + const assetId = `${token.caipChainId}/erc20:${token.address.toLowerCase()}`; + return { assetId }; +} diff --git a/app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx b/app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx index 437caae587b..306cdc89c63 100644 --- a/app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx +++ b/app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx @@ -88,7 +88,7 @@ export function isBailedOrderStatus( * - 'homeTokenList': Home → (token list with Buy buttons) → Buy * - undefined: Home → Buy → Token Selection → BuildQuote (standard flow) */ -export type BuyFlowOrigin = 'tokenInfo' | 'homeTokenList'; +export type BuyFlowOrigin = 'tokenInfo' | 'homeTokenList' | 'cardHome'; export interface BuildQuoteParams { assetId?: string; diff --git a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx index 4a9fdf21a42..03cf1460b9d 100644 --- a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { View } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, type ParamListBase } from '@react-navigation/native'; +import type { StackNavigationProp } from '@react-navigation/stack'; import { BottomSheet, type BottomSheetRef, @@ -44,7 +45,7 @@ export const createTokenNotAvailableModalNavigationDetails = function TokenNotAvailableModal() { const { trackEvent, createEventBuilder } = useAnalytics(); const { assetId, buyFlowOrigin } = useParams(); - const navigation = useNavigation(); + const navigation = useNavigation>(); const sheetRef = useRef(null); const { styles } = useStyles(styleSheet, {}); @@ -82,6 +83,10 @@ function TokenNotAvailableModal() { } else if (buyFlowOrigin === 'homeTokenList') { // Home token list buy flow: return to home screen navigation.navigate(Routes.WALLET.HOME as never); + } else if (buyFlowOrigin === 'cardHome') { + navigation.navigate(Routes.CARD.ROOT, { + screen: Routes.CARD.HOME, + }); } else { navigation.navigate(Routes.RAMP.TOKEN_SELECTION, { screen: Routes.RAMP.TOKEN_SELECTION, @@ -145,6 +150,10 @@ function TokenNotAvailableModal() { } else if (buyFlowOrigin === 'homeTokenList') { // Home token list buy flow: return to home screen navigation.navigate(Routes.WALLET.HOME as never); + } else if (buyFlowOrigin === 'cardHome') { + navigation.navigate(Routes.CARD.ROOT, { + screen: Routes.CARD.HOME, + }); } else { navigation.navigate(Routes.RAMP.TOKEN_SELECTION, { screen: Routes.RAMP.TOKEN_SELECTION,