/* eslint react/prop-types: 0 */ import React, { Component, useEffect, useState } from 'react'; import Ionicons from 'react-native-vector-icons/Ionicons'; import PropTypes from 'prop-types'; import { Icon, FormLabel, FormInput, Text, Header, List, ListItem } from 'react-native-elements'; import { TouchableOpacity, TouchableWithoutFeedback, Animated, ActivityIndicator, View, KeyboardAvoidingView, UIManager, StyleSheet, Dimensions, Image, Keyboard, SafeAreaView, InputAccessoryView, Clipboard, Platform, TextInput, } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { LightningCustodianWallet } from './class'; import Carousel from 'react-native-snap-carousel'; import { BitcoinUnit } from './models/bitcoinUnits'; import NavigationService from './NavigationService'; import WalletGradient from './class/walletGradient'; import ToolTip from 'react-native-tooltip'; import { BlurView } from '@react-native-community/blur'; import showPopupMenu from 'react-native-popup-menu-android'; import NetworkTransactionFees, { NetworkTransactionFeeType } from './models/networkTransactionFees'; import Biometric from './class/biometrics'; let loc = require('./loc/'); /** @type {AppStorage} */ let BlueApp = require('./BlueApp'); const { height, width } = Dimensions.get('window'); const aspectRatio = height / width; const BigNumber = require('bignumber.js'); let isIpad; if (aspectRatio > 1.6) { isIpad = false; } else { isIpad = true; } export class BlueButton extends Component { render() { let backgroundColor = this.props.backgroundColor ? this.props.backgroundColor : BlueApp.settings.buttonBackgroundColor; let fontColor = BlueApp.settings.buttonTextColor; if (this.props.hasOwnProperty('disabled') && this.props.disabled === true) { backgroundColor = BlueApp.settings.buttonDisabledBackgroundColor; fontColor = BlueApp.settings.buttonDisabledTextColor; } let buttonWidth = this.props.width ? this.props.width : width / 1.5; if (this.props.hasOwnProperty('noMinWidth')) { buttonWidth = 0; } return ( {this.props.icon && } {this.props.title && {this.props.title}} ); } } export class BitcoinButton extends Component { render() { return ( { // eslint-disable-next-line if (this.props.onPress) this.props.onPress(); }} > {loc.wallets.add.bitcoin} ); } } export class LightningButton extends Component { render() { return ( { // eslint-disable-next-line if (this.props.onPress) this.props.onPress(); }} > {loc.wallets.add.lightning} ); } } export class BlueWalletNavigationHeader extends Component { static propTypes = { wallet: PropTypes.shape().isRequired, onWalletUnitChange: PropTypes.func, }; static getDerivedStateFromProps(props, state) { return { wallet: props.wallet, onWalletUnitChange: props.onWalletUnitChange, allowOnchainAddress: state.allowOnchainAddress }; } constructor(props) { super(props); this.state = { wallet: props.wallet, walletPreviousPreferredUnit: props.wallet.getPreferredBalanceUnit(), showManageFundsButton: false, }; } handleCopyPress = _item => { Clipboard.setString(loc.formatBalance(this.state.wallet.getBalance(), this.state.wallet.getPreferredBalanceUnit()).toString()); }; componentDidMount() { if (this.state.wallet.type === LightningCustodianWallet.type) { this.state.wallet .allowOnchainAddress() .then(value => this.setState({ allowOnchainAddress: value })) .catch(e => console.log('This Lndhub wallet does not have an onchain address API.')); } } handleBalanceVisibility = async _item => { const wallet = this.state.wallet; const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled && wallet.hideBalance) { if (!(await Biometric.unlockWithBiometrics())) { return this.props.navigation.goBack(); } } wallet.hideBalance = !wallet.hideBalance; this.setState({ wallet }); await BlueApp.saveToDisk(); }; showAndroidTooltip = () => { showPopupMenu(this.toolTipMenuOptions(), this.handleToolTipSelection, this.walletBalanceText); }; handleToolTipSelection = item => { if (item === loc.transactions.details.copy || item.id === loc.transactions.details.copy) { this.handleCopyPress(); } else if (item === 'balancePrivacy' || item.id === 'balancePrivacy') { this.handleBalanceVisibility(); } }; toolTipMenuOptions() { return Platform.select({ // NOT WORKING ATM. // ios: [ // { text: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance', onPress: this.handleBalanceVisibility }, // { text: loc.transactions.details.copy, onPress: this.handleCopyPress }, // ], android: this.state.wallet.hideBalance ? [{ id: 'balancePrivacy', label: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance' }] : [ { id: 'balancePrivacy', label: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance' }, { id: loc.transactions.details.copy, label: loc.transactions.details.copy }, ], }); } changeWalletBalanceUnit() { let walletPreviousPreferredUnit = this.state.wallet.getPreferredBalanceUnit(); const wallet = this.state.wallet; if (walletPreviousPreferredUnit === BitcoinUnit.BTC) { wallet.preferredBalanceUnit = BitcoinUnit.SATS; walletPreviousPreferredUnit = BitcoinUnit.BTC; } else if (walletPreviousPreferredUnit === BitcoinUnit.SATS) { wallet.preferredBalanceUnit = BitcoinUnit.LOCAL_CURRENCY; walletPreviousPreferredUnit = BitcoinUnit.SATS; } else if (walletPreviousPreferredUnit === BitcoinUnit.LOCAL_CURRENCY) { wallet.preferredBalanceUnit = BitcoinUnit.BTC; walletPreviousPreferredUnit = BitcoinUnit.BTC; } else { wallet.preferredBalanceUnit = BitcoinUnit.BTC; walletPreviousPreferredUnit = BitcoinUnit.BTC; } this.setState({ wallet, walletPreviousPreferredUnit: walletPreviousPreferredUnit }, () => { this.props.onWalletUnitChange(wallet); }); } manageFundsPressed = () => { this.props.onManageFundsPressed(); }; render() { return ( {this.state.wallet.getLabel()} {Platform.OS === 'ios' && ( (this.tooltip = tooltip)} actions={ this.state.wallet.hideBalance ? [{ text: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance', onPress: this.handleBalanceVisibility }] : [ { text: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance', onPress: this.handleBalanceVisibility }, { text: loc.transactions.details.copy, onPress: this.handleCopyPress }, ] } /> )} this.changeWalletBalanceUnit()} ref={ref => (this.walletBalanceText = ref)} onLongPress={() => (Platform.OS === 'ios' ? this.tooltip.showMenu() : this.showAndroidTooltip())} > {this.state.wallet.hideBalance ? ( ) : ( {loc.formatBalance(this.state.wallet.getBalance(), this.state.wallet.getPreferredBalanceUnit(), true).toString()} )} {this.state.wallet.type === LightningCustodianWallet.type && this.state.allowOnchainAddress && ( {loc.lnd.title} )} ); } } export class BlueButtonLink extends Component { render() { return ( {this.props.title} ); } } export const BlueNavigationStyle = (navigation, withNavigationCloseButton = false, customCloseButtonFunction = undefined) => ({ headerStyle: { backgroundColor: BlueApp.settings.brandingColor, borderBottomWidth: 0, elevation: 0, }, headerTitleStyle: { fontWeight: '600', color: BlueApp.settings.foregroundColor, }, headerTintColor: BlueApp.settings.foregroundColor, headerRight: withNavigationCloseButton ? ( { Keyboard.dismiss(); navigation.goBack(null); } : customCloseButtonFunction } > ) : null, headerBackTitle: null, }); export const BlueCreateTxNavigationStyle = (navigation, withAdvancedOptionsMenuButton = false, advancedOptionsMenuButtonAction) => ({ headerStyle: { backgroundColor: BlueApp.settings.brandingColor, borderBottomWidth: 0, elevation: 0, }, headerTitleStyle: { fontWeight: '600', color: BlueApp.settings.foregroundColor, }, headerTintColor: BlueApp.settings.foregroundColor, headerLeft: ( { Keyboard.dismiss(); navigation.goBack(null); }} > ), headerRight: withAdvancedOptionsMenuButton ? ( ) : null, headerBackTitle: null, }); export const BluePrivateBalance = () => { return Platform.select({ ios: ( ), android: ( ), }); }; export const BlueCopyToClipboardButton = ({ stringToCopy, displayText = false }) => { return ( Clipboard.setString(stringToCopy)}> {displayText || loc.transactions.details.copy} ); }; export class BlueCopyTextToClipboard extends Component { static propTypes = { text: PropTypes.string, }; static defaultProps = { text: '', }; constructor(props) { super(props); if (Platform.OS === 'android') { UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); } this.state = { hasTappedText: false, address: props.text }; } static getDerivedStateFromProps(props, state) { if (state.hasTappedText) { return { hasTappedText: state.hasTappedText, address: state.address }; } else { return { hasTappedText: state.hasTappedText, address: props.text }; } } copyToClipboard = () => { this.setState({ hasTappedText: true }, () => { Clipboard.setString(this.props.text); this.setState({ address: loc.wallets.xpub.copiedToClipboard }, () => { setTimeout(() => { this.setState({ hasTappedText: false, address: this.props.text }); }, 1000); }); }); }; render() { return ( {this.state.address} ); } } const styleCopyTextToClipboard = StyleSheet.create({ address: { marginVertical: 32, fontSize: 15, color: '#9aa0aa', textAlign: 'center', }, }); export class SafeBlueArea extends Component { render() { return ( ); } } export class BlueCard extends Component { render() { return ; } } export class BlueText extends Component { render() { return ( ); } } export class BlueTextCentered extends Component { render() { return ; } } export class BlueListItem extends Component { render() { return ( ); } } export class BlueFormLabel extends Component { render() { return ; } } export class BlueFormInput extends Component { render() { return ( ); } } export class BlueFormMultiInput extends Component { constructor(props) { super(props); this.state = { selection: { start: 0, end: 0 }, }; } render() { return ( ); } } export class BlueHeader extends Component { render() { return (
); } } export class BlueHeaderDefaultSub extends Component { render() { return (
{ // eslint-disable-next-line this.props.leftText } } rightComponent={ { // eslint-disable-next-line if (this.props.onClose) this.props.onClose(); }} > } {...this.props} /> ); } } export class BlueHeaderDefaultMain extends Component { render() { return (
{ // eslint-disable-next-line this.props.leftText } } rightComponent={ } /> ); } } export class BlueSpacing extends Component { render() { return ; } } export class BlueSpacing40 extends Component { render() { return ; } } export class BlueSpacingVariable extends Component { render() { if (isIpad) { return ; } else { return ; } } } export class is { static ipad() { return isIpad; } } export class BlueSpacing20 extends Component { render() { return ; } } export class BlueSpacing10 extends Component { render() { return ; } } export class BlueList extends Component { render() { return ( ); } } export class BlueUseAllFundsButton extends Component { static InputAccessoryViewID = 'useMaxInputAccessoryViewID'; static propTypes = { wallet: PropTypes.shape().isRequired, onUseAllPressed: PropTypes.func.isRequired, }; render() { const inputView = ( Total: {this.props.wallet.allowSendMax() && this.props.wallet.getBalance() > 0 ? ( ) : ( {loc.formatBalanceWithoutSuffix(this.props.wallet.getBalance(), BitcoinUnit.BTC, true).toString()} {BitcoinUnit.BTC} )} Keyboard.dismiss()} /> ); if (Platform.OS === 'ios') { return {inputView}; } else { return {inputView}; } } } export class BlueDismissKeyboardInputAccessory extends Component { static InputAccessoryViewID = 'BlueDismissKeyboardInputAccessory'; render() { return Platform.OS !== 'ios' ? null : ( Keyboard.dismiss()} /> ); } } export class BlueDoneAndDismissKeyboardInputAccessory extends Component { static InputAccessoryViewID = 'BlueDoneAndDismissKeyboardInputAccessory'; onPasteTapped = async () => { const clipboard = await Clipboard.getString(); this.props.onPasteTapped(clipboard); }; render() { const inputView = ( Keyboard.dismiss()} /> ); if (Platform.OS === 'ios') { return {inputView}; } else { return {inputView}; } } } export class BlueLoading extends Component { render() { return ( ); } } const stylesBlueIcon = StyleSheet.create({ container: { flex: 1, }, box1: { position: 'relative', top: 15, }, box: { alignSelf: 'flex-end', paddingHorizontal: 14, paddingTop: 8, }, boxIncoming: { position: 'relative', }, ball: { width: 30, height: 30, borderRadius: 15, backgroundColor: '#ccddf9', }, ballIncoming: { width: 30, height: 30, borderRadius: 15, backgroundColor: '#d2f8d6', transform: [{ rotate: '-45deg' }], }, ballIncomingWithoutRotate: { width: 30, height: 30, borderRadius: 15, backgroundColor: '#d2f8d6', }, ballReceive: { width: 30, height: 30, borderBottomLeftRadius: 15, backgroundColor: '#d2f8d6', transform: [{ rotate: '-45deg' }], }, ballOutgoing: { width: 30, height: 30, borderRadius: 15, backgroundColor: '#f8d2d2', transform: [{ rotate: '225deg' }], }, ballOutgoingWithoutRotate: { width: 30, height: 30, borderRadius: 15, backgroundColor: '#f8d2d2', }, ballTransparrent: { width: 30, height: 30, borderRadius: 15, backgroundColor: 'transparent', }, ballDimmed: { width: 30, height: 30, borderRadius: 15, backgroundColor: 'gray', }, }); export class BluePlusIcon extends Component { render() { return ( ); } } export class BlueTransactionIncomingIcon extends Component { render() { return ( ); } } export class BlueTransactionPendingIcon extends Component { render() { return ( ); } } export class BlueTransactionExpiredIcon extends Component { render() { return ( ); } } export class BlueTransactionOnchainIcon extends Component { render() { return ( ); } } export class BlueTransactionOffchainIcon extends Component { render() { return ( ); } } export class BlueTransactionOffchainIncomingIcon extends Component { render() { return ( ); } } export class BlueTransactionOutgoingIcon extends Component { render() { return ( ); } } // export class BlueReceiveButtonIcon extends Component { render() { return ( {loc.receive.header} ); } } export class BlueSendButtonIcon extends Component { render() { return ( {loc.send.header} ); } } export class ManageFundsBigButton extends Component { render() { return ( {loc.lnd.title} ); } } export class BluePlusIconDimmed extends Component { render() { return ( ); } } export class NewWalletPanel extends Component { constructor(props) { super(props); // WalletsCarousel.handleClick = props.handleClick // because cant access `this` from _renderItem // eslint-disable-next-line this.handleClick = props.onPress; } render() { return ( { if (this.handleClick) { this.handleClick(); } }} style={{ marginVertical: 17 }} > {loc.wallets.list.create_a_wallet} {loc.wallets.list.create_a_wallet1} {loc.wallets.list.create_a_wallet2} ); } } export const BlueTransactionListItem = ({ item, itemPriceUnit = BitcoinUnit.BTC }) => { const calculateTimeLabel = () => { const transactionTimeToReadable = loc.transactionTimeToReadable(item.received); return setTransactionTimeToReadable(transactionTimeToReadable); }; const interval = setInterval(() => calculateTimeLabel(), 60000); const [transactionTimeToReadable, setTransactionTimeToReadable] = useState('...'); const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); useEffect(() => { calculateTimeLabel(); return () => clearInterval(interval); }, [item, itemPriceUnit]); const txMemo = () => { if (BlueApp.tx_metadata[item.hash] && BlueApp.tx_metadata[item.hash]['memo']) { return BlueApp.tx_metadata[item.hash]['memo']; } return ''; }; const rowTitle = () => { if (item.type === 'user_invoice' || item.type === 'payment_request') { if (isNaN(item.value)) { item.value = '0'; } const currentDate = new Date(); const now = (currentDate.getTime() / 1000) | 0; const invoiceExpiration = item.timestamp + item.expire_time; if (invoiceExpiration > now) { return loc.formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } else if (invoiceExpiration < now) { if (item.ispaid) { return loc.formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } else { return loc.lnd.expired; } } } else { return loc.formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } }; const rowTitleStyle = () => { let color = BlueApp.settings.successColor; if (item.type === 'user_invoice' || item.type === 'payment_request') { const currentDate = new Date(); const now = (currentDate.getTime() / 1000) | 0; const invoiceExpiration = item.timestamp + item.expire_time; if (invoiceExpiration > now) { color = BlueApp.settings.successColor; } else if (invoiceExpiration < now) { if (item.ispaid) { color = BlueApp.settings.successColor; } else { color = BlueApp.settings.failedColor; } } } else if (item.value / 100000000 < 0) { color = BlueApp.settings.foregroundColor; } return { fontWeight: '600', fontSize: 16, color: color, }; }; const avatar = () => { // is it lightning refill tx? if (item.category === 'receive' && item.confirmations < 3) { return ( ); } if (item.type && item.type === 'bitcoind_tx') { return ( ); } if (item.type === 'paid_invoice') { // is it lightning offchain payment? return ( ); } if (item.type === 'user_invoice' || item.type === 'payment_request') { if (!item.ispaid) { const currentDate = new Date(); const now = (currentDate.getTime() / 1000) | 0; const invoiceExpiration = item.timestamp + item.expire_time; if (invoiceExpiration < now) { return ( ); } } else { return ( ); } } if (!item.confirmations) { return ( ); } else if (item.value < 0) { return ( ); } else { return ( ); } }; const subtitle = () => { return (item.confirmations < 7 ? loc.transactions.list.conf + ': ' + item.confirmations + ' ' : '') + txMemo() + (item.memo || ''); }; const onPress = () => { if (item.hash) { NavigationService.navigate('TransactionStatus', { hash: item.hash }); } else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') { const lightningWallet = BlueApp.getWallets().filter(wallet => { if (typeof wallet === 'object') { if (wallet.hasOwnProperty('secret')) { return wallet.getSecret() === item.fromWallet; } } }); if (lightningWallet.length === 1) { NavigationService.navigate('LNDViewInvoice', { invoice: item, fromWallet: lightningWallet[0], isModal: false, }); } } }; const onLongPress = () => { if (subtitleNumberOfLines === 1) { setSubtitleNumberOfLines(0); } }; return ( ); }; export class BlueListTransactionItem extends Component { static propTypes = { item: PropTypes.shape().isRequired, itemPriceUnit: PropTypes.string, }; static defaultProps = { itemPriceUnit: BitcoinUnit.BTC, }; txMemo = () => { if (BlueApp.tx_metadata[this.props.item.hash] && BlueApp.tx_metadata[this.props.item.hash]['memo']) { return BlueApp.tx_metadata[this.props.item.hash]['memo']; } return ''; }; rowTitle = () => { const item = this.props.item; if (item.type === 'user_invoice' || item.type === 'payment_request') { if (isNaN(item.value)) { item.value = '0'; } const currentDate = new Date(); const now = (currentDate.getTime() / 1000) | 0; const invoiceExpiration = item.timestamp + item.expire_time; if (invoiceExpiration > now) { return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString(); } else if (invoiceExpiration < now) { if (item.ispaid) { return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString(); } else { return loc.lnd.expired; } } } else { return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString(); } }; rowTitleStyle = () => { const item = this.props.item; let color = '#37c0a1'; if (item.type === 'user_invoice' || item.type === 'payment_request') { const currentDate = new Date(); const now = (currentDate.getTime() / 1000) | 0; const invoiceExpiration = item.timestamp + item.expire_time; if (invoiceExpiration > now) { color = '#37c0a1'; } else if (invoiceExpiration < now) { if (item.ispaid) { color = '#37c0a1'; } else { color = '#FF0000'; } } } else if (item.value / 100000000 < 0) { color = BlueApp.settings.foregroundColor; } return { fontWeight: '600', fontSize: 16, color: color, }; }; avatar = () => { // is it lightning refill tx? if (this.props.item.category === 'receive' && this.props.item.confirmations < 3) { return ( ); } if (this.props.item.type && this.props.item.type === 'bitcoind_tx') { return ( ); } if (this.props.item.type === 'paid_invoice') { // is it lightning offchain payment? return ( ); } if (this.props.item.type === 'user_invoice' || this.props.item.type === 'payment_request') { if (!this.props.item.ispaid) { const currentDate = new Date(); const now = (currentDate.getTime() / 1000) | 0; const invoiceExpiration = this.props.item.timestamp + this.props.item.expire_time; if (invoiceExpiration < now) { return ( ); } } else { return ( ); } } if (!this.props.item.confirmations) { return ( ); } else if (this.props.item.value < 0) { return ( ); } else { return ( ); } }; subtitle = () => { return ( (this.props.item.confirmations < 7 ? loc.transactions.list.conf + ': ' + this.props.item.confirmations + ' ' : '') + this.txMemo() + (this.props.item.memo || '') ); }; onPress = () => { if (this.props.item.hash) { NavigationService.navigate('TransactionStatus', { hash: this.props.item.hash }); } else if ( this.props.item.type === 'user_invoice' || this.props.item.type === 'payment_request' || this.props.item.type === 'paid_invoice' ) { const lightningWallet = BlueApp.getWallets().filter(wallet => { if (typeof wallet === 'object') { if (wallet.hasOwnProperty('secret')) { return wallet.getSecret() === this.props.item.fromWallet; } } }); NavigationService.navigate('LNDViewInvoice', { invoice: this.props.item, fromWallet: lightningWallet[0], isModal: false, }); } }; render() { return ( ); } } const sliderWidth = width * 1; const itemWidth = width * 0.82; const sliderHeight = 190; export class WalletsCarousel extends Component { walletsCarousel = React.createRef(); constructor(props) { super(props); // eslint-disable-next-line WalletsCarousel.handleClick = props.handleClick; // because cant access `this` from _renderItem WalletsCarousel.handleLongPress = props.handleLongPress; // eslint-disable-next-line this.onSnapToItem = props.onSnapToItem; } _renderItem({ item, index }) { let scaleValue = new Animated.Value(1.0); let props = { duration: 50 }; if (Platform.OS === 'android') { props['useNativeDriver'] = true; } this.onPressedIn = () => { props.toValue = 0.9; Animated.spring(scaleValue, props).start(); }; this.onPressedOut = () => { props.toValue = 1.0; Animated.spring(scaleValue, props).start(); }; if (!item) { return ( { if (WalletsCarousel.handleClick) { WalletsCarousel.handleClick(index); } }} /> ); } return ( { if (WalletsCarousel.handleClick) { WalletsCarousel.handleClick(index); } }} > {item.getLabel()} {item.hideBalance ? ( ) : ( {loc.formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)} )} {loc.wallets.list.latest_transaction} {loc.transactionTimeToReadable(item.getLatestTransactionTime())} ); } snapToItem = item => { this.walletsCarousel.current.snapToItem(item); }; render() { return ( { if (this.onSnapToItem) { this.onSnapToItem(index); } console.log('snapped to card #', index); }} /> ); } } export class BlueAddressInput extends Component { static propTypes = { isLoading: PropTypes.bool, onChangeText: PropTypes.func, onBarScanned: PropTypes.func, address: PropTypes.string, placeholder: PropTypes.string, }; static defaultProps = { isLoading: false, address: '', placeholder: loc.send.details.address, }; render() { return ( { this.props.onChangeText(text); }} placeholder={this.props.placeholder} numberOfLines={1} value={this.props.address} style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }} editable={!this.props.isLoading} onSubmitEditing={() => Keyboard.dismiss()} {...this.props} /> { NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned }); Keyboard.dismiss(); }} style={{ height: 36, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#9AA0AA', borderRadius: 4, paddingVertical: 4, paddingHorizontal: 8, marginHorizontal: 4, }} > {loc.send.details.scan} ); } } export class BlueReplaceFeeSuggestions extends Component { static propTypes = { onFeeSelected: PropTypes.func.isRequired, transactionMinimum: PropTypes.number.isRequired, }; static defaultProps = { onFeeSelected: undefined, transactionMinimum: 1, }; state = { networkFees: undefined, selectedFeeType: NetworkTransactionFeeType.FAST, customFeeValue: 0 }; async componentDidMount() { const networkFees = await NetworkTransactionFees.recommendedFees(); this.setState({ networkFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST)); } onFeeSelected = selectedFeeType => { if (selectedFeeType !== NetworkTransactionFeeType.CUSTOM) { Keyboard.dismiss(); } if (selectedFeeType === NetworkTransactionFeeType.FAST) { this.props.onFeeSelected(this.state.networkFees.fastestFee); this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.fastestFee)); } else if (selectedFeeType === NetworkTransactionFeeType.MEDIUM) { this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.halfHourFee)); } else if (selectedFeeType === NetworkTransactionFeeType.SLOW) { this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.hourFee)); } else if (selectedFeeType === NetworkTransactionFeeType.CUSTOM) { this.props.onFeeSelected(this.state.customFeeValue); } }; onCustomFeeTextChange = customFee => { this.setState({ customFeeValue: Number(customFee), selectedFeeType: NetworkTransactionFeeType.CUSTOM }, () => { this.onFeeSelected(NetworkTransactionFeeType.CUSTOM); }); }; render() { return ( {this.state.networkFees && ( <> Suggestions this.onFeeSelected(NetworkTransactionFeeType.FAST)}> } : { hideChevron: true })} /> this.onFeeSelected(NetworkTransactionFeeType.MEDIUM)}> } : { hideChevron: true })} /> this.onFeeSelected(NetworkTransactionFeeType.SLOW)}> } : { hideChevron: true })} /> )} this.customTextInput.focus()}> Custom (this.customTextInput = ref)} maxLength={9} style={{ borderColor: '#d2d2d2', borderBottomColor: '#d2d2d2', borderWidth: 1.0, borderBottomWidth: 0.5, borderRadius: 4, minHeight: 33, maxWidth: 100, minWidth: 44, backgroundColor: '#f5f5f5', textAlign: 'right', }} onFocus={() => this.onCustomFeeTextChange(this.state.customFeeValue)} defaultValue={`${this.props.transactionMinimum}`} placeholder="Custom sat/b" inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID} /> sat/b {this.state.selectedFeeType === NetworkTransactionFeeType.CUSTOM && } The total fee rate (satoshi per byte) you want to pay should be higher than {this.props.transactionMinimum} sat/byte ); } } export class BlueBitcoinAmount extends Component { static propTypes = { isLoading: PropTypes.bool, amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChangeText: PropTypes.func, disabled: PropTypes.bool, unit: PropTypes.string, }; static defaultProps = { unit: BitcoinUnit.BTC, }; render() { const amount = this.props.amount || 0; let localCurrency = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false); if (this.props.unit === BitcoinUnit.BTC) { let sat = new BigNumber(amount); sat = sat.multipliedBy(100000000).toString(); localCurrency = loc.formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false); } else { localCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false); } if (amount === BitcoinUnit.MAX) localCurrency = ''; // we dont want to display NaN return ( this.textInput.focus()}> { text = text.trim(); text = text.replace(',', '.'); const split = text.split('.'); if (split.length >= 2) { text = `${parseInt(split[0], 10)}.${split[1]}`; } else { text = `${parseInt(split[0], 10)}`; } text = this.props.unit === BitcoinUnit.BTC ? text.replace(/[^0-9.]/g, '') : text.replace(/[^0-9]/g, ''); text = text.replace(/(\..*)\./g, '$1'); if (text.startsWith('.')) { text = '0.'; } text = text.replace(/(0{1,}.)\./g, '$1'); if (this.props.unit !== BitcoinUnit.BTC) { text = text.replace(/[^0-9.]/g, ''); } this.props.onChangeText(text); }} onBlur={() => { if (this.props.onBlur) this.props.onBlur(); }} onFocus={() => { if (this.props.onFocus) this.props.onFocus(); }} placeholder="0" maxLength={10} ref={textInput => (this.textInput = textInput)} editable={!this.props.isLoading && !this.props.disabled} value={amount} placeholderTextColor={this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2} style={{ color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2, fontSize: 36, fontWeight: '600', }} /> {' ' + this.props.unit} {localCurrency} ); } } const styles = StyleSheet.create({ balanceBlur: { height: 30, width: 100, marginRight: 16, }, });