BlueWallet/screen/wallets/list.js

496 lines
15 KiB
JavaScript
Raw Normal View History

import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import {
StatusBar,
View,
TouchableOpacity,
Text,
StyleSheet,
SectionList,
Platform,
2020-09-07 19:46:37 +02:00
Image,
Dimensions,
useWindowDimensions,
findNodeHandle,
2021-03-19 03:30:01 +01:00
I18nManager,
} from 'react-native';
import { BlueHeaderDefaultMain } from '../../BlueComponents';
2020-10-15 18:25:24 +02:00
import WalletsCarousel from '../../components/WalletsCarousel';
import { Icon } from 'react-native-elements';
2020-05-20 20:04:28 +02:00
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import ActionSheet from '../ActionSheet';
2020-07-20 15:38:46 +02:00
import loc from '../../loc';
2020-09-07 19:46:37 +02:00
import { FContainer, FButton } from '../../components/FloatButtons';
2021-07-04 06:21:31 +02:00
import { useFocusEffect, useIsFocused, useNavigation, useRoute, useTheme } from '@react-navigation/native';
import { BlueStorageContext } from '../../blue_modules/storage-context';
2021-05-29 07:57:40 +02:00
import { isDesktop, isMacCatalina, isTablet } from '../../blue_modules/environment';
import BlueClipboard from '../../blue_modules/clipboard';
2021-03-11 14:43:21 +01:00
import navigationStyle from '../../components/navigationStyle';
import { TransactionListItem } from '../../components/TransactionListItem';
2020-12-27 01:10:54 +01:00
const scanqrHelper = require('../../helpers/scan-qr');
const A = require('../../blue_modules/analytics');
2020-12-15 05:11:05 +01:00
const fs = require('../../blue_modules/fs');
2021-08-19 14:57:23 +02:00
const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' };
const WalletsList = () => {
const walletsCarousel = useRef();
const currentWalletIndex = useRef(0);
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWallet, isElectrumDisabled } = useContext(
BlueStorageContext,
);
const { width } = useWindowDimensions();
2021-09-27 18:40:58 +02:00
const { colors, scanImage, barStyle } = useTheme();
2020-10-11 09:07:22 +02:00
const { navigate, setOptions } = useNavigation();
2021-07-04 06:21:31 +02:00
const isFocused = useIsFocused();
const routeName = useRoute().name;
const [isLoading, setIsLoading] = useState(false);
const [isLargeScreen, setIsLargeScreen] = useState(
Platform.OS === 'android' ? isTablet() : (width >= Dimensions.get('screen').width / 2 && isTablet()) || isDesktop,
);
const dataSource = getTransactions(null, 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,
},
ltRoot: {
backgroundColor: colors.ballOutgoingExpired,
},
ltTextBig: {
color: colors.foregroundColor,
},
ltTextSmall: {
color: colors.alternativeTextColor,
},
});
useFocusEffect(
useCallback(() => {
verifyBalance();
setSelectedWallet('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []),
);
2018-01-30 23:42:38 +01:00
useEffect(() => {
// new wallet added
if (wallets.length > walletsCount.current) {
2021-06-16 08:05:04 +02:00
walletsCarousel.current?.scrollToItem({ item: wallets[walletsCount.current] });
2021-02-08 04:54:04 +01:00
}
// wallet has been deleted
if (wallets.length < walletsCount.current) {
walletsCarousel.current?.scrollToItem({ item: false });
}
walletsCount.current = wallets.length;
}, [wallets]);
const verifyBalance = () => {
if (getBalance() !== 0) {
A(A.ENUM.GOT_NONZERO_BALANCE);
} else {
A(A.ENUM.GOT_ZERO_BALANCE);
}
};
2020-10-11 09:07:22 +02:00
useEffect(() => {
setOptions({
2021-05-29 07:57:40 +02:00
headerShown: !isDesktop,
2020-10-11 09:07:22 +02:00
headerStyle: {
2021-09-14 07:36:00 +02:00
backgroundColor: colors.customHeader,
2020-10-11 09:07:22 +02:00
borderBottomWidth: 0,
elevation: 0,
shadowOpacity: 0,
shadowOffset: { height: 0, width: 0 },
},
2021-03-19 03:30:01 +01:00
headerRight: () =>
I18nManager.isRTL ? null : (
<TouchableOpacity accessibilityRole="button" testID="SettingsButton" style={styles.headerTouch} onPress={navigateToSettings}>
2021-03-19 03:30:01 +01:00
<Icon size={22} name="kebab-horizontal" type="octicon" color={colors.foregroundColor} />
</TouchableOpacity>
),
headerLeft: () =>
I18nManager.isRTL ? (
<TouchableOpacity accessibilityRole="button" testID="SettingsButton" style={styles.headerTouch} onPress={navigateToSettings}>
2021-03-19 03:30:01 +01:00
<Icon size={22} name="kebab-horizontal" type="octicon" color={colors.foregroundColor} />
</TouchableOpacity>
) : null,
2020-10-11 09:07:22 +02:00
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [colors]);
const navigateToSettings = () => {
navigate('Settings');
};
2018-07-02 13:09:34 +02:00
/**
* Forcefully fetches TXs and balance for ALL wallets.
* Triggered manually by user on pull-to-refresh.
2018-07-02 13:09:34 +02:00
*/
const refreshTransactions = async (showLoadingIndicator = true, showUpdateStatusIndicator = false) => {
2021-08-24 06:33:32 +02:00
if (isElectrumDisabled) return setIsLoading(false);
setIsLoading(showLoadingIndicator);
2021-01-28 01:30:42 +01:00
refreshAllWalletTransactions(showLoadingIndicator, showUpdateStatusIndicator).finally(() => setIsLoading(false));
};
2018-06-25 00:22:46 +02:00
useEffect(() => {
2021-01-28 01:30:42 +01:00
refreshTransactions(false, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // call refreshTransactions() only once, when screen mounts
const handleClick = index => {
console.log('click', index);
if (index <= wallets.length - 1) {
const wallet = wallets[index];
const walletID = wallet.getID();
navigate('WalletTransactions', {
walletID,
walletType: wallet.type,
key: `WalletTransactions-${walletID}`,
});
} else if (index >= wallets.length) {
navigate('AddWalletRoot');
2018-06-25 00:22:46 +02:00
}
2020-05-17 14:17:08 +02:00
};
2018-06-25 00:22:46 +02:00
2021-06-16 08:05:04 +02:00
const onSnapToItem = e => {
2021-07-05 19:56:11 +02: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 09:24:42 +02:00
const renderListHeaderComponent = () => {
const style = { opacity: isLoading ? 1.0 : 0.5 };
return (
<View style={[styles.listHeaderBack, stylesHook.listHeaderBack]}>
<Text textBreakStrategy="simple" style={[styles.listHeaderText, stylesHook.listHeaderText]}>
{`${loc.transactions.list_title}${' '}`}
</Text>
2021-05-29 07:57:40 +02:00
{isDesktop && (
<TouchableOpacity accessibilityRole="button" style={style} onPress={() => refreshTransactions(true)} disabled={isLoading}>
<Icon name="refresh" type="font-awesome" color={colors.feeText} />
2020-08-21 00:50:38 +02:00
</TouchableOpacity>
)}
</View>
);
};
const handleLongPress = () => {
if (wallets.length > 1) {
navigate('ReorderWallets');
} else {
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
}
};
const renderTransactionListsRow = data => {
return (
<View style={styles.transaction}>
<TransactionListItem item={data.item} itemPriceUnit={data.item.walletPreferredBalanceUnit} />
</View>
);
2019-02-17 02:22:14 +01:00
};
2019-12-28 01:53:34 +01:00
const renderWalletsCarousel = () => {
2020-05-17 03:41:38 +02:00
return (
<WalletsCarousel
data={wallets.concat(false)}
extraData={[wallets]}
onPress={handleClick}
handleLongPress={handleLongPress}
2021-06-16 08:05:04 +02:00
onMomentumScrollEnd={onSnapToItem}
ref={walletsCarousel}
2020-05-21 17:36:46 +02:00
testID="WalletsList"
2021-06-16 05:21:28 +02:00
horizontal
2021-07-04 06:21:31 +02:00
scrollEnabled={isFocused}
2020-05-17 03:41:38 +02:00
/>
);
};
const renderSectionItem = item => {
2020-05-17 03:41:38 +02:00
switch (item.section.key) {
case WalletsListSections.CAROUSEL:
return isLargeScreen ? null : renderWalletsCarousel();
2020-05-17 03:41:38 +02:00
case WalletsListSections.TRANSACTIONS:
return renderTransactionListsRow(item);
2020-05-17 03:41:38 +02:00
default:
return null;
}
};
const renderSectionHeader = section => {
switch (section.section.key) {
2020-05-17 03:41:38 +02:00
case WalletsListSections.CAROUSEL:
return isLargeScreen ? null : (
<BlueHeaderDefaultMain leftText={loc.wallets.list_title} onNewWalletPress={() => navigate('AddWalletRoot')} />
2020-05-17 03:41:38 +02:00
);
case WalletsListSections.TRANSACTIONS:
return renderListHeaderComponent();
2020-05-17 03:41:38 +02:00
default:
return null;
2018-01-30 23:42:38 +01:00
}
2020-05-17 03:41:38 +02:00
};
const renderSectionFooter = section => {
switch (section.section.key) {
2020-05-17 14:17:08 +02:00
case WalletsListSections.TRANSACTIONS:
if (dataSource.length === 0 && !isLoading) {
2020-05-17 14:17:08 +02:00
return (
2021-03-02 14:38:02 +01:00
<View style={styles.footerRoot} testID="NoTransactionsMessage">
2020-07-20 15:38:46 +02:00
<Text style={styles.footerEmpty}>{loc.wallets.list_empty_txs1}</Text>
<Text style={styles.footerStart}>{loc.wallets.list_empty_txs2}</Text>
2020-05-17 14:17:08 +02:00
</View>
);
} else {
return null;
}
default:
return null;
}
};
const renderScanButton = () => {
if (wallets.length > 0) {
return (
<FContainer ref={walletActionButtonsRef}>
2020-09-07 19:46:37 +02:00
<FButton
onPress={onScanButtonPressed}
onLongPress={isMacCatalina ? undefined : sendButtonLongPress}
icon={<Image resizeMode="stretch" source={scanImage} />}
2020-09-07 19:46:37 +02:00
text={loc.send.details_scan}
/>
</FContainer>
);
} else {
return null;
}
};
const sectionListKeyExtractor = (item, index) => {
2020-05-17 03:41:38 +02:00
return `${item}${index}}`;
};
const onScanButtonPressed = () => {
if (isMacCatalina) {
fs.showActionSheet({ anchor: findNodeHandle(walletActionButtonsRef.current) }).then(onBarScanned);
} else {
scanqrHelper(navigate, routeName, false).then(onBarScanned);
}
2020-05-20 20:04:28 +02:00
};
const onBarScanned = value => {
if (!value) return;
2020-05-20 20:04:28 +02:00
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false });
navigate(...completionValue);
2020-05-20 20:04:28 +02:00
});
};
const copyFromClipboard = async () => {
onBarScanned(await BlueClipboard.getClipboardContent());
};
const sendButtonLongPress = async () => {
const isClipboardEmpty = (await BlueClipboard.getClipboardContent()).trim().length === 0;
if (Platform.OS === 'ios') {
if (isMacCatalina) {
fs.showActionSheet({ anchor: findNodeHandle(walletActionButtonsRef.current) }).then(onBarScanned);
} else {
2020-12-20 08:03:22 +01:00
const options = [loc._.cancel, loc.wallets.list_long_choose, loc.wallets.list_long_scan];
if (!isClipboardEmpty) {
options.push(loc.wallets.list_long_clipboard);
}
ActionSheet.showActionSheetWithOptions(
{ options, cancelButtonIndex: 0, anchor: findNodeHandle(walletActionButtonsRef.current) },
buttonIndex => {
if (buttonIndex === 1) {
fs.showImagePickerAndReadImage().then(onBarScanned);
} else if (buttonIndex === 2) {
scanqrHelper(navigate, routeName, false).then(onBarScanned);
} else if (buttonIndex === 3) {
copyFromClipboard();
}
},
);
}
} else if (Platform.OS === 'android') {
const buttons = [
{
2020-07-20 15:38:46 +02:00
text: loc._.cancel,
onPress: () => {},
style: 'cancel',
},
{
2020-07-20 15:38:46 +02:00
text: loc.wallets.list_long_choose,
2021-04-28 16:27:45 +02:00
onPress: () => fs.showImagePickerAndReadImage().then(onBarScanned),
},
{
2020-07-20 15:38:46 +02:00
text: loc.wallets.list_long_scan,
onPress: () => scanqrHelper(navigate, routeName, false).then(onBarScanned),
},
];
if (!isClipboardEmpty) {
buttons.push({
2020-07-20 15:38:46 +02:00
text: loc.wallets.list_long_clipboard,
onPress: copyFromClipboard,
});
}
ActionSheet.showActionSheetWithOptions({
title: '',
message: '',
buttons,
});
}
};
const onLayout = _e => {
setIsLargeScreen(Platform.OS === 'android' ? isTablet() : (width >= Dimensions.get('screen').width / 2 && isTablet()) || isDesktop);
};
2021-01-28 01:30:42 +01:00
const onRefresh = () => {
refreshTransactions(true, false);
};
return (
2021-01-05 02:44:28 +01:00
<View style={styles.root} onLayout={onLayout}>
2021-09-27 18:40:58 +02:00
<StatusBar barStyle={barStyle} backgroundColor="transparent" translucent animated />
2021-01-05 02:44:28 +01:00
<View style={[styles.walletsListWrapper, stylesHook.walletsListWrapper]}>
<SectionList
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
2021-01-05 02:44:28 +01:00
refreshing={isLoading}
2021-08-24 06:33:32 +02:00
{...(isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh: onRefresh })}
2021-01-05 02:44:28 +01:00
renderItem={renderSectionItem}
keyExtractor={sectionListKeyExtractor}
renderSectionHeader={renderSectionHeader}
initialNumToRender={20}
contentInset={styles.scrollContent}
renderSectionFooter={renderSectionFooter}
sections={[
{ key: WalletsListSections.CAROUSEL, data: [WalletsListSections.CAROUSEL] },
{ key: WalletsListSections.TRANSACTIONS, data: dataSource },
]}
/>
{renderScanButton()}
</View>
</View>
);
};
export default WalletsList;
2021-09-14 07:36:00 +02:00
WalletsList.navigationOptions = navigationStyle({}, opts => ({ ...opts, headerTitle: '', headerBackTitle: loc.wallets.list_title }));
2019-12-28 01:53:34 +01:00
const styles = StyleSheet.create({
2021-01-05 02:44:28 +01:00
root: {
flex: 1,
},
scrollContent: {
top: 0,
left: 0,
bottom: 60,
right: 0,
},
wrapper: {
flex: 1,
},
walletsListWrapper: {
flex: 1,
},
headerStyle: {
...Platform.select({
ios: {
marginTop: 44,
height: 32,
alignItems: 'flex-end',
justifyContent: 'center',
},
android: {
marginTop: 8,
height: 44,
alignItems: 'flex-end',
justifyContent: 'center',
},
}),
},
headerTouch: {
height: 48,
paddingVertical: 10,
},
listHeaderBack: {
2020-08-21 00:50:38 +02: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,
},
ltRoot: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginHorizontal: 16,
marginVertical: 16,
padding: 16,
borderRadius: 6,
},
ltTextWrap: {
flexDirection: 'column',
},
ltTextBig: {
fontSize: 16,
fontWeight: '600',
2021-05-22 05:36:34 +02:00
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
},
ltTextSmall: {
fontSize: 13,
fontWeight: '500',
2021-05-22 05:36:34 +02:00
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
},
footerRoot: {
top: 80,
height: 160,
marginBottom: 80,
},
footerEmpty: {
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
},
footerStart: {
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
fontWeight: '600',
},
listHeader: {
backgroundColor: '#FFFFFF',
},
transaction: {
marginHorizontal: 0,
},
});