From dd92b078e320cac4471fc20b0affe9e03c4c7b67 Mon Sep 17 00:00:00 2001 From: Amitabh Aggarwal Date: Fri, 17 Oct 2025 21:49:13 -0500 Subject: [PATCH 1/2] fix: prevent Android ramps modal double press navigation issue Fixes #21350 This commit fixes an Android-specific issue where selecting a token, payment method, or region in ramps flows (deposit, buy, withdraw) would incorrectly navigate to the home screen instead of returning to the previous screen. Root Cause: - Race condition between BottomSheet's automatic navigation (shouldNavigateBack={true}) and manual navigation.goBack() calls - Both navigation actions fired simultaneously on Android - Navigation stack became confused and navigated to wrong screen Solution: - Set shouldNavigateBack={false} on all ramps BottomSheet components - Use BottomSheet's callback pattern: onCloseBottomSheet(() => navigation.goBack()) - This ensures navigation happens AFTER sheet closes, not during Changes: - Added useNavigation import to all affected modal components - Updated all selector modal callbacks to use navigation.goBack() in callback - Applied fix to deposit and aggregator (buy/sell) flows Files Modified: - TokenSelectorModal (Deposit) - PaymentMethodSelectorModal (Deposit) - RegionSelectorModal (Deposit) - StateSelectorModal (Deposit) - TokenSelectModal (Aggregator) - RegionSelectorModal (Aggregator) - PaymentMethodSelectorModal (Aggregator) - FiatSelectorModal (Aggregator) Testing: - No unit test changes required (navigation already mocked) - Manual testing required on Android devices - iOS behavior unchanged --- .../FiatSelectorModal/FiatSelectorModal.tsx | 18 +++++++++++---- .../PaymentMethodSelectorModal.tsx | 18 ++++++++++++--- .../RegionSelectorModal.tsx | 23 +++++++++++++++---- .../TokenSelectModal/TokenSelectModal.tsx | 18 +++++++++++---- .../PaymentMethodSelectorModal.tsx | 17 +++++++++++--- .../RegionSelectorModal.tsx | 18 +++++++++++---- .../StateSelectorModal/StateSelectorModal.tsx | 14 ++++++++--- .../TokenSelectorModal/TokenSelectorModal.tsx | 17 +++++++++++--- 8 files changed, 114 insertions(+), 29 deletions(-) diff --git a/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/FiatSelectorModal.tsx b/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/FiatSelectorModal.tsx index 5480efe75aa3..531d35495605 100644 --- a/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/FiatSelectorModal.tsx +++ b/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/FiatSelectorModal.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { View, useWindowDimensions } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import Fuse from 'fuse.js'; +import { useNavigation } from '@react-navigation/native'; import { strings } from '../../../../../../../locales/i18n'; import { FiatCurrency } from '@consensys/on-ramp-sdk'; import { @@ -41,6 +42,7 @@ export const createFiatSelectorModalNavigationDetails = function FiatSelectorModal() { const sheetRef = useRef(null); const listRef = useRef>(null); + const navigation = useNavigation(); const { currencies } = useParams(); const [searchString, setSearchString] = useState(''); @@ -75,9 +77,11 @@ function FiatSelectorModal() { const handleSelectCurrency = useCallback( (currency: FiatCurrency) => { setSelectedFiatCurrencyId(currency?.id); - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); }, - [setSelectedFiatCurrencyId], + [setSelectedFiatCurrencyId, navigation], ); const renderItem = useCallback( @@ -134,8 +138,14 @@ function FiatSelectorModal() { ); return ( - - sheetRef.current?.onCloseBottomSheet()}> + + + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }) + } + > {strings('fiat_on_ramp_aggregator.select_region_currency')} diff --git a/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx b/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx index b2a5ddce876d..37dcaa974071 100644 --- a/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx +++ b/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useRef } from 'react'; import { View, ScrollView, useWindowDimensions } from 'react-native'; import { Payment } from '@consensys/on-ramp-sdk'; +import { useNavigation } from '@react-navigation/native'; import Text, { TextVariant, @@ -37,6 +38,7 @@ export const createPaymentMethodSelectorModalNavigationDetails = function PaymentMethodSelectorModal() { const sheetRef = useRef(null); + const navigation = useNavigation(); const { paymentMethods, location } = useParams(); @@ -78,9 +80,12 @@ function PaymentMethodSelectorModal() { sheetRef.current?.onCloseBottomSheet(() => { setSelectedPaymentMethodId(paymentMethodId); + navigation.goBack(); }); } else { - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); } }, [ @@ -91,6 +96,7 @@ function PaymentMethodSelectorModal() { selectedPaymentMethodId, selectedRegion?.id, trackEvent, + navigation, ], ); @@ -99,8 +105,14 @@ function PaymentMethodSelectorModal() { ); return ( - - sheetRef.current?.onCloseBottomSheet()}> + + + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }) + } + > {title} diff --git a/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx b/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx index 2625f338ba9c..e8e1fe2d801d 100644 --- a/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx +++ b/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx @@ -8,6 +8,7 @@ import React, { import { View, useWindowDimensions } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import Fuse from 'fuse.js'; +import { useNavigation } from '@react-navigation/native'; import Text, { TextColor, @@ -58,6 +59,7 @@ export const createRegionSelectorModalNavigationDetails = function RegionSelectorModal() { const sheetRef = useRef(null); const listRef = useRef>(null); + const navigation = useNavigation(); const { selectedRegion, setSelectedRegion, isBuy } = useRampSDK(); const { regions } = useParams(); @@ -153,9 +155,18 @@ function RegionSelectorModal() { }); setSelectedRegion(region); - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); }, - [setSelectedRegion, trackEvent, regionInTransit, scrollToTop, isBuy], + [ + setSelectedRegion, + trackEvent, + regionInTransit, + scrollToTop, + isBuy, + navigation, + ], ); const renderRegionItem = useCallback( @@ -232,9 +243,11 @@ function RegionSelectorModal() { if (activeView === RegionViewType.STATE) { handleRegionBackButton(); } else { - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); } - }, [activeView, handleRegionBackButton]); + }, [activeView, handleRegionBackButton, navigation]); const onModalHide = useCallback(() => { setActiveView(RegionViewType.COUNTRY); @@ -246,7 +259,7 @@ function RegionSelectorModal() { return ( diff --git a/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/TokenSelectModal.tsx b/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/TokenSelectModal.tsx index da79bd4bfdb1..3f95fa0b5e15 100644 --- a/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/TokenSelectModal.tsx +++ b/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/TokenSelectModal.tsx @@ -3,6 +3,7 @@ import { View, useWindowDimensions } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import Fuse from 'fuse.js'; import { useSelector } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; import Text, { TextVariant, @@ -61,6 +62,7 @@ export const createTokenSelectModalNavigationDetails = function TokenSelectModal() { const sheetRef = useRef(null); const listRef = useRef(null); + const navigation = useNavigation(); const { tokens } = useParams(); const [searchString, setSearchString] = useState(''); @@ -150,9 +152,11 @@ function TokenSelectModal() { const handleSelectTokenCallback = useCallback( (newAsset: CryptoCurrency) => { setSelectedAsset(newAsset); - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); }, - [setSelectedAsset], + [setSelectedAsset, navigation], ); const scrollToTop = useCallback(() => { @@ -243,8 +247,14 @@ function TokenSelectModal() { }, [tokens]); return ( - - sheetRef.current?.onCloseBottomSheet()}> + + + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }) + } + > {strings('fiat_on_ramp_aggregator.select_a_cryptocurrency')} diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx index 0f7d56e49bfe..a7e5c44413e3 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/PaymentMethodSelectorModal.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useRef } from 'react'; import { View, useWindowDimensions } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; +import { useNavigation } from '@react-navigation/native'; import Text, { TextVariant, } from '../../../../../../../component-library/components/Texts/Text'; @@ -42,6 +43,7 @@ export const createPaymentMethodSelectorModalNavigationDetails = function PaymentMethodSelectorModal() { const sheetRef = useRef(null); const listRef = useRef(null); + const navigation = useNavigation(); const { height: screenHeight } = useWindowDimensions(); const { themeAppearance } = useTheme(); const { styles } = useStyles(styleSheet, { @@ -71,7 +73,9 @@ function PaymentMethodSelectorModal() { }); setSelectedPaymentMethod(foundPaymentMethod); } - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); }, [ paymentMethods, @@ -79,6 +83,7 @@ function PaymentMethodSelectorModal() { selectedRegion?.isoCode, isAuthenticated, setSelectedPaymentMethod, + navigation, ], ); @@ -121,8 +126,14 @@ function PaymentMethodSelectorModal() { ); return ( - - sheetRef.current?.onCloseBottomSheet()}> + + + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }) + } + > {strings('deposit.payment_modal.select_a_payment_method')} diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/RegionSelectorModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/RegionSelectorModal.tsx index ed4aaf4f3e4e..ec137086f6c5 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/RegionSelectorModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/RegionSelectorModal.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { View, useWindowDimensions } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import Fuse from 'fuse.js'; +import { useNavigation } from '@react-navigation/native'; import Text, { TextVariant, @@ -44,6 +45,7 @@ export const createRegionSelectorModalNavigationDetails = function RegionSelectorModal() { const sheetRef = useRef(null); const listRef = useRef>(null); + const navigation = useNavigation(); const { selectedRegion, setSelectedRegion, isAuthenticated } = useDepositSDK(); @@ -105,10 +107,12 @@ function RegionSelectorModal() { }); setSelectedRegion(region); - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); } }, - [setSelectedRegion, isAuthenticated, trackEvent], + [setSelectedRegion, isAuthenticated, trackEvent, navigation], ); const renderRegionItem = useCallback( @@ -180,8 +184,14 @@ function RegionSelectorModal() { }, [scrollToTop]); return ( - - sheetRef.current?.onCloseBottomSheet()}> + + + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }) + } + > {strings('deposit.region_modal.select_a_region')} diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx index 395f8096d480..48cad4fe7825 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx @@ -102,7 +102,9 @@ function StateSelectorModal() { }); } else { onStateSelect(state.code); - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); } }, [navigation, onStateSelect], @@ -155,8 +157,14 @@ function StateSelectorModal() { }, [scrollToTop]); return ( - - sheetRef.current?.onCloseBottomSheet()}> + + + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }) + } + > {strings('deposit.state_modal.select_a_state')} diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/TokenSelectorModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/TokenSelectorModal.tsx index e232f7114b2a..c080d7bd150f 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/TokenSelectorModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/TokenSelectorModal.tsx @@ -3,6 +3,7 @@ import { View, useWindowDimensions } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import { useSelector } from 'react-redux'; import { CaipChainId } from '@metamask/utils'; +import { useNavigation } from '@react-navigation/native'; import NetworksFilterBar from '../../../components/NetworksFilterBar'; import NetworksFilterSelector from '../../../components/NetworksFilterSelector/NetworksFilterSelector'; @@ -57,6 +58,7 @@ export const createTokenSelectorModalNavigationDetails = function TokenSelectorModal() { const sheetRef = useRef(null); const listRef = useRef(null); + const navigation = useNavigation(); const [searchString, setSearchString] = useState(''); const [networkFilter, setNetworkFilter] = useState( null, @@ -105,7 +107,9 @@ function TokenSelectorModal() { }); setSelectedCryptoCurrency(selectedToken); } - sheetRef.current?.onCloseBottomSheet(); + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }); }, [ supportedTokens, @@ -114,6 +118,7 @@ function TokenSelectorModal() { selectedRegion?.currency, isAuthenticated, setSelectedCryptoCurrency, + navigation, ], ); @@ -209,8 +214,14 @@ function TokenSelectorModal() { }, [supportedTokens]); return ( - - sheetRef.current?.onCloseBottomSheet()}> + + + sheetRef.current?.onCloseBottomSheet(() => { + navigation.goBack(); + }) + } + > {isEditingNetworkFilter ? strings('deposit.networks_filter_selector.select_network') From 27bdf394e52eb2411f05c0377f316e6371781130 Mon Sep 17 00:00:00 2001 From: Amitabh Aggarwal Date: Tue, 21 Oct 2025 12:44:29 -0500 Subject: [PATCH 2/2] refactor: eliminate code duplication and fix network selector - Added closeBottomSheetAndNavigate helper function to StateSelectorModal and RegionSelectorModal (Aggregator) to eliminate duplication - Fixed Checkbox duplicate onPress in NetworksFilterSelector that was causing Android double press issue (React Native bug #51835) - Updated dependency arrays to include helper function This addresses SonarCloud code duplication warnings and fixes the network selector double tap issue caused by competing onPress handlers (ListItemSelect + Checkbox both firing on Android). Related to #21350 --- .../RegionSelectorModal.tsx | 19 ++++++++++++++++--- .../StateSelectorModal/StateSelectorModal.tsx | 15 +++++++++++---- .../NetworksFilterSelector.tsx | 5 +---- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx b/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx index e8e1fe2d801d..8f1b6554eb45 100644 --- a/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx +++ b/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/RegionSelectorModal.tsx @@ -135,6 +135,13 @@ function RegionSelectorModal() { } }, []); + const closeBottomSheetAndNavigate = useCallback( + (navigateFunc: () => void) => { + sheetRef.current?.onCloseBottomSheet(navigateFunc); + }, + [], + ); + const handleOnRegionPressCallback = useCallback( async (region: Region) => { if (region.states && region.states.length > 0) { @@ -155,7 +162,7 @@ function RegionSelectorModal() { }); setSelectedRegion(region); - sheetRef.current?.onCloseBottomSheet(() => { + closeBottomSheetAndNavigate(() => { navigation.goBack(); }); }, @@ -166,6 +173,7 @@ function RegionSelectorModal() { scrollToTop, isBuy, navigation, + closeBottomSheetAndNavigate, ], ); @@ -243,11 +251,16 @@ function RegionSelectorModal() { if (activeView === RegionViewType.STATE) { handleRegionBackButton(); } else { - sheetRef.current?.onCloseBottomSheet(() => { + closeBottomSheetAndNavigate(() => { navigation.goBack(); }); } - }, [activeView, handleRegionBackButton, navigation]); + }, [ + activeView, + handleRegionBackButton, + navigation, + closeBottomSheetAndNavigate, + ]); const onModalHide = useCallback(() => { setActiveView(RegionViewType.COUNTRY); diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx index 48cad4fe7825..5a0119e84478 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx @@ -88,10 +88,17 @@ function StateSelectorModal() { } }, []); + const closeBottomSheetAndNavigate = useCallback( + (navigateFunc: () => void) => { + sheetRef.current?.onCloseBottomSheet(navigateFunc); + }, + [], + ); + const handleOnStatePressCallback = useCallback( (state: { code: string; name: string }) => { if (state.code === 'NY') { - sheetRef.current?.onCloseBottomSheet(() => { + closeBottomSheetAndNavigate(() => { navigation.navigate( ...createUnsupportedStateModalNavigationDetails({ stateCode: state.code, @@ -102,12 +109,12 @@ function StateSelectorModal() { }); } else { onStateSelect(state.code); - sheetRef.current?.onCloseBottomSheet(() => { + closeBottomSheetAndNavigate(() => { navigation.goBack(); }); } }, - [navigation, onStateSelect], + [navigation, onStateSelect, closeBottomSheetAndNavigate], ); const renderStateItem = useCallback( @@ -160,7 +167,7 @@ function StateSelectorModal() { - sheetRef.current?.onCloseBottomSheet(() => { + closeBottomSheetAndNavigate(() => { navigation.goBack(); }) } diff --git a/app/components/UI/Ramp/Deposit/components/NetworksFilterSelector/NetworksFilterSelector.tsx b/app/components/UI/Ramp/Deposit/components/NetworksFilterSelector/NetworksFilterSelector.tsx index d6de0fe186e5..025df02e9933 100644 --- a/app/components/UI/Ramp/Deposit/components/NetworksFilterSelector/NetworksFilterSelector.tsx +++ b/app/components/UI/Ramp/Deposit/components/NetworksFilterSelector/NetworksFilterSelector.tsx @@ -83,10 +83,7 @@ function NetworksFilterSelector({ renderItem={({ item: chainId }) => ( - +