/* global alert */ import React, { Component } from 'react'; import { Chain } from '../../models/bitcoinUnits'; import { Text, Platform, StyleSheet, View, Keyboard, ActivityIndicator, InteractionManager, FlatList, ScrollView, RefreshControl, TouchableOpacity, StatusBar, Linking, KeyboardAvoidingView, Alert, Clipboard, } from 'react-native'; import PropTypes from 'prop-types'; import { NavigationEvents } from 'react-navigation'; import ImagePicker from 'react-native-image-picker'; import { BlueSendButtonIcon, BlueListItem, BlueReceiveButtonIcon, BlueTransactionListItem, BlueWalletNavigationHeader, BlueAlertWalletExportReminder, } from '../../BlueComponents'; import WalletGradient from '../../class/walletGradient'; import { Icon } from 'react-native-elements'; import { LightningCustodianWallet, WatchOnlyWallet } from '../../class'; import Modal from 'react-native-modal'; import NavigationService from '../../NavigationService'; import HandoffSettings from '../../class/handoff'; import Handoff from 'react-native-handoff'; import ActionSheet from '../ActionSheet'; /** @type {AppStorage} */ let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); let EV = require('../../events'); let BlueElectrum = require('../../BlueElectrum'); const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); export default class WalletTransactions extends Component { static navigationOptions = ({ navigation }) => { return { headerRight: ( navigation.navigate('WalletDetails', { wallet: navigation.state.params.wallet, }) } > ), headerStyle: { backgroundColor: WalletGradient.headerColorFor(navigation.state.params.wallet.type), borderBottomWidth: 0, elevation: 0, shadowRadius: 0, }, headerTintColor: '#FFFFFF', }; }; walletBalanceText = null; constructor(props) { super(props); // here, when we receive REMOTE_TRANSACTIONS_COUNT_CHANGED we fetch TXs and balance for current wallet EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED, this.refreshTransactionsFunction.bind(this)); const wallet = props.navigation.getParam('wallet'); this.props.navigation.setParams({ wallet: wallet, isLoading: true }); this.state = { isHandOffUseEnabled: false, isLoading: true, isManageFundsModalVisible: false, showShowFlatListRefreshControl: false, wallet: wallet, dataSource: this.getTransactions(15), limit: 15, pageSize: 20, timeElapsed: 0, // this is to force a re-render for FlatList items. }; } async componentDidMount() { this.props.navigation.setParams({ isLoading: false }); this.interval = setInterval(() => { this.setState(prev => ({ timeElapsed: prev.timeElapsed + 1 })); }, 60000); const isHandOffUseEnabled = await HandoffSettings.isHandoffUseEnabled(); this.setState({ isHandOffUseEnabled }); } /** * Forcefully fetches TXs and balance for wallet */ refreshTransactionsFunction() { let that = this; setTimeout(function() { that.refreshTransactions(); }, 4000); // giving a chance to remote server to propagate } /** * Simple wrapper for `wallet.getTransactions()`, where `wallet` is current wallet. * Sorts. Provides limiting. * * @param limit {Integer} How many txs return, starting from the earliest. Default: all of them. * @returns {Array} */ getTransactions(limit = Infinity) { let wallet = this.props.navigation.getParam('wallet'); let txs = wallet.getTransactions(); for (let tx of txs) { tx.sort_ts = +new Date(tx.received); } txs = txs.sort(function(a, b) { return b.sort_ts - a.sort_ts; }); return txs.slice(0, limit); } redrawScreen() { InteractionManager.runAfterInteractions(async () => { console.log('wallets/transactions redrawScreen()'); this.setState({ isLoading: false, showShowFlatListRefreshControl: false, dataSource: this.getTransactions(this.state.limit), }); }); } isLightning() { let w = this.state.wallet; if (w && w.chain === Chain.OFFCHAIN) { return true; } return false; } /** * Forcefully fetches TXs and balance for wallet */ refreshTransactions() { if (this.state.isLoading) return; this.setState( { showShowFlatListRefreshControl: true, isLoading: true, }, async () => { let noErr = true; let smthChanged = false; try { await BlueElectrum.ping(); await BlueElectrum.waitTillConnected(); /** @type {LegacyWallet} */ let wallet = this.state.wallet; let balanceStart = +new Date(); const oldBalance = wallet.getBalance(); await wallet.fetchBalance(); if (oldBalance !== wallet.getBalance()) smthChanged = true; let balanceEnd = +new Date(); console.log(wallet.getLabel(), 'fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); let start = +new Date(); const oldTxLen = wallet.getTransactions().length; await wallet.fetchTransactions(); if (wallet.fetchPendingTransactions) { await wallet.fetchPendingTransactions(); } if (wallet.fetchUserInvoices) { await wallet.fetchUserInvoices(); } if (oldTxLen !== wallet.getTransactions().length) smthChanged = true; let end = +new Date(); console.log(wallet.getLabel(), 'fetch tx took', (end - start) / 1000, 'sec'); } catch (err) { noErr = false; alert(err.message); this.setState({ isLoading: false, showShowFlatListRefreshControl: false, }); } if (noErr && smthChanged) { console.log('saving to disk'); await BlueApp.saveToDisk(); // caching EV(EV.enum.TRANSACTIONS_COUNT_CHANGED); // let other components know they should redraw } this.redrawScreen(); }, ); } _keyExtractor = (_item, index) => index.toString(); renderListFooterComponent = () => { // if not all txs rendered - display indicator return (this.getTransactions(Infinity).length > this.state.limit && ) || ; }; renderListHeaderComponent = () => { return ( {/* So the idea here, due to Apple banning native Lapp marketplace, is: On Android everythins works as it worked before. Single "Marketplace" button that leads to LappBrowser that opens /marketplace/ url of offchain wallet type, and /marketplace-btc/ for onchain. On iOS its more complicated - we have one button that opens same page _externally_ (in Safari), and second button that opens actual LappBrowser but with _blank_ page. This is important to not trigger Apple. Blank page is also the way Trust Wallet does it with Dapp Browser. For ONCHAIN wallet type no LappBrowser button should be displayed, its Lightning-network specific. */} {this.renderMarketplaceButton()} {this.state.wallet.type === LightningCustodianWallet.type && Platform.OS === 'ios' && this.renderLappBrowserButton()} {this.state.wallet.allowHodlHodlTrading() && this.renderHodlHodlButton()} {loc.transactions.list.title} ); }; renderManageFundsModal = () => { return ( { Keyboard.dismiss(); this.setState({ isManageFundsModalVisible: false }); }} > { const wallets = [...BlueApp.getWallets().filter(item => item.chain === Chain.ONCHAIN && item.allowSend())]; if (wallets.length === 0) { alert('In order to proceed, please create a Bitcoin wallet to refill with.'); } else { this.setState({ isManageFundsModalVisible: false }); this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.ONCHAIN }); } }} title={loc.lnd.refill} /> { this.setState({ isManageFundsModalVisible: false }, () => this.props.navigation.navigate('ReceiveDetails', { secret: this.state.wallet.getSecret(), }), ); }} title={'Refill with External Wallet'} /> { this.setState({ isManageFundsModalVisible: false }, async () => { this.props.navigation.navigate('BuyBitcoin', { wallet: this.state.wallet, }); }); }} title={'Refill with bank card'} /> { this.setState({ isManageFundsModalVisible: false }); Linking.openURL('https://zigzag.io/?utm_source=integration&utm_medium=bluewallet&utm_campaign=withdrawLink'); }} /> ); }; renderMarketplaceButton = () => { return Platform.select({ android: ( { if (this.state.wallet.type === LightningCustodianWallet.type) { this.props.navigation.navigate('LappBrowser', { fromSecret: this.state.wallet.getSecret(), fromWallet: this.state.wallet }); } else { this.props.navigation.navigate('Marketplace', { fromWallet: this.state.wallet }); } }} style={{ backgroundColor: '#f2f2f2', borderRadius: 9, minHeight: 49, flex: 1, paddingHorizontal: 8, justifyContent: 'center', flexDirection: 'row', alignItems: 'center', }} > marketplace ), ios: this.state.wallet.getBalance() > 0 ? ( { if (this.state.wallet.type === LightningCustodianWallet.type) { Linking.openURL('https://bluewallet.io/marketplace/'); } else { let address = await this.state.wallet.getAddressAsync(); Linking.openURL('https://bluewallet.io/marketplace-btc/?address=' + address); } }} style={{ backgroundColor: '#f2f2f2', borderRadius: 9, minHeight: 49, flex: 1, paddingHorizontal: 8, justifyContent: 'center', flexDirection: 'row', alignItems: 'center', }} > marketplace ) : null, }); }; renderLappBrowserButton = () => { return ( { this.props.navigation.navigate('LappBrowser', { fromSecret: this.state.wallet.getSecret(), fromWallet: this.state.wallet, url: 'https://duckduckgo.com', }); }} style={{ marginLeft: 5, backgroundColor: '#f2f2f2', borderRadius: 9, minHeight: 49, flex: 1, paddingHorizontal: 8, justifyContent: 'center', flexDirection: 'row', alignItems: 'center', }} > LApp Browser ); }; renderHodlHodlButton = () => { return ( { this.props.navigation.navigate('HodlHodl', { fromWallet: this.state.wallet }); }} style={{ marginLeft: 5, backgroundColor: '#f2f2f2', borderRadius: 9, minHeight: 49, flex: 1, paddingHorizontal: 8, justifyContent: 'center', flexDirection: 'row', alignItems: 'center', }} > local trader ); }; onWalletSelect = async wallet => { if (wallet) { NavigationService.navigate('WalletTransactions', { key: `WalletTransactions-${wallet.getID()}`, }); /** @type {LightningCustodianWallet} */ let toAddress = false; if (this.state.wallet.refill_addressess.length > 0) { toAddress = this.state.wallet.refill_addressess[0]; } else { try { await this.state.wallet.fetchBtcAddress(); toAddress = this.state.wallet.refill_addressess[0]; } catch (Err) { return alert(Err.message); } } this.props.navigation.navigate('SendDetails', { memo: loc.lnd.refill_lnd_balance, fromSecret: wallet.getSecret(), address: toAddress, fromWallet: wallet, }); } }; onWillBlur() { StatusBar.setBarStyle('dark-content'); } componentWillUnmount() { this.onWillBlur(); clearInterval(this.interval); } navigateToSendScreen = () => { this.props.navigation.navigate('SendDetails', { fromAddress: this.state.wallet.getAddress(), fromSecret: this.state.wallet.getSecret(), fromWallet: this.state.wallet, }); }; renderItem = item => { return ( ); }; onBarCodeRead = ret => { if (!this.state.isLoading) { this.setState({ isLoading: true }, () => { this.setState({ isLoading: false }); this.props.navigation.navigate(this.state.wallet.chain === Chain.ONCHAIN ? 'SendDetails' : 'ScanLndInvoice', { fromSecret: this.state.wallet.getSecret(), uri: ret.data ? ret.data : ret, fromWallet: this.state.wallet, }); }); } }; 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.onBarCodeRead({ data: result }); } else { alert('The selected image does not contain a QR Code.'); } }); } }, ); }; copyFromClipbard = async () => { this.onBarCodeRead({ data: await Clipboard.getString() }); }; sendButtonLongPress = () => { if (Platform.OS === 'ios') { ActionSheet.showActionSheetWithOptions( { options: [loc.send.details.cancel, 'Choose Photo', 'Scan QR Code', 'Copy from Clipboard'], 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') { ActionSheet.showActionSheetWithOptions({ title: '', message: '', 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, }), }, { text: 'Copy From Clipboard', onPress: this.copyFromClipbard, }, ], }); } }; render() { const { navigate } = this.props.navigation; return ( {this.state.wallet.chain === Chain.ONCHAIN && this.state.isHandOffUseEnabled && ( )} { StatusBar.setBarStyle('light-content'); this.redrawScreen(); }} onWillBlur={() => this.onWillBlur()} onDidFocus={() => this.props.navigation.setParams({ isLoading: false })} /> InteractionManager.runAfterInteractions(async () => { this.setState({ wallet }, () => InteractionManager.runAfterInteractions(() => BlueApp.saveToDisk())); }) } onManageFundsPressed={() => { if (this.state.wallet.getUserHasSavedExport()) { this.setState({ isManageFundsModalVisible: true }); } else { BlueAlertWalletExportReminder({ onSuccess: async () => { this.state.wallet.setUserHasSavedExport(true); await BlueApp.saveToDisk(); this.setState({ isManageFundsModalVisible: true }); }, onFailure: () => this.props.navigation.navigate('WalletExport', { wallet: this.state.wallet, }), }); } }} /> { // pagination in works. in this block we will add more txs to flatlist // so as user scrolls closer to bottom it will render mode transactions if (this.getTransactions(Infinity).length < this.state.limit) { // all list rendered. nop return; } this.setState({ dataSource: this.getTransactions(this.state.limit + this.state.pageSize), limit: this.state.limit + this.state.pageSize, pageSize: this.state.pageSize * 2, }); }} ListFooterComponent={this.renderListFooterComponent} ListEmptyComponent={ {(this.isLightning() && loc.wallets.list.empty_txs1_lightning) || loc.wallets.list.empty_txs1} {(this.isLightning() && loc.wallets.list.empty_txs2_lightning) || loc.wallets.list.empty_txs2} {!this.isLightning() && ( this.props.navigation.navigate('BuyBitcoin', { wallet: this.state.wallet, }) } > {loc.wallets.list.tap_here_to_buy} )} } refreshControl={ this.refreshTransactions()} refreshing={this.state.showShowFlatListRefreshControl} /> } extraData={this.state.dataSource} data={this.state.dataSource} keyExtractor={this._keyExtractor} renderItem={this.renderItem} contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }} /> {this.renderManageFundsModal()} {(() => { if (this.state.wallet.allowReceive()) { return ( { if (this.state.wallet.chain === Chain.OFFCHAIN) { navigate('LNDCreateInvoice', { fromWallet: this.state.wallet }); } else { navigate('ReceiveDetails', { secret: this.state.wallet.getSecret() }); } }} /> ); } })()} {(() => { if ( this.state.wallet.allowSend() || (this.state.wallet.type === WatchOnlyWallet.type && this.state.wallet.isHd() && this.state.wallet.getSecret().startsWith('zpub')) ) { return ( { if (this.state.wallet.chain === Chain.OFFCHAIN) { navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() }); } else { if ( this.state.wallet.type === WatchOnlyWallet.type && this.state.wallet.isHd() && this.state.wallet.getSecret().startsWith('zpub') ) { if (this.state.wallet.useWithHardwareWalletEnabled()) { this.navigateToSendScreen(); } else { Alert.alert( 'Wallet', 'This wallet is not being used in conjunction with a hardwarde wallet. Would you like to enable hardware wallet use?', [ { text: loc._.ok, onPress: () => { const wallet = this.state.wallet; wallet.setUseWithHardwareWalletEnabled(true); this.setState({ wallet }, async () => { await BlueApp.saveToDisk(); this.navigateToSendScreen(); }); }, style: 'default', }, { text: loc.send.details.cancel, onPress: () => {}, style: 'cancel' }, ], { cancelable: false }, ); } } else { this.navigateToSendScreen(); } } }} /> ); } })()} ); } } const styles = StyleSheet.create({ modalContent: { backgroundColor: '#FFFFFF', padding: 22, justifyContent: 'center', alignItems: 'center', borderTopLeftRadius: 16, borderTopRightRadius: 16, borderColor: 'rgba(0, 0, 0, 0.1)', minHeight: 200, height: 200, }, advancedTransactionOptionsModalContent: { backgroundColor: '#FFFFFF', padding: 22, borderTopLeftRadius: 16, borderTopRightRadius: 16, borderColor: 'rgba(0, 0, 0, 0.1)', minHeight: 130, }, bottomModal: { justifyContent: 'flex-end', margin: 0, }, }); WalletTransactions.propTypes = { navigation: PropTypes.shape({ navigate: PropTypes.func, goBack: PropTypes.func, getParam: PropTypes.func, setParams: PropTypes.func, state: PropTypes.shape({ routeName: PropTypes.string, }), }), };