/* global alert */ import React, { Component } from 'react'; import { Chain } from '../../models/bitcoinUnits'; import { Text, Platform, StyleSheet, View, Keyboard, ActivityIndicator, InteractionManager, FlatList, RefreshControl, TouchableOpacity, StatusBar, Linking, KeyboardAvoidingView, } from 'react-native'; import PropTypes from 'prop-types'; import { NavigationEvents } from 'react-navigation'; import { BlueSendButtonIcon, BlueListItem, BlueReceiveButtonIcon, BlueTransactionListItem, BlueWalletNavigationHeader, } from '../../BlueComponents'; import { Icon } from 'react-native-elements'; import { LightningCustodianWallet } from '../../class'; import Handoff from 'react-native-handoff'; import { ScrollView } from 'react-native-gesture-handler'; import Modal from 'react-native-modal'; import NavigationService from '../../NavigationService'; /** @type {AppStorage} */ let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); let EV = require('../../events'); let BlueElectrum = require('../../BlueElectrum'); export default class WalletTransactions extends Component { static navigationOptions = ({ navigation }) => { return { headerRight: ( navigation.navigate('WalletDetails', { wallet: navigation.state.params.wallet, }) } > ), headerStyle: { backgroundColor: navigation.getParam('headerColor'), 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 = { isLoading: true, isManageFundsModalVisible: false, showShowFlatListRefreshControl: false, wallet: wallet, dataSource: this.getTransactions(15), limit: 15, pageSize: 20, }; } componentDidMount() { this.props.navigation.setParams({ isLoading: false }); } /** * 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.type === LightningCustodianWallet.type) { 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 (oldTxLen !== wallet.getTransactions().length) smthChanged = true; if (wallet.fetchPendingTransactions) { await wallet.fetchPendingTransactions(); } if (wallet.fetchUserInvoices) { await wallet.fetchUserInvoices(); } 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 ( {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 }); 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: '', }); }} style={{ marginLeft: 5, backgroundColor: '#f2f2f2', borderRadius: 9, minHeight: 49, flex: 1, paddingHorizontal: 8, justifyContent: 'center', flexDirection: 'row', alignItems: 'center', }} > LApp Browser ); }; onWalletSelect = async wallet => { NavigationService.navigate('WalletTransactions'); /** @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); } } if (wallet) { this.props.navigation.navigate('SendDetails', { memo: loc.lnd.refill_lnd_balance, fromSecret: wallet.getSecret(), address: toAddress, fromWallet: wallet, }); } else { return alert('Internal error'); } }; async onWillBlur() { StatusBar.setBarStyle('dark-content'); } componentWillUnmount() { this.onWillBlur(); } renderItem = item => { return ; }; render() { const { navigate } = this.props.navigation; return ( {this.state.wallet.chain === Chain.ONCHAIN && ( )} { 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={() => this.setState({ isManageFundsModalVisible: true })} /> {/* 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()} { // 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, }); }} ListHeaderComponent={this.renderListHeaderComponent} 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', { address: this.state.wallet.getAddress(), secret: this.state.wallet.getSecret(), }) } > {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.type === LightningCustodianWallet.type) { navigate('LNDCreateInvoice', { fromWallet: this.state.wallet }); } else { navigate('ReceiveDetails', { secret: this.state.wallet.getSecret() }); } }} /> ); } })()} {(() => { if (this.state.wallet.allowSend()) { return ( { if (this.state.wallet.type === LightningCustodianWallet.type) { navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() }); } else { navigate('SendDetails', { fromAddress: this.state.wallet.getAddress(), fromSecret: this.state.wallet.getSecret(), fromWallet: this.state.wallet, }); } }} /> ); } })()} ); } } 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, }), };