diff --git a/components/TransactionsNavigationHeader.tsx b/components/TransactionsNavigationHeader.tsx index 6722ad601..80f09684e 100644 --- a/components/TransactionsNavigationHeader.tsx +++ b/components/TransactionsNavigationHeader.tsx @@ -170,7 +170,7 @@ const TransactionsNavigationHeader: React.FC > - + {wallet.getLabel()} diff --git a/hooks/useExtendedNavigation.ts b/hooks/useExtendedNavigation.ts index 72c6fcec1..039f09bd4 100644 --- a/hooks/useExtendedNavigation.ts +++ b/hooks/useExtendedNavigation.ts @@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react'; const requiresBiometrics = [ 'WalletExportRoot', 'WalletXpubRoot', - 'ViewEditMultisigCosignersRoot', + 'ViewEditMultisigCosigners', 'ExportMultisigCoordinationSetupRoot', ]; diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index affe7cc19..f17ce097a 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -35,7 +35,6 @@ import ReceiveDetailsStackRoot from './ReceiveDetailsStack'; import ScanLndInvoiceRoot from './ScanLndInvoiceStack'; import SendDetailsStack from './SendDetailsStack'; import SignVerifyStackRoot from './SignVerifyStack'; -import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack'; import WalletExportStack from './WalletExportStack'; import WalletXpubStackRoot from './WalletXpubStack'; import SettingsButton from '../components/icons/SettingsButton'; @@ -66,6 +65,7 @@ import ToolsScreen from '../screen/settings/tools'; import SettingsPrivacy from '../screen/settings/SettingsPrivacy'; import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; import { useIsLargeScreen } from '../hooks/useIsLargeScreen'; +import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack'; const DetailViewStackScreensStack = () => { const theme = useTheme(); @@ -342,8 +342,8 @@ const DetailViewStackScreensStack = () => { /> diff --git a/navigation/DetailViewStackParamList.ts b/navigation/DetailViewStackParamList.ts index 497f6cc4d..c5d4b1980 100644 --- a/navigation/DetailViewStackParamList.ts +++ b/navigation/DetailViewStackParamList.ts @@ -79,7 +79,7 @@ export type DetailViewStackParamList = { ReleaseNotes: undefined; ToolsScreen: undefined; SettingsPrivacy: undefined; - ViewEditMultisigCosignersRoot: { walletID: string; cosigners: string[] }; + ViewEditMultisigCosigners: { walletID: string; cosigners: string[]; onBarScanned?: string }; WalletXpubRoot: undefined; SignVerifyRoot: { screen: 'SignVerify'; diff --git a/navigation/ViewEditMultisigCosignersStack.tsx b/navigation/ViewEditMultisigCosignersStack.tsx deleted file mode 100644 index 6e7d42aec..000000000 --- a/navigation/ViewEditMultisigCosignersStack.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import React from 'react'; - -import navigationStyle from '../components/navigationStyle'; -import { useTheme } from '../components/themes'; -import loc from '../loc'; -import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack'; -import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; -import { ScanQRCodeParamList } from './DetailViewStackParamList'; - -export type ViewEditMultisigCosignersStackParamList = { - ViewEditMultisigCosigners: { - walletID: string; - onBarScanned?: string; - }; - ScanQRCode: ScanQRCodeParamList; -}; - -const Stack = createNativeStackNavigator(); - -const ViewEditMultisigCosignersStackRoot = () => { - const theme = useTheme(); - - return ( - - - - - ); -}; - -export default ViewEditMultisigCosignersStackRoot; diff --git a/screen/wallets/ViewEditMultisigCosigners.tsx b/screen/wallets/ViewEditMultisigCosigners.tsx index 42138321f..4e2d4fb72 100644 --- a/screen/wallets/ViewEditMultisigCosigners.tsx +++ b/screen/wallets/ViewEditMultisigCosigners.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { RouteProp, useFocusEffect, useRoute, usePreventRemove, CommonActions } from '@react-navigation/native'; +import { RouteProp, useFocusEffect, useRoute, usePreventRemove, StackActions } from '@react-navigation/native'; import { ActivityIndicator, Alert, @@ -18,7 +18,15 @@ import { import { Badge, Icon } from '@rneui/themed'; import { isDesktop } from '../../blue_modules/environment'; import { encodeUR } from '../../blue_modules/ur'; -import { BlueCard, BlueFormMultiInput, BlueLoading, BlueSpacing10, BlueSpacing20, BlueTextCentered } from '../../BlueComponents'; +import { + BlueCard, + BlueFormMultiInput, + BlueLoading, + BlueSpacing10, + BlueSpacing20, + BlueSpacing40, + BlueTextCentered, +} from '../../BlueComponents'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import presentAlert from '../../components/Alert'; import BottomModal, { BottomModalHandle } from '../../components/BottomModal'; @@ -40,14 +48,14 @@ import { useStorage } from '../../hooks/context/useStorage'; import ToolTipMenu from '../../components/TooltipMenu'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { useSettings } from '../../hooks/context/useSettings'; -import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import SafeArea from '../../components/SafeArea'; import { TWallet } from '../../class/wallets/types'; import { AddressInputScanButton } from '../../components/AddressInputScanButton'; +import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; -type RouteParams = RouteProp; -type NavigationProp = NativeStackNavigationProp; +type RouteParams = RouteProp; +type NavigationProp = NativeStackNavigationProp; const ViewEditMultisigCosigners: React.FC = () => { const hasLoaded = useRef(false); @@ -169,9 +177,11 @@ const ViewEditMultisigCosigners: React.FC = () => { setIsSaveButtonDisabled(true); setWalletsWithNewOrder(newWallets); setTimeout(() => { - dispatch( - CommonActions.navigate({ name: 'WalletTransactions', params: { walletID: wallet.getID(), walletType: MultisigHDWallet.type } }), - ); + const popTo = StackActions.popTo('WalletTransactions', { + walletID, + walletType: wallet.type, + }); + dispatch(popTo); }, 500); }, 100); }; @@ -560,6 +570,7 @@ const ViewEditMultisigCosigners: React.FC = () => { {!isLoading && ( <> + { await provideMnemonicsModalRef.current?.dismiss(); @@ -568,7 +579,7 @@ const ViewEditMultisigCosigners: React.FC = () => { type="link" onChangeText={setImportText} /> - + )} diff --git a/screen/wallets/WalletDetails.tsx b/screen/wallets/WalletDetails.tsx index 71634f798..5779e7d1d 100644 --- a/screen/wallets/WalletDetails.tsx +++ b/screen/wallets/WalletDetails.tsx @@ -37,7 +37,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc, { formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import { useStorage } from '../../hooks/context/useStorage'; -import { useFocusEffect, useRoute, RouteProp, usePreventRemove } from '@react-navigation/native'; +import { useFocusEffect, useRoute, RouteProp, usePreventRemove, CommonActions } from '@react-navigation/native'; import { LightningTransaction, Transaction, TWallet } from '../../class/wallets/types'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; import HeaderMenuButton from '../../components/HeaderMenuButton'; @@ -66,7 +66,7 @@ const WalletDetails: React.FC = () => { const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState( wallet.getHideTransactionsInWalletsList ? !wallet.getHideTransactionsInWalletsList() : true, ); - const { setOptions, navigate } = useExtendedNavigation(); + const { setOptions, navigate, dispatch } = useExtendedNavigation(); const { colors } = useTheme(); const [walletName, setWalletName] = useState(wallet.getLabel()); @@ -305,13 +305,11 @@ const WalletDetails: React.FC = () => { }); }; const navigateToViewEditCosigners = () => { - navigate('ViewEditMultisigCosignersRoot', { - screen: 'ViewEditMultisigCosigners', - params: { - walletID, - }, + navigate('ViewEditMultisigCosigners', { + walletID, }); }; + const navigateToXPub = () => navigate('WalletXpubRoot', { screen: 'WalletXpub', @@ -395,6 +393,28 @@ const WalletDetails: React.FC = () => { wallet._hdWalletInstance._lastTxFetch = 0; // @ts-expect-error: Need to fix later wallet._hdWalletInstance._lastBalanceFetch = 0; + // Find the WalletTransactions screen in the navigation state and reset just that screen. + // It can be multiple WalletTransactions screen. + dispatch(state => { + // Find the route that contains 'WalletTransactions' in the navigation stack + const routes = state.routes.map(route => { + if (route.name === 'WalletTransactions' && (route.params as { walletID: string })?.walletID === walletID) { + // Reset this specific route with the same params to force a refresh + return { + ...route, + key: `WalletTransactions-${walletID}-${Date.now()}`, // Force new key to ensure fresh mount + }; + } + return route; + }); + + return CommonActions.reset({ + ...state, + routes, + index: state.index, + }); + }); + presentAlert({ message: msg }); } }; diff --git a/screen/wallets/WalletTransactions.tsx b/screen/wallets/WalletTransactions.tsx index 8db657ea0..479ea3a89 100644 --- a/screen/wallets/WalletTransactions.tsx +++ b/screen/wallets/WalletTransactions.tsx @@ -5,16 +5,15 @@ import { Alert, Dimensions, findNodeHandle, - FlatList, I18nManager, InteractionManager, LayoutAnimation, PixelRatio, - ScrollView, StyleSheet, Text, View, - RefreshControl, + Animated, + LayoutChangeEvent, } from 'react-native'; import { Icon } from '@rneui/themed'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; @@ -38,7 +37,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; import { Transaction, TWallet } from '../../class/wallets/types'; import getWalletTransactionsOptions from '../../navigation/helpers/getWalletTransactionsOptions'; -import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder'; import selectWallet from '../../helpers/select-wallet'; import assert from 'assert'; import useMenuElements from '../../hooks/useMenuElements'; @@ -46,7 +44,6 @@ import { useSettings } from '../../hooks/context/useSettings'; import { getClipboardContent } from '../../blue_modules/clipboard'; import HandOffComponent from '../../components/HandOffComponent'; import { HandOffActivityType } from '../../components/types'; -import WalletGradient from '../../class/wallet-gradient'; const buttonFontSize = PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22 @@ -56,6 +53,7 @@ const buttonFontSize = type WalletTransactionsProps = NativeStackScreenProps; type RouteProps = RouteProp; type TransactionListItem = Transaction & { type: 'transaction' | 'header' }; + const WalletTransactions: React.FC = ({ route }) => { const { wallets, saveToDisk, setSelectedWalletID } = useStorage(); const { setReloadTransactionsMenuActionFunction } = useMenuElements(); @@ -74,6 +72,14 @@ const WalletTransactions: React.FC = ({ route }) => { const [lastFetchTimestamp, setLastFetchTimestamp] = useState(() => wallet?._lastTxFetch || 0); const [fetchFailures, setFetchFailures] = useState(0); const MAX_FAILURES = 3; + const scrollY = useRef(new Animated.Value(0)).current; + const [headerHeight, setHeaderHeight] = useState(0); + + const headerTranslate = scrollY.interpolate({ + inputRange: [0, headerHeight], + outputRange: [0, -headerHeight], + extrapolate: 'clamp', + }); const stylesHook = StyleSheet.create({ listHeaderText: { @@ -256,11 +262,8 @@ const WalletTransactions: React.FC = ({ route }) => { ); const navigateToViewEditCosigners = useCallback(() => { - navigate('ViewEditMultisigCosignersRoot', { - screen: 'ViewEditMultisigCosigners', - params: { - walletID, - }, + navigate('ViewEditMultisigCosigners', { + walletID, }); }, [navigate, walletID]); @@ -280,9 +283,11 @@ const WalletTransactions: React.FC = ({ route }) => { walletID, }, }); + } else if (wallet?.type === MultisigHDWallet.type) { + navigateToViewEditCosigners(); } }, - [name, navigate, onWalletSelect, walletID, wallets], + [name, navigate, navigateToViewEditCosigners, onWalletSelect, wallet?.type, walletID, wallets], ); const getItemLayout = (_: any, index: number) => ({ @@ -397,7 +402,8 @@ const WalletTransactions: React.FC = ({ route }) => { console.debug('Next screen is focused, clearing reloadTransactionsMenuActionFunction'); setReloadTransactionsMenuActionFunction(() => {}); }; - }, [setReloadTransactionsMenuActionFunction, refreshTransactions]), + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []), ); const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0); @@ -418,8 +424,9 @@ const WalletTransactions: React.FC = ({ route }) => { const handleScroll = useCallback( (event: any) => { const offsetY = event.nativeEvent.contentOffset.y; - const combinedHeight = 180; - if (offsetY < combinedHeight) { + // Use the measured header height to determine when to show/hide the header title + const threshold = headerHeight * 0.75; + if (offsetY < threshold) { setOptions({ ...getWalletTransactionsOptions({ route }), headerTitle: undefined }); } else { navigation.setOptions({ @@ -427,104 +434,82 @@ const WalletTransactions: React.FC = ({ route }) => { }); } }, - [navigation, wallet, walletBalance, setOptions, route], + [navigation, wallet, walletBalance, setOptions, route, headerHeight], ); - const ListHeaderComponent = useCallback( - () => - wallet ? ( - <> - { - wallet.preferredBalanceUnit = selectedUnit; - await saveToDisk(); - }} - unit={wallet.preferredBalanceUnit} - onWalletBalanceVisibilityChange={async isShouldBeVisible => { - const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); - if (wallet.hideBalance && isBiometricsEnabled) { - const unlocked = await unlockWithBiometrics(); - if (!unlocked) throw new Error('Biometrics failed'); - } - wallet.hideBalance = isShouldBeVisible; - await saveToDisk(); - }} - onManageFundsPressed={id => { - if (wallet.type === MultisigHDWallet.type) { - navigateToViewEditCosigners(); - } else if (wallet.type === LightningCustodianWallet.type) { - if (wallet.getUserHasSavedExport()) { - if (!id) return; - onManageFundsPressed(id); - } else { - presentWalletExportReminder() - .then(async () => { - if (!id) return; - wallet.setUserHasSavedExport(true); - await saveToDisk(); - onManageFundsPressed(id); - }) - .catch(() => { - navigate('WalletExportRoot', { - screen: 'WalletExport', - params: { - walletID, - }, - }); - }); - } - } - }} - /> - <> - - - {loc.transactions.list_title} - - - - {wallet.type === WatchOnlyWallet.type && wallet.isWatchOnlyWarningVisible && ( - { - wallet.isWatchOnlyWarningVisible = false; - LayoutAnimation.configureNext(LayoutAnimation.Presets.linear); - saveToDisk(); - }} - /> - )} - - - - ) : undefined, - [ - wallet, - colors.background, - stylesHook.listHeaderText, - saveToDisk, - isBiometricUseCapableAndEnabled, - navigateToViewEditCosigners, - onManageFundsPressed, - navigate, - walletID, - ], + // Extracted named callbacks + const handleWalletUnitChange = useCallback( + async (selectedUnit: any) => { + if (wallet) { + wallet.preferredBalanceUnit = selectedUnit; + await saveToDisk(); + } + }, + [wallet, saveToDisk], ); + const handleWalletBalanceVisibilityChange = useCallback( + async (isShouldBeVisible: boolean) => { + if (wallet) { + const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); + if (wallet.hideBalance && isBiometricsEnabled) { + const unlocked = await unlockWithBiometrics(); + if (!unlocked) throw new Error('Biometrics failed'); + } + wallet.hideBalance = isShouldBeVisible; + await saveToDisk(); + } + }, + [wallet, saveToDisk, isBiometricUseCapableAndEnabled], + ); + + const handleHeaderLayout = useCallback((event: LayoutChangeEvent) => { + const { height } = event.nativeEvent.layout; + setHeaderHeight(height); + }, []); + + const refreshProps = + !isDesktop && !isElectrumDisabled ? { onRefresh: refreshTransactions, progressViewOffset: headerHeight, refreshing: isLoading } : {}; + + const renderHeader = useCallback(() => { + return ( + + + {loc.transactions.list_title} + + + {wallet?.type === WatchOnlyWallet.type && wallet.isWatchOnlyWarningVisible && ( + { + wallet.isWatchOnlyWarningVisible = false; + LayoutAnimation.configureNext(LayoutAnimation.Presets.linear); + saveToDisk(); + }} + /> + )} + + + ); + }, [colors.background, stylesHook.listHeaderText, wallet, saveToDisk]); + return ( - - {/* The color of the refresh indicator. Temporary hack */} - - - + + + {wallet ? ( + + ) : null} + + getItemLayout={getItemLayout} updateCellsBatchingPeriod={50} onEndReachedThreshold={0.3} + ListHeaderComponent={renderHeader} onEndReached={loadMoreTransactions} ListFooterComponent={renderListFooterComponent} data={getTransactions(limit)} @@ -532,26 +517,22 @@ const WalletTransactions: React.FC = ({ route }) => { keyExtractor={_keyExtractor} renderItem={renderItem} initialNumToRender={10} + style={{ marginTop: headerHeight }} removeClippedSubviews contentContainerStyle={{ backgroundColor: colors.background }} contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }} maxToRenderPerBatch={10} + testID="TransactionsListView" onScroll={handleScroll} scrollEventThrottle={16} - stickyHeaderHiddenOnScroll - ListHeaderComponent={ListHeaderComponent} + {...refreshProps} ListEmptyComponent={ - - + + {(isLightning() && loc.wallets.list_empty_txs1_lightning) || loc.wallets.list_empty_txs1} {isLightning() && {loc.wallets.list_empty_txs2_lightning}} - - } - refreshControl={ - !isDesktop && !isElectrumDisabled ? ( - refreshTransactions(true)} tintColor={colors.msSuccessCheck} /> - ) : undefined + } windowSize={15} maintainVisibleContentPosition={{ @@ -598,27 +579,27 @@ const WalletTransactions: React.FC = ({ route }) => { url={`https://www.blockonomics.co/#/search?q=${wallet.getXpub()}`} /> ) : null} - + ); }; export default WalletTransactions; const styles = StyleSheet.create({ + container: { flex: 1 }, flex: { flex: 1 }, - scrollViewContent: { flex: 1, justifyContent: 'center', paddingHorizontal: 16, paddingBottom: 500 }, activityIndicator: { marginVertical: 20 }, - listHeaderTextRow: { flex: 1, margin: 16, flexDirection: 'row', justifyContent: 'space-between' }, - listHeaderText: { marginTop: 8, marginBottom: 8, fontWeight: 'bold', fontSize: 24 }, - refreshIndicatorBackground: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - height: 140, - }, + listHeaderTextRow: { padding: 16, flexDirection: 'row' }, + listHeaderText: { fontWeight: 'bold', fontSize: 24 }, emptyTxs: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', marginVertical: 16 }, emptyTxsLightning: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', fontWeight: '600' }, sendIcon: { transform: [{ rotate: I18nManager.isRTL ? '-225deg' : '225deg' }] }, receiveIcon: { transform: [{ rotate: I18nManager.isRTL ? '45deg' : '-45deg' }] }, + stickyHeader: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 1, + }, }); diff --git a/tests/e2e/bluewallet2.spec.js b/tests/e2e/bluewallet2.spec.js index ca084683f..89b8a9e8a 100644 --- a/tests/e2e/bluewallet2.spec.js +++ b/tests/e2e/bluewallet2.spec.js @@ -758,7 +758,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => { await waitForId('TransactionsListEmpty'); assert.strictEqual(await countElements('TransactionListItem'), 0); - await element(by.id('TransactionsListView')).swipe('down', 'slow'); // pul-to-refresh + await element(by.id('TransactionsListView')).swipe('down', 'slow', 0.5, 0.3); // pul-to-refresh // asserting balance and txs loaded: await waitForText('0.00105526'); // the wait inside allows network request to propagate