mirror of https://github.com/trezor/trezor-wallet
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
511 lines
16 KiB
511 lines
16 KiB
/* @flow */
|
|
|
|
import React, { useState } from 'react';
|
|
import styled, { css } from 'styled-components';
|
|
import { FormattedMessage } from 'react-intl';
|
|
|
|
import { Button, Select, Input, Icon, Link, P, colors, icons as ICONS } from 'trezor-ui-components';
|
|
import { FONT_SIZE, FONT_WEIGHT, SCREEN_SIZE } from 'config/variables';
|
|
import { FIAT_CURRENCIES } from 'config/app';
|
|
import Title from 'views/Wallet/components/Title';
|
|
import l10nCommonMessages from 'views/common.messages';
|
|
import Content from 'views/Wallet/components/Content';
|
|
import PendingTransactions from '../components/PendingTransactions';
|
|
import AdvancedForm from './components/AdvancedForm';
|
|
|
|
import l10nMessages from './index.messages';
|
|
import l10nSendMessages from '../../common.messages';
|
|
|
|
import type { Props } from './Container';
|
|
|
|
const AmountInputLabelWrapper = styled.div`
|
|
display: flex;
|
|
justify-content: space-between;
|
|
`;
|
|
|
|
const AmountInputLabel = styled.span`
|
|
text-align: right;
|
|
color: ${colors.TEXT_SECONDARY};
|
|
`;
|
|
|
|
const InputRow = styled.div`
|
|
padding-bottom: 28px;
|
|
`;
|
|
|
|
const SetMaxAmountButton = styled(Button)`
|
|
padding: 0 10px;
|
|
font-size: ${FONT_SIZE.SMALL};
|
|
transition: all 0s;
|
|
border-radius: 0;
|
|
border-right: 0;
|
|
border-left: 0;
|
|
`;
|
|
|
|
const CurrencySelect = styled(Select)`
|
|
min-width: 77px;
|
|
height: 40px;
|
|
flex: 0.2;
|
|
`;
|
|
|
|
const FeeOptionWrapper = styled.div`
|
|
display: flex;
|
|
justify-content: space-between;
|
|
`;
|
|
|
|
const OptionValue = styled(P)`
|
|
flex: 1 0 auto;
|
|
min-width: 70px;
|
|
margin-right: 5px;
|
|
`;
|
|
|
|
const OptionLabel = styled(P)`
|
|
flex: 0 1 auto;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
text-align: right;
|
|
word-break: break-all;
|
|
`;
|
|
|
|
const FeeLabelWrapper = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
padding-bottom: 10px;
|
|
`;
|
|
|
|
const FeeLabel = styled.span`
|
|
color: ${colors.TEXT_SECONDARY};
|
|
`;
|
|
|
|
const UpdateFeeWrapper = styled.span`
|
|
margin-left: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: ${FONT_SIZE.SMALL};
|
|
color: ${colors.WARNING_PRIMARY};
|
|
`;
|
|
|
|
const StyledLink = styled(Link)`
|
|
margin-left: 4px;
|
|
white-space: nowrap;
|
|
`;
|
|
|
|
const ToggleAdvancedSettingsWrapper = styled.div`
|
|
min-height: 40px;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
|
|
@media screen and (max-width: ${SCREEN_SIZE.MD}) {
|
|
${props =>
|
|
props.isAdvancedSettingsHidden &&
|
|
css`
|
|
flex-direction: column;
|
|
`}
|
|
}
|
|
`;
|
|
|
|
const ToggleAdvancedSettingsButton = styled(Button)`
|
|
min-height: 40px;
|
|
padding: 0;
|
|
display: flex;
|
|
flex: 1 1 0;
|
|
align-items: center;
|
|
font-weight: ${FONT_WEIGHT.MEDIUM};
|
|
justify-content: flex-start;
|
|
`;
|
|
|
|
const FormButtons = styled.div`
|
|
display: flex;
|
|
flex: 1 1;
|
|
|
|
@media screen and (max-width: ${SCREEN_SIZE.MD}) {
|
|
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};
|
|
}
|
|
|
|
button + button {
|
|
margin-left: 5px;
|
|
}
|
|
`;
|
|
|
|
const SendButton = styled(Button)`
|
|
word-break: break-all;
|
|
flex: 1;
|
|
`;
|
|
|
|
const ClearButton = styled(Button)``;
|
|
|
|
const AdvancedSettingsIcon = styled(Icon)`
|
|
margin-left: 10px;
|
|
`;
|
|
|
|
const QrButton = styled(Button)`
|
|
border-top-left-radius: 0px;
|
|
border-bottom-left-radius: 0px;
|
|
border-left: 0px;
|
|
height: 40px;
|
|
padding: 0 10px;
|
|
`;
|
|
|
|
const LocalAmountWrapper = styled.div`
|
|
display: flex;
|
|
align-self: flex-start;
|
|
margin-top: 26px;
|
|
|
|
@media screen and (max-width: ${SCREEN_SIZE.MD}) {
|
|
flex: 1 0 100%;
|
|
justify-content: flex-end;
|
|
margin-top: 0px;
|
|
padding-top: 28px;
|
|
}
|
|
`;
|
|
|
|
const AmountRow = styled(InputRow)`
|
|
display: flex;
|
|
align-items: flex-end;
|
|
padding-bottom: 28px;
|
|
|
|
@media screen and (max-width: ${SCREEN_SIZE.MD}) {
|
|
flex-wrap: wrap;
|
|
}
|
|
`;
|
|
|
|
const LocalAmountInput = styled(Input)`
|
|
@media screen and (max-width: ${SCREEN_SIZE.MD}) {
|
|
flex: 1 1 100%;
|
|
}
|
|
`;
|
|
|
|
const LocalCurrencySelect = styled(Select)`
|
|
min-width: 77px;
|
|
height: 40px;
|
|
|
|
@media screen and (max-width: ${SCREEN_SIZE.MD}) {
|
|
flex: 1 1 0;
|
|
}
|
|
`;
|
|
|
|
const EqualsSign = styled.div`
|
|
align-self: center;
|
|
padding: 0 10px;
|
|
font-size: ${FONT_SIZE.BIGGER};
|
|
|
|
@media screen and (max-width: ${SCREEN_SIZE.MD}) {
|
|
display: none;
|
|
}
|
|
`;
|
|
|
|
const StyledIcon = styled(Icon)`
|
|
margin-right: 6px;
|
|
`;
|
|
|
|
// render helpers
|
|
const getAddressInputState = (
|
|
address: string,
|
|
addressErrors: boolean,
|
|
addressWarnings: boolean
|
|
): ?string => {
|
|
let state = null;
|
|
if (address && !addressErrors) {
|
|
state = 'success';
|
|
}
|
|
if (addressWarnings && !addressErrors) {
|
|
state = 'warning';
|
|
}
|
|
if (addressErrors) {
|
|
state = 'error';
|
|
}
|
|
return state;
|
|
};
|
|
|
|
const getAmountInputState = (amountErrors: boolean, amountWarnings: boolean): ?string => {
|
|
let state = null;
|
|
if (amountWarnings && !amountErrors) {
|
|
state = 'warning';
|
|
}
|
|
if (amountErrors) {
|
|
state = 'error';
|
|
}
|
|
return state;
|
|
};
|
|
|
|
const buildCurrencyOption = currency => {
|
|
return { value: currency, label: currency.toUpperCase() };
|
|
};
|
|
|
|
// stateless component
|
|
const AccountSend = (props: Props) => {
|
|
const device = props.wallet.selectedDevice;
|
|
const { account, network, discovery, shouldRender } = props.selectedAccount;
|
|
const {
|
|
address,
|
|
amount,
|
|
localAmount,
|
|
localCurrency,
|
|
setMax,
|
|
feeLevels,
|
|
selectedFeeLevel,
|
|
feeNeedsUpdate,
|
|
total,
|
|
errors,
|
|
warnings,
|
|
infos,
|
|
sending,
|
|
advanced,
|
|
} = props.sendForm;
|
|
|
|
const {
|
|
toggleAdvanced,
|
|
onAddressChange,
|
|
onAmountChange,
|
|
onLocalAmountChange,
|
|
onLocalCurrencyChange,
|
|
onSetMax,
|
|
onFeeLevelChange,
|
|
updateFeeLevels,
|
|
onSend,
|
|
onClear,
|
|
} = props.sendFormActions;
|
|
|
|
const [touched, setTouched] = useState(false);
|
|
|
|
if (!device || !account || !discovery || !network || !shouldRender) {
|
|
const { loader, exceptionPage } = props.selectedAccount;
|
|
return <Content loader={loader} exceptionPage={exceptionPage} isLoading />;
|
|
}
|
|
|
|
let isSendButtonDisabled: boolean =
|
|
Object.keys(errors).length > 0 ||
|
|
total === '0' ||
|
|
amount.length === 0 ||
|
|
address.length === 0 ||
|
|
sending ||
|
|
account.imported;
|
|
|
|
let sendButtonText = <FormattedMessage {...l10nSendMessages.TR_SEND} values={{ amount: '' }} />;
|
|
if (total !== '0') {
|
|
sendButtonText = (
|
|
<FormattedMessage
|
|
{...l10nSendMessages.TR_SEND}
|
|
values={{ amount: `${total} ${network.symbol}` }}
|
|
/>
|
|
);
|
|
}
|
|
if (!device.connected) {
|
|
sendButtonText = <FormattedMessage {...l10nSendMessages.TR_DEVICE_IS_NOT_CONNECTED} />;
|
|
isSendButtonDisabled = true;
|
|
} else if (!device.available) {
|
|
sendButtonText = <FormattedMessage {...l10nSendMessages.TR_DEVICE_IS_UNAVAILABLE} />;
|
|
isSendButtonDisabled = true;
|
|
} else if (!discovery.completed) {
|
|
sendButtonText = <FormattedMessage {...l10nSendMessages.TR_LOADING_ACCOUNTS} />;
|
|
isSendButtonDisabled = true;
|
|
}
|
|
|
|
const tokensSelectData: Array<{ value: string, label: string }> = [
|
|
{ value: network.symbol, label: network.symbol },
|
|
];
|
|
const tokensSelectValue = tokensSelectData[0];
|
|
const isAdvancedSettingsHidden = !advanced;
|
|
const accountReserve: ?string =
|
|
account.networkType === 'ripple' && !account.empty ? account.reserve : null;
|
|
|
|
return (
|
|
<Content>
|
|
<Title>
|
|
<FormattedMessage {...l10nMessages.TR_SEND_RIPPLE} />
|
|
</Title>
|
|
<InputRow>
|
|
<Input
|
|
state={getAddressInputState(address, !!errors.address, !!warnings.address)}
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
topLabel={props.intl.formatMessage(l10nCommonMessages.TR_ADDRESS)}
|
|
bottomText={
|
|
<>
|
|
{(errors.address && <FormattedMessage {...errors.address} />) ||
|
|
(warnings.address && <FormattedMessage {...warnings.address} />) ||
|
|
(infos.address && <FormattedMessage {...infos.address} />)}
|
|
</>
|
|
}
|
|
value={address}
|
|
onChange={event => onAddressChange(event.target.value)}
|
|
sideAddons={[
|
|
<QrButton key="qrButton" isWhite onClick={props.openQrModal}>
|
|
<Icon size={25} color={colors.TEXT_SECONDARY} icon={ICONS.QRCODE} />
|
|
</QrButton>,
|
|
]}
|
|
/>
|
|
</InputRow>
|
|
<AmountRow>
|
|
<Input
|
|
state={getAmountInputState(!!errors.amount, !!warnings.amount)}
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
topLabel={
|
|
<AmountInputLabelWrapper>
|
|
<AmountInputLabel>
|
|
<FormattedMessage {...l10nSendMessages.TR_AMOUNT} />
|
|
</AmountInputLabel>
|
|
{accountReserve && (
|
|
<AmountInputLabel>
|
|
<FormattedMessage
|
|
{...l10nMessages.TR_XRP_RESERVE}
|
|
values={{ value: `${accountReserve} ${network.symbol}` }}
|
|
/>
|
|
</AmountInputLabel>
|
|
)}
|
|
</AmountInputLabelWrapper>
|
|
}
|
|
value={amount}
|
|
onChange={event => onAmountChange(event.target.value)}
|
|
bottomText={
|
|
<>
|
|
{(errors.amount && <FormattedMessage {...errors.amount} />) ||
|
|
(warnings.amount && <FormattedMessage {...warnings.amount} />) ||
|
|
(infos.amount && <FormattedMessage {...infos.amount} />)}
|
|
</>
|
|
}
|
|
sideAddons={[
|
|
<SetMaxAmountButton key="icon" onClick={() => onSetMax()} isWhite={!setMax}>
|
|
{!setMax && (
|
|
<StyledIcon
|
|
icon={ICONS.TOP}
|
|
size={14}
|
|
color={colors.TEXT_SECONDARY}
|
|
/>
|
|
)}
|
|
{setMax && (
|
|
<StyledIcon icon={ICONS.SUCCESS} size={14} color={colors.WHITE} />
|
|
)}
|
|
<FormattedMessage {...l10nSendMessages.TR_SET_MAX} />
|
|
</SetMaxAmountButton>,
|
|
<CurrencySelect
|
|
key="currency"
|
|
isSearchable={false}
|
|
isClearable={false}
|
|
value={tokensSelectValue}
|
|
isDisabled
|
|
options={tokensSelectData}
|
|
/>,
|
|
]}
|
|
/>
|
|
<LocalAmountWrapper>
|
|
<EqualsSign>=</EqualsSign>
|
|
<LocalAmountInput
|
|
state={getAmountInputState(!!errors.amount, !!warnings.amount)}
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
value={localAmount}
|
|
onChange={event => onLocalAmountChange(event.target.value)}
|
|
sideAddons={[
|
|
<LocalCurrencySelect
|
|
key="local-currency"
|
|
isSearchable
|
|
isClearable={false}
|
|
onChange={option => onLocalCurrencyChange(option)}
|
|
value={buildCurrencyOption(localCurrency)}
|
|
options={FIAT_CURRENCIES.map(c => buildCurrencyOption(c))}
|
|
/>,
|
|
]}
|
|
/>
|
|
</LocalAmountWrapper>
|
|
</AmountRow>
|
|
|
|
<InputRow>
|
|
<FeeLabelWrapper>
|
|
<FeeLabel>
|
|
<FormattedMessage {...l10nSendMessages.TR_FEE} />
|
|
</FeeLabel>
|
|
{feeNeedsUpdate && (
|
|
<UpdateFeeWrapper>
|
|
<StyledIcon
|
|
icon={ICONS.WARNING}
|
|
color={colors.WARNING_PRIMARY}
|
|
size={12}
|
|
/>
|
|
<FormattedMessage {...l10nSendMessages.TR_RECOMMENDED_FEES_UPDATED} />{' '}
|
|
<StyledLink onClick={updateFeeLevels} isGreen>
|
|
<FormattedMessage {...l10nSendMessages.TR_CLICK_HERE_TO_USE_THEM} />
|
|
</StyledLink>
|
|
</UpdateFeeWrapper>
|
|
)}
|
|
</FeeLabelWrapper>
|
|
<Select
|
|
isSearchable={false}
|
|
isClearable={false}
|
|
value={selectedFeeLevel}
|
|
onChange={onFeeLevelChange}
|
|
options={feeLevels}
|
|
formatOptionLabel={option => (
|
|
<FeeOptionWrapper>
|
|
<OptionValue>{option.value}</OptionValue>
|
|
<OptionLabel>{option.label}</OptionLabel>
|
|
</FeeOptionWrapper>
|
|
)}
|
|
/>
|
|
</InputRow>
|
|
|
|
<ToggleAdvancedSettingsWrapper isAdvancedSettingsHidden={isAdvancedSettingsHidden}>
|
|
<ToggleAdvancedSettingsButton
|
|
isTransparent
|
|
onClick={() => {
|
|
toggleAdvanced();
|
|
setTouched(true);
|
|
}}
|
|
>
|
|
<FormattedMessage {...l10nSendMessages.TR_ADVANCED_SETTINGS} />
|
|
<AdvancedSettingsIcon
|
|
icon={ICONS.ARROW_DOWN}
|
|
color={colors.TEXT_SECONDARY}
|
|
size={12}
|
|
isActive={advanced}
|
|
canAnimate={touched || advanced}
|
|
/>
|
|
</ToggleAdvancedSettingsButton>
|
|
|
|
{isAdvancedSettingsHidden && (
|
|
<FormButtons isAdvancedSettingsHidden={isAdvancedSettingsHidden}>
|
|
<ClearButton isWhite onClick={() => onClear()}>
|
|
<FormattedMessage {...l10nCommonMessages.TR_CLEAR} />
|
|
</ClearButton>
|
|
<SendButton isDisabled={isSendButtonDisabled} onClick={() => onSend()}>
|
|
{sendButtonText}
|
|
</SendButton>
|
|
</FormButtons>
|
|
)}
|
|
</ToggleAdvancedSettingsWrapper>
|
|
|
|
{advanced && (
|
|
<AdvancedForm {...props}>
|
|
<FormButtons isAdvancedSettingsHidden={isAdvancedSettingsHidden}>
|
|
<ClearButton isWhite onClick={() => onClear()}>
|
|
<FormattedMessage {...l10nCommonMessages.TR_CLEAR} />
|
|
</ClearButton>
|
|
<SendButton isDisabled={isSendButtonDisabled} onClick={() => onSend()}>
|
|
{sendButtonText}
|
|
</SendButton>
|
|
</FormButtons>
|
|
</AdvancedForm>
|
|
)}
|
|
|
|
{props.selectedAccount.pending.length > 0 ||
|
|
(account.imported && (
|
|
<PendingTransactions
|
|
pending={props.selectedAccount.pending}
|
|
tokens={props.selectedAccount.tokens}
|
|
network={network}
|
|
/>
|
|
))}
|
|
</Content>
|
|
);
|
|
};
|
|
|
|
export default AccountSend;
|