/* eslint react/prop-types: 0 */ /* global alert */ /** @type {AppStorage} */ import React, { Component } from 'react'; import Ionicons from 'react-native-vector-icons/Ionicons'; import PropTypes from 'prop-types'; import { Icon, FormLabel, FormInput, Text, Header, List, ListItem } from 'react-native-elements'; import { TouchableOpacity, TouchableWithoutFeedback, Animated, ActivityIndicator, View, UIManager, StyleSheet, Dimensions, Image, Keyboard, SafeAreaView, InputAccessoryView, Clipboard, Platform, TextInput, } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { LightningCustodianWallet } from './class'; import Carousel from 'react-native-snap-carousel'; import DeviceInfo from 'react-native-device-info'; import { BitcoinUnit } from './models/bitcoinUnits'; import NavigationService from './NavigationService'; import ImagePicker from 'react-native-image-picker'; import WalletGradient from './class/walletGradient'; const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); let loc = require('./loc/'); /** @type {AppStorage} */ let BlueApp = require('./BlueApp'); const { height, width } = Dimensions.get('window'); const aspectRatio = height / width; 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 = 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 BlueButtonLink extends Component { render() { return ( {this.props.title} ); } } export const BlueNavigationStyle = (navigation, withNavigationCloseButton = false, customCloseButtonFunction = undefined) => ({ headerStyle: { backgroundColor: BlueApp.settings.brandingColor, borderBottomWidth: 0, elevation: 0, }, headerTitleStyle: { fontWeight: '600', color: BlueApp.settings.foregroundColor, }, headerTintColor: BlueApp.settings.foregroundColor, headerRight: withNavigationCloseButton ? ( { Keyboard.dismiss(); navigation.goBack(null); } : customCloseButtonFunction } > ) : null, headerBackTitle: null, }); export const BlueCopyToClipboardButton = ({ stringToCopy }) => { return ( Clipboard.setString(stringToCopy)}> {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 }; } 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: BlueApp.settings.alternativeTextColor, 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 }, }; } onSelectionChange = ({ nativeEvent: { selection, text } }) => { this.setState({ selection: { start: selection.end, end: selection.end } }); }; render() { return ( ); } } export class BlueFormInputAddress extends Component { render() { return ( ); } } export class BlueHeader extends Component { render() { return (
); } } export class BlueHeaderDefaultSub extends Component { render() { return (
{ // eslint-disable-next-line this.props.leftText } } rightComponent={ { // eslint-disable-next-line if (this.props.onClose) this.props.onClose(); }} > } {...this.props} /> ); } } export class BlueHeaderDefaultMain extends Component { render() { return (
{ // eslint-disable-next-line this.props.leftText } } rightComponent={ } /> ); } } export class BlueSpacing extends Component { render() { return ; } } export class BlueSpacing40 extends Component { render() { return ; } } export class BlueSpacingVariable extends Component { render() { if (isIpad) { return ; } else { return ; } } } export class is { static ipad() { return isIpad; } static iphone8() { if (Platform.OS !== 'ios') { return false; } return DeviceInfo.getDeviceId() === 'iPhone10,4'; } } export class BlueSpacing20 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() { return ( Total: {this.props.wallet.getBalance()} {BitcoinUnit.BTC} ); } } export class BlueDismissKeyboardInputAccessory extends Component { static InputAccessoryViewID = 'BlueDismissKeyboardInputAccessory'; render() { return Platform.OS !== 'ios' ? null : ( ); } } 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: BlueApp.settings.buttonBackgroundColor, }, ballIncoming: { width: 30, height: 30, borderRadius: 15, backgroundColor: BlueApp.settings.incomingBackgroundColor, transform: [{ rotate: '-45deg' }], }, ballIncomingWithoutRotate: { width: 30, height: 30, borderRadius: 15, backgroundColor: BlueApp.settings.incomingBackgroundColor, }, ballReceive: { width: 30, height: 30, borderBottomLeftRadius: 15, backgroundColor: BlueApp.settings.incomingBackgroundColor, transform: [{ rotate: '-45deg' }], }, ballOutgoing: { width: 30, height: 30, borderRadius: 15, backgroundColor: BlueApp.settings.outgoingBackgroundColor, transform: [{ rotate: '225deg' }], }, ballOutgoingWithoutRotate: { width: 30, height: 30, borderRadius: 15, backgroundColor: BlueApp.settings.outgoingBackgroundColor, }, 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 class BlueTransactionListItem 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 = 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, }; }; 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('TransactionDetails', { 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; } } }); if (lightningWallet.length === 1) { NavigationService.navigate('LNDViewInvoice', { invoice: this.props.item, fromWallet: lightningWallet[0], isModal: false, }); } } }; render() { 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('TransactionDetails', { 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 { constructor(props) { super(props); // eslint-disable-next-line WalletsCarousel.handleClick = props.handleClick; // because cant access `this` from _renderItem WalletsCarousel.handleLongPress = props.handleLongPress; // eslint-disable-next-line this.onSnapToItem = props.onSnapToItem; } _renderItem({ item, index }) { let scaleValue = new Animated.Value(1.0); let props = { duration: 50 }; if (Platform.OS === 'android') { props['useNativeDriver'] = true; } this.onPressedIn = () => { props.toValue = 0.9; Animated.spring(scaleValue, props).start(); }; this.onPressedOut = () => { props.toValue = 1.0; Animated.spring(scaleValue, props).start(); }; if (!item) { return ( { if (WalletsCarousel.handleClick) { WalletsCarousel.handleClick(index); } }} /> ); } return ( { if (WalletsCarousel.handleClick) { WalletsCarousel.handleClick(index); } }} > {item.getLabel()} {loc.formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)} {loc.wallets.list.latest_transaction} {loc.transactionTimeToReadable(item.getLatestTransactionTime())} ); } render() { return ( { WalletsCarousel.carousel = c; }} renderItem={this._renderItem} sliderWidth={sliderWidth} sliderHeight={sliderHeight} itemWidth={itemWidth} inactiveSlideScale={1} inactiveSlideOpacity={0.7} contentContainerCustomStyle={{ left: -20 }} onSnapToItem={index => { if (this.onSnapToItem) { this.onSnapToItem(index); } console.log('snapped to card #', index); }} /> ); } } export class BlueAddressInput extends Component { static propTypes = { isLoading: PropTypes.bool, onChangeText: PropTypes.func, onBarScanned: PropTypes.func, address: PropTypes.string, placeholder: PropTypes.string, }; static defaultProps = { isLoading: false, address: '', placeholder: loc.send.details.address, }; render() { return ( { this.props.onChangeText(text); }} placeholder={this.props.placeholder} numberOfLines={1} value={this.props.address} style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }} editable={!this.props.isLoading} onSubmitEditing={Keyboard.dismiss} {...this.props} /> { Keyboard.dismiss(); ImagePicker.showImagePicker( { title: null, mediaType: 'photo', takePhotoButtonTitle: null, customButtons: [{ name: 'navigatetoQRScan', title: 'Use Camera' }], }, response => { if (response.customButton) { NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned }); } else if (response.uri) { const uri = response.uri.toString().replace('file://', ''); LocalQRCode.decode(uri, (error, result) => { if (!error) { this.props.onBarScanned(result); } else { alert('The selected image does not contain a QR Code.'); } }); } }, ); }} style={{ width: 75, height: 36, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#bebebe', borderRadius: 4, paddingVertical: 4, paddingHorizontal: 8, marginHorizontal: 4, }} > {loc.send.details.scan} ); } } export class BlueBitcoinAmount extends Component { static propTypes = { isLoading: PropTypes.bool, amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onChangeText: PropTypes.func, disabled: PropTypes.bool, unit: PropTypes.string, }; static defaultProps = { unit: BitcoinUnit.BTC, }; render() { const amount = typeof this.props.amount === 'number' ? this.props.amount.toString() : this.props.amount; return ( this.textInput.focus()}> { text = text.replace(',', '.'); 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.'; } this.props.onChangeText(text); }} 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} {loc.formatBalance( this.props.unit === BitcoinUnit.BTC ? amount || 0 : loc.formatBalanceWithoutSuffix(amount || 0, BitcoinUnit.BTC, false), BitcoinUnit.LOCAL_CURRENCY, false, )} ); } }