Update dropdowns, inputs, and textareas (#1169)

* Align footer to bottom

* Fix request payment offset padding

* Update request payment padding

* Add new Input and Dropdown components

* Fix offset margins in equiv vals

* Update all send tx inputs & dropdowns

* Update generate wallet dropdowns

* Update inputs & dropdowns for contracts tab

* Add inputs & dropdowns for all but swap tab

* amend

* Fix imports

* inputs are invalid when not disabled or readonly

* Fix offset refresh button

* Add togglable password back to wallet generation

* Update swap inputs, textareas, and dropdowns

* Update any outstanding inputs

* Make UnitDropDown searchable

* unitdropdown searchanble if options > 10

* Fix css issues

* Reset before setting currentTo
This commit is contained in:
James Prado 2018-03-01 12:53:29 -05:00 committed by Daniel Ternyak
parent 95072d350a
commit 9ef1920fe0
77 changed files with 1131 additions and 929 deletions

View File

@ -8,7 +8,7 @@ export interface Pairs {
}
export interface SwapInput {
id: string;
label: string;
amount: number | string;
}

View File

@ -1,6 +1,8 @@
import React from 'react';
import { AddressFieldFactory } from './AddressFieldFactory';
import { donationAddressMap } from 'config';
import translate from 'translations';
import { Input } from 'components/ui';
interface Props {
isReadOnly?: boolean;
@ -9,16 +11,20 @@ interface Props {
export const AddressField: React.SFC<Props> = ({ isReadOnly }) => (
<AddressFieldFactory
withProps={({ currentTo, isValid, onChange, readOnly }) => (
<React.Fragment>
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
type="text"
value={currentTo.raw}
placeholder={donationAddressMap.ETH}
readOnly={!!(isReadOnly || readOnly)}
onChange={onChange}
/>
</React.Fragment>
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('SEND_addr')}</div>
<Input
className={`input-group-input ${isValid ? '' : 'invalid'}`}
type="text"
value={currentTo.raw}
placeholder={donationAddressMap.ETH}
readOnly={!!(isReadOnly || readOnly)}
spellCheck={false}
onChange={onChange}
/>
</label>
</div>
)}
/>
);

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react';
import { Identicon, Spinner } from 'components/ui';
import translate from 'translations';
import { Query } from 'components/renderCbs';
import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction';
import { connect } from 'react-redux';
@ -49,7 +48,6 @@ class AddressInputFactoryClass extends Component<Props> {
return (
<div className="row form-group">
<div className="col-xs-11">
<label>{translate('SEND_addr')}:</label>
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>

View File

@ -2,6 +2,7 @@ import React from 'react';
import { AmountFieldFactory } from './AmountFieldFactory';
import { UnitDropDown } from 'components';
import translate, { translateRaw } from 'translations';
import { Input } from 'components/ui';
interface Props {
hasUnitDropdown?: boolean;
@ -16,12 +17,12 @@ export const AmountField: React.SFC<Props> = ({
}) => (
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<React.Fragment>
<label>{translate('SEND_amount')}</label>
<div className="input-group">
<input
className={`form-control ${
isAmountValid(raw, customValidator, isValid) ? 'is-valid' : 'is-invalid'
<div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown">
<div className="input-group-header">{translate('SEND_amount')}</div>
<Input
className={`input-group-input ${
isAmountValid(raw, customValidator, isValid) ? '' : 'invalid'
}`}
type="number"
placeholder={translateRaw('SEND_amount_short')}
@ -30,8 +31,8 @@ export const AmountField: React.SFC<Props> = ({
onChange={onChange}
/>
{hasUnitDropdown && <UnitDropDown showAllTokens={showAllTokens} />}
</div>
</React.Fragment>
</label>
</div>
)}
/>
);

View File

@ -41,7 +41,6 @@
flex-wrap: nowrap;
align-items: center;
&-fiat-symbol {
height: 18px;
width: 18px;
margin-right: 0.5rem;
text-align: center;

View File

@ -1,9 +1,8 @@
import React from 'react';
import classnames from 'classnames';
import { HELP_ARTICLE } from 'config';
import { isPositiveIntegerOrZero, isValidETHAddress } from 'libs/validators';
import translate from 'translations';
import { HelpLink } from 'components/ui';
import { HelpLink, Input } from 'components/ui';
import './AddCustomTokenForm.scss';
import { Token } from 'types/network';
@ -42,7 +41,6 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
public render() {
const { address, symbol, decimal } = this.state;
const inputClasses = 'AddCustom-field-input form-control input-sm';
const errors = this.getErrors();
const fields = [
@ -69,11 +67,10 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
return (
<label className="AddCustom-field form-group" key={field.name}>
<span className="AddCustom-field-label">{field.label}</span>
<input
className={classnames(
inputClasses,
errors[field.name] ? 'is-invalid' : field.value ? 'is-valid' : ''
)}
<Input
className={`${
errors[field.name] ? 'invalid' : field.value ? 'valid' : ''
} AddCustom-field-input input-sm`}
type="text"
name={field.name}
value={field.value}

View File

@ -2,21 +2,24 @@ import { DataFieldFactory } from './DataFieldFactory';
import React from 'react';
import translate from 'translations';
import { donationAddressMap } from 'config';
import { Input } from 'components/ui';
export const DataField: React.SFC<{}> = () => (
<DataFieldFactory
withProps={({ data: { raw }, dataExists, onChange, readOnly }) => (
<>
<label>{translate('OFFLINE_Step2_Label_6')}</label>
<input
className={`form-control ${dataExists ? 'is-valid' : 'is-invalid'}`}
type="text"
placeholder={donationAddressMap.ETH}
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
</>
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('OFFLINE_Step2_Label_6')}</div>
<Input
className={dataExists ? 'is-valid' : 'is-invalid'}
type="text"
placeholder={donationAddressMap.ETH}
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
</label>
</div>
)}
/>
);

View File

@ -4,6 +4,7 @@ import translate from 'translations';
import { gasLimitValidator } from 'libs/validators';
import { InlineSpinner } from 'components/ui/InlineSpinner';
import './GasLimitField.scss';
import { Input } from 'components/ui';
interface Props {
customLabel?: string;
@ -13,22 +14,24 @@ interface Props {
export const GasLimitField: React.SFC<Props> = ({ customLabel, disabled }) => (
<GasLimitFieldFactory
withProps={({ gasLimit: { raw }, onChange, readOnly, gasEstimationPending }) => (
<React.Fragment>
<div className="gaslimit-label-wrapper flex-wrapper">
{customLabel ? <label>{customLabel} </label> : <label>{translate('TRANS_gas')} </label>}
<div className="flex-spacer" />
<InlineSpinner active={gasEstimationPending} text="Calculating" />
</div>
<input
className={`form-control ${gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder="e.g. 21000"
readOnly={!!readOnly}
value={raw}
onChange={onChange}
disabled={disabled}
/>
</React.Fragment>
<div className="input-group-wrapper AdvancedGas-gas-price">
<label className="input-group">
<div className="input-group-header">
{customLabel ? customLabel : translate('TRANS_gas')}
<div className="flex-spacer" />
<InlineSpinner active={gasEstimationPending} text="Calculating" />
</div>
<Input
className={gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}
type="number"
placeholder="e.g. 21000"
readOnly={!!readOnly}
value={raw}
onChange={onChange}
disabled={disabled}
/>
</label>
</div>
)}
/>
);

View File

@ -60,27 +60,31 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
handleClose={this.handleClose}
>
<form className="GenKeystore" onSubmit={this.handleSubmit}>
<label className="GenKeystore-field">
<h4 className="GenKeystore-field-label">Private Key</h4>
<TogglablePassword
name="privateKey"
value={privateKey}
disabled={!!privateKey}
onChange={this.handleInput}
placeholder="f1d0e0789c6d40f39..."
isValid={isPrivateKeyValid}
/>
</label>
<label className="GenKeystore-field">
<h4 className="GenKeystore-field-label">Password</h4>
<TogglablePassword
name="password"
value={password}
onChange={this.handleInput}
placeholder={translateRaw('Minimum 9 characters')}
isValid={isPasswordValid}
/>
</label>
<div className="input-group-wrapper GenKeystore-field">
<label className="input-group input-group-inline-dropdown">
<div className="input-group-header">Private Key</div>
<TogglablePassword
name="privateKey"
value={privateKey}
disabled={!!privateKey}
onChange={this.handleInput}
placeholder="f1d0e0789c6d40f39..."
isValid={isPrivateKeyValid}
/>
</label>
</div>
<div className="input-group-wrapper GenKeystore-field">
<label className="input-group input-group-inline-dropdown">
<div className="input-group-header">Password</div>
<TogglablePassword
name="password"
value={password}
onChange={this.handleInput}
placeholder={translateRaw('Minimum 9 characters')}
isValid={isPasswordValid}
/>
</label>
</div>
{!keystoreFile ? (
<button

View File

@ -1,5 +1,4 @@
import React from 'react';
import classnames from 'classnames';
import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations';
import { CustomNetworkConfig } from 'types/network';
@ -13,10 +12,11 @@ import {
getStaticNetworkConfigs
} from 'selectors/config';
import { CustomNode } from 'libs/nodes';
import { Input } from 'components/ui';
const CUSTOM = 'custom';
interface Input {
interface InputProps {
name: string;
placeholder?: string;
type?: string;
@ -228,13 +228,10 @@ class CustomNodeModal extends React.Component<Props, State> {
);
}
private renderInput(input: Input, invalids: { [key: string]: boolean }) {
private renderInput(input: InputProps, invalids: { [key: string]: boolean }) {
return (
<input
className={classnames({
'form-control': true,
'is-invalid': this.state[input.name] && invalids[input.name]
})}
<Input
className={`${this.state[input.name] && invalids[input.name] ? 'invalid' : ''}`}
value={this.state[input.name]}
onChange={this.handleChange}
autoComplete="off"

View File

@ -81,8 +81,7 @@ $small-size: 900px;
color: white;
padding: 0;
background: #274e7e;
background-image: url('~assets/images/header-bg.jpg'),
linear-gradient(130deg, #37709e, #274e7e);
background-image: url('~assets/images/header-bg.jpg'), linear-gradient(130deg, #37709e, #274e7e);
background-size: cover;
background-position: left;
@ -111,8 +110,7 @@ $small-size: 900px;
width: auto;
padding: 8px;
padding-right: 0;
filter: drop-shadow(0 1px 0 rgba(#000, 0.12))
drop-shadow(1px 1px 0 rgba(#000, 0.12));
filter: drop-shadow(0 1px 0 rgba(#000, 0.12)) drop-shadow(1px 1px 0 rgba(#000, 0.12));
}
}

View File

@ -12,7 +12,7 @@ import {
addCustomNetwork
} from 'actions/config';
import logo from 'assets/images/logo-mycrypto.svg';
import { Dropdown, ColorDropdown } from 'components/ui';
import { OldDropDown, ColorDropdown } from 'components/ui';
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
@ -100,7 +100,7 @@ class Header extends Component<Props, State> {
} = this.props;
const { isAddingCustomNode } = this.state;
const selectedLanguage = languageSelection;
const LanguageDropDown = Dropdown as new () => Dropdown<typeof selectedLanguage>;
const LanguageDropDown = OldDropDown as new () => OldDropDown<typeof selectedLanguage>;
const options = nodeOptions.map(n => {
if (n.isCustom) {
const { label, isCustom, id, ...rest } = n;

View File

@ -15,7 +15,6 @@
&-refresh {
@include reset-button;
height: $input-height-base;
opacity: 0.3;
transition: opacity 300ms;
@ -31,16 +30,11 @@
}
}
&-spinner {
height: 1rem;
}
&-spinner,
&-refresh {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%) translateZ(0);
margin: 0 1rem;
bottom: 1rem;
padding: 0.75rem 1rem;
}
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import translate from 'translations';
import { NonceFieldFactory } from 'components/NonceFieldFactory';
import Help from 'components/ui/Help';
import { Spinner } from 'components/ui';
import { Spinner, Input } from 'components/ui';
import { connect } from 'react-redux';
import { getNonceRequested, TGetNonceRequested } from 'actions/transaction';
import { nonceRequestPending } from 'selectors/transaction';
@ -32,19 +32,17 @@ class NonceField extends React.Component<Props> {
<NonceFieldFactory
withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => {
return alwaysDisplay || shouldDisplay ? (
<React.Fragment>
<div className="Nonce-label flex-wrapper">
<label className="Nonce-label-text">{translate('OFFLINE_Step2_Label_5')}</label>
<Help
size="x1"
link="https://support.mycrypto.com/transactions/what-is-nonce.html"
/>
</div>
<div className="Nonce-field">
<input
className={`Nonce-field-input form-control ${
!!value ? 'is-valid' : 'is-invalid'
}`}
<div className="input-group-wrapper Nonce-label">
<label className="input-group">
<div className="input-group-header">
{translate('OFFLINE_Step2_Label_5')}
<Help
size="x1"
link="https://support.mycrypto.com/transactions/what-is-nonce.html"
/>
</div>
<Input
className={`Nonce-field-input ${!!value ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder="e.g. 7"
value={raw}
@ -63,8 +61,8 @@ class NonceField extends React.Component<Props> {
</button>
)
)}
</div>
</React.Fragment>
</label>
</div>
) : null;
}}
/>

View File

@ -9,6 +9,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { ConfirmationModal } from 'components/ConfirmationModal';
import { TextArea } from 'components/ui';
export interface CallbackProps {
onClick(): void;
@ -42,12 +43,7 @@ class SendButtonFactoryClass extends Component<Props> {
? 'Transaction Parameters'
: translate('SEND_raw')}
</label>
<textarea
className="form-control"
value={getStringifiedTx(serializedTransaction)}
rows={4}
readOnly={true}
/>
<TextArea value={getStringifiedTx(serializedTransaction)} rows={4} readOnly={true} />
</div>
{!onlyTransactionParameters && (
<div className="col-sm-6">
@ -56,12 +52,7 @@ class SendButtonFactoryClass extends Component<Props> {
? 'Serialized Transaction Parameters'
: translate('SEND_signed')}
</label>
<textarea
className="form-control"
value={addHexPrefix(serializedTransaction)}
rows={4}
readOnly={true}
/>
<TextArea value={addHexPrefix(serializedTransaction)} rows={4} readOnly={true} />
</div>
)}
<OfflineBroadcast />

View File

@ -27,7 +27,7 @@ class SendEverythingClass extends Component<Props> {
!readOnly ? (
<span className="help-block">
<a onClick={this.onSendEverything}>
<span className="strong">{translate('SEND_TransferTotal')}</span>
<span className="">{translate('SEND_TransferTotal')}</span>
</a>
</span>
) : null

View File

@ -111,11 +111,9 @@ class TXMetaDataPanel extends React.Component<Props, State> {
!disableToggle && (
<div className="help-block">
<a className="Gas-toggle" onClick={this.toggleAdvanced}>
<strong>
{showAdvanced
? `- ${translateRaw('Back to simple')}`
: `+ ${translateRaw('Advanced Settings')}`}
</strong>
{showAdvanced
? `- ${translateRaw('Back to simple')}`
: `+ ${translateRaw('Advanced Settings')}`}
</a>
</div>
)}

View File

@ -1,5 +1,4 @@
import React from 'react';
import classnames from 'classnames';
import translate, { translateRaw } from 'translations';
import FeeSummary from './FeeSummary';
import './AdvancedGas.scss';
@ -11,6 +10,7 @@ import { connect } from 'react-redux';
import { getAutoGasLimitEnabled } from 'selectors/config';
import { isValidGasPrice } from 'selectors/transaction';
import { sanitizeNumericalInput } from 'libs/values';
import { Input } from 'components/ui';
export interface AdvancedOptions {
gasPriceField?: boolean;
@ -71,17 +71,19 @@ class AdvancedGas extends React.Component<Props, State> {
<div className="AdvancedGas-flex-wrapper flex-wrapper">
{gasPriceField && (
<div className="AdvancedGas-gas-price">
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
<input
className={classnames('form-control', {
'is-invalid': !!gasPrice.raw && !validGasPrice
})}
type="number"
placeholder="40"
value={gasPrice.raw}
onChange={this.handleGasPriceChange}
/>
<div className="input-group-wrapper AdvancedGas-gas-price">
<label className="input-group">
<div className="input-group-header">
{translate('OFFLINE_Step2_Label_3')} (gwei)
</div>
<Input
className={!!gasPrice.raw && !validGasPrice ? 'is-invalid' : ''}
type="number"
placeholder="40"
value={gasPrice.raw}
onChange={this.handleGasPriceChange}
/>
</label>
</div>
)}

View File

@ -4,9 +4,11 @@
// yourself, otherwise all visibiility changes are managed in internal state.
import React from 'react';
import './TogglablePassword.scss';
import { Input, TextArea } from 'components/ui';
interface Props {
// Shared props
className?: string;
value: string;
placeholder?: string;
name?: string;
@ -46,6 +48,7 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
public render() {
const {
className,
value,
placeholder,
name,
@ -66,10 +69,10 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
: isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
return (
<div className="TogglablePassword input-group">
<div className={`TogglablePassword input-group input-group-inline-dropdown ${className}`}>
{isTextareaWhenVisible && isVisible ? (
<textarea
className={`form-control ${validClass}`}
<TextArea
className={validClass}
value={value}
name={name}
disabled={disabled}
@ -82,12 +85,12 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
aria-label={ariaLabel}
/>
) : (
<input
<Input
value={value}
name={name}
disabled={disabled}
type={isVisible ? 'text' : 'password'}
className={`form-control ${validClass}`}
className={`${validClass}`}
placeholder={placeholder}
onChange={onChange}
onFocus={onFocus}

View File

@ -1,6 +1,6 @@
import React from 'react';
import translate from 'translations';
import { Identicon, UnitDisplay, NewTabLink, Address } from 'components/ui';
import { Identicon, UnitDisplay, NewTabLink, TextArea, Address } from 'components/ui';
import { TransactionData, TransactionReceipt } from 'libs/nodes';
import { NetworkConfig } from 'types/network';
import './TransactionDataTable.scss';
@ -152,9 +152,7 @@ const TransactionDataTable: React.SFC<Props> = ({ data, receipt, network }) => {
},
{
label: translate('TRANS_data'),
data: hasInputData ? (
<textarea className="form-control" value={data.input} disabled={true} />
) : null
data: hasInputData ? <TextArea value={data.input} disabled={true} /> : null
}
];

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react';
import { setUnitMeta, TSetUnitMeta } from 'actions/transaction';
import Dropdown from 'components/ui/Dropdown';
import { withConditional } from 'components/hocs';
import { TokenBalance, MergedToken, getShownTokenBalances, getTokens } from 'selectors/wallet';
import { Query } from 'components/renderCbs';
import { connect } from 'react-redux';
@ -22,34 +21,29 @@ interface StateProps {
network: NetworkConfig;
}
const StringDropdown = Dropdown as new () => Dropdown<string>;
const ConditionalStringDropDown = withConditional(StringDropdown);
class UnitDropdownClass extends Component<DispatchProps & StateProps> {
public render() {
const { tokens, allTokens, showAllTokens, unit, network } = this.props;
const focusedTokens = showAllTokens ? allTokens : tokens;
const options = [network.unit, ...getTokenSymbols(focusedTokens)];
return (
<div className="input-group-btn">
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<ConditionalStringDropDown
options={[network.unit, ...getTokenSymbols(focusedTokens)]}
value={unit === 'ether' ? network.unit : unit}
condition={!readOnly}
conditionalProps={{
onChange: this.handleOnChange
}}
ariaLabel={'dropdown'}
/>
)}
/>
</div>
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<Dropdown
options={options}
value={unit === 'ether' ? network.unit : unit}
onChange={this.handleOnChange}
clearable={false}
searchable={options.length > 10}
disabled={!!readOnly}
/>
)}
/>
);
}
private handleOnChange = (unit: string) => {
this.props.setUnitMeta(unit);
private handleOnChange = unit => {
this.props.setUnitMeta(unit.value);
};
}
const getTokenSymbols = (tokens: (TokenBalance | MergedToken)[]) => tokens.map(t => t.symbol);

View File

@ -13,7 +13,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { getNetworkConfig } from 'selectors/config';
import { getTokens, MergedToken } from 'selectors/wallet';
import { UnitDisplay } from 'components/ui';
import { UnitDisplay, Input } from 'components/ui';
import './DeterministicWalletsModal.scss';
import { StaticNetworkConfig } from 'types/network';
import Select from 'react-select';
@ -97,7 +97,6 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
walletType
} = this.props;
const { selectedAddress, customPath, page } = this.state;
const validPathClass = isValidPath(customPath) ? 'is-valid' : 'is-invalid';
const buttons: IButton[] = [
{
@ -137,8 +136,8 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
/>
{/* TODO/Hack - Custom Paths are temporarily disabled. `false` is used for smallest diff */}
{false && (
<input
className={`form-control ${validPathClass}`}
<Input
className={isValidPath(customPath) ? '' : 'invalid'}
value={customPath}
placeholder="m/44'/60'/0'/0"
onChange={this.handleChangeCustomPath}

View File

@ -3,6 +3,7 @@ import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import Spinner from 'components/ui/Spinner';
import { TShowNotification } from 'actions/notifications';
import { Input } from 'components/ui';
export interface KeystoreValue {
file: string;
@ -36,7 +37,7 @@ export class KeystoreDecrypt extends PureComponent {
};
public render() {
const { isWalletPending, isPasswordPending, value: { file, password } } = this.props;
const { isWalletPending, value: { file, password } } = this.props;
const passReq = isPassRequired(file);
const unlockDisabled = !file || (passReq && !password);
@ -44,7 +45,7 @@ export class KeystoreDecrypt extends PureComponent {
<form id="selectedUploadKey" onSubmit={this.unlock}>
<div className="form-group">
<input
className={'hidden'}
className="hidden"
type="file"
id="fselector"
onChange={this.handleFileSelection}
@ -55,17 +56,16 @@ export class KeystoreDecrypt extends PureComponent {
</a>
</label>
{isWalletPending ? <Spinner /> : ''}
<div className={file.length && isPasswordPending ? '' : 'hidden'}>
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>
<Input
className={`${password.length > 0 ? 'is-valid' : 'is-invalid'} ${
file.length && isWalletPending ? 'hidden' : ''
}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>
<button className="btn btn-primary btn-block" disabled={unlockDisabled}>

View File

@ -8,6 +8,7 @@ import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { getSingleDPath, getPaths } from 'selectors/config/wallet';
import { TogglablePassword } from 'components';
import { Input } from 'components/ui';
interface OwnProps {
onUnlock(param: any): void;
@ -63,8 +64,7 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
</div>
<div className="form-group">
<p>Password (optional):</p>
<input
className="form-control"
<Input
value={pass}
onChange={this.onPasswordChange}
placeholder={translateRaw('x_Password')}

View File

@ -3,6 +3,7 @@ import { stripHexPrefix } from 'libs/values';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import { TogglablePassword } from 'components';
import { Input } from 'components/ui';
export interface PrivateKeyValue {
key: string;
@ -53,29 +54,32 @@ export class PrivateKeyDecrypt extends PureComponent<Props> {
return (
<form id="selectedTypeKey" onSubmit={this.unlock}>
<div className="form-group">
<TogglablePassword
value={key}
rows={4}
placeholder={translateRaw('x_PrivKey2')}
isValid={isValidPkey}
isTextareaWhenVisible={true}
onChange={this.onPkeyChange}
onEnter={this.props.onUnlock}
/>
<div className="input-group-wrapper">
<label className="input-group">
<TogglablePassword
value={key}
rows={4}
placeholder={translateRaw('x_PrivKey2')}
isValid={isValidPkey}
onChange={this.onPkeyChange}
onEnter={this.props.onUnlock}
/>
</label>
</div>
{isValidPkey &&
isPassRequired && (
<div className="form-group">
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('ADD_Label_3')}</div>
<Input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</label>
</div>
)}
<button className="btn btn-block btn-primary" disabled={unlockDisabled}>

View File

@ -3,6 +3,7 @@ import translate from 'translations';
import { donationAddressMap } from 'config';
import { isValidETHAddress } from 'libs/validators';
import { AddressOnlyWallet } from 'libs/wallet';
import { TextArea } from 'components/ui';
interface Props {
onUnlock(param: any): void;
@ -24,10 +25,8 @@ export class ViewOnlyDecrypt extends PureComponent<Props, State> {
return (
<div id="selectedUploadKey">
<form className="form-group" onSubmit={this.openWallet}>
<textarea
className={`form-control
${isValid ? 'is-valid' : 'is-invalid'}
`}
<TextArea
className={isValid ? 'is-valid' : 'is-invalid'}
value={address}
onChange={this.changeAddress}
onKeyDown={this.handleEnterKey}

View File

@ -1,5 +1,6 @@
import React from 'react';
import { withConditional } from 'components/hocs';
import { Input } from 'components/ui';
const Input: React.SFC<React.InputHTMLAttributes<any>> = props => <input {...props} />;
export const ConditionalInput = withConditional(Input);
const inpt: React.SFC<React.InputHTMLAttributes<any>> = props => <Input {...props} />;
export const ConditionalInput = withConditional(inpt);

View File

@ -1,123 +1,63 @@
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import DropdownShell from './DropdownShell';
import React from 'react';
import Select, { ReactSelectProps } from 'react-select';
interface Props<T> {
value: T | undefined;
options: T[];
ariaLabel: string;
label?: string;
extra?: any;
size?: string;
color?: string;
menuAlign?: string;
formatTitle?(option: T): any;
onChange?(value: T): void;
interface Props extends ReactSelectProps {
className?: string;
options: any;
onChange: any;
}
interface State {
search: string;
}
export default class DropdownComponent<T> extends PureComponent<Props<T>, State> {
export default class Dropdown extends React.Component<Props> {
public state = {
search: ''
selectedOption: { value: '', label: '' },
hasBlurred: false
};
private dropdownShell: DropdownShell | null;
public handleChange = selectedOption => {
this.setState({ selectedOption });
};
public formatOptions = options => {
if (typeof options[0] === 'object') {
return options;
}
const formatted = options.map(opt => {
return { value: opt, label: opt };
});
return formatted;
};
public render() {
const { ariaLabel, color, size } = this.props;
const { onChange } = this.props;
const { selectedOption } = this.state;
const value = selectedOption && selectedOption.value;
const options = this.formatOptions(this.props.options);
return (
<DropdownShell
renderLabel={this.renderLabel}
renderOptions={this.renderOptions}
size={size}
color={color}
ariaLabel={ariaLabel}
ref={el => (this.dropdownShell = el)}
<Select
// use ref to prevent <label /> from stealing focus when used inline with an input
ref={el => {
if (!!el && !!(el as any).control) {
(el as any).control.addEventListener('click', e => {
e.preventDefault();
});
}
}}
className={`${this.props.className} ${this.state.hasBlurred ? 'has-blurred' : ''}`}
value={value}
onChange={obj => {
this.handleChange(obj);
onChange();
}}
{...this.props}
onBlur={e => {
this.setState({ hasBlurred: true });
if (this.props && this.props.onBlur) {
this.props.onBlur(e);
}
}}
options={options}
/>
);
}
private renderLabel = () => {
const { value } = this.props;
const labelStr = this.props.label ? `${this.props.label}:` : '';
return (
<span>
{labelStr} {this.formatTitle(value)}
</span>
);
};
private renderOptions = () => {
const { options, value, menuAlign, extra } = this.props;
const { search } = this.state;
const searchable = options.length > 20;
const menuClass = classnames({
'dropdown-menu': true,
[`dropdown-menu-${menuAlign || ''}`]: !!menuAlign
});
const searchableStyle = {
maxHeight: '300px',
overflowY: 'auto'
};
const searchRegex = new RegExp(search, 'gi');
const onSearchChange = e => {
this.setState({ search: e.target.value });
};
return (
<ul className={menuClass} style={searchable ? searchableStyle : {}}>
{searchable && (
<input
className="form-control"
placeholder={'Search'}
onChange={onSearchChange}
value={search}
/>
)}
{options
.filter(option => {
if (searchable && search.length) {
return option.toString().match(searchRegex);
}
return true;
})
.map((option, i) => {
return (
<li key={i}>
<a
className={option === value ? 'active' : ''}
onClick={this.onChange.bind(null, option)}
>
{this.props.formatTitle ? this.formatTitle(option) : option}
</a>
</li>
);
})}
{extra && <li key={'separator'} role="separator" className="divider" />}
{extra}
</ul>
);
};
private formatTitle = (option: any) => {
if (this.props.formatTitle) {
return this.props.formatTitle(option);
} else {
return option;
}
};
private onChange = (value: any) => {
if (this.props.onChange) {
this.props.onChange(value);
}
if (this.dropdownShell) {
this.dropdownShell.close();
}
};
}

View File

@ -1,8 +1,7 @@
.Help {
display: inline-block;
margin-left: 8px;
padding: 4px;
box-sizing: content-box;
box-sizing: border-box;
line-height: inherit;
vertical-align: top;
transition: opacity 0.3s;
@ -14,17 +13,17 @@
}
&-x1 {
height: 16px;
width: 16px;
height: 1rem;
width: 1rem;
}
&-x2 {
height: 24px;
width: 24px;
height: 1.5rem;
width: 1.5rem;
}
&-x3 {
height: 36px;
width: 36px;
height: 2rem;
width: 2rem;
}
}

View File

@ -0,0 +1,84 @@
@import 'common/sass/variables';
.example-form {
max-width: 744px;
margin: auto;
}
.input-group {
margin: auto;
display: flex;
flex-direction: column;
> .TogglablePassword {
width: 100%;
}
&-header {
display: flex;
font-size: 1rem;
margin-bottom: 8px;
font-weight: 400;
align-items: center;
flex-wrap: wrap;
> *:first-child {
margin-right: 8px;
}
> .flex-spacer {
flex-grow: 1;
}
> .small {
color: rgba(0, 0, 0, 0.54);
}
}
&-input {
width: 100%;
border: 1px solid #e5ecf3;
border-radius: 2px;
padding: 0.75rem 1rem;
font-weight: 400;
font-size: 1rem;
color: rgba(0, 0, 0, 0.87);
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
transition: border-color 120ms, box-shadow 120ms;
margin-bottom: 1rem;
&::placeholder {
color: rgba(0, 0, 0, 0.3);
}
&:not([disabled]):not([readonly]) {
&.invalid.has-blurred.has-value {
border-color: $brand-danger;
box-shadow: inset 0px 0px 0px 1px $brand-danger;
}
&:focus {
border-color: #4295bc;
box-shadow: inset 0px 0px 0px 1px #4295bc;
&.valid {
border-color: #8dd17b;
box-shadow: inset 0px 0px 0px 1px #8dd17b;
}
}
}
}
}
.input-group-inline-dropdown {
display: flex;
flex-direction: row;
font-size: 1rem;
flex-wrap: wrap;
> .input-group-header {
width: 100%;
}
> .input-group-input {
flex-grow: 1;
width: auto;
}
> .Select {
margin-left: 8px;
}
}
.Swap-dropdown {
.Select-input {
left: 24px;
}
}

View File

@ -0,0 +1,30 @@
import React, { HTMLProps } from 'react';
import './Input.scss';
interface State {
hasBlurred: boolean;
}
class Input extends React.Component<HTMLProps<HTMLInputElement>, State> {
public state: State = {
hasBlurred: false
};
public render() {
return (
<input
{...this.props}
onBlur={e => {
this.setState({ hasBlurred: true });
if (this.props && this.props.onBlur) {
this.props.onBlur(e);
}
}}
className={`input-group-input ${this.props.className} ${
this.state.hasBlurred ? 'has-blurred' : ''
} ${!!this.props.value && this.props.value.toString().length > 0 ? 'has-value' : ''}`}
/>
);
}
}
export default Input;

View File

@ -0,0 +1,123 @@
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import DropdownShell from './DropdownShell';
interface Props<T> {
value: T | undefined;
options: T[];
ariaLabel: string;
label?: string;
extra?: any;
size?: string;
color?: string;
menuAlign?: string;
formatTitle?(option: T): any;
onChange?(value: T): void;
}
interface State {
search: string;
}
export default class DropdownComponent<T> extends PureComponent<Props<T>, State> {
public state = {
search: ''
};
private dropdownShell: DropdownShell | null;
public render() {
const { ariaLabel, color, size } = this.props;
return (
<DropdownShell
renderLabel={this.renderLabel}
renderOptions={this.renderOptions}
size={size}
color={color}
ariaLabel={ariaLabel}
ref={el => (this.dropdownShell = el)}
/>
);
}
private renderLabel = () => {
const { value } = this.props;
const labelStr = this.props.label ? `${this.props.label}:` : '';
return (
<span>
{labelStr} {this.formatTitle(value)}
</span>
);
};
private renderOptions = () => {
const { options, value, menuAlign, extra } = this.props;
const { search } = this.state;
const searchable = options.length > 20;
const menuClass = classnames({
'dropdown-menu': true,
[`dropdown-menu-${menuAlign || ''}`]: !!menuAlign
});
const searchableStyle = {
maxHeight: '300px',
overflowY: 'auto'
};
const searchRegex = new RegExp(search, 'gi');
const onSearchChange = e => {
this.setState({ search: e.target.value });
};
return (
<ul className={menuClass} style={searchable ? searchableStyle : {}}>
{searchable && (
<input
className="form-control"
placeholder={'Search'}
onChange={onSearchChange}
value={search}
/>
)}
{options
.filter(option => {
if (searchable && search.length) {
return option.toString().match(searchRegex);
}
return true;
})
.map((option, i) => {
return (
<li key={i}>
<a
className={option === value ? 'active' : ''}
onClick={this.onChange.bind(null, option)}
>
{this.props.formatTitle ? this.formatTitle(option) : option}
</a>
</li>
);
})}
{extra && <li key={'separator'} role="separator" className="divider" />}
{extra}
</ul>
);
};
private formatTitle = (option: any) => {
if (this.props.formatTitle) {
return this.props.formatTitle(option);
} else {
return option;
}
};
private onChange = (value: any) => {
if (this.props.onChange) {
this.props.onChange(value);
}
if (this.dropdownShell) {
this.dropdownShell.close();
}
};
}

View File

@ -0,0 +1,7 @@
.SimpleButton {
display: flex;
align-items: center;
> .Spinner {
margin-right: 16px;
}
}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import Spinner from './Spinner';
import './SimpleButton.scss';
const DEFAULT_BUTTON_TYPE = 'primary';
const DEFAULT_BUTTON_SIZE = 'lg';
@ -29,8 +30,8 @@ export default class SimpleButton extends Component<Props, {}> {
<div>
<button onClick={onClick} disabled={loading || disabled} className={this.computedClass()}>
{loading ? (
<div>
<Spinner /> {loadingText || text}
<div className="SimpleButton">
<Spinner light={true} /> {loadingText || text}
</div>
) : (
<div>{text}</div>

View File

@ -1,26 +0,0 @@
import React, { PureComponent } from 'react';
import Dropdown from './Dropdown';
interface Props {
value: string | undefined;
options: string[];
ariaLabel?: string;
onChange(value: string): void;
}
export default class SimpleDropdown extends PureComponent<Props, void> {
public render() {
const { options, value, onChange, ariaLabel } = this.props;
const StringDropdown = Dropdown as new () => Dropdown<string>;
return (
<StringDropdown
options={options}
value={value}
onChange={onChange}
ariaLabel={ariaLabel || 'dropdown'}
/>
);
}
}

View File

@ -112,3 +112,16 @@
padding-right: 1px;
}
}
.swap-option {
&-wrapper {
font-size: 1rem;
display: flex;
align-items: center;
padding: 0.75rem 1rem;
}
&-img {
width: 1rem;
margin-right: 8px;
}
}

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import './SwapDropdown.scss';
import classnames from 'classnames';
import { DropDown } from 'components/ui';
export interface SingleCoin {
id: string;
@ -10,91 +10,70 @@ export interface SingleCoin {
}
interface Props<T> {
ariaLabel: string;
options: SingleCoin[];
value: string;
onChange(value: T): void;
}
class SwapDropdown<T> extends PureComponent<Props<T>, {}> {
public state = {
open: false
const ValueComp: React.SFC = (props: any) => {
return (
<div className={`${props.className} swap-option-wrapper`}>
<img src={props.value.img} className="swap-option-img" />
<span className="swap-option-label">{props.value.label}</span>
</div>
);
};
const OptionComp: React.SFC = (props: any) => {
const handleMouseDown = event => {
event.preventDefault();
event.stopPropagation();
props.onSelect(props.option, event);
};
private dropdown: HTMLElement | null;
public componentDidMount() {
document.addEventListener('click', this.clickHandler);
}
public componentWillUnmount() {
document.removeEventListener('click', this.clickHandler);
}
public handleClickOutside() {
this.toggleDropdown();
}
public render() {
const { open } = this.state;
const { options, value } = this.props;
const dropdownGrid = classnames(open && 'open', 'SwapDropdown-grid');
const mappedCoins = options.sort((a, b) => (a.id > b.id ? 1 : -1)).map((coin: SingleCoin) => {
const cn = classnames(coin.status !== 'available' && 'inactive', 'SwapDropdown-item');
return (
<li className={cn} key={coin.id}>
<a onClick={coin.status === 'available' ? this.onChange.bind(null, coin.id) : null}>
<img src={coin.image} height="20" width="20" />
{/* <div className="SwapDropdown-desc"> */}
<strong>{coin.id}</strong>
<br />
<small>{coin.name}</small>
{/* </div> */}
</a>
</li>
);
});
return (
<div className="SwapDropdown" ref={el => (this.dropdown = el)}>
<button onClick={this.toggleDropdown}>
{value}
<i className="caret" />
</button>
<ul className={dropdownGrid}>{mappedCoins}</ul>
</div>
);
}
private toggleDropdown = () => {
this.setState({
open: !this.state.open
});
const handleMouseEnter = event => {
props.onFocus(props.option, event);
};
private onChange = (value: any) => {
this.props.onChange(value);
if (this.state.open) {
this.setState({
open: false
});
}
};
private clickHandler = (ev: Event) => {
if (!this.state.open || !this.dropdown) {
const handleMouseMove = event => {
if (props.isFocused) {
return;
}
if (
this.dropdown !== ev.target &&
ev.target instanceof HTMLElement &&
!this.dropdown.contains(ev.target)
) {
this.setState({
open: false
});
}
props.onFocus(props.option, event);
};
return (
<div
className={`${props.className} swap-option-wrapper`}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
>
<img src={props.option.img} className="swap-option-img" />
<span className="swap-option-label">{props.option.label}</span>
</div>
);
};
class SwapDropdown<T> extends PureComponent<Props<T>> {
public render() {
const { options, value, onChange } = this.props;
const mappedOptions = options.map(opt => {
return { label: opt.id, value: opt.name, img: opt.image, status: opt.status };
});
return (
<DropDown
className="Swap-dropdown"
options={mappedOptions}
optionComponent={(props: any) => {
return <OptionComp {...props} />;
}}
value={value}
clearable={false}
onChange={onChange}
valueComponent={(props: any) => {
return <ValueComp {...props} />;
}}
/>
);
}
}
export default SwapDropdown;

View File

@ -0,0 +1,30 @@
import React, { HTMLProps } from 'react';
import './Input.scss';
interface State {
hasBlurred: boolean;
}
class TextArea extends React.Component<HTMLProps<HTMLTextAreaElement>, State> {
public state: State = {
hasBlurred: false
};
public render() {
return (
<textarea
{...this.props}
onBlur={e => {
this.setState({ hasBlurred: true });
if (this.props && this.props.onBlur) {
this.props.onBlur(e);
}
}}
className={`input-group-input ${this.props.className} ${
this.state.hasBlurred ? 'has-blurred' : ''
}`}
/>
);
}
}
export default TextArea;

View File

@ -1,5 +1,6 @@
export { default as ColorDropdown } from './ColorDropdown';
export { default as Dropdown } from './Dropdown';
export { default as OldDropDown } from './OldDropdown';
export { default as DropDown } from './Dropdown';
export { default as DropdownShell } from './DropdownShell';
export { default as Identicon } from './Identicon';
export { default as Modal } from './Modal';
@ -12,6 +13,8 @@ export { default as SwapDropdown } from './SwapDropdown';
export { default as Tooltip } from './Tooltip';
export { default as TitleBar } from './TitleBar';
export { default as HelpLink } from './HelpLink';
export { default as Input } from './Input';
export { default as TextArea } from './TextArea';
export { default as Address } from './Address';
export * from './ConditionalInput';
export * from './Expandable';

View File

@ -9,9 +9,8 @@ import {
TSignTransactionFailed
} from 'actions/transaction';
import { computeIndexingHash } from 'libs/transaction';
import { QRCode } from 'components/ui';
import { QRCode, TextArea } from 'components/ui';
import EthTx from 'ethereumjs-tx';
import classnames from 'classnames';
import { SendButton } from 'components/SendButton';
import { toBuffer, bufferToHex } from 'ethereumjs-util';
import { getSerializedTransaction } from 'selectors/transaction';
@ -40,11 +39,6 @@ class BroadcastTx extends Component<Props> {
public render() {
const { userInput } = this.state;
const { stateTransaction } = this.props;
const inputClasses = classnames({
'form-control': true,
'is-valid': !!stateTransaction,
'is-invalid': !stateTransaction
});
const currentPath = this.props.match.url;
return (
<TabSection isUnavailableOffline={true}>
@ -59,13 +53,19 @@ class BroadcastTx extends Component<Props> {
<p className="BroadcastTx-help">
Paste a signed transaction and press the "SEND TRANSACTION" button.
</p>
<label>{translateRaw('SEND_signed')}</label>
<textarea
className={inputClasses}
rows={7}
value={userInput}
onChange={this.handleChange}
/>
<div className="input-group-wrapper InteractForm-interface">
<label className="input-group">
<div className="input-group-header">{translateRaw('SEND_signed')}</div>
<TextArea
className={stateTransaction ? '' : 'invalid'}
rows={7}
value={userInput}
onChange={this.handleChange}
/>
</label>
</div>
<SendButton onlyTransactionParameters={true} />
<div className="BroadcastTx-qr">

View File

@ -2,6 +2,7 @@ import React from 'react';
import translate from 'translations';
import { isValidTxHash, isValidETHAddress } from 'libs/validators';
import './TxHashInput.scss';
import { Input } from 'components/ui';
interface Props {
hash?: string;
@ -26,14 +27,14 @@ export default class TxHashInput extends React.Component<Props, State> {
public render() {
const { hash } = this.state;
const validClass = hash ? (isValidTxHash(hash) ? 'is-valid' : 'is-invalid') : '';
const validClass = hash ? (isValidTxHash(hash) ? '' : 'invalid') : '';
return (
<form className="TxHashInput" onSubmit={this.handleSubmit}>
<input
<Input
value={hash}
placeholder="0x16e521..."
className={`TxHashInput-field form-control ${validClass}`}
className={`TxHashInput-field ${validClass}`}
onChange={this.handleChange}
/>

View File

@ -2,13 +2,10 @@
&-field {
margin-top: 0;
&-label {
float: left;
}
&-reset {
float: right;
display: block;
margin: auto;
margin-bottom: 0.25rem;
.fa {
margin-right: 8px;
}

View File

@ -13,6 +13,7 @@ import { FullWalletOnly } from 'components/renderCbs';
import { NonceField, TXMetaDataPanel } from 'components';
import './Deploy.scss';
import { ConfirmationModal } from 'components/ConfirmationModal';
import { TextArea } from 'components/ui';
interface DispatchProps {
setToField: TSetToField;
@ -23,27 +24,30 @@ class DeployClass extends Component<DispatchProps> {
public render() {
const makeContent = () => (
<main className="Deploy Tab-content-pane" role="main">
<div className="Deploy-field form-group">
<h3 className="Deploy-field-label">{translate('CONTRACT_ByteCode')}</h3>
<button className="Deploy-field-reset btn btn-default btn-sm" onClick={this.changeWallet}>
<i className="fa fa-refresh" />
{translate('Change Wallet')}
</button>
<DataFieldFactory
withProps={({ data: { raw, value }, onChange, readOnly }) => (
<textarea
name="byteCode"
placeholder="0x8f87a973e..."
rows={6}
onChange={onChange}
disabled={readOnly}
className={classnames('Deploy-field-input', 'form-control', {
'is-valid': value && value.length > 0
})}
value={raw}
/>
)}
/>
<button className="Deploy-field-reset btn btn-default btn-sm" onClick={this.changeWallet}>
<i className="fa fa-refresh" />
{translate('Change Wallet')}
</button>
<div className="input-group-wrapper Deploy-field">
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_ByteCode')}</div>
<DataFieldFactory
withProps={({ data: { raw, value }, onChange, readOnly }) => (
<TextArea
name="byteCode"
placeholder="0x8f87a973e..."
rows={6}
onChange={onChange}
disabled={readOnly}
className={classnames('Deploy-field-input', 'form-control', {
'is-valid': value && value.length > 0
})}
value={raw}
/>
)}
/>
</label>
</div>
<div className="row form-group">

View File

@ -1,22 +1,25 @@
import { AmountFieldFactory } from 'components/AmountFieldFactory';
import React from 'react';
import classnames from 'classnames';
import { Input } from 'components/ui';
export const AmountField: React.SFC<{}> = () => (
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Value</h4>
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<input
name="value"
value={raw}
onChange={onChange}
readOnly={readOnly}
className={classnames('InteractExplorer-field-input', 'form-control', {
'is-invalid': !(isValid || raw === '')
})}
/>
)}
/>
</label>
export const AmountField: React.SFC = () => (
<div className="input-group-wrapper InteractExplorer-field">
<label className="input-group">
<div className="input-group-header">Value</div>
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<Input
name="value"
value={raw}
onChange={onChange}
readOnly={readOnly}
className={classnames('InteractExplorer-field-input', 'form-control', {
'is-invalid': !(isValid || raw === '')
})}
/>
)}
/>
</label>
</div>
);

View File

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { Fields } from './components';
import { setDataField, TSetDataField } from 'actions/transaction';
import { Data } from 'libs/units';
import Select from 'react-select';
import { Web3Node } from 'libs/nodes';
import RpcNode from 'libs/nodes/rpc';
import { Input } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
interface StateProps {
nodeLib: RpcNode | Web3Node;
@ -83,22 +84,25 @@ class InteractExplorerClass extends Component<Props, State> {
return (
<div className="InteractExplorer">
<h3 className="InteractExplorer-title">
{translate('CONTRACT_Interact_Title')}
<span className="InteractExplorer-title-address">{to.raw}</span>
</h3>
<Select
name="exploreContract"
value={selectedFunction as any}
placeholder="Please select a function..."
onChange={this.handleFunctionSelect}
options={contractFunctionsOptions}
clearable={false}
searchable={false}
labelKey="name"
valueKey="contract"
/>
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">
{translate('CONTRACT_Interact_Title')}
<div className="flex-spacer" />
<span className="small">{to.raw}</span>
</div>
<Dropdown
name="exploreContract"
value={selectedFunction as any}
placeholder="Please select a function..."
onChange={this.handleFunctionSelect}
options={contractFunctionsOptions}
clearable={false}
labelKey="name"
valueKey="contract"
/>
</label>
</div>
{selectedFunction && (
<div key={selectedFunction.name} className="InteractExplorer-func">
@ -107,18 +111,17 @@ class InteractExplorerClass extends Component<Props, State> {
const { type, name } = input;
return (
<label key={name} className="InteractExplorer-func-in form-group">
<h4 className="InteractExplorer-func-in-label">
{name}
<span className="InteractExplorer-func-in-label-type">{type}</span>
</h4>
<input
className="InteractExplorer-func-in-input form-control"
name={name}
value={(inputs[name] && inputs[name].rawData) || ''}
onChange={this.handleInputChange}
/>
</label>
<div key={name} className="input-group-wrapper InteractExplorer-func-in">
<label className="input-group">
<div className="input-group-header">{name + ' ' + type}</div>
<Input
className="InteractExplorer-func-in-input"
name={name}
value={(inputs[name] && inputs[name].rawData) || ''}
onChange={this.handleInputChange}
/>
</label>
</div>
);
})}
{selectedFunction.contract.outputs.map((output, index) => {
@ -126,17 +129,16 @@ class InteractExplorerClass extends Component<Props, State> {
const parsedName = name === '' ? index : name;
return (
<label key={parsedName} className="InteractExplorer-func-out form-group">
<h4 className="InteractExplorer-func-out-label">
{name}
<span className="InteractExplorer-func-out-label-type">{type}</span>
</h4>
<input
className="InteractExplorer-func-out-input form-control"
value={outputs[parsedName] || ''}
disabled={true}
/>
</label>
<div key={parsedName} className="input-group-wrapper InteractExplorer-func-out">
<label className="input-group">
<div className="input-group-header"> {name + ' ' + type}</div>
<Input
className="InteractExplorer-func-out-input "
value={outputs[parsedName] || ''}
disabled={true}
/>
</label>
</div>
);
})}

View File

@ -5,9 +5,10 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import classnames from 'classnames';
import Select from 'react-select';
import { NetworkContract } from 'types/network';
import { donationAddressMap } from 'config';
import { Input, TextArea } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
interface ContractOption {
name: string;
@ -83,45 +84,44 @@ class InteractForm extends Component<Props, State> {
return (
<div className="InteractForm">
<div className="InteractForm-address row">
<label className="InteractForm-address-field form-group col-sm-6">
<h4>{translate('CONTRACT_Title')}</h4>
<input
placeholder={`ensdomain.eth or ${donationAddressMap.ETH}`}
name="contract_address"
autoComplete="off"
value={address}
className={classnames('InteractForm-address-field-input', 'form-control', {
'is-invalid': !validEthAddress
})}
onChange={this.handleInput('address')}
/>
</label>
<div className="input-group-wrapper InteractForm-address-field col-sm-6">
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_Title')}</div>
<Input
placeholder={`ensdomain.eth or ${donationAddressMap.ETH}`}
name="contract_address"
autoComplete="off"
value={address}
className={classnames('InteractForm-address-field-input', {
invalid: !validEthAddress
})}
onChange={this.handleInput('address')}
/>
</label>
</div>
<label className="InteractForm-address-contract form-group col-sm-6">
<h4>{translate('CONTRACT_Title_2')}</h4>
<Select
name="interactContract"
className={`${!contract ? 'is-invalid' : ''}`}
value={contract as any}
placeholder={this.state.contractPlaceholder}
onChange={this.handleSelectContract}
options={contractOptions}
clearable={false}
searchable={false}
labelKey="name"
/>
</label>
<div className="input-group-wrapper InteractForm-address-field col-sm-6">
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_Title_2')}</div>
<Dropdown
className={`${!contract ? 'invalid' : ''}`}
value={contract as any}
placeholder={this.state.contractPlaceholder}
onChange={this.handleSelectContract}
options={contractOptions}
clearable={false}
labelKey="name"
/>
</label>
</div>
</div>
<div className="InteractForm-interface">
<label className="InteractForm-interface-field form-group">
<h4 className="InteractForm-interface-field-label">{translate('CONTRACT_Json')}</h4>
<textarea
<div className="input-group-wrapper InteractForm-interface">
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_Json')}</div>
<TextArea
placeholder={this.abiJsonPlaceholder}
name="abiJson"
className={classnames('InteractForm-interface-field-input', 'form-control', {
'is-invalid': !validAbiJson
})}
className={`InteractForm-interface-field-input ${validAbiJson ? '' : 'invalid'}`}
onChange={this.handleInput('abiJson')}
value={abiJson}
rows={6}

View File

@ -5,10 +5,6 @@
padding: $space-sm 0;
margin: 0 auto;
&-name {
margin-bottom: $space-md;
}
&-button {
.Spinner {
margin-left: $space-md;

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { resolveDomainRequested, TResolveDomainRequested } from 'actions/ens';
import { isValidENSName } from 'libs/validators';
import './NameInput.scss';
import { Input } from 'components/ui';
interface State {
domainToCheck: string;
@ -32,21 +32,20 @@ class NameInput extends Component<Props, State> {
return (
<form className="ENSInput" onSubmit={this.onSubmit}>
<div className="ENSInput-name input-group">
<input
value={domainToCheck}
className={classnames(
'form-control',
!domainToCheck ? '' : isValidDomain ? 'is-valid' : 'is-invalid'
)}
type="text"
placeholder="mycrypto"
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={isLoading}
/>
<span className="input-group-addon">.eth</span>
<div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown ENSInput-name">
<Input
value={domainToCheck}
className={!domainToCheck ? '' : isValidDomain ? 'is-valid' : 'is-invalid'}
type="text"
placeholder="mycrypto"
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={isLoading}
/>
<span className="input-group-addon">.eth</span>
</label>
</div>
{domainToCheck &&
!isValidDomain &&

View File

@ -1,10 +1,10 @@
@import "common/sass/variables";
@import 'common/sass/variables';
$pw-max-width: 40rem;
.EnterPw {
&-title {
margin: $space auto $space * 2.5;
margin: $space auto;
}
&-password {
@ -12,18 +12,16 @@ $pw-max-width: 40rem;
position: relative;
max-width: $pw-max-width;
width: 100%;
margin: 0 auto $space;
margin: 0 auto;
&-label {
margin-bottom: $space;
}
&-feedback {
position: absolute;
bottom: -$space;
top: 100%;
text-align: left;
font-size: $font-size-small;
margin-bottom: 1rem;
}
}

View File

@ -2,10 +2,10 @@ import React, { Component } from 'react';
import zxcvbn, { ZXCVBNResult } from 'zxcvbn';
import translate, { translateRaw } from 'translations';
import { MINIMUM_PASSWORD_LENGTH } from 'config';
import { TogglablePassword } from 'components';
import { Spinner } from 'components/ui';
import Template from '../Template';
import './EnterPassword.scss';
import { TogglablePassword } from 'components';
interface Props {
isGenerating: boolean;
@ -41,36 +41,36 @@ export default class EnterPassword extends Component<Props, State> {
Generate a {translate('x_Keystore2')}
</h1>
<label className="EnterPw-password">
<h4 className="EnterPw-password-label">{translate('GEN_Label_1')}</h4>
<TogglablePassword
value={password}
placeholder={`Password must be uncommon and ${MINIMUM_PASSWORD_LENGTH}+ characters long`}
validity={passwordValidity}
ariaLabel={translateRaw('GEN_Aria_1')}
toggleAriaLabel={translateRaw('GEN_Aria_2')}
onChange={this.onPasswordChange}
onBlur={this.showFeedback}
/>
{!isPasswordValid &&
feedback && (
<p className={`EnterPw-password-feedback help-block is-${passwordValidity}`}>
{feedback}
</p>
)}
</label>
<div className="input-group-wrapper EnterPw-password">
<label className="input-group">
<div className="input-group-header">{translate('GEN_Label_1')}</div>
<TogglablePassword
className={!isPasswordValid && password.length > 0 ? 'invalid' : ''}
value={password}
placeholder={`Password must be uncommon and ${MINIMUM_PASSWORD_LENGTH}+ characters long`}
onChange={this.onPasswordChange}
onBlur={this.showFeedback}
/>
{!isPasswordValid &&
feedback && (
<p className={`EnterPw-password-feedback help-block is-${passwordValidity}`}>
{feedback}
</p>
)}
</label>
</div>
<label className="EnterPw-password">
<h4 className="EnterPw-password-label">Confirm password</h4>
<TogglablePassword
value={confirmedPassword}
placeholder={translateRaw('GEN_Placeholder_1')}
ariaLabel="Confirm Password"
toggleAriaLabel="toggle confirm password visibility"
isValid={isConfirmValid}
onChange={this.onConfirmChange}
/>
</label>
<div className="input-group-wrapper EnterPw-password">
<label className="input-group">
<div className="input-group-header">Confirm password</div>
<TogglablePassword
className={!isConfirmValid && password.length > 0 ? 'invalid' : ''}
value={confirmedPassword}
placeholder={translateRaw('GEN_Placeholder_1')}
onChange={this.onConfirmChange}
/>
</label>
</div>
<button disabled={!canSubmit} className="EnterPw-submit btn btn-primary btn-lg btn-block">
{isGenerating ? <Spinner light={true} /> : translate('NAV_GenerateWallet')}

View File

@ -5,6 +5,7 @@ import translate from 'translations';
import { stripHexPrefix } from 'libs/values';
import './PaperWallet.scss';
import Template from '../Template';
import { Input } from 'components/ui';
interface Props {
keystore: IV3Wallet;
@ -17,8 +18,8 @@ const PaperWallet: React.SFC<Props> = props => (
<div className="GenPaper">
{/* Private Key */}
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
<input
className="GenPaper-private form-control"
<Input
className="GenPaper-private"
value={stripHexPrefix(props.privateKey)}
aria-label={translate('x_PrivKey', true)}
aria-describedby="x_PrivKeyDesc"

View File

@ -2,6 +2,7 @@ import React from 'react';
import classnames from 'classnames';
import { translateRaw } from 'translations';
import './Word.scss';
import { Input } from 'components/ui';
interface Props {
index: number;
@ -26,15 +27,11 @@ export default class MnemonicWord extends React.Component<Props, State> {
const readOnly = isReadOnly || isShowingWord;
return (
<div className="MnemonicWord">
<span className="MnemonicWord-number">{index + 1}.</span>
<div className="MnemonicWord-word input-group">
<input
className={classnames(
'MnemonicWord-word-input',
'form-control',
word === value && 'is-valid'
)}
<div className="input-group-wrapper MnemonicWord">
<label className="input-group input-group-inline-dropdown ENSInput-name">
<span className="input-group-addon input-group-addon--transparent">{index + 1}.</span>
<Input
className={classnames('MnemonicWord-word-input', word === value && 'valid')}
value={readOnly ? word : value}
onChange={this.handleChange}
readOnly={readOnly}
@ -55,7 +52,7 @@ export default class MnemonicWord extends React.Component<Props, State> {
/>
</span>
)}
</div>
</label>
</div>
);
}

View File

@ -1,16 +1,17 @@
@import "common/sass/variables";
@import 'common/sass/variables';
.RequestPayment {
&-title {
display: block;
margin: 0.5rem 1rem;
margin-bottom: 0.25rem;
}
&-qr {
position: relative;
background: #FFF;
background: #fff;
cursor: pointer;
}
&-codeContainer {
padding-top: 20px;
}
&-codeBox {
min-height: 180px;
overflow-x: hidden;

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
import translate from 'translations';
import { IWallet } from 'libs/wallet';
import { QRCode } from 'components/ui';
import { QRCode, TextArea } from 'components/ui';
import { getUnit, getDecimal } from 'selectors/transaction/meta';
import {
getCurrentTo,
@ -51,10 +51,10 @@ class RequestPayment extends React.Component<Props, {}> {
};
public componentDidMount() {
this.props.reset();
if (this.props.wallet) {
this.setWalletAsyncState(this.props.wallet);
}
this.props.reset();
}
public componentWillUnmount() {
@ -121,18 +121,14 @@ class RequestPayment extends React.Component<Props, {}> {
{!!eip681String.length && (
<div className="row form-group">
<label className="RequestPayment-title">{translate('Payment QR & Code')}</label>
<div className="col-xs-6">
<label>{translate('Payment QR & Code')}</label>
<div className="RequestPayment-qr well well-lg">
<QRCode data={eip681String} />
</div>
</div>
<div className="col-xs-6 RequestPayment-codeContainer">
<textarea
className="RequestPayment-codeBox form-control"
value={eip681String}
disabled={true}
/>
<TextArea className="RequestPayment-codeBox" value={eip681String} disabled={true} />
</div>
</div>
)}

View File

@ -3,7 +3,7 @@ import { toChecksumAddress } from 'ethereumjs-util';
import translate, { translateRaw } from 'translations';
import { IWallet } from 'libs/wallet';
import { print } from 'components/PrintableWallet';
import { Identicon, QRCode } from 'components/ui';
import { Identicon, QRCode, Input } from 'components/ui';
import { GenerateKeystoreModal, TogglablePassword } from 'components';
import './WalletInfo.scss';
@ -44,8 +44,12 @@ export default class WalletInfo extends React.PureComponent<Props, State> {
<div className="Tab-content-pane">
<div className="row form-group">
<div className="col-xs-11">
<label>{translate('x_Address')}</label>
<input className="form-control" disabled={true} value={address} />
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('x_Address')}</div>
<Input readOnly={true} value={address} />
</label>
</div>
</div>
<div className="col-xs-1" style={{ padding: 0 }}>
<Identicon address={address} />

View File

@ -7,7 +7,9 @@
}
&-reset {
float: right;
display: block;
margin: auto;
margin-bottom: 0.25rem;
.fa {
margin-right: 8px;
@ -19,9 +21,9 @@
}
&-help {
margin-top: 10px;
font-size: 13px;
font-style: italic;
margin-bottom: 1rem;
}
&-inputBox {

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
@ -11,6 +10,7 @@ import { AppState } from 'reducers';
import SignButton from './SignButton';
import { isWalletFullyUnlocked } from 'selectors/wallet';
import './index.scss';
import { TextArea } from 'components/ui';
interface Props {
wallet: IFullWallet;
@ -42,17 +42,11 @@ export class SignMessage extends Component<Props, State> {
public render() {
const { wallet, unlocked } = this.props;
const { message, signedMessage } = this.state;
const messageBoxClass = classnames([
'SignMessage-inputBox',
'form-control',
message ? 'is-valid' : 'is-invalid'
]);
return (
<div>
{unlocked ? (
<div className="Tab-content-pane">
<h3 className="SignMessage-label">{translate('MSG_message')}</h3>
<button
className="SignMessage-reset btn btn-default btn-sm"
onClick={this.changeWallet}
@ -60,13 +54,17 @@ export class SignMessage extends Component<Props, State> {
<i className="fa fa-refresh" />
{translate('Change Wallet')}
</button>
<div className="form-group">
<textarea
className={messageBoxClass}
placeholder={messagePlaceholder}
value={message}
onChange={this.handleMessageChange}
/>
<div className="input-group-wrapper Deploy-field">
<label className="input-group">
<div className="input-group-header">{translate('MSG_message')}</div>
<TextArea
className={`SignMessage-inputBox ${message ? 'is-valid' : 'is-invalid'}`}
placeholder={messagePlaceholder}
value={message}
onChange={this.handleMessageChange}
/>
</label>
<div className="SignMessage-help">{translate('MSG_info2')}</div>
</div>
@ -78,16 +76,16 @@ export class SignMessage extends Component<Props, State> {
/>
{!!signedMessage && (
<div>
<h4>{translate('MSG_signature')}</h4>
<div className="form-group">
<textarea
className="SignMessage-inputBox form-control"
<div className="input-group-wrapper SignMessage-inputBox">
<label className="input-group">
<div className="input-group-header">{translate('MSG_signature')}</div>
<TextArea
className="SignMessage-inputBox"
value={JSON.stringify(signedMessage, null, 2)}
disabled={true}
onChange={this.handleMessageChange}
/>
</div>
</label>
</div>
)}
</div>

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { verifySignedMessage, ISignedMessage } from 'libs/signing';
import './index.scss';
import { TextArea } from 'components/ui';
interface Props {
showNotification: TShowNotification;
@ -34,24 +34,20 @@ export class VerifyMessage extends Component<Props, State> {
public render() {
const { verifiedAddress, verifiedMessage, signature } = this.state;
const signatureBoxClass = classnames([
'VerifyMessage-inputBox',
'form-control',
signature ? 'is-valid' : 'is-invalid'
]);
return (
<div>
<div className="Tab-content-pane">
<h4>{translate('MSG_signature')}</h4>
<div className="form-group">
<textarea
className={signatureBoxClass}
placeholder={signaturePlaceholder}
value={signature}
onChange={this.handleSignatureChange}
onPaste={this.handleSignaturePaste}
/>
<div className="input-group-wrapper ">
<label className="input-group">
<div className="input-group-header">{translate('MSG_signature')}</div>
<TextArea
className={`VerifyMessage-inputBox ${signature ? 'is-valid' : 'is-invalid'}`}
placeholder={signaturePlaceholder}
value={signature}
onChange={this.handleSignatureChange}
onPaste={this.handleSignaturePaste}
/>
</label>
</div>
<button

View File

@ -23,13 +23,24 @@
text-align: left;
}
&-inner-wrap {
display: block;
display: flex;
justify-content: center;
margin: 0 -8px;
margin-bottom: 2rem;
> .input-group-wrapper {
margin: 0 8px;
width: 100%;
max-width: 400px;
}
}
@media (min-width: $screen-xs-min) {
@media (max-width: $screen-sm) {
&-inner-wrap {
display: flex;
align-items: center;
justify-content: center;
display: block;
margin: 0;
> .input-group-wrapper {
max-width: 100%;
margin: 0;
}
}
}
&-dropdown {
@ -45,13 +56,13 @@
}
&-divider {
height: $input-height-base;
font-size: 2.1125rem;
margin: 0rem 1rem;
line-height: $input-height-base;
margin: 0 $space * 2;
font-weight: 100;
font-weight: 300;
}
&-submit {
margin-top: $space * 2.5;
margin-top: 0.5rem;
}
}

View File

@ -10,7 +10,7 @@ import { generateKindMax, generateKindMin, WhitelistedCoins, bityConfig } from '
import React, { PureComponent } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';
import { SwapDropdown } from 'components/ui';
import { SwapDropdown, Input } from 'components/ui';
import Spinner from 'components/ui/Spinner';
import { merge, reject, debounce } from 'lodash';
import './CurrencySwap.scss';
@ -41,21 +41,30 @@ interface State {
type Props = StateProps & ActionProps;
interface SwapOpt extends SwapInput {
value: string;
status: string;
image: string;
amount: number;
}
export default class CurrencySwap extends PureComponent<Props, State> {
public state = {
disabled: true,
origin: {
id: 'BTC',
label: 'BTC',
value: 'Bitcoin',
status: 'available',
image: 'https://shapeshift.io/images/coins/bitcoin.png',
amount: NaN
} as SwapInput,
} as SwapOpt,
destination: {
id: 'ETH',
label: 'ETH',
value: 'Ether',
status: 'available',
image: 'https://shapeshift.io/images/coins/ether.png',
amount: NaN
} as SwapInput,
} as SwapOpt,
originKindOptions: [],
destinationKindOptions: [],
originErr: '',
@ -78,9 +87,11 @@ export default class CurrencySwap extends PureComponent<Props, State> {
}
return errString;
};
const originErr = showError ? createErrString(origin.id, origin.amount, destination.id) : '';
const originErr = showError
? createErrString(origin.label, origin.amount, destination.label)
: '';
const destinationErr = showError
? createErrString(destination.id, destination.amount, origin.id)
? createErrString(destination.label, destination.amount, origin.label)
: '';
this.setErrorMessages(originErr, destinationErr);
}, 1000);
@ -99,7 +110,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
if (options.allIds && options.byId) {
const originKindOptions: any[] = Object.values(options.byId);
const destinationKindOptions: any[] = Object.values(
reject<any>(options.byId, o => o.id === origin.id)
reject<any>(options.byId, o => o.id === origin.label)
);
this.setState({
@ -125,7 +136,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
if (options.allIds !== prevProps.options.allIds && options.byId) {
const originKindOptions: any[] = Object.values(options.byId);
const destinationKindOptions: any[] = Object.values(
reject<any>(options.byId, o => o.id === origin.id)
reject<any>(options.byId, o => o.id === origin.label)
);
this.setState({
@ -175,7 +186,11 @@ export default class CurrencySwap extends PureComponent<Props, State> {
public setDisabled(origin: SwapInput, destination: SwapInput) {
this.clearErrMessages();
const amountsValid = origin.amount && destination.amount;
const minMaxValid = this.isMinMaxValid(origin.amount as number, origin.id, destination.id);
const minMaxValid = this.isMinMaxValid(
origin.amount as number,
origin.label,
destination.label
);
const disabled = !(amountsValid && minMaxValid);
const showError = disabled && amountsValid;
@ -216,7 +231,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
public updateOriginAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
if (amount || amount === 0) {
const pairName = combineAndUpper(origin.id, destination.id);
const pairName = combineAndUpper(origin.label, destination.label);
const rate = this.rateMixer().byId[pairName].rate;
const destinationAmount = amount * rate;
this.setState({
@ -230,7 +245,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
public updateDestinationAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
if (amount || amount === 0) {
const pairNameReversed = combineAndUpper(destination.id, origin.id);
const pairNameReversed = combineAndUpper(destination.label, origin.label);
const rate = this.rateMixer().byId[pairNameReversed].rate;
const originAmount = amount * rate;
this.setState({
@ -254,28 +269,30 @@ export default class CurrencySwap extends PureComponent<Props, State> {
: this.updateDestinationAmount(origin, destination, amount);
};
public onChangeOriginKind = (newOption: WhitelistedCoins) => {
public onChangeOriginKind = newOption => {
const { origin, destination, destinationKindOptions } = this.state;
const { options, initSwap } = this.props;
const newOrigin = { ...origin, id: newOption, amount: '' };
const newOrigin = { ...origin, label: newOption.label, value: newOption.value, amount: '' };
const newDest = {
id: newOption === destination.id ? origin.id : destination.id,
label: newOption.label === destination.label ? origin.label : destination.label,
value: newOption.value === destination.value ? origin.value : destination.value,
amount: ''
};
this.setState({
origin: newOrigin,
destination: newDest,
destinationKindOptions: reject(
[...destinationKindOptions, options.byId[origin.id]],
o => o.id === newOption
[...destinationKindOptions, options.byId[origin.label]],
o => o.id === newOption.label
)
});
initSwap({ origin: newOrigin, destination: newDest });
};
public onChangeDestinationKind = (newOption: WhitelistedCoins) => {
public onChangeDestinationKind = newOption => {
const { initSwap } = this.props;
const { origin, destination } = this.state;
@ -284,7 +301,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
amount: ''
};
const newDest = { ...destination, id: newOption, amount: '' };
const newDest = { ...destination, label: newOption.label, value: newOption.value, amount: '' };
this.setState({
origin: newOrigin,
destination: newDest
@ -304,9 +321,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
destinationErr,
timeout
} = this.state;
const OriginKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
const DestinationKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
const pairName = combineAndUpper(origin.id, destination.id);
const pairName = combineAndUpper(origin.label, destination.label);
const bityLoaded = bityRates.byId && bityRates.byId[pairName] ? true : false;
const shapeshiftLoaded = shapeshiftRates.byId && shapeshiftRates.byId[pairName] ? true : false;
// This ensures both are loaded
@ -314,73 +329,75 @@ export default class CurrencySwap extends PureComponent<Props, State> {
const timeoutLoaded = (bityLoaded && timeout) || (shapeshiftLoaded && timeout);
return (
<article className="CurrencySwap">
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
{loaded || timeoutLoaded ? (
<div className="CurrencySwap-inner-wrap">
<div className="CurrencySwap-input-group">
{originErr && <span className="CurrencySwap-error-message">{originErr}</span>}
<input
id="origin-swap-input"
className={`CurrencySwap-input form-control ${
String(origin.amount) !== '' &&
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(origin.amount as number) ? '' : origin.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
<OriginKindDropDown
ariaLabel={`change origin kind. current origin kind ${origin.id}`}
options={originKindOptions}
value={origin.id}
onChange={this.onChangeOriginKind}
/>
<React.Fragment>
<div className="CurrencySwap-inner-wrap">
<div className="flex-spacer" />
<div className="input-group-wrapper">
<div className="input-group-header">Deposit</div>
<label className="input-group input-group-inline-dropdown">
<Input
id="origin-swap-input"
className={`input-group-input ${
String(origin.amount) !== '' &&
this.isMinMaxValid(origin.amount, origin.label, destination.label)
? ''
: 'invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(origin.amount) ? '' : origin.amount}
onChange={this.onChangeAmount}
/>
<SwapDropdown
options={originKindOptions}
value={origin.value}
onChange={this.onChangeOriginKind}
/>
</label>
{originErr && <span className="CurrencySwap-error-message">{originErr}</span>}
</div>
</div>
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
<div className="CurrencySwap-input-group">
{destinationErr && (
<span className="CurrencySwap-error-message">{destinationErr}</span>
)}
<input
id="destination-swap-input"
className={`CurrencySwap-input form-control ${
String(destination.amount) !== '' &&
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(destination.amount as number) ? '' : destination.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
<DestinationKindDropDown
ariaLabel={`change destination kind. current destination kind ${destination.id}`}
options={destinationKindOptions}
value={destination.id}
onChange={this.onChangeDestinationKind}
/>
<div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown">
<div className="input-group-header">Recieve</div>
<Input
id="destination-swap-input"
className={`${
String(destination.amount) !== '' &&
this.isMinMaxValid(origin.amount, origin.label, destination.label)
? ''
: 'invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(destination.amount) ? '' : destination.amount}
onChange={this.onChangeAmount}
/>
<SwapDropdown
options={destinationKindOptions}
value={destination.value}
onChange={this.onChangeDestinationKind}
/>
</label>
{destinationErr && (
<span className="CurrencySwap-error-message">{destinationErr}</span>
)}
</div>
<div className="flex-spacer" />
</div>
</div>
<div className="CurrencySwap-submit">
<SimpleButton
onClick={this.onClickStartSwap}
text={translate('SWAP_init_CTA')}
disabled={this.state.disabled}
type="primary"
/>
</div>
</React.Fragment>
) : (
<Spinner />
)}
<div className="CurrencySwap-submit">
<SimpleButton
onClick={this.onClickStartSwap}
text={translate('SWAP_init_CTA')}
disabled={this.state.disabled}
type="primary"
/>
</div>
</article>
);
}

View File

@ -10,6 +10,7 @@ import { getUnit } from 'selectors/transaction';
import { getCurrentBalance } from 'selectors/wallet';
import Spinner from 'components/ui/Spinner';
import { Wei, TokenValue } from 'libs/units';
import { Input } from 'components/ui';
interface StateProps {
unit: string;
@ -36,7 +37,7 @@ class FieldsClass extends Component<Props> {
<div className="col-xs-12">
<AddressFieldFactory
withProps={({ currentTo }) => (
<input className="form-control" type="text" value={currentTo.raw} readOnly={true} />
<Input type="text" value={currentTo.raw} readOnly={true} />
)}
/>
</div>
@ -61,8 +62,7 @@ class FieldsClass extends Component<Props> {
</h5>
)}
{isValid && (
<input
className="form-control"
<Input
type="text"
value={`${currentValue.raw} ${this.props.unit}`}
readOnly={true}

View File

@ -69,8 +69,8 @@ export default class PartThree extends PureComponent<ReduxActionProps & ReduxSta
} = this.props;
const SwapProgressProps = {
originId: origin.id,
destinationId: destination.id,
originId: origin.label,
destinationId: destination.label,
provider,
bityOrderStatus,
shapeshiftOrderStatus,
@ -98,7 +98,7 @@ export default class PartThree extends PureComponent<ReduxActionProps & ReduxSta
<PaymentInfo {...PaymentInfoProps} />
<LiteSend />
{OpenOrder && origin.id === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
{OpenOrder && origin.label === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
</div>
);
}

View File

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import translate from 'translations';
import { SwapInput } from 'reducers/swap/types';
import './PaymentInfo.scss';
import { Input } from 'components/ui';
export interface Props {
origin: SwapInput;
@ -17,11 +18,11 @@ export default class PaymentInfo extends PureComponent<Props, {}> {
<span>{translate('SWAP_order_CTA')}</span>
<strong>
{' '}
{origin.amount} {origin.id}
{origin.amount} {origin.label}
</strong>
<span> {translate('SENDModal_Content_2')}</span>
<input
className="SwapPayment-address form-control"
<Input
className="SwapPayment-address"
value={this.props.paymentAddress || undefined}
disabled={true}
/>

View File

@ -6,7 +6,6 @@ import {
TStopLoadBityRatesSwap
} from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import classnames from 'classnames';
import SimpleButton from 'components/ui/SimpleButton';
import { donationAddressMap } from 'config';
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
@ -14,6 +13,7 @@ import React, { PureComponent } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';
import './ReceivingAddress.scss';
import { Input } from 'components/ui';
export interface StateProps {
origin: SwapInput;
@ -46,7 +46,7 @@ export default class ReceivingAddress extends PureComponent<StateProps & ActionP
if (provider === 'shapeshift') {
this.props.shapeshiftOrderCreateRequestedSwap(
destinationAddress,
origin.id,
origin.label,
destinationId,
destinationKind
);
@ -54,7 +54,7 @@ export default class ReceivingAddress extends PureComponent<StateProps & ActionP
this.props.bityOrderCreateRequestedSwap(
origin.amount as number,
this.props.destinationAddress,
combineAndUpper(origin.id, destinationId)
combineAndUpper(origin.label, destinationId)
);
}
};
@ -69,13 +69,6 @@ export default class ReceivingAddress extends PureComponent<StateProps & ActionP
validAddress = isValidETHAddress(destinationAddress);
}
const inputClasses = classnames({
'SwapAddress-address-input': true,
'form-control': true,
'is-valid': validAddress,
'is-invalid': !validAddress
});
return (
<section className="SwapAddress block">
<section className="row">
@ -85,8 +78,8 @@ export default class ReceivingAddress extends PureComponent<StateProps & ActionP
{translate('SWAP_rec_add')} ({destinationId})
</h4>
<input
className={inputClasses}
<Input
className={`SwapAddress-address-input ${!validAddress ? 'invalid' : ''}`}
type="text"
value={destinationAddress}
onChange={this.onChangeDestinationAddress}

View File

@ -2,6 +2,7 @@ import React from 'react';
import './SupportFooter.scss';
import { SwapInput } from 'actions/swap';
import { NormalizedBityRates, NormalizedShapeshiftRates } from 'reducers/swap/types';
import { TextArea } from 'components/ui';
interface Props {
origin: SwapInput;
@ -30,7 +31,7 @@ class SupportFooter extends React.PureComponent<Props, {}> {
shapeshiftRates,
bityRates
} = this.props;
const pair = origin && destination ? origin.id + destination.id : 'BTCETH';
const pair = origin && destination ? origin.label + destination.label : 'BTCETH';
const rates = provider === 'shapeshift' ? shapeshiftRates.byId : bityRates.byId;
const emailTo =
provider === 'shapeshift' ? 'support@mycrypto.com' : 'support@mycrypto.com,mew@bity.com';
@ -45,26 +46,26 @@ Provider: ${serviceProvider}
REF ID#: ${reference || ''}
Amount to send: ${origin.amount || ''} ${origin.id}
Amount to send: ${origin.amount || ''} ${origin.label}
Amount to receive: ${destination.amount || ''} ${destination.id}
Amount to receive: ${destination.amount || ''} ${destination.label}
Payment Address: ${paymentAddress || ''}
Receiving Address: ${destinationAddress || ''}
Rate: ${rates[pair].rate} ${origin.id}/${destination.id}
Rate: ${rates[pair].rate} ${origin.label}/${destination.label}
`);
fallbackBody = `To: ${emailTo}
Subject: Issue regarding my Swap via MyCrypto
Message:
Provider: ${serviceProvider}
REF ID#: ${reference || ''}
Amount to send: ${origin.amount || ''} ${origin.id}
Amount to receive: ${destination.amount || ''} ${destination.id}
Amount to send: ${origin.amount || ''} ${origin.label}
Amount to receive: ${destination.amount || ''} ${destination.label}
Payment Address: ${paymentAddress || ''}
Receiving Address: ${destinationAddress || ''}
Rate: ${rates[pair].rate} ${origin.id}/${destination.id}`;
Rate: ${rates[pair].rate} ${origin.label}/${destination.label}`;
}
return (
<section className="SupportFooter">
@ -81,7 +82,7 @@ Rate: ${rates[pair].rate} ${origin.id}/${destination.id}`;
<small>Click here if link doesn't work</small>
</p>
{open ? (
<textarea defaultValue={fallbackBody} className="form-control input-sm" rows={9} />
<TextArea defaultValue={fallbackBody} className="form-control input-sm" rows={9} />
) : null}
</div>
</section>

View File

@ -69,7 +69,9 @@ export default class SwapInfoHeader extends PureComponent<SwapInfoHeaderProps, {
{/*Amount to send*/}
{!this.isExpanded() && (
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">{` ${origin.amount} ${origin.id}`}</h3>
<h3 className="SwapInfo-details-block-value">{` ${origin.amount} ${
origin.label
}`}</h3>
<p className="SwapInfo-details-block-label">{translate('SEND_amount')}</p>
</div>
)}
@ -93,7 +95,7 @@ export default class SwapInfoHeader extends PureComponent<SwapInfoHeaderProps, {
{/*Amount to Receive*/}
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">
{` ${toFixedIfLarger(destination.amount as number, 4)} ${destination.id}`}
{` ${toFixedIfLarger(destination.amount as number, 4)} ${destination.label}`}
</h3>
<p className="SwapInfo-details-block-label">{translate('SWAP_rec_amt')}</p>
</div>
@ -102,7 +104,9 @@ export default class SwapInfoHeader extends PureComponent<SwapInfoHeaderProps, {
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">
{`${computedOriginDestinationRatio &&
toFixedIfLarger(computedOriginDestinationRatio, 4)} ${destination.id}/${origin.id}`}
toFixedIfLarger(computedOriginDestinationRatio, 4)} ${destination.label}/${
origin.label
}`}
</h3>
<p className="SwapInfo-details-block-label">{translate('SWAP_your_rate')}</p>
</div>

View File

@ -139,7 +139,7 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps & RouteComponent
const ReceivingAddressProps = {
isPostingOrder,
origin,
destinationId: destination.id,
destinationId: destination.label,
destinationKind: destination.amount as number,
destinationAddressSwap,
destinationAddress,

View File

@ -32,8 +32,8 @@ export interface State {
export const INITIAL_STATE: State = {
step: 1,
origin: { id: 'BTC', amount: NaN },
destination: { id: 'ETH', amount: NaN },
origin: { label: 'BTC', amount: NaN },
destination: { label: 'ETH', amount: NaN },
options: {
byId: {},
allIds: []

View File

@ -4,7 +4,7 @@ import { WhitelistedCoins } from 'config';
export type ProviderName = ProviderName;
export interface SwapInput {
id: WhitelistedCoins;
label: WhitelistedCoins;
amount: number | string;
}

View File

@ -20,7 +20,7 @@ import { isUnlocked, isEtherBalancePending } from 'selectors/wallet';
type SwapState = AppState['swap'];
export function* configureLiteSendSaga(): SagaIterator {
const { amount, id }: SwapState['origin'] = yield select(getOrigin);
const { amount, label }: SwapState['origin'] = yield select(getOrigin);
const paymentAddress: SwapState['paymentAddress'] = yield call(fetchPaymentAddress);
if (!paymentAddress) {
@ -28,7 +28,7 @@ export function* configureLiteSendSaga(): SagaIterator {
return yield put(showLiteSend(false));
}
const supportedUnit: boolean = yield select(isSupportedUnit, id);
const supportedUnit: boolean = yield select(isSupportedUnit, label);
if (!supportedUnit) {
return yield put(showLiteSend(false));
}
@ -42,8 +42,8 @@ export function* configureLiteSendSaga(): SagaIterator {
}
//if it's a token, manually scan for that tokens balance and wait for it to resolve
if (!isEtherUnit(id)) {
yield put(setTokenBalancePending({ tokenSymbol: id }));
if (!isEtherUnit(label)) {
yield put(setTokenBalancePending({ tokenSymbol: label }));
yield take([
WalletTK.WALLET_SET_TOKEN_BALANCE_FULFILLED,
WalletTK.WALLET_SET_TOKEN_BALANCE_REJECTED
@ -55,7 +55,7 @@ export function* configureLiteSendSaga(): SagaIterator {
}
}
yield put(setUnitMeta(id));
yield put(setUnitMeta(label));
yield put(setCurrentValue(amount.toString()));
yield put(setCurrentTo(paymentAddress));
}

View File

@ -6,7 +6,7 @@
&-nowrap {
flex-wrap: nowrap;
}
.flex-spacer {
flex-grow: 2;
}
}
.flex-spacer {
flex-grow: 2;
}

View File

@ -3,7 +3,6 @@
label {
margin-bottom: $space-xs;
font-size: $font-size-bump-more;
&.is-required:after {
content: '*';
@ -49,7 +48,6 @@ select.form-control {
.form-group {
display: block;
margin-top: $form-group-margin-bottom * 2;
margin-bottom: $form-group-margin-bottom;
}

View File

@ -1,5 +1,9 @@
// Input groups overrides
@import "common/sass/variables";
@import 'common/sass/variables';
label {
font-weight: normal;
}
.input-group {
> * {
@ -15,7 +19,15 @@
// Align with inputs
.input-group-addon {
padding: 0 $padding-base-horizontal;
width: auto;
font-size: 1rem;
line-height: 1.4;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
&--transparent {
background-color: transparent;
border: none;
}
}
.input-group-btn {
@ -23,7 +35,7 @@
margin-bottom: 0;
margin-top: 0;
font-size: $font-size-base;
padding: .5rem $space-xl;
padding: 0.5rem $space-xl;
}
// Fix for nested dropdown button

View File

@ -1,92 +1,92 @@
// This syntax is necessary to override css styles in react-select
@import 'common/sass/variables';
.Select {
border-radius: 2px;
background-color: white;
border: 1px solid #e5ecf3;
border-radius: 2px;
font-weight: 400;
font-size: 1rem;
.Select-control {
height: $input-height-base;
display: block;
box-sizing: border-box;
font-weight: 400;
border-radius: 0px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
border: 1px solid $input-border;
transition: $transition;
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
transition: border-color 120ms, box-shadow 120ms;
height: 3rem;
&-control {
min-width: 7.5rem;
height: inherit;
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
border: none !important;
&:hover {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
border: 1px solid $input-border;
box-shadow: none;
}
.Select-value {
padding-left: $input-padding-x;
padding-right: $input-padding-x;
.Select-value-label {
vertical-align: middle;
}
}
&-placeholder {
line-height: $line-height-base;
}
&-input {
position: absolute;
> input {
line-height: $line-height-base;
padding: 0;
}
.Select-arrow-zone {
float: right;
}
&-placeholder,
&-input {
padding-left: 0;
padding-right: 0;
padding: 0rem 1rem;
top: 50%;
transform: translateY(-50%);
display: block;
height: auto;
}
&-option {
padding: 0.75rem 1rem;
}
&-menu-outer {
z-index: 3;
width: calc(100% + 2px);
left: -1px;
border-radius: 2px;
background-color: white;
border: none;
border-left: 2px solid #5694b8;
border-right: 2px solid #5694b8;
border-bottom: 2px solid #5694b8;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
border-color: inherit;
}
&.invalid.has-blurred {
border-color: $brand-danger;
box-shadow: inset 0px 0px 0px 1px $brand-danger;
}
&.is-focused {
border-color: #4295bc;
box-shadow: inset 0px 0px 0px 1px #4295bc;
&.valid {
border-color: #8dd17b;
box-shadow: inset 0px 0px 0px 1px #8dd17b;
}
}
&-value {
line-height: $line-height-base !important;
padding-left: 0 !important;
padding: 0.75rem 1rem !important;
height: inherit !important;
&-label {
position: relative;
top: 50%;
transform: translateY(-50%);
display: block;
}
}
.Select-menu-outer {
box-sizing: content-box;
font-weight: 400;
border: 1px solid $input-border;
width: calc(100% - 1px);
}
.Select-placeholder {
color: #d3d3d3;
padding: 0px $input-padding-x;
line-height: $input-height-base;
}
.Select-input {
opacity: 0;
}
&.is-open {
.Select-control {
border: 1px solid $input-border;
}
}
&.is-focused {
&:not(.is-open):not(.is-invalid) {
.Select-control {
border-color: $brand-primary;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba(14, 151, 192, 0.5);
}
}
}
@mixin react-select-control-state($color) {
.Select-control {
border-color: lighten($color, 20%);
@include input-shadow($color);
}
.Select-menu-outer {
border-color: lighten($color, 20%);
@include input-shadow($color);
}
&.is-focused {
.Select-control {
border-color: lighten($color, 5%);
@include input-focus-shadow($color);
}
.Select-menu-outer {
border-color: lighten($color, 5%);
@include input-focus-shadow($color);
}
}
}
&.is-valid {
@include react-select-control-state($brand-success);
}
&.is-invalid {
@include react-select-control-state($brand-danger);
}
&.is-warning {
@include react-select-control-state($brand-warning);
&-multi-value-wrapper {
display: block;
position: relative;
height: inherit;
}
}

View File

@ -13,7 +13,7 @@ $brand-primary: $ether-blue;
$brand-success: #5dba5a;
$brand-info: $ether-navy;
$brand-warning: #ff9800;
$brand-danger: #ea4b40;
$brand-danger: #db5846;
$body-bg: #fbfbfb;
$text-color: $gray-dark;

View File

@ -15,7 +15,7 @@ exports[`render snapshot 1`] = `
destination={
Object {
"amount": NaN,
"id": "ETH",
"label": "ETH",
}
}
destinationAddress=""
@ -71,7 +71,7 @@ exports[`render snapshot 1`] = `
origin={
Object {
"amount": NaN,
"id": "BTC",
"label": "BTC",
}
}
outputTx={null}

View File

@ -119,8 +119,8 @@ describe('swap reducer', () => {
...INITIAL_STATE,
bityRates: normalizedBityRates,
shapeshiftRates: normalizedShapeshiftRates,
origin: { id: 'BTC', amount: 1 },
destination: { id: 'ETH', amount: 3 }
origin: { label: 'BTC', amount: 1 },
destination: { label: 'ETH', amount: 3 }
},
swapActions.restartSwap()
)