BlueWallet/screen/wallets/WalletsList.tsx

486 lines
15 KiB
TypeScript
Raw Normal View History

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';
2024-05-20 10:54:13 +01:00
import A from '../../blue_modules/analytics';
import BlueClipboard from '../../blue_modules/clipboard';
2024-05-20 10:54:13 +01:00
import { isDesktop } from '../../blue_modules/environment';
import * as fs from '../../blue_modules/fs';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
2024-05-20 10:54:13 +01:00
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import { ExtendedTransaction, Transaction, TWallet } from '../../class/wallets/types';
2024-03-13 17:44:53 -04:00
import presentAlert from '../../components/Alert';
2024-05-20 10:54:13 +01:00
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';
2024-04-18 18:31:06 -04:00
import { useIsLargeScreen } from '../../hooks/useIsLargeScreen';
2024-05-20 10:54:13 +01:00
import loc from '../../loc';
import ActionSheet from '../ActionSheet';
2024-05-22 20:35:36 +01:00
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
2024-05-31 13:18:01 -04:00
import { useStorage } from '../../hooks/context/useStorage';
2020-12-26 19:10:54 -05:00
2021-08-19 08:57:23 -04:00
const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' };
2024-04-14 21:01:30 -04:00
type SectionData = {
key: string;
data: Transaction[] | string[];
};
2024-04-15 13:36:42 -04:00
enum ActionTypes {
2024-04-18 18:32:59 -04:00
SET_LOADING,
SET_WALLETS,
SET_CURRENT_INDEX,
SET_REFRESH_FUNCTION,
2024-04-15 13:36:42 -04:00
}
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;
}
2024-04-18 18:31:06 -04:00
type WalletListAction = SetLoadingAction | SetWalletsAction | SetCurrentIndexAction | SetRefreshFunctionAction;
2024-04-15 13:36:42 -04:00
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;
}
}
2024-05-22 20:35:36 +01:00
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList, 'WalletsList'>;
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletsList'>;
2024-05-22 20:35:36 +01:00
2024-04-15 13:36:42 -04:00
const WalletsList: React.FC = () => {
const [state, dispatch] = useReducer<React.Reducer<WalletListState, WalletListAction>>(reducer, initialState);
2024-04-18 18:31:06 -04:00
const { isLoading } = state;
const isLargeScreen = useIsLargeScreen();
2024-04-14 21:01:30 -04:00
const walletsCarousel = useRef<any>();
const currentWalletIndex = useRef<number>(0);
const {
wallets,
getTransactions,
getBalance,
refreshAllWalletTransactions,
setSelectedWalletID,
isElectrumDisabled,
setReloadTransactionsMenuActionFunction,
} = useStorage();
const { width } = useWindowDimensions();
2023-10-20 13:47:37 -04:00
const { colors, scanImage } = useTheme();
const { navigate } = useExtendedNavigation<NavigationProps>();
2021-07-04 00:21:31 -04:00
const isFocused = useIsFocused();
const route = useRoute<RouteProps>();
const routeName = route.name;
2024-04-14 21:01:30 -04:00
const dataSource = getTransactions(undefined, 10);
const walletsCount = useRef<number>(wallets.length);
const walletActionButtonsRef = useRef<any>();
const stylesHook = StyleSheet.create({
walletsListWrapper: {
backgroundColor: colors.brandingColor,
},
listHeaderBack: {
backgroundColor: colors.background,
},
listHeaderText: {
color: colors.foregroundColor,
},
});
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
2024-02-15 18:35:21 -04:00
setReloadTransactionsMenuActionFunction(() => onRefresh);
verifyBalance();
setSelectedWalletID(undefined);
2024-02-15 18:35:21 -04:00
});
return () => {
task.cancel();
setReloadTransactionsMenuActionFunction(() => {});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []),
);
2018-01-30 22:42:38 +00:00
useEffect(() => {
// new wallet added
if (wallets.length > walletsCount.current) {
2021-06-16 02:05:04 -04:00
walletsCarousel.current?.scrollToItem({ item: wallets[walletsCount.current] });
2021-02-07 22:54:04 -05:00
}
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]);
2024-05-27 13:26:47 -04:00
const verifyBalance = useCallback(() => {
if (getBalance() !== 0) {
A(A.ENUM.GOT_NONZERO_BALANCE);
} else {
A(A.ENUM.GOT_ZERO_BALANCE);
}
2024-05-27 13:26:47 -04:00
}, [getBalance]);
2018-07-02 12:09:34 +01:00
/**
* Forcefully fetches TXs and balance for ALL wallets.
* Triggered manually by user on pull-to-refresh.
2018-07-02 12:09:34 +01:00
*/
2024-05-27 13:26:47 -04:00
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],
);
2018-06-24 23:22:46 +01:00
useEffect(() => {
2021-01-27 19:30:42 -05:00
refreshTransactions(false, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
2024-05-27 13:26:47 -04:00
}, []);
const handleClick = useCallback(
(item?: TWallet) => {
if (item?.getID) {
const walletID = item.getID();
navigate('WalletTransactions', {
walletID,
walletType: item.type,
});
} else {
navigate('AddWalletRoot');
}
},
[navigate],
);
2018-06-24 23:22:46 +01:00
2024-05-27 13:26:47 -04:00
const setIsLoading = useCallback((value: boolean) => {
2024-04-15 13:36:42 -04:00
dispatch({ type: ActionTypes.SET_LOADING, payload: value });
2024-05-27 13:26:47 -04:00
}, []);
2024-04-15 13:36:42 -04:00
2024-05-27 13:26:47 -04:00
const onSnapToItem = useCallback(
(e: { nativeEvent: { contentOffset: any } }) => {
if (!isFocused) return;
2021-07-05 13:56:11 -04:00
2024-05-27 13:26:47 -04:00
const contentOffset = e.nativeEvent.contentOffset;
const index = Math.ceil(contentOffset.x / width);
2024-05-27 13:26:47 -04:00
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;
}
2024-05-27 13:26:47 -04:00
},
[isFocused, refreshAllWalletTransactions, setIsLoading, wallets, width],
);
2018-09-18 03:24:42 -04:00
2024-05-27 13:26:47 -04:00
const renderListHeaderComponent = useCallback(() => {
return (
<View style={[styles.listHeaderBack, stylesHook.listHeaderBack]}>
<Text textBreakStrategy="simple" style={[styles.listHeaderText, stylesHook.listHeaderText]}>
{`${loc.transactions.list_title}${' '}`}
</Text>
</View>
);
2024-05-27 13:26:47 -04:00
}, [stylesHook.listHeaderBack, stylesHook.listHeaderText]);
2024-05-27 13:26:47 -04:00
const handleLongPress = useCallback(() => {
if (wallets.length > 1) {
2024-07-24 20:22:06 -04:00
navigate('ManageWalletsRoot');
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
}
2024-05-27 13:26:47 -04:00
}, [navigate, wallets.length]);
2024-05-27 13:26:47 -04:00
const renderTransactionListsRow = useCallback(
(item: ExtendedTransaction) => (
<View style={styles.transaction}>
2024-05-08 19:08:52 +01:00
<TransactionListItem item={item} itemPriceUnit={item.walletPreferredBalanceUnit} walletID={item.walletID} />
</View>
2024-05-27 13:26:47 -04:00
),
[],
);
2019-12-27 18:53:34 -06:00
2024-05-27 13:26:47 -04:00
const renderWalletsCarousel = useCallback(() => {
2020-05-16 21:41:38 -04:00
return (
<WalletsCarousel
2024-07-20 09:10:55 -04:00
data={wallets}
extraData={[wallets]}
onPress={handleClick}
handleLongPress={handleLongPress}
2021-06-16 02:05:04 -04:00
onMomentumScrollEnd={onSnapToItem}
ref={walletsCarousel}
2024-07-20 09:10:55 -04:00
onNewWalletPress={handleClick}
2020-05-21 11:36:46 -04:00
testID="WalletsList"
2021-06-15 23:21:28 -04:00
horizontal
2021-07-04 00:21:31 -04:00
scrollEnabled={isFocused}
2020-05-16 21:41:38 -04:00
/>
);
2024-05-27 13:26:47 -04:00
}, [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],
);
2020-05-16 21:41:38 -04:00
2024-05-27 13:26:47 -04:00
const renderSectionHeader = useCallback(
(section: { section: { key: any } }) => {
switch (section.section.key) {
case WalletsListSections.TRANSACTIONS:
return renderListHeaderComponent();
default:
return null;
}
},
[renderListHeaderComponent],
2024-05-27 13:26:47 -04:00
);
2020-05-16 21:41:38 -04:00
2024-05-27 13:26:47 -04:00
const renderSectionFooter = useCallback(
(section: { section: { key: any } }) => {
switch (section.section.key) {
case WalletsListSections.TRANSACTIONS:
if (dataSource.length === 0 && !isLoading) {
return (
<View style={styles.footerRoot} testID="NoTransactionsMessage">
<Text style={styles.footerEmpty}>{loc.wallets.list_empty_txs1}</Text>
<Text style={styles.footerStart}>{loc.wallets.list_empty_txs2}</Text>
</View>
);
} else {
return null;
}
default:
2020-05-17 08:17:08 -04:00
return null;
2024-05-27 13:26:47 -04:00
}
},
[dataSource.length, isLoading],
);
2020-05-17 08:17:08 -04:00
2024-05-27 13:26:47 -04:00
const renderScanButton = useCallback(() => {
if (wallets.length > 0) {
return (
2023-11-11 07:33:50 -04:00
<FContainer ref={walletActionButtonsRef.current}>
2020-09-07 20:46:37 +03:00
<FButton
onPress={onScanButtonPressed}
2023-02-25 12:24:06 -04:00
onLongPress={sendButtonLongPress}
icon={<Image resizeMode="stretch" source={scanImage} />}
2020-09-07 20:46:37 +03:00
text={loc.send.details_scan}
/>
</FContainer>
);
} else {
return null;
}
2024-05-27 13:26:47 -04:00
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scanImage, wallets.length]);
2024-04-14 21:01:30 -04:00
const sectionListKeyExtractor = (item: any, index: any) => {
2020-05-16 21:41:38 -04:00
return `${item}${index}}`;
};
2024-05-27 13:26:47 -04:00
const onScanButtonPressed = useCallback(() => {
scanQrHelper(routeName, true, undefined, false);
}, [routeName]);
2024-05-27 13:26:47 -04:00
const onBarScanned = useCallback(
(value: any) => {
if (!value) return;
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
// @ts-ignore: for now
navigate(...completionValue);
});
},
[navigate],
);
2020-05-20 14:04:28 -04:00
2024-05-27 13:26:47 -04:00
const copyFromClipboard = useCallback(async () => {
2023-03-29 20:46:11 -04:00
onBarScanned(await BlueClipboard().getClipboardContent());
2024-05-27 13:26:47 -04:00
}, [onBarScanned]);
2024-05-27 13:26:47 -04:00
const sendButtonLongPress = useCallback(async () => {
2023-03-29 20:46:11 -04:00
const isClipboardEmpty = (await BlueClipboard().getClipboardContent()).trim().length === 0;
2024-03-13 17:44:53 -04:00
const options = [loc._.cancel, loc.wallets.list_long_choose, loc.wallets.list_long_scan];
if (!isClipboardEmpty) {
options.push(loc.wallets.list_long_clipboard);
}
2024-03-13 17:44:53 -04:00
2024-04-14 21:01:30 -04:00
const props = { title: loc.send.header, options, cancelButtonIndex: 0 };
const anchor = findNodeHandle(walletActionButtonsRef.current);
if (anchor) {
2024-06-02 19:31:28 +01:00
options.push(String(anchor));
2024-04-14 21:01:30 -04:00
}
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);
2024-04-14 21:01:30 -04:00
break;
case 3:
if (!isClipboardEmpty) {
copyFromClipboard();
}
break;
}
});
2024-05-27 13:26:47 -04:00
}, [copyFromClipboard, onBarScanned, routeName]);
2024-05-27 13:26:47 -04:00
const onRefresh = useCallback(() => {
2021-01-27 19:30:42 -05:00
refreshTransactions(true, false);
2024-05-27 13:26:47 -04:00
// Optimized for Mac option doesn't like RN Refresh component. Menu Elements now handles it for macOS
}, [refreshTransactions]);
2024-02-16 10:13:52 -04:00
const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh };
2024-04-14 21:01:30 -04:00
const sections: SectionData[] = [
{ key: WalletsListSections.CAROUSEL, data: [WalletsListSections.CAROUSEL] },
{ key: WalletsListSections.TRANSACTIONS, data: dataSource },
];
return (
2024-04-18 18:31:06 -04:00
<View style={styles.root}>
2021-01-04 20:44:28 -05:00
<View style={[styles.walletsListWrapper, stylesHook.walletsListWrapper]}>
2024-04-14 21:01:30 -04:00
<SectionList<any | string, SectionData>
removeClippedSubviews
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
{...refreshProps}
2021-01-04 20:44:28 -05:00
renderItem={renderSectionItem}
keyExtractor={sectionListKeyExtractor}
renderSectionHeader={renderSectionHeader}
initialNumToRender={20}
contentInset={styles.scrollContent}
renderSectionFooter={renderSectionFooter}
2024-04-14 21:01:30 -04:00
sections={sections}
2024-05-27 13:26:47 -04:00
windowSize={21}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
2021-01-04 20:44:28 -05:00
/>
{renderScanButton()}
</View>
</View>
);
};
export default WalletsList;
2019-12-27 18:53:34 -06:00
const styles = StyleSheet.create({
2021-01-04 20:44:28 -05:00
root: {
flex: 1,
},
scrollContent: {
top: 0,
left: 0,
bottom: 60,
right: 0,
},
walletsListWrapper: {
flex: 1,
},
listHeaderBack: {
2020-08-20 18:50:38 -04:00
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginHorizontal: 16,
},
listHeaderText: {
fontWeight: 'bold',
fontSize: 24,
2021-08-19 15:27:57 +02:00
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,
},
});