/* 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, Alert, 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, PlaceholderWallet } 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 BlueAlertWalletExportReminder = ({ onSuccess = () => {}, onFailure }) => { Alert.alert( 'Wallet', `Have your saved your wallet's backup phrase? This backup phrase is required to access your funds in case you lose this device. Without the backup phrase, your funds will be permanently lost.`, [ { text: 'Yes, I have', onPress: onSuccess, style: 'cancel' }, { text: 'No, I have not', onPress: onFailure, }, ], { cancelable: false }, ); }; 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={ this.props.onNewWalletPress && ( ) } /> ); } } 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, shouldRefresh }) => { const [transactionTimeToReadable, setTransactionTimeToReadable] = useState('...'); const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); const calculateTimeLabel = () => { const transactionTimeToReadable = loc.transactionTimeToReadable(item.received); return setTransactionTimeToReadable(transactionTimeToReadable); }; useEffect(() => { calculateTimeLabel(); }, [item, itemPriceUnit, shouldRefresh]); 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) { this.onPressedOut(); WalletsCarousel.handleClick(index); this.onPressedOut(); } }} /> ); } if (item.type === PlaceholderWallet.type) { return ( { if (item.getIsFailure() && WalletsCarousel.handleClick) { this.onPressedOut(); WalletsCarousel.handleClick(index); this.onPressedOut(); } }} > {item.getLabel()} {item.getIsFailure() ? ( An error was encountered when attempting to import this wallet. ) : ( )} ); } else { return ( { if (WalletsCarousel.handleClick) { this.onPressedOut(); WalletsCarousel.handleClick(index); this.onPressedOut(); } }} > {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.isRequired, launchedBy: PropTypes.string.isRequired, 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('ScanQRCode', { onBarScanned: this.props.onBarScanned, launchedBy: this.props.launchedBy }); 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, }, });