diff --git a/src/hooks/bottomSheet.ts b/src/hooks/bottomSheet.ts index 60b1c370f..d6436bbdf 100644 --- a/src/hooks/bottomSheet.ts +++ b/src/hooks/bottomSheet.ts @@ -1,14 +1,19 @@ -import { useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { BackHandler, NativeEventSubscription } from 'react-native'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; import { useAppDispatch, useAppSelector } from './redux'; -import { closeSheet } from '../store/slices/ui'; -import { viewControllerIsOpenSelector } from '../store/reselect/ui'; +import { objectKeys } from '../utils/objectKeys'; import { TViewController } from '../store/types/ui'; +import { closeAllSheets, closeSheet } from '../store/slices/ui'; +import { + viewControllerIsOpenSelector, + viewControllersSelector, +} from '../store/reselect/ui'; export const useSnapPoints = ( size: 'small' | 'medium' | 'large' | 'calendar', @@ -44,6 +49,10 @@ export const useSnapPoints = ( return snapPoints; }; +/** + * Hook to handle hardware back press (Android) when bottom sheet is open + * for simple one-sheet screens + */ export const useBottomSheetBackPress = ( viewController: TViewController, ): void => { @@ -75,3 +84,47 @@ export const useBottomSheetBackPress = ( }; }, [isBottomSheetOpen, viewController, dispatch]); }; + +/** + * Hook to handle hardware back press (Android) when bottom sheet is open + * for screens that are part of a navigator nested in a bottom sheet + */ +export const useBottomSheetScreenBackPress = (): void => { + const dispatch = useAppDispatch(); + const navigation = useNavigation(); + const viewControllers = useAppSelector(viewControllersSelector); + + const isBottomSheetOpen = useMemo(() => { + const viewControllerKeys = objectKeys(viewControllers); + return viewControllerKeys.some((view) => viewControllers[view].isOpen); + }, [viewControllers]); + + const backHandlerSubscriptionRef = useRef( + null, + ); + + useFocusEffect( + useCallback(() => { + if (!isBottomSheetOpen) { + return; + } + + backHandlerSubscriptionRef.current = BackHandler.addEventListener( + 'hardwareBackPress', + () => { + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + dispatch(closeAllSheets()); + } + return true; + }, + ); + + return (): void => { + backHandlerSubscriptionRef.current?.remove(); + backHandlerSubscriptionRef.current = null; + }; + }, [dispatch, isBottomSheetOpen, navigation]), + ); +}; diff --git a/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx b/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx index a4c766385..8384ec55a 100644 --- a/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx +++ b/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx @@ -11,7 +11,10 @@ import { NavigationContainer } from '../../styles/components'; import BottomSheetWrapper from '../../components/BottomSheetWrapper'; import Amount from '../../screens/Wallets/LNURLWithdraw/Amount'; import Confirm from '../../screens/Wallets/LNURLWithdraw/Confirm'; -import { useSnapPoints } from '../../hooks/bottomSheet'; +import { + useBottomSheetBackPress, + useSnapPoints, +} from '../../hooks/bottomSheet'; import { useAppSelector } from '../../hooks/redux'; import { viewControllerSelector } from '../../store/reselect/ui'; import { __E2E__ } from '../../constants/env'; @@ -37,6 +40,8 @@ const LNURLWithdrawNavigation = (): ReactElement => { return viewControllerSelector(state, 'lnurlWithdraw'); }); + useBottomSheetBackPress('lnurlWithdraw'); + if (!wParams) { return <>; } diff --git a/src/navigation/bottom-sheet/PubkyAuth.tsx b/src/navigation/bottom-sheet/PubkyAuth.tsx index eb00b99cb..8f65e5aeb 100644 --- a/src/navigation/bottom-sheet/PubkyAuth.tsx +++ b/src/navigation/bottom-sheet/PubkyAuth.tsx @@ -14,7 +14,10 @@ import SafeAreaInset from '../../components/SafeAreaInset'; import Button from '../../components/buttons/Button'; import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader'; import { useAppSelector } from '../../hooks/redux'; -import { useSnapPoints } from '../../hooks/bottomSheet'; +import { + useBottomSheetBackPress, + useSnapPoints, +} from '../../hooks/bottomSheet'; import { viewControllerSelector } from '../../store/reselect/ui.ts'; import { auth, parseAuthUrl } from '@synonymdev/react-native-pubky'; import { getPubkySecretKey } from '../../utils/pubky'; @@ -89,6 +92,8 @@ const PubkyAuth = (): ReactElement => { const [authorizing, setAuthorizing] = React.useState(false); const [authSuccess, setAuthSuccess] = React.useState(false); + useBottomSheetBackPress('pubkyAuth'); + useEffect(() => { const fetchParsed = async (): Promise => { const res = await parseAuthUrl(url); diff --git a/src/screens/Settings/Backup/ConfirmPassphrase.tsx b/src/screens/Settings/Backup/ConfirmPassphrase.tsx index bb2188656..75a08c994 100644 --- a/src/screens/Settings/Backup/ConfirmPassphrase.tsx +++ b/src/screens/Settings/Backup/ConfirmPassphrase.tsx @@ -9,7 +9,6 @@ import SafeAreaInset from '../../../components/SafeAreaInset'; import GradientView from '../../../components/GradientView'; import Button from '../../../components/buttons/Button'; import { capitalize } from '../../../utils/helpers'; -import { useBottomSheetBackPress } from '../../../hooks/bottomSheet'; import type { BackupScreenProps } from '../../../navigation/types'; const ConfirmPassphrase = ({ @@ -20,8 +19,6 @@ const ConfirmPassphrase = ({ const { bip39Passphrase: origPass } = route.params; const [bip39Passphrase, setPassphrase] = useState(''); - useBottomSheetBackPress('backupNavigation'); - return ( diff --git a/src/screens/Settings/Backup/ShowPassphrase.tsx b/src/screens/Settings/Backup/ShowPassphrase.tsx index c6464326e..fa87f6f4f 100644 --- a/src/screens/Settings/Backup/ShowPassphrase.tsx +++ b/src/screens/Settings/Backup/ShowPassphrase.tsx @@ -9,7 +9,6 @@ import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigati import SafeAreaInset from '../../../components/SafeAreaInset'; import GradientView from '../../../components/GradientView'; import Button from '../../../components/buttons/Button'; -import { useBottomSheetBackPress } from '../../../hooks/bottomSheet'; import type { BackupScreenProps } from '../../../navigation/types'; const ShowPassphrase = ({ @@ -19,8 +18,6 @@ const ShowPassphrase = ({ const { t } = useTranslation('security'); const { bip39Passphrase, seed } = route.params; - useBottomSheetBackPress('backupNavigation'); - return ( diff --git a/src/screens/Settings/PIN/AskForBiometrics.tsx b/src/screens/Settings/PIN/AskForBiometrics.tsx index 8cd160b54..343799153 100644 --- a/src/screens/Settings/PIN/AskForBiometrics.tsx +++ b/src/screens/Settings/PIN/AskForBiometrics.tsx @@ -24,6 +24,7 @@ import GradientView from '../../../components/GradientView'; import Button from '../../../components/buttons/Button'; import { IsSensorAvailableResult } from '../../../components/Biometrics'; import { useAppDispatch } from '../../../hooks/redux'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import rnBiometrics from '../../../utils/biometrics'; import { showToast } from '../../../utils/notifications'; import { updateSettings } from '../../../store/slices/settings'; @@ -39,6 +40,8 @@ const AskForBiometrics = ({ const [biometryData, setBiometricData] = useState(); const [shouldEnableBiometrics, setShouldEnableBiometrics] = useState(false); + useBottomSheetScreenBackPress(); + useEffect(() => { (async (): Promise => { const data = await rnBiometrics.isSensorAvailable(); diff --git a/src/screens/Settings/PIN/ChoosePIN.tsx b/src/screens/Settings/PIN/ChoosePIN.tsx index 29a7868e0..b0341b4f5 100644 --- a/src/screens/Settings/PIN/ChoosePIN.tsx +++ b/src/screens/Settings/PIN/ChoosePIN.tsx @@ -14,13 +14,12 @@ import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigati import GradientView from '../../../components/GradientView'; import NumberPad from '../../../components/NumberPad'; import useColors from '../../../hooks/colors'; +import { useAppDispatch } from '../../../hooks/redux'; import { vibrate } from '../../../utils/helpers'; -import { useBottomSheetBackPress } from '../../../hooks/bottomSheet'; import { addPin } from '../../../utils/settings'; import { hideTodo } from '../../../store/slices/todos'; import { pinTodo } from '../../../store/shapes/todos'; import type { PinScreenProps } from '../../../navigation/types'; -import { useAppDispatch } from '../../../hooks/redux'; const ChoosePIN = ({ navigation, @@ -49,8 +48,6 @@ const ChoosePIN = ({ // reset pin on back useFocusEffect(useCallback(() => setPin(''), [])); - useBottomSheetBackPress('PINNavigation'); - useEffect(() => { const timer = setTimeout(async () => { if (pin.length !== 4) { diff --git a/src/screens/Settings/PIN/Result.tsx b/src/screens/Settings/PIN/Result.tsx index 3533da7ff..3a3c8c057 100644 --- a/src/screens/Settings/PIN/Result.tsx +++ b/src/screens/Settings/PIN/Result.tsx @@ -9,6 +9,7 @@ import SafeAreaInset from '../../../components/SafeAreaInset'; import GradientView from '../../../components/GradientView'; import Button from '../../../components/buttons/Button'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import { closeSheet } from '../../../store/slices/ui'; import { updateSettings } from '../../../store/slices/settings'; import { pinForPaymentsSelector } from '../../../store/reselect/settings'; @@ -22,6 +23,8 @@ const Result = ({ route }: PinScreenProps<'Result'>): ReactElement => { const dispatch = useAppDispatch(); const pinForPayments = useAppSelector(pinForPaymentsSelector); + useBottomSheetScreenBackPress(); + const biometricsName = useMemo( () => type === 'TouchID' diff --git a/src/screens/Wallets/LNURLPay/Amount.tsx b/src/screens/Wallets/LNURLPay/Amount.tsx index fcd6d40a7..a5e494568 100644 --- a/src/screens/Wallets/LNURLPay/Amount.tsx +++ b/src/screens/Wallets/LNURLPay/Amount.tsx @@ -31,10 +31,11 @@ import { } from '../../../store/reselect/settings'; import { useAppSelector } from '../../../hooks/redux'; import { useBalance, useSwitchUnit } from '../../../hooks/wallet'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import { getNumberPadText } from '../../../utils/numberpad'; import { convertToSats } from '../../../utils/conversion'; -import type { SendScreenProps } from '../../../navigation/types'; import { showToast } from '../../../utils/notifications'; +import type { SendScreenProps } from '../../../navigation/types'; const LNURLAmount = ({ navigation, @@ -52,6 +53,8 @@ const LNURLAmount = ({ const [text, setText] = useState(''); const [error, setError] = useState(false); + useBottomSheetScreenBackPress(); + const amount = useMemo((): number => { return convertToSats(text, conversionUnit); }, [text, conversionUnit]); diff --git a/src/screens/Wallets/LNURLPay/Confirm.tsx b/src/screens/Wallets/LNURLPay/Confirm.tsx index cd3feda0e..34f03777b 100644 --- a/src/screens/Wallets/LNURLPay/Confirm.tsx +++ b/src/screens/Wallets/LNURLPay/Confirm.tsx @@ -9,6 +9,9 @@ import { useTranslation } from 'react-i18next'; import { StyleSheet, TouchableOpacity, View } from 'react-native'; import { FadeIn, FadeOut } from 'react-native-reanimated'; +import { BodySSB, Caption13Up } from '../../../styles/text'; +import { Checkmark, LightningHollow } from '../../../styles/icons'; +import { AnimatedView, BottomSheetTextInput } from '../../../styles/components'; import AmountToggle from '../../../components/AmountToggle'; import Biometrics from '../../../components/Biometrics'; import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader'; @@ -19,6 +22,7 @@ import SwipeToConfirm from '../../../components/SwipeToConfirm'; import useColors from '../../../hooks/colors'; import useKeyboard, { Keyboard } from '../../../hooks/keyboard'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import type { SendScreenProps } from '../../../navigation/types'; import { pinForPaymentsSelector, @@ -28,9 +32,6 @@ import { import { addPendingPayment } from '../../../store/slices/lightning'; import { updateMetaTxComment } from '../../../store/slices/metadata'; import { EActivityType } from '../../../store/types/activity'; -import { AnimatedView, BottomSheetTextInput } from '../../../styles/components'; -import { Checkmark, LightningHollow } from '../../../styles/icons'; -import { BodySSB, Caption13Up } from '../../../styles/text'; import { FeeText } from '../../../utils/fees'; import { decodeLightningInvoice, @@ -78,6 +79,8 @@ const LNURLConfirm = ({ const [showBiotmetrics, setShowBiometrics] = useState(false); const [comment, setComment] = useState(''); + useBottomSheetScreenBackPress(); + const onError = useCallback( (errorMessage: string) => { navigation.navigate('Error', { errorMessage }); diff --git a/src/screens/Wallets/Receive/ReceiveAmount.tsx b/src/screens/Wallets/Receive/ReceiveAmount.tsx index 9526ea6a9..09862ec61 100644 --- a/src/screens/Wallets/Receive/ReceiveAmount.tsx +++ b/src/screens/Wallets/Receive/ReceiveAmount.tsx @@ -21,6 +21,7 @@ import UnitButton from '../UnitButton'; import { useSwitchUnit } from '../../../hooks/wallet'; import { useTransfer } from '../../../hooks/transfer'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import { updateInvoice } from '../../../store/slices/receive'; import { receiveSelector } from '../../../store/reselect/receive'; import { estimateOrderFee } from '../../../utils/blocktank'; @@ -48,6 +49,8 @@ const ReceiveAmount = ({ const { defaultLspBalance: lspBalance, maxClientBalance } = useTransfer(0); + useBottomSheetScreenBackPress(); + useFocusEffect( useCallback(() => { refreshBlocktankInfo().then(); diff --git a/src/screens/Wallets/Send/Amount.tsx b/src/screens/Wallets/Send/Amount.tsx index 9275a1fbb..27be4dcab 100644 --- a/src/screens/Wallets/Send/Amount.tsx +++ b/src/screens/Wallets/Send/Amount.tsx @@ -45,6 +45,8 @@ import { } from '../../../store/reselect/settings'; import { useAppSelector } from '../../../hooks/redux'; import { useBalance, useSwitchUnit } from '../../../hooks/wallet'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; +import { sendTransactionSelector } from '../../../store/reselect/ui'; import { setupFeeForOnChainTransaction, setupOnChainTransaction, @@ -55,7 +57,6 @@ import { showToast } from '../../../utils/notifications'; import { convertToSats } from '../../../utils/conversion'; import { TRANSACTION_DEFAULTS } from '../../../utils/wallet/constants'; import type { SendScreenProps } from '../../../navigation/types'; -import { sendTransactionSelector } from '../../../store/reselect/ui'; const Amount = ({ navigation }: SendScreenProps<'Amount'>): ReactElement => { const route = useRoute(); @@ -78,6 +79,8 @@ const Amount = ({ navigation }: SendScreenProps<'Amount'>): ReactElement => { const { paymentMethod } = useAppSelector(sendTransactionSelector); const usesLightning = paymentMethod === 'lightning'; + useBottomSheetScreenBackPress(); + const outputAmount = useMemo(() => { const amount = getTransactionOutputValue({ outputs: transaction.outputs }); return amount; diff --git a/src/screens/Wallets/Send/Quickpay.tsx b/src/screens/Wallets/Send/Quickpay.tsx index b8d30ea3f..d9a741f4c 100644 --- a/src/screens/Wallets/Send/Quickpay.tsx +++ b/src/screens/Wallets/Send/Quickpay.tsx @@ -12,6 +12,7 @@ import SyncSpinner from '../../../components/SyncSpinner'; import HourglassSpinner from '../../../components/HourglassSpinner'; import LightningSyncing from '../../../components/LightningSyncing'; import { useAppDispatch } from '../../../hooks/redux'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import { addPendingPayment } from '../../../store/slices/lightning'; import { EActivityType } from '../../../store/types/activity'; import type { SendScreenProps } from '../../../navigation/types'; @@ -30,6 +31,8 @@ const Quickpay = ({ const { invoice, amount } = route.params; const dispatch = useAppDispatch(); + useBottomSheetScreenBackPress(); + useFocusEffect( useCallback(() => { const pay = async (): Promise => { diff --git a/src/screens/Wallets/Send/Recipient.tsx b/src/screens/Wallets/Send/Recipient.tsx index 04699b625..2c1b0ca4d 100644 --- a/src/screens/Wallets/Send/Recipient.tsx +++ b/src/screens/Wallets/Send/Recipient.tsx @@ -12,6 +12,7 @@ import { ClipboardTextIcon, ScanIcon, } from '../../../styles/icons'; +import GradientView from '../../../components/GradientView'; import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader'; import SafeAreaInset from '../../../components/SafeAreaInset'; import ContactImage from '../../../components/ContactImage'; @@ -20,11 +21,10 @@ import { showToast } from '../../../utils/notifications'; import useColors from '../../../hooks/colors'; import { useAppSelector } from '../../../hooks/redux'; import { useScreenSize } from '../../../hooks/screen'; -import { useBottomSheetBackPress } from '../../../hooks/bottomSheet'; -import type { SendScreenProps } from '../../../navigation/types'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import { lastPaidSelector } from '../../../store/reselect/slashtags'; import { selectedNetworkSelector } from '../../../store/reselect/wallet'; -import GradientView from '../../../components/GradientView'; +import type { SendScreenProps } from '../../../navigation/types'; const imageSrc = require('../../../assets/illustrations/coin-stack-logo.png'); @@ -64,7 +64,7 @@ const Recipient = ({ const selectedNetwork = useAppSelector(selectedNetworkSelector); const lastPaidContacts = useAppSelector(lastPaidSelector); - useBottomSheetBackPress('sendNavigation'); + useBottomSheetScreenBackPress(); const onOpenContacts = (): void => { navigation.navigate('Contacts'); diff --git a/src/screens/Wallets/Send/ReviewAndSend.tsx b/src/screens/Wallets/Send/ReviewAndSend.tsx index 44d0ca957..667b27a49 100644 --- a/src/screens/Wallets/Send/ReviewAndSend.tsx +++ b/src/screens/Wallets/Send/ReviewAndSend.tsx @@ -44,6 +44,7 @@ import { import useColors from '../../../hooks/colors'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; import { useLightningBalance } from '../../../hooks/lightning'; +import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import { EFeeId } from '../../../store/types/fees'; import { decodeLightningInvoice, @@ -139,6 +140,8 @@ const ReviewAndSend = ({ const [rawTx, setRawTx] = useState<{ hex: string; id: string }>(); const [decodedInvoice, setDecodedInvoice] = useState(); + useBottomSheetScreenBackPress(); + useEffect(() => { const decodeAndSetLightningInvoice = async (): Promise => { if (!usesLightning || !transaction.lightningInvoice) { diff --git a/src/store/slices/ui.ts b/src/store/slices/ui.ts index b8c79334d..6194d2ba0 100644 --- a/src/store/slices/ui.ts +++ b/src/store/slices/ui.ts @@ -55,6 +55,11 @@ export const uiSlice = createSlice({ isMounted: true, }; }, + closeAllSheets: (state) => { + Object.keys(state.viewControllers).forEach((key) => { + state.viewControllers[key].isOpen = false; + }); + }, updateProfileLink: (state, action: PayloadAction) => { state.profileLink = Object.assign(state.profileLink, action.payload); }, @@ -79,6 +84,7 @@ export const { showSheet, toggleSheet, closeSheet, + closeAllSheets, updateProfileLink, updateSendTransaction, resetUiState,