import React, { useCallback, useEffect, useReducer, useRef } from 'react'; import { useFocusEffect, useIsFocused, useRoute, RouteProp } from '@react-navigation/native'; import { findNodeHandle, Image, InteractionManager, SectionList, StyleSheet, Text, useWindowDimensions, View } from 'react-native'; import A from '../../blue_modules/analytics'; import BlueClipboard from '../../blue_modules/clipboard'; import { isDesktop } from '../../blue_modules/environment'; import * as fs from '../../blue_modules/fs'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import { ExtendedTransaction, Transaction, TWallet } from '../../class/wallets/types'; import presentAlert from '../../components/Alert'; import { FButton, FContainer } from '../../components/FloatButtons'; import { useTheme } from '../../components/themes'; import { TransactionListItem } from '../../components/TransactionListItem'; import WalletsCarousel from '../../components/WalletsCarousel'; import { scanQrHelper } from '../../helpers/scan-qr'; import { useIsLargeScreen } from '../../hooks/useIsLargeScreen'; import loc from '../../loc'; import ActionSheet from '../ActionSheet'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useStorage } from '../../hooks/context/useStorage'; import TotalWalletsBalance from '../../components/TotalWalletsBalance'; import { useSettings } from '../../hooks/context/useSettings'; const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' }; type SectionData = { key: string; data: Transaction[] | string[]; }; enum ActionTypes { SET_LOADING, SET_WALLETS, SET_CURRENT_INDEX, SET_REFRESH_FUNCTION, } interface SetLoadingAction { type: ActionTypes.SET_LOADING; payload: boolean; } interface SetWalletsAction { type: ActionTypes.SET_WALLETS; payload: TWallet[]; } interface SetCurrentIndexAction { type: ActionTypes.SET_CURRENT_INDEX; payload: number; } interface SetRefreshFunctionAction { type: ActionTypes.SET_REFRESH_FUNCTION; payload: () => void; } type WalletListAction = SetLoadingAction | SetWalletsAction | SetCurrentIndexAction | SetRefreshFunctionAction; interface WalletListState { isLoading: boolean; wallets: TWallet[]; currentWalletIndex: number; refreshFunction: () => void; } const initialState = { isLoading: false, wallets: [], currentWalletIndex: 0, refreshFunction: () => {}, }; function reducer(state: WalletListState, action: WalletListAction) { switch (action.type) { case ActionTypes.SET_LOADING: return { ...state, isLoading: action.payload }; case ActionTypes.SET_WALLETS: return { ...state, wallets: action.payload }; case ActionTypes.SET_CURRENT_INDEX: return { ...state, currentWalletIndex: action.payload }; case ActionTypes.SET_REFRESH_FUNCTION: return { ...state, refreshFunction: action.payload }; default: return state; } } type NavigationProps = NativeStackNavigationProp; type RouteProps = RouteProp; const WalletsList: React.FC = () => { const [state, dispatch] = useReducer>(reducer, initialState); const { isLoading } = state; const isLargeScreen = useIsLargeScreen(); const walletsCarousel = useRef(); const currentWalletIndex = useRef(0); const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID, isElectrumDisabled, setReloadTransactionsMenuActionFunction, } = useStorage(); const { isTotalBalanceEnabled } = useSettings(); const { width } = useWindowDimensions(); const { colors, scanImage } = useTheme(); const { navigate } = useExtendedNavigation(); const isFocused = useIsFocused(); const route = useRoute(); const routeName = route.name; const dataSource = getTransactions(undefined, 10); const walletsCount = useRef(wallets.length); const walletActionButtonsRef = useRef(); const stylesHook = StyleSheet.create({ walletsListWrapper: { backgroundColor: colors.brandingColor, }, listHeaderBack: { backgroundColor: colors.background, }, listHeaderText: { color: colors.foregroundColor, }, }); useFocusEffect( useCallback(() => { const task = InteractionManager.runAfterInteractions(() => { setReloadTransactionsMenuActionFunction(() => onRefresh); verifyBalance(); setSelectedWalletID(undefined); }); return () => { task.cancel(); setReloadTransactionsMenuActionFunction(() => {}); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []), ); useEffect(() => { // new wallet added if (wallets.length > walletsCount.current) { walletsCarousel.current?.scrollToItem({ item: wallets[walletsCount.current] }); } walletsCount.current = wallets.length; }, [wallets]); useEffect(() => { const scannedData = route.params?.scannedData; if (scannedData) { onBarScanned(scannedData); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [route.params?.scannedData]); const verifyBalance = useCallback(() => { if (getBalance() !== 0) { A(A.ENUM.GOT_NONZERO_BALANCE); } else { A(A.ENUM.GOT_ZERO_BALANCE); } }, [getBalance]); /** * Forcefully fetches TXs and balance for ALL wallets. * Triggered manually by user on pull-to-refresh. */ const refreshTransactions = useCallback( async (showLoadingIndicator = true, showUpdateStatusIndicator = false) => { if (isElectrumDisabled) { dispatch({ type: ActionTypes.SET_LOADING, payload: false }); return; } dispatch({ type: ActionTypes.SET_LOADING, payload: showLoadingIndicator }); refreshAllWalletTransactions(undefined, showUpdateStatusIndicator).finally(() => { dispatch({ type: ActionTypes.SET_LOADING, payload: false }); }); }, [isElectrumDisabled, refreshAllWalletTransactions], ); useEffect(() => { refreshTransactions(false, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleClick = useCallback( (item?: TWallet) => { if (item?.getID) { const walletID = item.getID(); navigate('WalletTransactions', { walletID, walletType: item.type, }); } else { navigate('AddWalletRoot'); } }, [navigate], ); const setIsLoading = useCallback((value: boolean) => { dispatch({ type: ActionTypes.SET_LOADING, payload: value }); }, []); const onSnapToItem = useCallback( (e: { nativeEvent: { contentOffset: any } }) => { if (!isFocused) return; const contentOffset = e.nativeEvent.contentOffset; const index = Math.ceil(contentOffset.x / width); if (currentWalletIndex.current !== index) { console.debug('onSnapToItem', wallets.length === index ? 'NewWallet/Importing card' : index); if (wallets[index] && (wallets[index].timeToRefreshBalance() || wallets[index].timeToRefreshTransaction())) { refreshAllWalletTransactions(index, false).finally(() => setIsLoading(false)); } currentWalletIndex.current = index; } }, [isFocused, refreshAllWalletTransactions, setIsLoading, wallets, width], ); const renderListHeaderComponent = useCallback(() => { return ( {`${loc.transactions.list_title}${' '}`} ); }, [stylesHook.listHeaderBack, stylesHook.listHeaderText]); const handleLongPress = useCallback(() => { navigate('ManageWallets'); }, [navigate]); const renderTransactionListsRow = useCallback( (item: ExtendedTransaction) => ( ), [], ); const renderWalletsCarousel = useCallback(() => { return ( <> ); }, [handleClick, handleLongPress, isFocused, onSnapToItem, wallets]); const renderSectionItem = useCallback( (item: { section: any; item: ExtendedTransaction }) => { switch (item.section.key) { case WalletsListSections.CAROUSEL: return isLargeScreen ? null : renderWalletsCarousel(); case WalletsListSections.TRANSACTIONS: return renderTransactionListsRow(item.item); default: return null; } }, [isLargeScreen, renderTransactionListsRow, renderWalletsCarousel], ); const renderSectionHeader = useCallback( (section: { section: { key: any } }) => { switch (section.section.key) { case WalletsListSections.TRANSACTIONS: return renderListHeaderComponent(); case WalletsListSections.CAROUSEL: { return !isLargeScreen && isTotalBalanceEnabled ? ( ) : null; } default: return null; } }, [isLargeScreen, isTotalBalanceEnabled, renderListHeaderComponent, stylesHook.walletsListWrapper], ); const renderSectionFooter = useCallback( (section: { section: { key: any } }) => { switch (section.section.key) { case WalletsListSections.TRANSACTIONS: if (dataSource.length === 0 && !isLoading) { return ( {loc.wallets.list_empty_txs1} {loc.wallets.list_empty_txs2} ); } else { return null; } default: return null; } }, [dataSource.length, isLoading], ); const renderScanButton = useCallback(() => { if (wallets.length > 0) { return ( } text={loc.send.details_scan} /> ); } else { return null; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [scanImage, wallets.length]); const sectionListKeyExtractor = (item: any, index: any) => { return `${item}${index}}`; }; const onScanButtonPressed = useCallback(() => { scanQrHelper(routeName, true, undefined, false); }, [routeName]); const onBarScanned = useCallback( (value: any) => { if (!value) return; DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); // @ts-ignore: for now navigate(...completionValue); }); }, [navigate], ); const copyFromClipboard = useCallback(async () => { onBarScanned(await BlueClipboard().getClipboardContent()); }, [onBarScanned]); const sendButtonLongPress = useCallback(async () => { const isClipboardEmpty = (await BlueClipboard().getClipboardContent()).trim().length === 0; const options = [loc._.cancel, loc.wallets.list_long_choose, loc.wallets.list_long_scan]; if (!isClipboardEmpty) { options.push(loc.wallets.list_long_clipboard); } const props = { title: loc.send.header, options, cancelButtonIndex: 0 }; const anchor = findNodeHandle(walletActionButtonsRef.current); if (anchor) { options.push(String(anchor)); } ActionSheet.showActionSheetWithOptions(props, buttonIndex => { switch (buttonIndex) { case 0: break; case 1: fs.showImagePickerAndReadImage() .then(onBarScanned) .catch(error => { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ title: loc.errors.error, message: error.message }); }); break; case 2: scanQrHelper(routeName, true, undefined, false); break; case 3: if (!isClipboardEmpty) { copyFromClipboard(); } break; } }); }, [copyFromClipboard, onBarScanned, routeName]); const onRefresh = useCallback(() => { refreshTransactions(true, false); // Optimized for Mac option doesn't like RN Refresh component. Menu Elements now handles it for macOS }, [refreshTransactions]); const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh }; const sections: SectionData[] = [ { key: WalletsListSections.CAROUSEL, data: [WalletsListSections.CAROUSEL] }, { key: WalletsListSections.TRANSACTIONS, data: dataSource }, ]; return ( removeClippedSubviews contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets {...refreshProps} renderItem={renderSectionItem} keyExtractor={sectionListKeyExtractor} renderSectionHeader={renderSectionHeader} initialNumToRender={20} contentInset={styles.scrollContent} renderSectionFooter={renderSectionFooter} sections={sections} windowSize={21} maxToRenderPerBatch={10} updateCellsBatchingPeriod={50} /> {renderScanButton()} ); }; export default WalletsList; const styles = StyleSheet.create({ root: { flex: 1, }, scrollContent: { top: 0, left: 0, bottom: 60, right: 0, }, walletsListWrapper: { flex: 1, }, listHeaderBack: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginHorizontal: 16, }, listHeaderText: { fontWeight: 'bold', fontSize: 24, marginVertical: 16, }, footerRoot: { top: 80, height: 160, marginBottom: 80, }, footerEmpty: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', }, footerStart: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', fontWeight: '600', }, transaction: { marginHorizontal: 0, }, });