/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */ /* global alert */ import React, { Component, useState, useMemo, useCallback } from 'react'; import PropTypes from 'prop-types'; import { Icon, Input, Text, Header, ListItem, Avatar } from 'react-native-elements'; import { ActivityIndicator, Alert, Animated, Dimensions, FlatList, Image, InputAccessoryView, Keyboard, KeyboardAvoidingView, PixelRatio, Platform, PlatformColor, SafeAreaView, StyleSheet, Switch, TextInput, TouchableOpacity, TouchableWithoutFeedback, UIManager, useWindowDimensions, View, } from 'react-native'; import Clipboard from '@react-native-community/clipboard'; import LinearGradient from 'react-native-linear-gradient'; import ActionSheet from './screen/ActionSheet'; import { LightningCustodianWallet, MultisigHDWallet } from './class'; import { BitcoinUnit } from './models/bitcoinUnits'; import * as NavigationService from './NavigationService'; import WalletGradient from './class/wallet-gradient'; import ToolTip from 'react-native-tooltip'; import { BlurView } from '@react-native-community/blur'; import ImagePicker from 'react-native-image-picker'; import showPopupMenu from 'react-native-popup-menu-android'; import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees'; import Biometric from './class/biometrics'; import { getSystemName } from 'react-native-device-info'; import { encodeUR } from 'bc-ur/dist'; import QRCode from 'react-native-qrcode-svg'; import AsyncStorage from '@react-native-community/async-storage'; import { useTheme } from '@react-navigation/native'; import { BlueCurrentTheme } from './components/themes'; import loc, { formatBalance, formatBalanceWithoutSuffix, formatBalancePlain, removeTrailingZeros, transactionTimeToReadable } from './loc'; import Lnurl from './class/lnurl'; import { presentCameraNotAuthorizedAlert } from './class/camera'; /** @type {AppStorage} */ const BlueApp = require('./BlueApp'); const { height, width } = Dimensions.get('window'); const aspectRatio = height / width; const BigNumber = require('bignumber.js'); const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); const currency = require('./blue_modules/currency'); let isIpad; if (aspectRatio > 1.6) { isIpad = false; } else { isIpad = true; } // eslint-disable-next-line no-unused-expressions Platform.OS === 'android' ? (ActivityIndicator.defaultProps.color = PlatformColor('?attr/colorControlActivated')) : null; export const BlueButton = props => { const { colors } = useTheme(); const { width } = useWindowDimensions(); let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor || BlueCurrentTheme.colors.mainColor; let fontColor = colors.buttonTextColor; if (props.disabled === true) { backgroundColor = colors.buttonDisabledBackgroundColor; fontColor = colors.buttonDisabledTextColor; } let buttonWidth = props.width ? props.width : width / 1.5; if ('noMinWidth' in props) { buttonWidth = 0; } return ( {props.icon && } {props.title && {props.title}} ); }; export const BlueButtonHook = props => { const { colors } = useTheme(); let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor; let fontColor = colors.buttonTextColor; if (props.disabled === true) { backgroundColor = colors.buttonDisabledBackgroundColor; fontColor = colors.buttonDisabledTextColor; } return ( {props.icon && } {props.title && {props.title}} ); }; export class SecondButton extends Component { render() { let backgroundColor = this.props.backgroundColor ? this.props.backgroundColor : BlueCurrentTheme.colors.buttonBlueBackgroundColor; let fontColor = BlueCurrentTheme.colors.buttonTextColor; if (this.props.disabled === true) { backgroundColor = BlueCurrentTheme.colors.buttonDisabledBackgroundColor; fontColor = BlueCurrentTheme.colors.buttonDisabledTextColor; } // let buttonWidth = this.props.width ? this.props.width : width / 1.5; // if ('noMinWidth' in this.props) { // buttonWidth = 0; // } return ( {this.props.icon && } {this.props.title && {this.props.title}} ); } } export const BitcoinButton = props => { const { colors } = useTheme(); return ( {loc.wallets.add_bitcoin} ); }; export const LightningButton = props => { const { colors } = useTheme(); return ( {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(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 ( { switch (this.state.wallet.type) { case LightningCustodianWallet.type: return require('./img/lnd-shape.png'); case MultisigHDWallet.type: return require('./img/vault-shape.png'); default: return require('./img/btc-shape.png'); } })()} style={{ width: 99, height: 94, position: 'absolute', bottom: 0, right: 0, }} /> {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 ? ( ) : ( {formatBalance(this.state.wallet.getBalance(), this.state.wallet.getPreferredBalanceUnit(), true).toString()} )} {this.state.wallet.type === LightningCustodianWallet.type && this.state.allowOnchainAddress && ( {loc.lnd.title} )} ); } } export const BlueButtonLinkHook = props => { const { colors } = useTheme(); return ( {props.title} ); }; export class BlueButtonLink extends Component { render() { return ( {this.props.title} ); } } export const BlueAlertWalletExportReminder = ({ onSuccess = () => {}, onFailure }) => { Alert.alert( loc.wallets.details_title, loc.pleasebackup.ask, [ { text: loc.pleasebackup.ask_yes, onPress: onSuccess, style: 'cancel' }, { text: loc.pleasebackup.ask_no, onPress: onFailure }, ], { cancelable: false }, ); }; export const BlueNavigationStyle = (navigation, withNavigationCloseButton = false, customCloseButtonFunction = undefined) => { let headerRight; const { colors, closeImage } = useTheme(); if (withNavigationCloseButton) { headerRight = () => ( { Keyboard.dismiss(); navigation.goBack(null); } : customCloseButtonFunction } > ); } else { headerRight = null; } return { headerStyle: { borderBottomWidth: 0, elevation: 0, shadowOpacity: 0, shadowOffset: { height: 0, width: 0 }, }, headerTitleStyle: { fontWeight: '600', color: colors.foregroundColor, }, headerRight, headerBackTitleVisible: false, headerTintColor: colors.foregroundColor, }; }; export const BlueCreateTxNavigationStyle = (navigation, withAdvancedOptionsMenuButton = false, advancedOptionsMenuButtonAction) => { let headerRight; if (withAdvancedOptionsMenuButton) { headerRight = () => ( ); } else { headerRight = null; } return { headerStyle: { borderBottomWidth: 0, elevation: 0, shadowOffset: { height: 0, width: 0 }, }, headerTitleStyle: { fontWeight: '600', color: BlueCurrentTheme.colors.foregroundColor, }, headerTintColor: BlueCurrentTheme.colors.foregroundColor, headerLeft: () => ( { Keyboard.dismiss(); navigation.goBack(null); }} > ), headerRight, 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 const BlueTextHooks = props => { const { colors } = useTheme(); return ( ); }; export class BlueTextCentered extends Component { render() { return ; } } export const BlueTextCenteredHooks = props => { const { colors } = useTheme(); return ; }; export const BlueListItem = React.memo(props => { const { colors } = useTheme(); return ( {props.leftAvatar && {props.leftAvatar}} {props.leftIcon && } {props.title} {props.subtitle && ( {props.subtitle} )} {props.rightTitle && ( {props.rightTitle} )} {props.isLoading ? ( ) : ( <> {props.chevron && } {props.rightIcon && } {props.switch && } )} ); }); export const BlueFormLabel = props => { const { colors } = useTheme(); 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 (
{this.props.leftText} } rightComponent={ { if (this.props.onClose) this.props.onClose(); }} > } {...this.props} /> ); } } export const BlueHeaderDefaultSubHooks = props => { const { colors } = useTheme(); return (
{props.leftText} } {...props} /> ); }; export const BlueHeaderDefaultMain = props => { const { colors } = useTheme(); const { isDrawerList } = props; return (
} /> ); }; 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, }; static defaultProps = { unit: BitcoinUnit.BTC, }; render() { const inputView = ( {loc.send.input_total} {this.props.wallet.allowSendMax() && this.props.wallet.getBalance() > 0 ? ( ) : ( {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 ( ); } } export const BlueLoadingHook = () => { 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, }, ballIncoming: { width: 30, height: 30, borderRadius: 15, transform: [{ rotate: '-45deg' }], justifyContent: 'center', }, ballIncomingWithoutRotate: { width: 30, height: 30, borderRadius: 15, }, ballReceive: { width: 30, height: 30, borderBottomLeftRadius: 15, transform: [{ rotate: '-45deg' }], }, ballOutgoing: { width: 30, height: 30, borderRadius: 15, transform: [{ rotate: '225deg' }], justifyContent: 'center', }, ballOutgoingWithoutRotate: { width: 30, height: 30, borderRadius: 15, }, ballOutgoingExpired: { width: 30, height: 30, borderRadius: 15, justifyContent: 'center', }, ballTransparrent: { width: 30, height: 30, borderRadius: 15, backgroundColor: 'transparent', }, ballDimmed: { width: 30, height: 30, borderRadius: 15, backgroundColor: 'gray', }, }); export const BluePlusIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ball: { backgroundColor: colors.buttonBackgroundColor, }, }); return ( ); }; export const BlueTransactionIncomingIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ballIncoming: { backgroundColor: colors.ballReceive, }, }); return ( ); }; export const BlueTransactionPendingIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ball: { backgroundColor: colors.buttonBackgroundColor, }, }); return ( ); }; export const BlueTransactionExpiredIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ballOutgoingExpired: { backgroundColor: colors.ballOutgoingExpired, }, }); return ( ); }; export const BlueTransactionOnchainIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ballIncoming: { backgroundColor: colors.ballReceive, }, }); return ( ); }; export const BlueTransactionOffchainIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ballOutgoingWithoutRotate: { backgroundColor: colors.ballOutgoing, }, }); return ( ); }; export const BlueTransactionOffchainIncomingIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ballIncomingWithoutRotate: { backgroundColor: colors.ballReceive, }, }); return ( ); }; export const BlueTransactionOutgoingIcon = props => { const { colors } = useTheme(); const stylesBlueIconHooks = StyleSheet.create({ ballOutgoing: { backgroundColor: colors.ballOutgoing, }, }); return ( ); }; const sendReceiveScanButtonFontSize = PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22 ? 22 : PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26); export const BlueReceiveButtonIcon = props => { const { colors } = useTheme(); return ( {loc.receive.header} ); }; export class BlueScanButton extends Component { render() { return ( {loc.send.details_scan} ); } } export class BlueSendButtonIcon extends Component { render() { return ( {loc.send.header} ); } } export class ManageFundsBigButton extends Component { render() { return ( {loc.lnd.title} ); } } export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUnit.BTC, timeElapsed }) => { const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); const { colors } = useTheme(); const containerStyle = useMemo( () => ({ backgroundColor: 'transparent', borderBottomColor: colors.lightBorder, paddingTop: 16, paddingBottom: 16, paddingRight: 0, }), [colors.lightBorder], ); const title = useMemo(() => transactionTimeToReadable(item.received), [item.received]); const txMemo = BlueApp.tx_metadata[item.hash]?.memo ?? ''; const subtitle = useMemo(() => { let sub = item.confirmations < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : ''; if (sub !== '') sub += ' '; sub += txMemo; if (item.memo) sub += item.memo; return sub || null; }, [txMemo, item.confirmations, item.memo]); const rowTitle = useMemo(() => { 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 formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } else if (invoiceExpiration < now) { if (item.ispaid) { return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } else { return loc.lnd.expired; } } } else { return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } }, [item, itemPriceUnit]); const rowTitleStyle = useMemo(() => { let color = colors.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 = colors.successColor; } else if (invoiceExpiration < now) { if (item.ispaid) { color = colors.successColor; } else { color = '#9AA0AA'; } } } else if (item.value / 100000000 < 0) { color = colors.foregroundColor; } return { color, fontSize: 14, fontWeight: '600', textAlign: 'right', width: 96, }; }, [item, colors.foregroundColor, colors.successColor]); const avatar = useMemo(() => { // 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 ( ); } }, [item]); const onPress = useCallback(async () => { 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 ('secret' in wallet) { return wallet.getSecret() === item.fromWallet; } } }); if (lightningWallet.length === 1) { // is it a successful lnurl-pay? const LN = new Lnurl(false, AsyncStorage); let paymentHash = item.payment_hash; if (typeof paymentHash === 'object') { paymentHash = Buffer.from(paymentHash.data).toString('hex'); } const loaded = await LN.loadSuccessfulPayment(paymentHash); if (loaded) { NavigationService.navigate('ScanLndInvoiceRoot', { screen: 'LnurlPaySuccess', params: { paymentHash, justPaid: false, fromWalletID: lightningWallet[0].getID(), }, }); return; } NavigationService.navigate('LNDViewInvoice', { invoice: item, fromWallet: lightningWallet[0], isModal: false, }); } } }, [item]); const onLongPress = useCallback(() => { if (subtitleNumberOfLines === 1) { setSubtitleNumberOfLines(0); } }, [subtitleNumberOfLines]); const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]); return ( ); }); const isDesktop = getSystemName() === 'Mac OS X'; 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, }; 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.props.onBarScanned(result); } else { alert(loc.send.qr_error_no_qrcode); } }); } }, ); }; takePhoto = () => { ImagePicker.launchCamera( { 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.props.onBarScanned(result); } else { alert(loc.send.qr_error_no_qrcode); } }); } else if (response.error) { presentCameraNotAuthorizedAlert(response.error); } }, ); }; copyFromClipbard = async () => { this.props.onBarScanned(await Clipboard.getString()); }; showActionSheet = async () => { const isClipboardEmpty = (await Clipboard.getString()).trim().length === 0; let copyFromClipboardIndex; if (Platform.OS === 'ios') { const options = [loc._.cancel, loc.wallets.list_long_choose, isDesktop ? loc.wallets.take_photo : loc.wallets.list_long_scan]; if (!isClipboardEmpty) { options.push(loc.wallets.list_long_clipboard); copyFromClipboardIndex = options.length - 1; } ActionSheet.showActionSheetWithOptions({ options, cancelButtonIndex: 0 }, buttonIndex => { if (buttonIndex === 1) { this.choosePhoto(); } else if (buttonIndex === 2) { this.takePhoto(); } else if (buttonIndex === copyFromClipboardIndex) { this.copyFromClipbard(); } }); } }; render() { return ( { this.props.onChangeText(text); }} placeholder={this.props.placeholder} numberOfLines={1} placeholderTextColor="#81868e" value={this.props.address} style={{ flex: 1, marginHorizontal: 8, minHeight: 33, color: '#81868e' }} editable={!this.props.isLoading} onSubmitEditing={Keyboard.dismiss} {...this.props} /> { Keyboard.dismiss(); if (isDesktop) { this.showActionSheet(); } else { NavigationService.navigate('ScanQRCodeRoot', { screen: 'ScanQRCode', params: { launchedBy: this.props.launchedBy, onBarScanned: this.props.onBarScanned, }, }); } }} style={{ height: 36, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: BlueCurrentTheme.colors.scanLabel, 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 = { transactionMinimum: 1, }; state = { customFeeValue: '1', }; async componentDidMount() { try { const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey)); if (cachedNetworkTransactionFees && 'fastestFee' in cachedNetworkTransactionFees) { this.setState({ networkFees: cachedNetworkTransactionFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST)); } } catch (_) {} 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.mediumFee)); } else if (selectedFeeType === NetworkTransactionFeeType.SLOW) { this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.slowFee)); } else if (selectedFeeType === NetworkTransactionFeeType.CUSTOM) { this.props.onFeeSelected(Number(this.state.customFeeValue)); } }; onCustomFeeTextChange = customFee => { const customFeeValue = customFee.replace(/[^0-9]/g, ''); this.setState({ customFeeValue, selectedFeeType: NetworkTransactionFeeType.CUSTOM }, () => { this.onFeeSelected(NetworkTransactionFeeType.CUSTOM); }); }; render() { const { networkFees, selectedFeeType } = this.state; return ( {networkFees && [ { label: loc.send.fee_fast, time: loc.send.fee_10m, type: NetworkTransactionFeeType.FAST, rate: networkFees.fastestFee, active: selectedFeeType === NetworkTransactionFeeType.FAST, }, { label: loc.send.fee_medium, time: loc.send.fee_3h, type: NetworkTransactionFeeType.MEDIUM, rate: networkFees.mediumFee, active: selectedFeeType === NetworkTransactionFeeType.MEDIUM, }, { label: loc.send.fee_slow, time: loc.send.fee_1d, type: NetworkTransactionFeeType.SLOW, rate: networkFees.slowFee, active: selectedFeeType === NetworkTransactionFeeType.SLOW, }, ].map(({ label, type, time, rate, active }, index) => ( this.onFeeSelected(type)} style={[ { paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 }, active && { borderRadius: 8, backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor }, ]} > {label} ~{time} {rate} sat/byte ))} this.customTextInput.focus()} style={[ { paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 }, selectedFeeType === NetworkTransactionFeeType.CUSTOM && { borderRadius: 8, backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor, }, ]} > {loc.send.fee_custom} (this.customTextInput = ref)} maxLength={9} style={{ backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor, borderBottomColor: BlueCurrentTheme.colors.formBorder, borderBottomWidth: 0.5, borderColor: BlueCurrentTheme.colors.formBorder, borderRadius: 4, borderWidth: 1.0, color: '#81868e', flex: 1, marginRight: 10, minHeight: 33, paddingRight: 5, paddingLeft: 5, }} onFocus={() => this.onCustomFeeTextChange(this.state.customFeeValue)} defaultValue={`${this.props.transactionMinimum}`} placeholder={loc.send.fee_satbyte} placeholderTextColor="#81868e" inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID} /> sat/byte {loc.formatString(loc.send.fee_replace_min, { min: this.props.transactionMinimum })} ); } } export class BlueBitcoinAmount extends Component { static propTypes = { isLoading: PropTypes.bool, /** * amount is a sting thats always in current unit denomination, e.g. '0.001' or '9.43' or '10000' */ amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** * callback that returns currently typed amount, in current denomination, e.g. 0.001 or 10000 or $9.34 * (btc, sat, fiat) */ onChangeText: PropTypes.func, /** * callback thats fired to notify of currently selected denomination, returns */ onAmountUnitChange: PropTypes.func, disabled: PropTypes.bool, }; /** * cache of conversions fiat amount => satoshi * @type {{}} */ static conversionCache = {}; static getCachedSatoshis(amount) { return BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY] || false; } static setCachedSatoshis(amount, sats) { BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY] = sats; } constructor(props) { super(props); this.state = { unit: props.unit || BitcoinUnit.BTC, previousUnit: BitcoinUnit.SATS }; } /** * here we must recalculate old amont value (which was denominated in `previousUnit`) to new denomination `newUnit` * and fill this value in input box, so user can switch between, for example, 0.001 BTC <=> 100000 sats * * @param previousUnit {string} one of {BitcoinUnit.*} * @param newUnit {string} one of {BitcoinUnit.*} */ onAmountUnitChange(previousUnit, newUnit) { const amount = this.props.amount || 0; console.log('was:', amount, previousUnit, '; converting to', newUnit); let sats = 0; switch (previousUnit) { case BitcoinUnit.BTC: sats = new BigNumber(amount).multipliedBy(100000000).toString(); break; case BitcoinUnit.SATS: sats = amount; break; case BitcoinUnit.LOCAL_CURRENCY: sats = new BigNumber(currency.fiatToBTC(amount)).multipliedBy(100000000).toString(); break; } if (previousUnit === BitcoinUnit.LOCAL_CURRENCY && BlueBitcoinAmount.conversionCache[amount + previousUnit]) { // cache hit! we reuse old value that supposedly doesnt have rounding errors sats = BlueBitcoinAmount.conversionCache[amount + previousUnit]; } console.log('so, in sats its', sats); const newInputValue = formatBalancePlain(sats, newUnit, false); console.log('and in', newUnit, 'its', newInputValue); if (newUnit === BitcoinUnit.LOCAL_CURRENCY && previousUnit === BitcoinUnit.SATS) { // we cache conversion, so when we will need reverse conversion there wont be a rounding error BlueBitcoinAmount.conversionCache[newInputValue + newUnit] = amount; } this.props.onChangeText(newInputValue); if (this.props.onAmountUnitChange) this.props.onAmountUnitChange(newUnit); } /** * responsible for cycling currently selected denomination, BTC->SAT->LOCAL_CURRENCY->BTC */ changeAmountUnit = () => { let previousUnit = this.state.unit; let newUnit; if (previousUnit === BitcoinUnit.BTC) { newUnit = BitcoinUnit.SATS; } else if (previousUnit === BitcoinUnit.SATS) { newUnit = BitcoinUnit.LOCAL_CURRENCY; } else if (previousUnit === BitcoinUnit.LOCAL_CURRENCY) { newUnit = BitcoinUnit.BTC; } else { newUnit = BitcoinUnit.BTC; previousUnit = BitcoinUnit.SATS; } this.setState({ unit: newUnit, previousUnit }, () => this.onAmountUnitChange(previousUnit, newUnit)); }; maxLength = () => { switch (this.state.unit) { case BitcoinUnit.BTC: return 10; case BitcoinUnit.SATS: return 15; default: return 15; } }; textInput = React.createRef(); handleTextInputOnPress = () => { this.textInput.current.focus(); }; render() { const amount = this.props.amount || 0; let secondaryDisplayCurrency = formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false); // if main display is sat or btc - secondary display is fiat // if main display is fiat - secondary dislay is btc let sat; switch (this.state.unit) { case BitcoinUnit.BTC: sat = new BigNumber(amount).multipliedBy(100000000).toString(); secondaryDisplayCurrency = formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false); break; case BitcoinUnit.SATS: secondaryDisplayCurrency = formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false); break; case BitcoinUnit.LOCAL_CURRENCY: secondaryDisplayCurrency = currency.fiatToBTC(parseFloat(amount)); if (BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]) { // cache hit! we reuse old value that supposedly doesnt have rounding errors const sats = BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]; secondaryDisplayCurrency = currency.satoshiToBTC(sats); } break; } if (amount === BitcoinUnit.MAX) secondaryDisplayCurrency = ''; // we dont want to display NaN return ( this.textInput.focus()}> {!this.props.disabled && } {this.state.unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && ( {currency.getCurrencySymbol() + ' '} )} { text = text.trim(); if (this.state.unit !== BitcoinUnit.LOCAL_CURRENCY) { 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.state.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.state.unit !== BitcoinUnit.BTC) { text = text.replace(/[^0-9.]/g, ''); } } else if (this.state.unit === BitcoinUnit.LOCAL_CURRENCY) { text = text.replace(/,/gi, ''); if (text.split('.').length > 2) { // too many dots. stupid code to remove all but first dot: let rez = ''; let first = true; for (const part of text.split('.')) { rez += part; if (first) { rez += '.'; first = false; } } text = rez; } text = text.replace(/[^\d.,-]/g, ''); // remove all but numberd, dots & commas } this.props.onChangeText(text); }} onBlur={() => { if (this.props.onBlur) this.props.onBlur(); }} onFocus={() => { if (this.props.onFocus) this.props.onFocus(); }} placeholder="0" maxLength={this.maxLength()} ref={textInput => (this.textInput = textInput)} editable={!this.props.isLoading && !this.props.disabled} value={parseFloat(amount) > 0 || amount === BitcoinUnit.MAX ? amount : undefined} placeholderTextColor={ this.props.disabled ? BlueCurrentTheme.colors.buttonDisabledTextColor : BlueCurrentTheme.colors.alternativeTextColor2 } style={{ color: this.props.disabled ? BlueCurrentTheme.colors.buttonDisabledTextColor : BlueCurrentTheme.colors.alternativeTextColor2, fontWeight: 'bold', fontSize: amount.length > 10 ? 20 : 36, }} /> {this.state.unit !== BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && ( {' ' + this.state.unit} )} {this.state.unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? removeTrailingZeros(secondaryDisplayCurrency) : secondaryDisplayCurrency} {this.state.unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? ` ${BitcoinUnit.BTC}` : null} {!this.props.disabled && amount !== BitcoinUnit.MAX && ( )} ); } } const styles = StyleSheet.create({ balanceBlur: { height: 30, width: 100, marginRight: 16, }, }); export function BlueBigCheckmark({ style }) { const defaultStyles = { backgroundColor: '#ccddf9', width: 120, height: 120, borderRadius: 60, alignSelf: 'center', justifyContent: 'center', marginTop: 0, marginBottom: 0, }; const mergedStyles = { ...defaultStyles, ...style }; return ( ); } const tabsStyles = StyleSheet.create({ root: { flexDirection: 'row', height: 50, borderColor: '#e3e3e3', borderBottomWidth: 1, }, tabRoot: { flex: 1, justifyContent: 'center', alignItems: 'center', borderColor: 'white', borderBottomWidth: 2, }, }); export const BlueTabs = ({ active, onSwitch, tabs }) => ( {tabs.map((Tab, i) => ( onSwitch(i)} style={[ tabsStyles.tabRoot, active === i && { borderColor: BlueCurrentTheme.colors.buttonAlternativeTextColor, borderBottomWidth: 2, }, ]} > ))} ); export class DynamicQRCode extends Component { constructor() { super(); const qrCodeHeight = height > width ? width - 40 : width / 3; const qrCodeMaxHeight = 370; this.state = { index: 0, total: 0, qrCodeHeight: Math.min(qrCodeHeight, qrCodeMaxHeight), intervalHandler: null, }; } fragments = []; componentDidMount() { const { value, capacity = 800 } = this.props; this.fragments = encodeUR(value, capacity); this.setState( { total: this.fragments.length, }, () => { this.startAutoMove(); }, ); } moveToNextFragment = () => { const { index, total } = this.state; if (index === total - 1) { this.setState({ index: 0, }); } else { this.setState(state => ({ index: state.index + 1, })); } }; startAutoMove = () => { if (!this.state.intervalHandler) this.setState(() => ({ intervalHandler: setInterval(this.moveToNextFragment, 500), })); }; stopAutoMove = () => { clearInterval(this.state.intervalHandler); this.setState(() => ({ intervalHandler: null, })); }; moveToPreviousFragment = () => { const { index, total } = this.state; if (index > 0) { this.setState(state => ({ index: state.index - 1, })); } else { this.setState(state => ({ index: total - 1, })); } }; render() { const currentFragment = this.fragments[this.state.index]; return currentFragment ? ( {loc.formatString(loc._.of, { number: this.state.index + 1, total: this.state.total })} {loc.send.dynamic_prev} {this.state.intervalHandler ? loc.send.dynamic_stop : loc.send.dynamic_start} {loc.send.dynamic_next} ) : ( {loc.send.dynamic_init} ); } } const animatedQRCodeStyle = StyleSheet.create({ container: { flex: 1, flexDirection: 'column', alignItems: 'center', }, qrcodeContainer: { alignItems: 'center', justifyContent: 'center', borderWidth: 6, borderRadius: 8, borderColor: '#FFFFFF', margin: 6, }, controller: { width: '90%', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderRadius: 25, height: 45, paddingHorizontal: 18, }, button: { alignItems: 'center', height: 45, justifyContent: 'center', }, text: { fontSize: 14, color: BlueCurrentTheme.colors.foregroundColor, fontWeight: 'bold', }, });