BlueWallet/screen/wallets/WalletsList.tsx

490 lines
15 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useEffect, useReducer, useRef } from 'react';
import {
View,
TouchableOpacity,
Text,
StyleSheet,
SectionList,
2020-09-07 20:46:37 +03:00
Image,
useWindowDimensions,
findNodeHandle,
2021-03-18 22:30:01 -04:00
I18nManager,
2024-02-15 18:35:21 -04:00
InteractionManager,
} from 'react-native';
import { Icon } from 'react-native-elements';
2020-10-15 19:25:24 +03:00
import WalletsCarousel from '../../components/WalletsCarousel';
2020-05-20 14:04:28 -04:00
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import ActionSheet from '../ActionSheet';
2020-07-20 16:38:46 +03:00
import loc from '../../loc';
2020-09-07 20:46:37 +03:00
import { FContainer, FButton } from '../../components/FloatButtons';
2024-03-24 10:52:10 -04:00
import { useFocusEffect, useIsFocused, useRoute } from '@react-navigation/native';
import { useStorage } from '../../blue_modules/storage-context';
2024-04-18 18:32:59 -04:00
import { isDesktop } from '../../blue_modules/environment';
import BlueClipboard from '../../blue_modules/clipboard';
import { TransactionListItem } from '../../components/TransactionListItem';
2023-10-17 09:35:10 -04:00
import { scanQrHelper } from '../../helpers/scan-qr';
2023-10-23 21:28:44 -04:00
import { useTheme } from '../../components/themes';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
2024-03-13 17:44:53 -04:00
import presentAlert from '../../components/Alert';
2024-03-24 10:52:10 -04:00
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import A from '../../blue_modules/analytics';
import * as fs from '../../blue_modules/fs';
2024-04-14 21:01:30 -04:00
import { TWallet, Transaction } from '../../class/wallets/types';
2024-04-18 18:31:06 -04:00
import { useIsLargeScreen } from '../../hooks/useIsLargeScreen';
2024-05-03 18:33:23 -04:00
import { Header } from '../../components/Header';
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;
}
}
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();
2024-03-24 10:52:10 -04:00
const { navigate, setOptions } = useExtendedNavigation();
2021-07-04 00:21:31 -04:00
const isFocused = useIsFocused();
const routeName = useRoute().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]);
const verifyBalance = () => {
if (getBalance() !== 0) {
A(A.ENUM.GOT_NONZERO_BALANCE);
} else {
A(A.ENUM.GOT_ZERO_BALANCE);
}
};
useEffect(() => {
2020-10-11 03:07:22 -04:00
setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
2021-03-18 22:30:01 -04:00
headerRight: () =>
I18nManager.isRTL ? null : (
<TouchableOpacity accessibilityRole="button" accessibilityLabel={loc._.more} testID="SettingsButton" onPress={navigateToSettings}>
2022-06-19 17:17:33 +01:00
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
2021-03-18 22:30:01 -04:00
</TouchableOpacity>
),
// eslint-disable-next-line react/no-unstable-nested-components
2021-03-18 22:30:01 -04:00
headerLeft: () =>
I18nManager.isRTL ? (
<TouchableOpacity accessibilityRole="button" accessibilityLabel={loc._.more} testID="SettingsButton" onPress={navigateToSettings}>
2022-06-19 17:17:33 +01:00
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
2021-03-18 22:30:01 -04:00
</TouchableOpacity>
) : null,
2020-10-11 03:07:22 -04:00
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [colors]);
const navigateToSettings = () => {
navigate('Settings');
};
2020-10-11 03:07:22 -04:00
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
*/
const refreshTransactions = async (showLoadingIndicator = true, showUpdateStatusIndicator = false) => {
2024-04-15 13:36:42 -04:00
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 });
});
};
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
}, []); // call refreshTransactions() only once, when screen mounts
2024-04-14 21:01:30 -04:00
const handleClick = (item?: TWallet) => {
if (item?.getID) {
const walletID = item.getID();
navigate('WalletTransactions', {
walletID,
walletType: item.type,
});
} else {
navigate('AddWalletRoot');
2018-06-24 23:22:46 +01:00
}
2020-05-17 08:17:08 -04:00
};
2018-06-24 23:22:46 +01:00
2024-04-15 13:36:42 -04:00
const setIsLoading = (value: boolean) => {
dispatch({ type: ActionTypes.SET_LOADING, payload: value });
};
2024-04-14 21:01:30 -04:00
const onSnapToItem = (e: { nativeEvent: { contentOffset: any } }) => {
2021-07-05 13:56:11 -04:00
if (!isFocused) return;
const contentOffset = e.nativeEvent.contentOffset;
const index = Math.ceil(contentOffset.x / width);
if (currentWalletIndex.current !== index) {
console.log('onSnapToItem', wallets.length === index ? 'NewWallet/Importing card' : index);
if (wallets[index] && (wallets[index].timeToRefreshBalance() || wallets[index].timeToRefreshTransaction())) {
console.log(wallets[index].getLabel(), 'thinks its time to refresh either balance or transactions. refetching both');
refreshAllWalletTransactions(index, false).finally(() => setIsLoading(false));
}
currentWalletIndex.current = index;
} else {
console.log('onSnapToItem did not change. Most likely momentum stopped at the same index it started.');
}
};
2018-09-18 03:24:42 -04:00
const renderListHeaderComponent = () => {
return (
<View style={[styles.listHeaderBack, stylesHook.listHeaderBack]}>
<Text textBreakStrategy="simple" style={[styles.listHeaderText, stylesHook.listHeaderText]}>
{`${loc.transactions.list_title}${' '}`}
</Text>
</View>
);
};
const handleLongPress = () => {
if (wallets.length > 1) {
navigate('ReorderWallets');
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
}
};
2024-04-14 21:01:30 -04:00
const renderTransactionListsRow = (data: { item: Transaction }) => {
return (
<View style={styles.transaction}>
2024-04-14 21:01:30 -04:00
{/** @ts-ignore: Fix later **/}
<TransactionListItem item={data.item} itemPriceUnit={data.item.walletPreferredBalanceUnit} walletID={data.item.walletID} />
</View>
);
2019-02-16 20:22:14 -05:00
};
2019-12-27 18:53:34 -06:00
const renderWalletsCarousel = () => {
2020-05-16 21:41:38 -04:00
return (
<WalletsCarousel
2024-04-14 21:01:30 -04:00
// @ts-ignore: Convert to TS later
data={wallets.concat(false)}
extraData={[wallets]}
onPress={handleClick}
handleLongPress={handleLongPress}
2021-06-16 02:05:04 -04:00
onMomentumScrollEnd={onSnapToItem}
ref={walletsCarousel}
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-04-14 21:01:30 -04:00
const renderSectionItem = (item: { section?: any; item?: Transaction }) => {
2020-05-16 21:41:38 -04:00
switch (item.section.key) {
case WalletsListSections.CAROUSEL:
return isLargeScreen ? null : renderWalletsCarousel();
2020-05-16 21:41:38 -04:00
case WalletsListSections.TRANSACTIONS:
2024-04-14 21:01:30 -04:00
/* @ts-ignore: fix later */
return renderTransactionListsRow(item);
2020-05-16 21:41:38 -04:00
default:
return null;
}
};
2024-04-14 21:01:30 -04:00
const renderSectionHeader = (section: { section: { key: any } }) => {
switch (section.section.key) {
2020-05-16 21:41:38 -04:00
case WalletsListSections.CAROUSEL:
2024-05-03 18:33:23 -04:00
return isLargeScreen ? null : <Header leftText={loc.wallets.list_title} onNewWalletPress={() => navigate('AddWalletRoot')} />;
2020-05-16 21:41:38 -04:00
case WalletsListSections.TRANSACTIONS:
return renderListHeaderComponent();
2020-05-16 21:41:38 -04:00
default:
return null;
2018-01-30 22:42:38 +00:00
}
2020-05-16 21:41:38 -04:00
};
2024-04-14 21:01:30 -04:00
const renderSectionFooter = (section: { section: { key: any } }) => {
switch (section.section.key) {
2020-05-17 08:17:08 -04:00
case WalletsListSections.TRANSACTIONS:
if (dataSource.length === 0 && !isLoading) {
2020-05-17 08:17:08 -04:00
return (
2021-03-02 16:38:02 +03:00
<View style={styles.footerRoot} testID="NoTransactionsMessage">
2020-07-20 16:38:46 +03:00
<Text style={styles.footerEmpty}>{loc.wallets.list_empty_txs1}</Text>
<Text style={styles.footerStart}>{loc.wallets.list_empty_txs2}</Text>
2020-05-17 08:17:08 -04:00
</View>
);
} else {
return null;
}
default:
return null;
}
};
const renderScanButton = () => {
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-04-14 21:01:30 -04:00
const sectionListKeyExtractor = (item: any, index: any) => {
2020-05-16 21:41:38 -04:00
return `${item}${index}}`;
};
const onScanButtonPressed = () => {
scanQrHelper(navigate, routeName).then(onBarScanned);
2020-05-20 14:04:28 -04:00
};
2024-04-14 21:01:30 -04:00
const onBarScanned = (value: any) => {
if (!value) return;
2020-05-20 14:04:28 -04:00
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
navigate(...completionValue);
2020-05-20 14:04:28 -04:00
});
};
const copyFromClipboard = async () => {
2023-03-29 20:46:11 -04:00
onBarScanned(await BlueClipboard().getClipboardContent());
};
const sendButtonLongPress = 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) {
options.push(anchor);
}
ActionSheet.showActionSheetWithOptions(props, buttonIndex => {
switch (buttonIndex) {
case 0:
break;
case 1:
fs.showImagePickerAndReadImage()
.then(onBarScanned)
.catch(error => {
console.log(error);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ title: loc.errors.error, message: error.message });
});
break;
case 2:
scanQrHelper(navigate, routeName, true).then(data => onBarScanned(data));
break;
case 3:
if (!isClipboardEmpty) {
copyFromClipboard();
}
break;
}
});
};
2021-01-27 19:30:42 -05:00
const onRefresh = () => {
refreshTransactions(true, false);
};
2024-02-16 10:42:21 -04:00
// Optimized for Mac option doesn't like RN Refresh component. Menu Elements now handles it for macOS
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}
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,
},
});