Update code blocks & generate / send tx buttons (#1333)

* Update account view routing

* update styles

* Fix WalletDecrypt types

* Replace disabled textareas with code blocks

* Fix broken animation

* Update snapshot

* Make contract interact dropdowns clearable & searchable

* Update node-sass to v4.8.3

* Fix swap inputs incorrectly incorrectly displaying invalid

* Refactor send tx & generate tx button

* Update broadcast tx & add more transaction details in tx confirmation

* Add signing prop to send button

* Update lite send

* Update codeblock styles

* Update snapshot

* Revert renaming Dropdown
This commit is contained in:
James Prado 2018-03-23 12:41:47 -04:00 committed by Daniel Ternyak
parent 221e9cf4a3
commit 910093b761
42 changed files with 467 additions and 311 deletions

View File

@ -8,5 +8,11 @@
.tx-modal-testnet-warn {
text-align: center;
margin: 0;
background-color: $brand-warning;
border-radius: 2px;
padding: 0.5rem 0.75rem;
font-weight: 400;
color: white;
margin: auto;
margin-bottom: 1rem;
}

View File

@ -1,11 +1,11 @@
@import 'common/sass/variables';
.tx-modal-address {
display: flex;
flex-direction: column;
margin: auto;
// Table is necessary here so the size of the div fits to it's content, and margins can be applied to it.
// width: fit-content; margin: auto; isn't widely supported, so for now, this is the best option
display: table;
padding: 1rem 0;
align-items: center;
margin: auto;
.Identicon {
margin-right: 1rem;
}

View File

@ -33,12 +33,16 @@ class AddressesClass extends Component<StateProps> {
<div className="tx-modal-address">
<div className="tx-modal-address-from">
{from && (
<Identicon className="tx-modal-address-from-icon" size={size} address={from} />
<React.Fragment>
<Identicon className="tx-modal-address-from-icon" size={size} address={from} />
<div className="tx-modal-address-from-content">
<h5 className="tx-modal-address-from-title">
{translate('CONFIRM_TX_FROM')}{' '}
</h5>
<h5 className="tx-modal-address-from-address small">{from}</h5>
</div>
</React.Fragment>
)}
<div className="tx-modal-address-from-content">
<h5 className="tx-modal-address-from-title">{translate('CONFIRM_TX_FROM')} </h5>
<h5 className="tx-modal-address-from-address small">{from}</h5>
</div>
</div>
{isToken && (
<div className="tx-modal-address-tkn-contract">
@ -59,15 +63,19 @@ class AddressesClass extends Component<StateProps> {
</div>
)}
<div className="tx-modal-address-to">
<Identicon
className="tx-modal-address-from-icon"
size={size}
address={toFormatted}
/>
<div className="tx-modal-address-to-content">
<h5 className="tx-modal-address-to-title">{translate('CONFIRM_TX_TO')} </h5>
<h5 className="small tx-modal-address-to-address">{toFormatted}</h5>
</div>
{to && (
<React.Fragment>
<Identicon
className="tx-modal-address-from-icon"
size={size}
address={toFormatted}
/>
<div className="tx-modal-address-to-content">
<h5 className="tx-modal-address-to-title">{translate('CONFIRM_TX_TO')} </h5>
<h5 className="small tx-modal-address-to-address">{toFormatted}</h5>
</div>
</React.Fragment>
)}
</div>
</div>
);

View File

@ -1,12 +1,13 @@
import React, { Component } from 'react';
import Code from 'components/ui/Code';
import './Details.scss';
import { SerializedTransaction } from 'components/renderCbs';
import { AppState } from 'reducers';
import { getNodeConfig } from 'selectors/config';
import { connect } from 'react-redux';
import { TokenValue } from 'libs/units';
import { NodeConfig } from 'types/node';
import translate from 'translations';
import { CodeBlock, Input } from 'components/ui';
import { addHexPrefix } from 'ethereumjs-util';
interface StateProps {
node: NodeConfig;
@ -17,21 +18,25 @@ class DetailsClass extends Component<StateProps> {
const { node: { network, service } } = this.props;
return (
<div className="tx-modal-details">
<p className="tx-modal-details-network-info">
Interacting with the {network} network provided by {service}
</p>
<label className="input-group">
<div className="input-group-header">Network</div>
<Input readOnly={true} value={`${network} network - provided by ${service}`} />
</label>
<SerializedTransaction
withSerializedTransaction={(_, fields) => {
const { chainId, data, to, ...convertRestToBase10 } = fields;
const base10Fields = Object.entries(convertRestToBase10).reduce(
(convertedFields, [currName, currValue]) => ({
...convertedFields,
[currName]: TokenValue(currValue).toString()
}),
{} as typeof convertRestToBase10
return (
<React.Fragment>
<label className="input-group">
<div className="input-group-header">{translate('SEND_RAW')}</div>
<CodeBlock>{JSON.stringify(fields, null, 2)} </CodeBlock>
</label>
<label className="input-group">
<div className="input-group-header">{translate('SEND_SIGNED')}</div>
<CodeBlock>{addHexPrefix(_)} </CodeBlock>
</label>
</React.Fragment>
);
return <Code>{JSON.stringify({ chainId, data, to, ...base10Fields }, null, 2)} </Code>;
}}
/>
</div>

View File

@ -0,0 +1,3 @@
.GenerateTransaction {
margin-bottom: 1rem;
}

View File

@ -1,13 +1,24 @@
import { GenerateTransactionFactory } from './GenerateTransactionFactory';
import React from 'react';
import translate from 'translations';
import { SigningStatus } from 'components';
import './GenerateTransaction.scss';
export const GenerateTransaction: React.SFC<{}> = () => (
<GenerateTransactionFactory
withProps={({ disabled, isWeb3Wallet, onClick }) => (
<button disabled={disabled} className="btn btn-info btn-block" onClick={onClick}>
{isWeb3Wallet ? translate('SEND_GENERATE') : translate('DEP_SIGNTX')}
</button>
)}
/>
<React.Fragment>
<GenerateTransactionFactory
withProps={({ disabled, isWeb3Wallet, onClick }) => (
<React.Fragment>
<button
disabled={disabled}
className="btn btn-info btn-block GenerateTransaction"
onClick={onClick}
>
{isWeb3Wallet ? translate('SEND_GENERATE') : translate('DEP_SIGNTX')}
</button>
</React.Fragment>
)}
/>
<SigningStatus />
</React.Fragment>
);

View File

@ -7,9 +7,16 @@ import {
getTransaction,
isNetworkRequestPending,
isValidGasPrice,
isValidGasLimit
isValidGasLimit,
getSignedTx,
getSerializedTransaction
} from 'selectors/transaction';
import { getWalletType } from 'selectors/wallet';
import { getWalletType, IWalletType } from 'selectors/wallet';
import { OfflineBroadcast } from 'components/SendButtonFactory/OfflineBroadcast';
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import translate from 'translations';
import { addHexPrefix } from 'ethereumjs-util';
import { CodeBlock } from 'components/ui';
export interface CallbackProps {
disabled: boolean;
@ -19,11 +26,14 @@ export interface CallbackProps {
interface StateProps {
transaction: EthTx;
walletType: IWalletType;
serializedTransaction: AppState['transaction']['sign']['local']['signedTransaction'];
networkRequestPending: boolean;
isFullTransaction: boolean;
isWeb3Wallet: boolean;
validGasPrice: boolean;
validGasLimit: boolean;
signedTx: boolean;
}
interface OwnProps {
@ -35,35 +45,68 @@ type Props = OwnProps & StateProps;
class GenerateTransactionFactoryClass extends Component<Props> {
public render() {
const {
walletType,
serializedTransaction,
isFullTransaction,
isWeb3Wallet,
networkRequestPending,
validGasPrice,
validGasLimit,
transaction
transaction,
signedTx
} = this.props;
const getStringifiedTx = (serializedTx: Buffer) =>
JSON.stringify(getTransactionFields(makeTransaction(serializedTx)), null, 2);
const isButtonDisabled =
!isFullTransaction || networkRequestPending || !validGasPrice || !validGasLimit;
return (
<WithSigner
isWeb3={isWeb3Wallet}
withSigner={signer =>
this.props.withProps({
disabled: isButtonDisabled,
isWeb3Wallet,
onClick: () => signer(transaction)
})
}
/>
<React.Fragment>
<WithSigner
isWeb3={isWeb3Wallet}
withSigner={signer =>
this.props.withProps({
disabled: isButtonDisabled,
isWeb3Wallet,
onClick: () => signer(transaction)
})
}
/>
{signedTx && (
<React.Fragment>
{/* shows the json representation of the transaction */}
<div className="col-xs-12">
<label>
{walletType.isWeb3Wallet ? 'Transaction Parameters' : translate('SEND_RAW')}
</label>
<CodeBlock>{getStringifiedTx(serializedTransaction as Buffer)}</CodeBlock>
</div>
{serializedTransaction && (
<div className="col-xs-12">
<label>
{walletType.isWeb3Wallet
? 'Serialized Transaction Parameters'
: translate('SEND_SIGNED')}
</label>
<CodeBlock>{addHexPrefix(serializedTransaction.toString('hex'))}</CodeBlock>
</div>
)}
<OfflineBroadcast />
</React.Fragment>
)}
</React.Fragment>
);
}
}
export const GenerateTransactionFactory = connect((state: AppState) => ({
...getTransaction(state),
walletType: getWalletType(state),
serializedTransaction: getSerializedTransaction(state),
networkRequestPending: isNetworkRequestPending(state),
isWeb3Wallet: getWalletType(state).isWeb3Wallet,
validGasPrice: isValidGasPrice(state),
validGasLimit: isValidGasLimit(state)
validGasLimit: isValidGasLimit(state),
signedTx: !!getSignedTx(state)
}))(GenerateTransactionFactoryClass);

View File

@ -12,8 +12,7 @@ import {
getStaticNetworkConfigs
} from 'selectors/config';
import { CustomNode } from 'libs/nodes';
import { Input } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
import { Input, Dropdown } from 'components/ui';
import './CustomNodeModal.scss';
const CUSTOM = { label: 'Custom', value: 'custom' };
@ -128,7 +127,6 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="col-sm-3 input-group">
<div className="input-group-header">Network</div>
<Dropdown
className="input-group-dropdown"
value={network}
options={options}
clearable={false}

View File

@ -0,0 +1,3 @@
.SendButton {
margin-bottom: 1rem;
}

View File

@ -2,24 +2,32 @@ import React from 'react';
import { SendButtonFactory } from './SendButtonFactory';
import translate from 'translations';
import { ConfirmationModal } from 'components/ConfirmationModal';
import { SigningStatus } from 'components';
import './SendButton.scss';
export const SendButton: React.SFC<{
onlyTransactionParameters?: boolean;
toggleDisabled?: boolean;
className?: string;
signing?: boolean;
customModal?: typeof ConfirmationModal;
}> = ({ onlyTransactionParameters, toggleDisabled, customModal }) => (
<SendButtonFactory
onlyTransactionParameters={!!onlyTransactionParameters}
toggleDisabled={toggleDisabled}
Modal={customModal ? customModal : ConfirmationModal}
withProps={({ disabled, onClick }: { disabled: boolean; onClick(): void }) => (
<div className="row form-group">
<div className="col-xs-12">
<button disabled={disabled} className="btn btn-primary btn-block" onClick={onClick}>
}> = ({ signing, customModal, className }) => (
<React.Fragment>
<SendButtonFactory
signing={signing}
Modal={customModal ? customModal : ConfirmationModal}
withProps={({ disabled, openModal, signTx }) => (
<React.Fragment>
<button
disabled={disabled}
className={`SendButton btn btn-primary btn-block ${className}`}
onClick={() => {
!!signing ? (signTx(), openModal()) : openModal();
}}
>
{translate('SEND_TRANS')}
</button>
</div>
</div>
)}
/>
</React.Fragment>
)}
/>
<SigningStatus />
</React.Fragment>
);

View File

@ -2,16 +2,29 @@ import React, { Component } from 'react';
import { getOffline } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { getCurrentTransactionStatus, currentTransactionBroadcasted } from 'selectors/transaction';
import {
getCurrentTransactionStatus,
currentTransactionBroadcasted,
signaturePending,
getSignedTx,
getWeb3Tx
} from 'selectors/transaction';
import { showNotification, TShowNotification } from 'actions/notifications';
import { ITransactionStatus } from 'reducers/transaction/broadcast';
import { reset, TReset } from 'actions/transaction';
import {
reset,
TReset,
TSignTransactionRequested,
signTransactionRequested
} from 'actions/transaction';
import { ConfirmationModal } from 'components/ConfirmationModal';
interface StateProps {
offline: boolean;
currentTransaction: false | ITransactionStatus | null;
transactionBroadcasted: boolean;
signaturePending: boolean;
signedTx: boolean;
}
interface State {
@ -21,11 +34,15 @@ interface State {
interface DispatchProps {
showNotification: TShowNotification;
reset: TReset;
signTransactionRequested: TSignTransactionRequested;
}
interface OwnProps {
Modal: typeof ConfirmationModal;
withOnClick(onClick: { onClick(): void }): React.ReactElement<any> | null;
withOnClick(onClick: {
openModal(): void;
signer(signer: any): void;
}): React.ReactElement<any> | null;
}
const INITIAL_STATE: State = {
@ -38,12 +55,18 @@ class OnlineSendClass extends Component<Props, State> {
public state: State = INITIAL_STATE;
public render() {
return !this.props.offline ? (
return (
<React.Fragment>
{this.props.withOnClick({ onClick: this.openModal })}
<this.props.Modal isOpen={this.state.showModal} onClose={this.closeModal} />
{this.props.withOnClick({
openModal: this.openModal,
signer: this.props.signTransactionRequested
})}
<this.props.Modal
isOpen={!this.props.signaturePending && this.props.signedTx && this.state.showModal}
onClose={this.closeModal}
/>
</React.Fragment>
) : null;
);
}
public componentWillReceiveProps(nextProps: Props) {
@ -73,7 +96,9 @@ export const OnlineSend = connect(
(state: AppState) => ({
offline: getOffline(state),
currentTransaction: getCurrentTransactionStatus(state),
transactionBroadcasted: currentTransactionBroadcasted(state)
transactionBroadcasted: currentTransactionBroadcasted(state),
signaturePending: signaturePending(state).isSignaturePending,
signedTx: !!getSignedTx(state) || !!getWeb3Tx(state)
}),
{ showNotification, reset }
{ showNotification, reset, signTransactionRequested }
)(OnlineSendClass);

View File

@ -1,102 +1,89 @@
import translate from 'translations';
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { OfflineBroadcast } from './OfflineBroadcast';
import EthTx from 'ethereumjs-tx';
import { OnlineSend } from './OnlineSend';
import { addHexPrefix } from 'ethereumjs-util';
import { getWalletType, IWalletType } from 'selectors/wallet';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { ConfirmationModal } from 'components/ConfirmationModal';
import { TextArea } from 'components/ui';
import { getSerializedTransaction } from 'selectors/transaction';
import {
getSerializedTransaction,
getTransaction,
isNetworkRequestPending,
isValidGasPrice,
isValidGasLimit,
getSignedTx,
getWeb3Tx
} from 'selectors/transaction';
export interface CallbackProps {
disabled: boolean;
onClick(): void;
signTx(): void;
openModal(): void;
}
interface StateProps {
walletType: IWalletType;
serializedTransaction: AppState['transaction']['sign']['local']['signedTransaction'];
transaction: EthTx;
isFullTransaction: boolean;
networkRequestPending: boolean;
validGasPrice: boolean;
validGasLimit: boolean;
signedTx: boolean;
}
interface OwnProps {
onlyTransactionParameters?: boolean;
toggleDisabled?: boolean;
signing?: boolean;
Modal: typeof ConfirmationModal;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const getStringifiedTx = (serializedTransaction: Buffer) =>
JSON.stringify(getTransactionFields(makeTransaction(serializedTransaction)), null, 2);
type Props = StateProps & OwnProps;
class SendButtonFactoryClass extends Component<Props> {
public render() {
const {
onlyTransactionParameters,
signing,
signedTx,
transaction,
isFullTransaction,
serializedTransaction,
toggleDisabled,
walletType
networkRequestPending,
validGasPrice,
validGasLimit
} = this.props;
const columnSize = onlyTransactionParameters ? 12 : 6;
/* Left and right transaction comparision boxes, only displayed when a serialized transaction
exists in state */
// shows the json representation of the transaction
const leftTxCompare = serializedTransaction && (
<div className={`col-sm-${columnSize}`}>
<label>{walletType.isWeb3Wallet ? 'Transaction Parameters' : translate('SEND_RAW')}</label>
<TextArea value={getStringifiedTx(serializedTransaction)} rows={4} readOnly={true} />
</div>
);
// shows the serialized representation of the transaction
// "onlyTransactionParameters" used in broadcast tx so the same serialized tx isnt redundantly
// displayed
const rightTxCompare = serializedTransaction &&
!onlyTransactionParameters && (
<div className="col-sm-6">
<label>
{walletType.isWeb3Wallet
? 'Serialized Transaction Parameters'
: translate('SEND_SIGNED')}
</label>
<TextArea
value={addHexPrefix(serializedTransaction.toString('hex'))}
rows={4}
readOnly={true}
/>
</div>
);
const shouldDisplayOnlineSend = toggleDisabled || serializedTransaction;
// return signing ? true : signedTx ? true : false
return (
<>
{leftTxCompare}
{rightTxCompare}
<OfflineBroadcast />
{shouldDisplayOnlineSend && (
<OnlineSend
withOnClick={({ onClick }) =>
this.props.withProps({
disabled: !!(toggleDisabled && !serializedTransaction),
onClick
})
}
Modal={this.props.Modal}
/>
)}
</>
(signing || (!signing && signedTx)) && (
<OnlineSend
withOnClick={({ openModal, signer }) =>
this.props.withProps({
disabled: signing
? !isFullTransaction || networkRequestPending || !validGasPrice || !validGasLimit
: !!(signing && !serializedTransaction),
signTx: () => signer(transaction),
openModal
})
}
Modal={this.props.Modal}
/>
)
);
}
}
export const SendButtonFactory = connect((state: AppState) => ({
walletType: getWalletType(state),
serializedTransaction: getSerializedTransaction(state)
}))(SendButtonFactoryClass);
const mapStateToProps = (state: AppState) => {
return {
walletType: getWalletType(state),
serializedTransaction: getSerializedTransaction(state),
...getTransaction(state),
networkRequestPending: isNetworkRequestPending(state),
validGasPrice: isValidGasPrice(state),
validGasLimit: isValidGasLimit(state),
signedTx: !!getSignedTx(state) || !!getWeb3Tx(state)
};
};
export const SendButtonFactory = connect(mapStateToProps)(SendButtonFactoryClass);

View File

@ -0,0 +1,3 @@
.SigningStatus {
margin-bottom: 1rem;
}

View File

@ -3,7 +3,8 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { signaturePending } from 'selectors/transaction';
import { Spinner } from 'components/ui';
import translate from 'translations';
import './SigningStatus.scss';
import { translate } from 'translations';
interface StateProps {
isSignaturePending: boolean;
isHardwareWallet: boolean;
@ -13,7 +14,7 @@ class SigningStatusClass extends Component<StateProps> {
public render() {
const { isHardwareWallet, isSignaturePending } = this.props;
const HWWalletPrompt: React.SFC<{}> = _ =>
const HWWalletPrompt: React.SFC<{}> = () =>
isHardwareWallet ? (
<p>
<b>{translate('CONFIRM_HARDWARE_WALLET_TRANSACTION')}</b>
@ -21,11 +22,9 @@ class SigningStatusClass extends Component<StateProps> {
) : null;
return isSignaturePending ? (
<div className="container">
<div className="row form-group text-center">
<HWWalletPrompt />
<Spinner size="x2" />
</div>
<div className="SigningStatus text-center">
<HWWalletPrompt />
<Spinner size="x2" />
</div>
) : null;
}

View File

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

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react';
import { setUnitMeta, TSetUnitMeta } from 'actions/transaction';
import Dropdown from 'components/ui/Dropdown';
import { TokenBalance, MergedToken, getShownTokenBalances, getTokens } from 'selectors/wallet';
import { Query } from 'components/renderCbs';
import { connect } from 'react-redux';
@ -8,6 +7,7 @@ import { AppState } from 'reducers';
import { getUnit } from 'selectors/transaction';
import { getNetworkUnit } from 'selectors/config';
import { Option } from 'react-select';
import { Dropdown } from 'components/ui';
interface DispatchProps {
setUnitMeta: TSetUnitMeta;

View File

@ -1,14 +0,0 @@
pre {
color: #333;
background-color: #fafafa;
border: 1px solid #ececec;
padding: 0.5rem 1rem;
margin: 0;
font-size: 1rem;
border-radius: 5px;
code {
font-size: 14px;
line-height: 20px;
white-space: pre;
}
}

View File

@ -1,10 +0,0 @@
import React from 'react';
import './Code.scss';
const Code = ({ children }: React.Props<{}>) => (
<pre>
<code>{children}</code>
</pre>
);
export default Code;

View File

@ -0,0 +1,32 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.CodeBlock {
font-weight: 400;
font-size: 1rem;
color: rgba(0, 0, 0, 0.87);
background-color: $gray-lightest;
border: 1px solid $border-disabled;
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
padding: 0.75rem 1rem;
margin: 0;
margin-bottom: 1rem;
font-size: 1rem;
border-radius: 2px;
overflow: auto;
& > code {
display: block;
text-align: left;
max-height: 320px;
border: none;
background-color: inherit;
font-size: 14px;
white-space: pre;
@include mono;
}
&.wrap {
& > code {
white-space: normal;
}
}
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import './CodeBlock.scss';
interface Props {
children?: React.ReactNode;
className?: string;
}
const CodeBlock = ({ children, className }: Props) => (
<pre className={`${className} CodeBlock`}>
<code>{children}</code>
</pre>
);
export default CodeBlock;

View File

@ -12,6 +12,11 @@
> .TogglablePassword {
width: 100%;
}
& > pre,
& > .Select,
&-input {
margin-bottom: 1rem;
}
&-inline {
display: flex;
flex-direction: row;
@ -45,12 +50,9 @@
color: rgba(0, 0, 0, 0.54);
}
}
&-dropdown {
margin-bottom: 1rem;
}
&-input {
width: 100%;
border: 1px solid #e5ecf3;
border: 1px solid $border-idle;
border-radius: 2px;
height: $input-height-base;
padding: 0.75rem 1rem;

View File

@ -13,10 +13,10 @@ interface Props {
}
export default class ModalBody extends React.Component<Props> {
public firstTabStop: HTMLElement;
private modal: HTMLElement;
private modalContent: HTMLElement;
private focusedElementBeforeModal: HTMLElement;
private firstTabStop: HTMLElement;
private lastTabStop: HTMLElement;
public componentDidMount() {
@ -32,9 +32,6 @@ export default class ModalBody extends React.Component<Props> {
this.firstTabStop = focusableElements[0];
this.lastTabStop = focusableElements[focusableElements.length - 1];
// Focus first child
this.firstTabStop.focus();
this.modal.addEventListener('keydown', this.keyDownListener);
}

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import ModalBody from './ModalBody';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import './index.scss';
export interface IButton {
@ -23,10 +23,8 @@ interface ModalStyle {
maxWidth?: string;
}
const Fade = ({ children, ...props }: any) => (
<CSSTransition {...props} timeout={300} classNames="animate-modal">
{children}
</CSSTransition>
const Fade = ({ ...props }: any) => (
<CSSTransition {...props} timeout={300} classNames="animate-modal" />
);
export default class Modal extends PureComponent<Props, {}> {
@ -57,7 +55,8 @@ export default class Modal extends PureComponent<Props, {}> {
return (
<TransitionGroup>
{isOpen && (
<Fade>
// Trap focus in modal by focusing the first element after the animation is complete
<Fade onEntered={() => this.modalBody.firstTabStop.focus()}>
<div>
<div className="Modal-overlay" onClick={handleClose} />
<ModalBody {...modalBodyProps} ref={div => (this.modalBody = div as ModalBody)} />

View File

@ -1,6 +1,6 @@
export { default as Dropdown } from './Dropdown';
export { default as ColorDropdown } from './ColorDropdown';
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';
@ -16,6 +16,7 @@ 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 { default as CodeBlock } from './CodeBlock';
export * from './ConditionalInput';
export * from './Expandable';
export * from './InlineSpinner';

View File

@ -8,8 +8,8 @@ import {
signTransactionFailed,
TSignTransactionFailed
} from 'actions/transaction';
import { computeIndexingHash } from 'libs/transaction';
import { QRCode, TextArea } from 'components/ui';
import { computeIndexingHash, getTransactionFields, makeTransaction } from 'libs/transaction';
import { QRCode, Input, CodeBlock } from 'components/ui';
import EthTx from 'ethereumjs-tx';
import { SendButton } from 'components/SendButton';
import { toBuffer, bufferToHex } from 'ethereumjs-util';
@ -33,6 +33,9 @@ const INITIAL_STATE: State = { userInput: '' };
type Props = DispatchProps & StateProps & RouteComponentProps<{}>;
const getStringifiedTx = (serializedTx: Buffer) =>
JSON.stringify(getTransactionFields(makeTransaction(serializedTx)), null, 2);
class BroadcastTx extends Component<Props> {
public state: State = INITIAL_STATE;
@ -42,29 +45,41 @@ class BroadcastTx extends Component<Props> {
const currentPath = this.props.match.url;
return (
<TabSection isUnavailableOffline={true}>
<div className="Tab-content-pane row block text-center">
<div className="Tab-content-pane row block">
<Switch>
<Route
exact={true}
path={currentPath}
render={() => (
<div className="BroadcastTx">
<h1 className="BroadcastTx-title">{translate('BROADCAST_TX_TITLE')}</h1>
<p className="BroadcastTx-help">{translate('BROADCAST_TX_DESCRIPTION')}</p>
<h1 className="BroadcastTx-title text-center">
{translate('BROADCAST_TX_TITLE')}
</h1>
<p className="BroadcastTx-help text-center">
{translate('BROADCAST_TX_DESCRIPTION')}
</p>
<div className="input-group-wrapper InteractForm-interface">
<label className="input-group">
<div className="input-group-header">{translate('SEND_SIGNED')}</div>
<TextArea
<Input
type="text"
placeholder="0xf86b0284ee6b2800825208944bbeeb066ed09b7aed07bf39eee0460dfa26152088016345785d8a00008029a03ba7a0cc6d1756cd771f2119cf688b6d4dc9d37096089f0331fe0de0d1cc1254a02f7bcd19854c8d46f8de09e457aec25b127ab4328e1c0d24bfbff8702ee1f474"
className={stateTransaction ? '' : 'invalid'}
rows={7}
value={userInput}
onChange={this.handleChange}
/>
</label>
</div>
<SendButton toggleDisabled={true} onlyTransactionParameters={true} />
{stateTransaction && (
<React.Fragment>
<label>{translate('SEND_RAW')}</label>
<CodeBlock>{getStringifiedTx(stateTransaction)}</CodeBlock>
</React.Fragment>
)}
<SendButton className="form-group" />
<div className="BroadcastTx-qr">
{stateTransaction && <QRCode data={bufferToHex(stateTransaction)} />}
@ -79,7 +94,7 @@ class BroadcastTx extends Component<Props> {
);
}
protected handleChange = ({ currentTarget }: React.FormEvent<HTMLTextAreaElement>) => {
protected handleChange = ({ currentTarget }: React.FormEvent<HTMLInputElement>) => {
const { value } = currentTarget;
this.setState({ userInput: value });
try {

View File

@ -11,4 +11,7 @@
}
}
}
&-submit {
margin-bottom: 1rem;
}
}

View File

@ -2,15 +2,13 @@ import translate from 'translations';
import classnames from 'classnames';
import { DataFieldFactory } from 'components/DataFieldFactory';
import { SendButtonFactory } from 'components/SendButtonFactory';
import { SigningStatus } from 'components/SigningStatus';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import { GenerateTransaction } from 'components/GenerateTransaction';
import React, { Component } from 'react';
import { setToField, TSetToField } from 'actions/transaction';
import { resetWallet, TResetWallet } from 'actions/wallet';
import { connect } from 'react-redux';
import { FullWalletOnly } from 'components/renderCbs';
import { NonceField, TXMetaDataPanel } from 'components';
import { NonceField, TXMetaDataPanel, SigningStatus } from 'components';
import './Deploy.scss';
import { ConfirmationModal } from 'components/ConfirmationModal';
import { TextArea } from 'components/ui';
@ -40,7 +38,7 @@ class DeployClass extends Component<DispatchProps> {
rows={6}
onChange={onChange}
disabled={readOnly}
className={classnames('Deploy-field-input', 'form-control', {
className={classnames('Deploy-field-input', {
'is-valid': value && value.length > 0
})}
value={raw}
@ -66,20 +64,23 @@ class DeployClass extends Component<DispatchProps> {
</div>
</div>
<div className="row form-group">
<div className="col-xs-12 clearfix">
<GenerateTransaction />
</div>
</div>
<SigningStatus />
<SendButtonFactory
signing={true}
Modal={ConfirmationModal}
withProps={({ onClick }) => (
<button className="Deploy-submit btn btn-primary" onClick={onClick}>
withProps={({ disabled, signTx, openModal }) => (
<button
disabled={disabled}
className="Deploy-submit btn btn-primary btn-block"
onClick={() => {
signTx();
openModal();
}}
>
{translate('NAV_DEPLOYCONTRACT')}
</button>
)}
/>
<SigningStatus />
</main>
);

View File

@ -1,6 +1,6 @@
import { AmountField } from './AmountField';
import React, { Component } from 'react';
import { SendButton, SigningStatus, TXMetaDataPanel } from 'components';
import { SendButton, TXMetaDataPanel } from 'components';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import { FullWalletOnly } from 'components/renderCbs';
@ -21,7 +21,6 @@ export class Fields extends Component<OwnProps> {
resetIncludeExcludeProperties={{ exclude: { fields: ['to'] }, include: {} }}
/>
{this.props.button}
<SigningStatus />
<SendButton />
</React.Fragment>
);

View File

@ -12,8 +12,7 @@ import { setDataField, TSetDataField } from 'actions/transaction';
import { Data } from 'libs/units';
import { Web3Node } from 'libs/nodes';
import RpcNode from 'libs/nodes/rpc';
import { Input } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
import { Input, Dropdown } from 'components/ui';
interface StateProps {
nodeLib: RpcNode | Web3Node;
@ -97,7 +96,8 @@ class InteractExplorerClass extends Component<Props, State> {
placeholder={translate('SELECT_A_THING', { $thing: 'function' })}
onChange={this.handleFunctionSelect}
options={contractFunctionsOptions}
clearable={false}
clearable={true}
searchable={true}
labelKey="name"
valueKey="contract"
/>

View File

@ -7,8 +7,7 @@ import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import classnames from 'classnames';
import { NetworkContract } from 'types/network';
import { donationAddressMap } from 'config';
import { Input, TextArea } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
import { Input, TextArea, CodeBlock, Dropdown } from 'components/ui';
interface ContractOption {
name: string;
@ -68,22 +67,39 @@ class InteractForm extends Component<Props, State> {
const validEthAddress = isValidETHAddress(address);
const validAbiJson = isValidAbiJson(abiJson);
const showContractAccessButton = validEthAddress && validAbiJson;
let contractOptions: ContractOption[] = [];
let options: ContractOption[] = [];
if (this.isContractsValid()) {
contractOptions = contracts.map(con => {
const contractOptions = contracts.map(con => {
const addr = con.address ? `(${con.address.substr(0, 10)}...)` : '';
return {
name: `${con.name} ${addr}`,
value: this.makeContractValue(con)
};
});
options = [{ name: 'Custom', value: '' }, ...contractOptions];
}
// TODO: Use common components for address, abi json
return (
<div className="InteractForm">
<div className="InteractForm-address row">
<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={options}
searchable={true}
clearable={true}
labelKey="name"
/>
</label>
</div>
<div className="input-group-wrapper InteractForm-address-field col-sm-6">
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_TITLE')}</div>
@ -99,26 +115,23 @@ class InteractForm extends Component<Props, State> {
/>
</label>
</div>
<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="input-group-wrapper InteractForm-interface">
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_JSON')}</div>
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_JSON')}</div>
{!!contract ? (
contract.name === 'Custom' ? (
<TextArea
placeholder={this.abiJsonPlaceholder}
className={`InteractForm-interface-field-input ${validAbiJson ? '' : 'invalid'}`}
onChange={this.handleInput('abiJson')}
value={abiJson}
rows={6}
/>
) : (
<CodeBlock className="wrap">{abiJson}</CodeBlock>
)
) : (
<TextArea
placeholder={this.abiJsonPlaceholder}
className={`InteractForm-interface-field-input ${validAbiJson ? '' : 'invalid'}`}
@ -126,8 +139,8 @@ class InteractForm extends Component<Props, State> {
value={abiJson}
rows={6}
/>
</label>
</div>
)}
</label>
<button
className="InteractForm-submit btn btn-primary"
@ -150,7 +163,7 @@ class InteractForm extends Component<Props, State> {
private handleSelectContract = (contract: ContractOption) => {
this.props.resetState();
const fullContract = this.props.contracts.find(currContract => {
return this.makeContractValue(currContract) === contract.value;
return contract && this.makeContractValue(currContract) === contract.value;
});
this.setState({

View File

@ -7,45 +7,14 @@ import {
TXMetaDataPanel,
CurrentCustomMessage,
GenerateTransaction,
SendButton,
SigningStatus
SendButton
} from 'components';
import { OnlyUnlocked, WhenQueryExists } from 'components/renderCbs';
import translate from 'translations';
import { AppState } from 'reducers';
import { NonStandardTransaction } from './components';
const content = (
<div className="Tab-content-pane">
<AddressField />
<div className="row form-group">
<div className="col-xs-12">
<AmountField hasUnitDropdown={true} hasSendEverything={true} />
</div>
</div>
<div className="row form-group">
<div className="col-xs-12">
<TXMetaDataPanel />
</div>
</div>
<CurrentCustomMessage />
<NonStandardTransaction />
<div className="row form-group">
<div className="col-xs-12 clearfix">
<GenerateTransaction />
</div>
</div>
<SigningStatus />
<div className="row form-group">
<SendButton />
</div>
</div>
);
import { getOffline } from 'selectors/config';
const QueryWarning: React.SFC<{}> = () => (
<WhenQueryExists
@ -59,17 +28,38 @@ const QueryWarning: React.SFC<{}> = () => (
interface StateProps {
shouldDisplay: boolean;
offline: boolean;
}
class FieldsClass extends Component<StateProps> {
public render() {
const { shouldDisplay } = this.props;
const { shouldDisplay, offline } = this.props;
return (
<OnlyUnlocked
whenUnlocked={
<React.Fragment>
<QueryWarning />
{shouldDisplay ? content : null}
{shouldDisplay && (
<div className="Tab-content-pane">
<AddressField />
<div className="row form-group">
<div className="col-xs-12">
<AmountField hasUnitDropdown={true} hasSendEverything={true} />
</div>
</div>
<div className="row form-group">
<div className="col-xs-12">
<TXMetaDataPanel />
</div>
</div>
<CurrentCustomMessage />
<NonStandardTransaction />
{offline ? <GenerateTransaction /> : <SendButton signing={true} />}
</div>
)}
</React.Fragment>
}
/>
@ -78,5 +68,6 @@ class FieldsClass extends Component<StateProps> {
}
export const Fields = connect((state: AppState) => ({
shouldDisplay: !isAnyOfflineWithWeb3(state)
shouldDisplay: !isAnyOfflineWithWeb3(state),
offline: getOffline(state)
}))(FieldsClass);

View File

@ -9,7 +9,6 @@
&-qr {
position: relative;
background: #fff;
cursor: pointer;
}
&-codeBox {

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, TextArea } from 'components/ui';
import { QRCode, CodeBlock } from 'components/ui';
import { getUnit, getDecimal } from 'selectors/transaction/meta';
import {
getCurrentTo,
@ -132,7 +132,7 @@ class RequestPayment extends React.Component<Props, {}> {
</div>
</div>
<div className="col-xs-6 RequestPayment-codeContainer">
<TextArea className="RequestPayment-codeBox" value={eip681String} disabled={true} />
<CodeBlock className="wrap">{eip681String}</CodeBlock>
</div>
</div>
)}

View File

@ -10,7 +10,7 @@ import { AppState } from 'reducers';
import SignButton from './SignButton';
import { isWalletFullyUnlocked } from 'selectors/wallet';
import './index.scss';
import { TextArea } from 'components/ui';
import { TextArea, CodeBlock } from 'components/ui';
interface Props {
wallet: IFullWallet;
@ -78,12 +78,9 @@ export class SignMessage extends Component<Props, State> {
<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}
/>
<CodeBlock className="SignMessage-inputBox">
{JSON.stringify(signedMessage, null, 2)}
</CodeBlock>
</label>
</div>
)}

View File

@ -317,7 +317,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
<Input
id="origin-swap-input"
className={`input-group-input ${
!origin.amount &&
!!origin.amount &&
this.isMinMaxValid(origin.amount, origin.label, destination.label)
? ''
: 'invalid'
@ -342,7 +342,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
<Input
id="destination-swap-input"
className={`${
!destination.amount &&
!!destination.amount &&
this.isMinMaxValid(origin.amount, origin.label, destination.label)
? ''
: 'invalid'

View File

@ -3,7 +3,7 @@ import { AmountFieldFactory } from 'components/AmountFieldFactory';
import { AddressFieldFactory } from 'components/AddressFieldFactory';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { GenerateTransaction, SendButton, SigningStatus, TXMetaDataPanel } from 'components';
import { SendButton, TXMetaDataPanel } from 'components';
import { resetWallet, TResetWallet } from 'actions/wallet';
import translate from 'translations';
import { getUnit } from 'selectors/transaction';
@ -76,7 +76,6 @@ class FieldsClass extends Component<Props> {
<TXMetaDataPanel initialState={'simple'} disableToggle={true} />
</div>
</div>
<SigningStatus />
<div className="row form-group">
<div className="col-xs-12 clearfix">
{currentBalance === null ? (
@ -84,13 +83,10 @@ class FieldsClass extends Component<Props> {
<Spinner />
</div>
) : (
<GenerateTransaction />
<SendButton signing={true} />
)}
</div>
</div>
<div className="row form-group">
<SendButton />
</div>
</div>
);
}

View File

@ -35,6 +35,7 @@
@import './styles/tab';
@import './styles/flexbox';
@import './styles/helpers';
@import './styles/code';
@import './fonts';
[data-whatintent='mouse'] *:focus {

View File

@ -0,0 +1,9 @@
code {
color: #333;
background-color: #eff5fe;
font-size: 14px;
line-height: inherit;
border-radius: 2px;
padding: 2px 0.25rem;
border: 1px solid $border-idle;
}

View File

@ -25,7 +25,7 @@ input[type='checkbox'] {
}
input[readonly] {
background-color: #fafafa;
background-color: $gray-lightest;
cursor: text !important;
}

View File

@ -62,7 +62,7 @@
border-color: inherit;
}
&-menu {
max-height: 8.625rem;
max-height: 10.0625rem;
}
&.invalid.has-blurred {
border-color: $brand-danger;

View File

@ -9,6 +9,9 @@ $gray-light: #9a9a9a;
$gray-lighter: #ececec;
$gray-lightest: #fafafa;
$border-idle: #e5ecf3;
$border-disabled: #e6e6e6;
$brand-primary: #007896;
$brand-success: #5dba5a;
$brand-info: $ether-navy;

View File

@ -145,10 +145,14 @@
"prebuild": "check-node-version --package",
"build:downloadable": "webpack --config webpack_config/webpack.html.js",
"prebuild:downloadable": "check-node-version --package",
"build:electron": "webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js",
"build:electron:osx": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js",
"build:electron:windows": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js",
"build:electron:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js",
"build:electron":
"webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js",
"build:electron:osx":
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js",
"build:electron:windows":
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js",
"build:electron:linux":
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js",
"prebuild:electron": "check-node-version --package",
"jenkins:build:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=JENKINS_LINUX node webpack_config/buildElectron.js",
"jenkins:build:mac": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=JENKINS_MAC node webpack_config/buildElectron.js",
@ -163,14 +167,18 @@
"predev": "check-node-version --package",
"dev:https": "HTTPS=true node webpack_config/devServer.js",
"predev:https": "check-node-version --package",
"dev:electron": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"dev:electron:https": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"dev:electron":
"concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"dev:electron:https":
"concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"tslint": "tslint --project . --exclude common/vendor/**/*",
"tscheck": "tsc --noEmit",
"start": "npm run dev",
"precommit": "lint-staged",
"formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
"prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
"formatAll":
"find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
"prettier:diff":
"prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
"prepush": "npm run tslint && npm run tscheck"
},
"lint-staged": {