/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
/* global alert */
import React, { Component, useState } from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
import { Icon, Input, Text, Header, ListItem } from 'react-native-elements';
import {
TouchableOpacity,
TouchableWithoutFeedback,
Animated,
Alert,
ActivityIndicator,
View,
KeyboardAvoidingView,
UIManager,
StyleSheet,
Dimensions,
Image,
Keyboard,
SafeAreaView,
InputAccessoryView,
Platform,
FlatList,
TextInput,
PixelRatio,
} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import LinearGradient from 'react-native-linear-gradient';
import ActionSheet from './screen/ActionSheet';
import { LightningCustodianWallet, PlaceholderWallet } from './class';
import Carousel from 'react-native-snap-carousel';
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, { 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 { useTheme } from '@react-navigation/native';
import { BlueCurrentTheme } from './components/themes';
import loc, { formatBalance, formatBalanceWithoutSuffix, formatBalancePlain, removeTrailingZeros, transactionTimeToReadable } from './loc';
import AsyncStorage from '@react-native-community/async-storage';
import Lnurl from './class/lnurl';
/** @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;
}
export class BlueButton extends Component {
render() {
let backgroundColor = this.props.backgroundColor ? this.props.backgroundColor : BlueCurrentTheme.colors.mainColor;
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 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;
}
let buttonWidth = props.width ? props.width : width / 1.5;
if ('noMinWidth' in props) {
buttonWidth = 0;
}
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 (
{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 => (
));
export const BlueListItemHooks = props => {
const { colors } = useTheme();
return (
);
};
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 class BlueHeaderDefaultMain extends Component {
render() {
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,
backgroundColor: BlueCurrentTheme.colors.buttonBackgroundColor,
},
ballIncoming: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: BlueCurrentTheme.colors.ballReceive,
transform: [{ rotate: '-45deg' }],
justifyContent: 'center',
},
ballIncomingWithoutRotate: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: BlueCurrentTheme.colors.ballReceive,
},
ballReceive: {
width: 30,
height: 30,
borderBottomLeftRadius: 15,
backgroundColor: BlueCurrentTheme.colors.ballReceive,
transform: [{ rotate: '-45deg' }],
},
ballOutgoing: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: BlueCurrentTheme.colors.ballOutgoing,
transform: [{ rotate: '225deg' }],
justifyContent: 'center',
},
ballOutgoingWithoutRotate: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: BlueCurrentTheme.colors.ballOutgoing,
},
ballOutgoingExpired: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: BlueCurrentTheme.colors.ballOutgoingExpired,
justifyContent: 'center',
},
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 (
);
}
}
const sendReceiveScanButtonFontSize = PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22 ? 22 : PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
export class BlueReceiveButtonIcon extends Component {
render() {
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 class NewWalletPanel extends Component {
render() {
return (
{loc.wallets.list_create_a_wallet}
{loc.wallets.list_create_a_wallet1}
{loc.wallets.list_create_a_wallet2}
{loc.wallets.list_create_a_button}
);
}
}
export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUnit.BTC, timeElapsed }) => {
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
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 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();
}
};
const rowTitleStyle = () => {
let color = BlueCurrentTheme.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 = BlueCurrentTheme.colors.successColor;
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = BlueCurrentTheme.colors.successColor;
} else {
color = '#9AA0AA';
}
}
} else if (item.value / 100000000 < 0) {
color = BlueCurrentTheme.colors.foregroundColor;
}
return {
fontWeight: '600',
fontSize: 14,
color: color,
textAlign: 'right',
width: 96,
};
};
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 = 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: paymentHash,
justPaid: false,
fromWalletID: lightningWallet[0].getID(),
},
});
return;
}
NavigationService.navigate('LNDViewInvoice', {
invoice: item,
fromWallet: lightningWallet[0],
isModal: false,
});
}
}
};
const onLongPress = () => {
if (subtitleNumberOfLines === 1) {
setSubtitleNumberOfLines(0);
}
};
return (
);
});
const WalletCarouselItem = ({ item, index, onPress, handleLongPress }) => {
const scaleValue = new Animated.Value(1.0);
const onPressedIn = () => {
const props = { duration: 50 };
props.useNativeDriver = true;
props.toValue = 0.9;
Animated.spring(scaleValue, props).start();
};
const onPressedOut = () => {
const props = { duration: 50 };
props.useNativeDriver = true;
props.toValue = 1.0;
Animated.spring(scaleValue, props).start();
};
if (!item) {
return (
{
onPressedOut();
onPress(index);
}}
/>
);
}
if (item.type === PlaceholderWallet.type) {
return (
{
if (item.getIsFailure()) {
onPressedOut();
onPress(index);
onPressedOut();
}
}}
>
{item.getLabel()}
{item.getIsFailure() ? (
{loc.wallets.list_import_error}
) : (
)}
);
} else {
return (
{
onPressedOut();
onPress(index);
onPressedOut();
}}
>
{item.getLabel()}
{item.hideBalance ? (
) : (
{formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)}
)}
{loc.wallets.list_latest_transaction}
{transactionTimeToReadable(item.getLatestTransactionTime())}
);
}
};
const sliderWidth = width * 1;
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
const sliderHeight = 190;
export class WalletsCarousel extends Component {
walletsCarousel = React.createRef();
state = { isLoading: true };
_renderItem = ({ item, index }) => {
return ;
};
snapToItem = item => {
this.walletsCarousel.current.snapToItem(item);
};
onLayout = () => {
this.setState({ isLoading: false });
};
render() {
return (
<>
{this.state.isLoading && (
)}
>
);
}
}
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);
}
});
}
},
);
};
copyFromClipbard = async () => {
this.props.onBarScanned(await Clipboard.getString());
};
showActionSheet = async () => {
const isClipboardEmpty = (await Clipboard.getString()).replace(' ', '').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 = {
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.mediumFee));
} else if (selectedFeeType === NetworkTransactionFeeType.SLOW) {
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.slowFee));
} 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)}
containerStyle={{ paddingHorizontal: 0, marginHorizontal: 0, backgroundColor: BlueCurrentTheme.colors.transparent }}
bottomDivider={false}
title="Fast"
rightTitle={`${this.state.networkFees.fastestFee} sat/b`}
rightTitleStyle={{ fontSize: 13, color: BlueCurrentTheme.colors.alternativeTextColor }}
{...(this.state.selectedFeeType === NetworkTransactionFeeType.FAST
? { rightIcon: }
: { hideChevron: true })}
/>
this.onFeeSelected(NetworkTransactionFeeType.MEDIUM)}
containerStyle={{ paddingHorizontal: 0, marginHorizontal: 0, backgroundColor: BlueCurrentTheme.colors.transparent }}
bottomDivider={false}
title="Medium"
rightTitle={`${this.state.networkFees.mediumFee} sat/b`}
rightTitleStyle={{ fontSize: 13, color: BlueCurrentTheme.colors.alternativeTextColor }}
{...(this.state.selectedFeeType === NetworkTransactionFeeType.MEDIUM
? { rightIcon: }
: { hideChevron: true })}
/>
this.onFeeSelected(NetworkTransactionFeeType.SLOW)}
containerStyle={{ paddingHorizontal: 0, marginHorizontal: 0, backgroundColor: BlueCurrentTheme.colors.transparent }}
bottomDivider={false}
title="Slow"
rightTitle={`${this.state.networkFees.slowFee} sat/b`}
rightTitleStyle={{ fontSize: 13, color: BlueCurrentTheme.colors.alternativeTextColor }}
{...(this.state.selectedFeeType === NetworkTransactionFeeType.SLOW
? { rightIcon: }
: { hideChevron: true })}
/>
>
)}
this.customTextInput.focus()}>
Custom
(this.customTextInput = ref)}
maxLength={9}
style={{
borderColor: BlueCurrentTheme.colors.formBorder,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderWidth: 1.0,
borderBottomWidth: 0.5,
borderRadius: 4,
minHeight: 33,
maxWidth: 100,
minWidth: 44,
color: '#81868e',
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
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 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 && (
{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 && (
{' ' + 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 && (
)}
);
}
}
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: {
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,
},
{ width: width / tabs.length },
]}
>
))}
);
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',
},
});