/* global alert */ import React, { Component } from 'react'; import { StatusBar, View, TouchableOpacity, Text, StyleSheet, InteractionManager, Clipboard, RefreshControl, SectionList, Alert, Platform, } from 'react-native'; import { BlueScanButton, WalletsCarousel, BlueHeaderDefaultMain, BlueTransactionListItem } from '../../BlueComponents'; import { Icon } from 'react-native-elements'; import { NavigationEvents } from 'react-navigation'; import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import PropTypes from 'prop-types'; import { PlaceholderWallet } from '../../class'; import WalletImport from '../../class/walletImport'; import ActionSheet from '../ActionSheet'; import ImagePicker from 'react-native-image-picker'; const EV = require('../../events'); const A = require('../../analytics'); /** @type {AppStorage} */ const BlueApp = require('../../BlueApp'); const loc = require('../../loc'); const BlueElectrum = require('../../BlueElectrum'); const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); const WalletsListSections = { CAROUSEL: 'CAROUSEL', LOCALTRADER: 'LOCALTRADER', TRANSACTIONS: 'TRANSACTIONS' }; export default class WalletsList extends Component { walletsCarousel = React.createRef(); constructor(props) { super(props); this.state = { isLoading: true, isFlatListRefreshControlHidden: true, wallets: BlueApp.getWallets().concat(false), timeElpased: 0, dataSource: [], }; EV(EV.enum.WALLETS_COUNT_CHANGED, () => this.redrawScreen(true)); // here, when we receive TRANSACTIONS_COUNT_CHANGED we do not query // remote server, we just redraw the screen EV(EV.enum.TRANSACTIONS_COUNT_CHANGED, this.redrawScreen); } componentDidMount() { // the idea is that upon wallet launch we will refresh // all balances and all transactions here: this.redrawScreen(); InteractionManager.runAfterInteractions(async () => { try { await BlueElectrum.waitTillConnected(); let balanceStart = +new Date(); await BlueApp.fetchWalletBalances(); let balanceEnd = +new Date(); console.log('fetch all wallet balances took', (balanceEnd - balanceStart) / 1000, 'sec'); let start = +new Date(); await BlueApp.fetchWalletTransactions(); let end = +new Date(); console.log('fetch all wallet txs took', (end - start) / 1000, 'sec'); } catch (error) { console.log(error); } }); } /** * Forcefully fetches TXs and balance for lastSnappedTo (i.e. current) wallet. * Triggered manually by user on pull-to-refresh. */ refreshTransactions = () => { this.setState( { isFlatListRefreshControlHidden: false, }, () => { InteractionManager.runAfterInteractions(async () => { let noErr = true; try { await BlueElectrum.ping(); await BlueElectrum.waitTillConnected(); let balanceStart = +new Date(); await BlueApp.fetchWalletBalances(this.walletsCarousel.current.currentIndex || 0); let balanceEnd = +new Date(); console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); let start = +new Date(); await BlueApp.fetchWalletTransactions(this.walletsCarousel.current.currentIndex || 0); let end = +new Date(); console.log('fetch tx took', (end - start) / 1000, 'sec'); } catch (err) { noErr = false; console.warn(err); } if (noErr) await BlueApp.saveToDisk(); // caching this.redrawScreen(); }); }, ); }; redrawScreen = (scrollToEnd = false) => { console.log('wallets/list redrawScreen()'); if (BlueApp.getBalance() !== 0) { A(A.ENUM.GOT_NONZERO_BALANCE); } else { A(A.ENUM.GOT_ZERO_BALANCE); } const wallets = BlueApp.getWallets().concat(false); if (scrollToEnd) { scrollToEnd = wallets.length > this.state.wallets.length; } this.setState( { isLoading: false, isFlatListRefreshControlHidden: true, dataSource: BlueApp.getTransactions(null, 10), wallets: BlueApp.getWallets().concat(false), }, () => { if (scrollToEnd) { this.walletsCarousel.current.snapToItem(this.state.wallets.length - 2); } }, ); }; txMemo(hash) { if (BlueApp.tx_metadata[hash] && BlueApp.tx_metadata[hash]['memo']) { return BlueApp.tx_metadata[hash]['memo']; } return ''; } handleClick = index => { console.log('click', index); let wallet = BlueApp.wallets[index]; if (wallet) { if (wallet.type === PlaceholderWallet.type) { Alert.alert( loc.wallets.add.details, 'There was a problem importing this wallet.', [ { text: loc.wallets.details.delete, onPress: () => { WalletImport.removePlaceholderWallet(); EV(EV.enum.WALLETS_COUNT_CHANGED); }, style: 'destructive', }, { text: 'Try Again', onPress: () => { this.props.navigation.navigate('ImportWallet', { label: wallet.getSecret() }); WalletImport.removePlaceholderWallet(); EV(EV.enum.WALLETS_COUNT_CHANGED); }, style: 'default', }, ], { cancelable: false }, ); } else { this.props.navigation.navigate('WalletTransactions', { wallet: wallet, key: `WalletTransactions-${wallet.getID()}`, }); } } else { // if its out of index - this must be last card with incentive to create wallet if (!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) { this.props.navigation.navigate('AddWallet'); } } }; onSnapToItem = index => { console.log('onSnapToItem', index); if (index < BlueApp.getWallets().length) { // not the last } if (this.state.wallets[index].type === PlaceholderWallet.type) { return; } // now, lets try to fetch balance and txs for this wallet in case it has changed this.lazyRefreshWallet(index); }; /** * Decides whether wallet with such index shoud be refreshed, * refreshes if yes and redraws the screen * @param index {Integer} Index of the wallet. * @return {Promise.} */ async lazyRefreshWallet(index) { /** @type {Array.} wallets */ let wallets = BlueApp.getWallets(); if (!wallets[index]) { return; } let oldBalance = wallets[index].getBalance(); let noErr = true; let didRefresh = false; try { if (wallets && wallets[index] && wallets[index].type !== PlaceholderWallet.type && wallets[index].timeToRefreshBalance()) { console.log('snapped to, and now its time to refresh wallet #', index); await wallets[index].fetchBalance(); if (oldBalance !== wallets[index].getBalance() || wallets[index].getUnconfirmedBalance() !== 0) { console.log('balance changed, thus txs too'); // balance changed, thus txs too await wallets[index].fetchTransactions(); this.redrawScreen(); didRefresh = true; } else if (wallets[index].timeToRefreshTransaction()) { console.log(wallets[index].getLabel(), 'thinks its time to refresh TXs'); await wallets[index].fetchTransactions(); if (wallets[index].fetchPendingTransactions) { await wallets[index].fetchPendingTransactions(); } if (wallets[index].fetchUserInvoices) { await wallets[index].fetchUserInvoices(); await wallets[index].fetchBalance(); // chances are, paid ln invoice was processed during `fetchUserInvoices()` call and altered user's balance, so its worth fetching balance again } this.redrawScreen(); didRefresh = true; } else { console.log('balance not changed'); } } } catch (Err) { noErr = false; console.warn(Err); } if (noErr && didRefresh) { await BlueApp.saveToDisk(); // caching } } _keyExtractor = (_item, index) => index.toString(); renderListHeaderComponent = () => { return ( {loc.transactions.list.title} ); }; handleLongPress = () => { if (BlueApp.getWallets().length > 1 && !BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) { this.props.navigation.navigate('ReorderWallets'); } else { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); } }; renderTransactionListsRow = data => { return ( ); }; renderNavigationHeader = () => { return ( this.props.navigation.navigate('Settings')} > ); }; renderLocalTrader = () => { if (BlueApp.getWallets().length > 0 && !BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) { return ( { this.props.navigation.navigate('HodlHodl', { fromWallet: this.state.wallet }); }} style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginHorizontal: 16, marginVertical: 16, backgroundColor: '#eef0f4', padding: 16, borderRadius: 6, }} > Local Trader A p2p exchange New ); } else { return null; } }; renderWalletsCarousel = () => { return ( ); }; renderSectionItem = item => { switch (item.section.key) { case WalletsListSections.CAROUSEL: return this.renderWalletsCarousel(); case WalletsListSections.LOCALTRADER: return this.renderLocalTrader(); case WalletsListSections.TRANSACTIONS: return this.renderTransactionListsRow(item); default: return null; } }; renderSectionHeader = ({ section }) => { switch (section.key) { case WalletsListSections.CAROUSEL: return ( wallet.type === PlaceholderWallet.type) ? () => this.props.navigation.navigate('AddWallet') : null } /> ); case WalletsListSections.TRANSACTIONS: return this.renderListHeaderComponent(); default: return null; } }; renderSectionFooter = ({ section }) => { switch (section.key) { case WalletsListSections.TRANSACTIONS: if (this.state.dataSource.length === 0 && !this.state.isLoading) { return ( {loc.wallets.list.empty_txs1} {loc.wallets.list.empty_txs2} ); } else { return null; } default: return null; } }; renderScanButton = () => { if (BlueApp.getWallets().length > 0 && !BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) { return ( ); } else { return null; } }; sectionListKeyExtractor = (item, index) => { return `${item}${index}}`; }; onScanButtonPressed = () => { this.props.navigation.navigate('ScanQRCode', { launchedBy: this.props.navigation.state.routeName, onBarScanned: this.onBarScanned, showFileImportButton: false, }); }; onBarScanned = value => { DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false }); this.props.navigation.navigate(completionValue); }); }; onNavigationEventDidFocus = () => { StatusBar.setBarStyle('dark-content'); this.redrawScreen(); }; choosePhoto = () => { ImagePicker.launchImageLibrary( { title: null, mediaType: 'photo', takePhotoButtonTitle: null, }, response => { if (response.uri) { const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString(); LocalQRCode.decode(uri, (error, result) => { if (!error) { this.onBarScanned(result); } else { alert('The selected image does not contain a QR Code.'); } }); } }, ); }; copyFromClipbard = async () => { this.onBarScanned(await Clipboard.getString()); }; sendButtonLongPress = async () => { const isClipboardEmpty = (await Clipboard.getString()).replace(' ', '').length === 0; if (Platform.OS === 'ios') { let options = [loc.send.details.cancel, 'Choose Photo', 'Scan QR Code']; if (!isClipboardEmpty) { options.push('Copy from Clipboard'); } ActionSheet.showActionSheetWithOptions({ options, cancelButtonIndex: 0 }, buttonIndex => { if (buttonIndex === 1) { this.choosePhoto(); } else if (buttonIndex === 2) { this.props.navigation.navigate('ScanQRCode', { launchedBy: this.props.navigation.state.routeName, onBarScanned: this.onBarCodeRead, showFileImportButton: false, }); } else if (buttonIndex === 3) { this.copyFromClipbard(); } }); } else if (Platform.OS === 'android') { let buttons = [ { text: loc.send.details.cancel, onPress: () => {}, style: 'cancel', }, { text: 'Choose Photo', onPress: this.choosePhoto, }, { text: 'Scan QR Code', onPress: () => this.props.navigation.navigate('ScanQRCode', { launchedBy: this.props.navigation.state.routeName, onBarScanned: this.onBarCodeRead, showFileImportButton: false, }), }, ]; if (!isClipboardEmpty) { buttons.push({ text: 'Copy From Clipboard', onPress: this.copyFromClipbard, }); } ActionSheet.showActionSheetWithOptions({ title: '', message: '', buttons, }); } }; render() { return ( {this.renderNavigationHeader()} } renderItem={this.renderSectionItem} keyExtractor={this.sectionListKeyExtractor} renderSectionHeader={this.renderSectionHeader} contentInset={{ top: 0, left: 0, bottom: 60, right: 0 }} renderSectionFooter={this.renderSectionFooter} sections={[ { key: WalletsListSections.CAROUSEL, data: [WalletsListSections.CAROUSEL] }, { key: WalletsListSections.LOCALTRADER, data: [WalletsListSections.LOCALTRADER] }, { key: WalletsListSections.TRANSACTIONS, data: this.state.dataSource }, ]} /> {this.renderScanButton()} ); } } const styles = StyleSheet.create({ wrapper: { backgroundColor: '#FFFFFF', flex: 1, }, walletsListWrapper: { flex: 1, backgroundColor: '#FFFFFF', }, headerStyle: { ...Platform.select({ ios: { marginTop: 44, height: 32, alignItems: 'flex-end', justifyContent: 'center', }, android: { marginTop: 8, height: 44, alignItems: 'flex-end', justifyContent: 'center', }, }), }, }); WalletsList.propTypes = { navigation: PropTypes.shape({ state: PropTypes.shape({ routeName: PropTypes.string, }), navigate: PropTypes.func, }), };