323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import * as React from 'react';
|
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
import { inject, observer } from 'mobx-react';
|
|
import BigNumber from 'bignumber.js';
|
|
|
|
import Amount from './Amount';
|
|
import { Row } from './layout/Row';
|
|
|
|
import { themeColor } from '../utils/ThemeUtils';
|
|
import { getSatAmount } from '../utils/AmountUtils';
|
|
import {
|
|
SATS_PER_BTC,
|
|
formatBitcoinWithSpaces,
|
|
numberWithCommas
|
|
} from '../utils/UnitsUtils';
|
|
|
|
import { fiatStore, settingsStore, unitsStore } from '../stores/Stores';
|
|
import FiatStore from '../stores/FiatStore';
|
|
import SettingsStore from '../stores/SettingsStore';
|
|
import UnitsStore from '../stores/UnitsStore';
|
|
|
|
import ExchangeBitcoinSVG from '../assets/images/SVG/ExchangeBitcoin.svg';
|
|
import ExchangeFiatSVG from '../assets/images/SVG/ExchangeFiat.svg';
|
|
|
|
import NavigationService from '../NavigationService';
|
|
|
|
interface AmountInputProps {
|
|
onAmountChange: (amount: string, satAmount: string | number) => void;
|
|
amount?: string;
|
|
sats?: string;
|
|
locked?: boolean;
|
|
title?: string;
|
|
hideConversion?: boolean;
|
|
hideUnitChangeButton?: boolean;
|
|
forceUnit?: 'sats' | 'BTC' | 'fiat';
|
|
FiatStore?: FiatStore;
|
|
SettingsStore?: SettingsStore;
|
|
UnitsStore?: UnitsStore;
|
|
prefix?: any;
|
|
error?: boolean;
|
|
}
|
|
|
|
interface AmountInputState {
|
|
satAmount: string | number;
|
|
}
|
|
|
|
const getAmount = (sats: string | number, forceUnit?: string) => {
|
|
const { fiatRates } = fiatStore;
|
|
const { settings } = settingsStore;
|
|
const { fiat } = settings;
|
|
const units = forceUnit || unitsStore.units;
|
|
|
|
// replace , with . for unit separator
|
|
const value = sats ? sats.toString().replace(/,/g, '.') : '';
|
|
|
|
const fiatEntry =
|
|
fiat && fiatRates
|
|
? fiatRates.find((entry: any) => entry.code === fiat)
|
|
: null;
|
|
|
|
const rate = fiat && fiatRates && fiatEntry ? fiatEntry.rate : 0;
|
|
|
|
let amount: string = '';
|
|
switch (units) {
|
|
case 'sats':
|
|
amount = value;
|
|
break;
|
|
case 'BTC':
|
|
amount = new BigNumber(value || 0).div(SATS_PER_BTC).toString();
|
|
break;
|
|
case 'fiat':
|
|
const { decimalPlaces } = fiatStore.getSymbol();
|
|
const decimals = decimalPlaces !== undefined ? decimalPlaces : 2;
|
|
amount = rate
|
|
? new BigNumber(value.toString().replace(/,/g, '.'))
|
|
.times(rate)
|
|
.div(SATS_PER_BTC)
|
|
.toNumber()
|
|
.toFixed(decimals)
|
|
: '0';
|
|
break;
|
|
}
|
|
|
|
return amount;
|
|
};
|
|
|
|
@inject('FiatStore', 'SettingsStore', 'UnitsStore')
|
|
@observer
|
|
export default class AmountInput extends React.Component<
|
|
AmountInputProps,
|
|
AmountInputState
|
|
> {
|
|
constructor(props: any) {
|
|
super(props);
|
|
|
|
const { amount, onAmountChange } = props;
|
|
let satAmount = '0';
|
|
if (amount)
|
|
satAmount = getSatAmount(amount, props.forceUnit).toString();
|
|
|
|
onAmountChange(amount, satAmount);
|
|
this.state = {
|
|
satAmount
|
|
};
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Readonly<AmountInputProps>): void {
|
|
const { amount, forceUnit, onAmountChange } = this.props;
|
|
if (amount !== prevProps.amount || forceUnit !== prevProps.forceUnit) {
|
|
if (forceUnit === 'sats' && forceUnit !== prevProps.forceUnit) {
|
|
const currentSatAmount = getSatAmount(amount || '', forceUnit);
|
|
this.setState({ satAmount: currentSatAmount });
|
|
|
|
onAmountChange(currentSatAmount.toString(), currentSatAmount);
|
|
} else {
|
|
const satAmount = getSatAmount(amount || '', forceUnit);
|
|
if (satAmount !== this.state.satAmount) {
|
|
this.setState({ satAmount });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onChangeUnits = () => {
|
|
const { amount, onAmountChange, UnitsStore }: any = this.props;
|
|
UnitsStore.changeUnits();
|
|
const satAmount = getSatAmount(amount, this.props.forceUnit);
|
|
onAmountChange(amount, satAmount);
|
|
this.setState({ satAmount });
|
|
};
|
|
|
|
getDisplayValue = (): string => {
|
|
const { amount, sats, forceUnit } = this.props;
|
|
return amount !== undefined
|
|
? amount
|
|
: sats
|
|
? getAmount(sats, forceUnit)
|
|
: '0';
|
|
};
|
|
|
|
openKeypad = () => {
|
|
if (!this.props.locked) {
|
|
const { hideUnitChangeButton } = this.props;
|
|
let displayValue = this.getDisplayValue();
|
|
|
|
// Strip trailing zeros after decimal point so the keypad
|
|
// doesn't think decimal places are already filled
|
|
// e.g. "0.00" → "0", "50.20" → "50.2", "50.25" → "50.25"
|
|
if (displayValue.includes('.')) {
|
|
displayValue = displayValue
|
|
.replace(/0+$/, '')
|
|
.replace(/\.$/, '');
|
|
}
|
|
|
|
// Navigate to keypad view with callback
|
|
NavigationService.navigate('AmountKeypad', {
|
|
initialAmount: displayValue || '0',
|
|
hideUnitChangeButton,
|
|
forceUnit: this.props.forceUnit,
|
|
onConfirm: (newAmount: string) => {
|
|
const { onAmountChange, forceUnit } = this.props;
|
|
const satAmount = getSatAmount(newAmount, forceUnit);
|
|
onAmountChange(newAmount, satAmount);
|
|
this.setState({ satAmount });
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const { satAmount } = this.state;
|
|
const {
|
|
title,
|
|
locked,
|
|
hideConversion,
|
|
hideUnitChangeButton,
|
|
FiatStore,
|
|
UnitsStore,
|
|
SettingsStore,
|
|
forceUnit,
|
|
prefix,
|
|
error
|
|
} = this.props;
|
|
const { units }: any = UnitsStore;
|
|
const effectiveUnits = forceUnit || units;
|
|
const { getRate }: any = FiatStore;
|
|
const { settings }: any = SettingsStore;
|
|
const { fiatEnabled } = settings;
|
|
const displayValue = this.getDisplayValue();
|
|
|
|
let formattedAmount: string;
|
|
if (effectiveUnits === 'BTC') {
|
|
formattedAmount = `₿${formatBitcoinWithSpaces(
|
|
displayValue || '0'
|
|
)}`;
|
|
} else if (effectiveUnits === 'fiat') {
|
|
formattedAmount = FiatStore!.formatAmountForDisplay(
|
|
displayValue || '0'
|
|
);
|
|
} else {
|
|
const isSingular = parseFloat(displayValue) === 1;
|
|
formattedAmount = `${numberWithCommas(displayValue || '0')} ${
|
|
isSingular ? 'sat' : 'sats'
|
|
}`;
|
|
}
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{title && (
|
|
<Text
|
|
style={{
|
|
fontFamily: 'PPNeueMontreal-Book',
|
|
color: themeColor('secondaryText'),
|
|
fontSize: 14,
|
|
marginBottom: 5
|
|
}}
|
|
>
|
|
{title}
|
|
</Text>
|
|
)}
|
|
<Row>
|
|
{prefix ? prefix : undefined}
|
|
<TouchableOpacity
|
|
onPress={this.openKeypad}
|
|
disabled={locked}
|
|
activeOpacity={0.7}
|
|
style={[
|
|
styles.amountDisplay,
|
|
{
|
|
backgroundColor: themeColor('secondary'),
|
|
borderWidth: error ? 1 : 0,
|
|
borderColor: error
|
|
? themeColor('error')
|
|
: undefined,
|
|
opacity: locked ? 0.8 : 1
|
|
}
|
|
]}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.amountText,
|
|
{
|
|
color:
|
|
displayValue === '0' ||
|
|
displayValue === ''
|
|
? themeColor('secondaryText')
|
|
: themeColor('text')
|
|
}
|
|
]}
|
|
>
|
|
{formattedAmount}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
{!hideUnitChangeButton && !locked && (
|
|
<TouchableOpacity
|
|
onPress={() => this.onChangeUnits()}
|
|
style={{ marginLeft: 15 }}
|
|
>
|
|
{UnitsStore!.getNextUnit() === 'fiat' ? (
|
|
<ExchangeFiatSVG
|
|
fill={themeColor('text')}
|
|
width="35"
|
|
height="35"
|
|
/>
|
|
) : (
|
|
<ExchangeBitcoinSVG
|
|
fill={themeColor('text')}
|
|
width="35"
|
|
height="35"
|
|
/>
|
|
)}
|
|
</TouchableOpacity>
|
|
)}
|
|
</Row>
|
|
{!hideConversion && (
|
|
<View style={{ marginBottom: 10 }}>
|
|
{fiatEnabled && (
|
|
<Text
|
|
style={{
|
|
fontFamily: 'PPNeueMontreal-Book',
|
|
color: themeColor('text')
|
|
}}
|
|
>
|
|
{getRate(effectiveUnits === 'sats')}
|
|
</Text>
|
|
)}
|
|
{fiatEnabled && effectiveUnits !== 'fiat' && (
|
|
<Amount sats={satAmount} fixedUnits="fiat" />
|
|
)}
|
|
{effectiveUnits !== 'BTC' && (
|
|
<Amount sats={satAmount} fixedUnits="BTC" />
|
|
)}
|
|
{effectiveUnits !== 'sats' && (
|
|
<Amount sats={satAmount} fixedUnits="sats" />
|
|
)}
|
|
</View>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
export { getSatAmount };
|
|
|
|
const styles = StyleSheet.create({
|
|
amountDisplay: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
height: 60,
|
|
top: 10,
|
|
borderRadius: 6,
|
|
marginBottom: 20,
|
|
paddingLeft: 10,
|
|
paddingRight: 10,
|
|
overflow: 'hidden'
|
|
},
|
|
amountText: {
|
|
fontFamily: 'PPNeueMontreal-Book',
|
|
fontSize: 20
|
|
}
|
|
});
|