Merge branch 'develop' into hide-button

This commit is contained in:
Daniel Ternyak 2018-03-11 18:10:02 -05:00 committed by GitHub
commit 46ccc15665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1690 additions and 561 deletions

View File

@ -1,36 +1,35 @@
// Mixins
// --------------------------------------------------
// Utilities
@import "mixins/hide-text.less";
@import "mixins/opacity.less";
@import "mixins/image.less";
@import "mixins/labels.less";
@import "mixins/reset-filter.less";
@import "mixins/resize.less";
@import "mixins/responsive-visibility.less";
@import "mixins/size.less";
@import "mixins/tab-focus.less";
@import "mixins/reset-text.less";
@import "mixins/text-emphasis.less";
@import "mixins/text-overflow.less";
@import "mixins/vendor-prefixes.less";
@import 'mixins/hide-text.less';
@import 'mixins/opacity.less';
@import 'mixins/image.less';
@import 'mixins/labels.less';
@import 'mixins/reset-filter.less';
@import 'mixins/resize.less';
@import 'mixins/responsive-visibility.less';
@import 'mixins/size.less';
@import 'mixins/reset-text.less';
@import 'mixins/text-emphasis.less';
@import 'mixins/text-overflow.less';
@import 'mixins/vendor-prefixes.less';
// Components
@import "mixins/alerts.less";
@import "mixins/buttons.less";
@import "mixins/panels.less";
@import "mixins/pagination.less";
@import "mixins/list-group.less";
@import "mixins/nav-divider.less";
@import "mixins/forms.less";
@import "mixins/progress-bar.less";
@import "mixins/table-row.less";
@import 'mixins/alerts.less';
@import 'mixins/buttons.less';
@import 'mixins/panels.less';
@import 'mixins/pagination.less';
@import 'mixins/list-group.less';
@import 'mixins/nav-divider.less';
@import 'mixins/forms.less';
@import 'mixins/progress-bar.less';
@import 'mixins/table-row.less';
// Skins
@import "mixins/background-variant.less";
@import "mixins/border-radius.less";
@import "mixins/gradients.less";
@import 'mixins/background-variant.less';
@import 'mixins/border-radius.less';
@import 'mixins/gradients.less';
// Layout
@import "mixins/clearfix.less";
@import "mixins/center-block.less";
@import "mixins/nav-vertical-align.less";
@import "mixins/grid-framework.less";
@import "mixins/grid.less";
@import 'mixins/clearfix.less';
@import 'mixins/center-block.less';
@import 'mixins/nav-vertical-align.less';
@import 'mixins/grid-framework.less';
@import 'mixins/grid.less';

View File

@ -20,10 +20,10 @@
// Set the border and box shadow on specific inputs to match
.form-control {
border-color: @border-color;
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075)); // Redeclare so transitions work
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, 0.075)); // Redeclare so transitions work
&:focus {
border-color: darken(@border-color, 10%);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 3px rgba(@brand-primary, .5);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 3px rgba(@brand-primary, 0.5);
}
}
// Set validation states also for addons
@ -51,11 +51,10 @@
// Example usage: change the default blue border and shadow to white for better
// contrast against a dark gray background.
.form-control-focus(@color: @input-border-focus) {
@color-rgba: rgba(red(@color), green(@color), blue(@color), .6);
@color-rgba: rgba(red(@color), green(@color), blue(@color), 0.6);
&:focus {
border-color: @color;
outline: 0;
.box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}");
.box-shadow(~'inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}');
}
}

View File

@ -1,6 +0,0 @@
// WebKit-style focus
.tab-focus() {
outline: thin dotted;
outline-offset: 3px;
}

View File

@ -7,10 +7,6 @@
position: absolute;
left: 5px;
top: 5px;
&:hover,
&:active {
outline: 0;
}
}
}

View File

@ -18,7 +18,7 @@ export const AmountField: React.SFC<Props> = ({
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown">
<label className="input-group input-group-inline">
<div className="input-group-header">{translate('SEND_amount')}</div>
<Input
className={`input-group-input ${

View File

@ -17,7 +17,7 @@ export const Coinbase: React.SFC<Props> = ({ address }) => (
<h5 key="2">Buy ETH with USD</h5>
</div>
<div className="Promos-promo-images">
<img src={CoinbaseLogo} />
<img src={CoinbaseLogo} alt="Coinbase logo" />
</div>
</div>
</NewTabLink>

View File

@ -11,8 +11,8 @@ export const HardwareWallets: React.SFC = () => (
<h6>Learn more about protecting your funds.</h6>
</div>
<div className="Promos-promo-images">
<img src={ledgerLogo} />
<img src={trezorLogo} />
<img src={ledgerLogo} alt="Ledger Logo" />
<img src={trezorLogo} alt="Trezor Logo" />
</div>
</div>
</HelpLink>

View File

@ -13,7 +13,7 @@ export const Shapeshift: React.SFC = () => (
</h5>
</div>
<div className="Promos-promo-images">
<img src={ShapeshiftLogo} />
<img src={ShapeshiftLogo} alt="Shapeshift Logo" />
</div>
</div>
</Link>

View File

@ -85,7 +85,6 @@
height: 12px;
border: 3px solid $gray-lightest;
border-radius: 100%;
outline: none;
opacity: 0.6;
&.is-active {
@ -96,7 +95,7 @@
// Per-promo customizations
&--shapeshift {
background-color: #263A52;
background-color: #263a52;
.Promos-promo-images {
max-width: 130px;

View File

@ -1,4 +1,4 @@
@import "common/sass/variables";
@import 'common/sass/variables';
.AddCustom {
&-field {
@ -11,23 +11,20 @@
&-buttons {
padding-top: 10px;
display: flex;
justify-content: center;
flex-wrap: wrap;
&-help {
text-align: center;
display: block;
font-size: 13px;
margin-bottom: 10px;
}
&-btn {
margin-right: 10px;
&.btn-primary {
width: 120px;
}
&.btn-default {
width: 110px;
}
padding: 0.5rem 1.5rem;
margin: 0.25rem 0.5rem;
}
}
}

View File

@ -66,11 +66,11 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
{fields.map(field => {
return (
<label className="AddCustom-field form-group" key={field.name}>
<span className="AddCustom-field-label">{field.label}</span>
<div className="input-group-header">{field.label}</div>
<Input
className={`${
errors[field.name] ? 'invalid' : field.value ? 'valid' : ''
} AddCustom-field-input input-sm`}
} input-group-input-small`}
type="text"
name={field.name}
value={field.value}
@ -83,22 +83,22 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
);
})}
<div className="AddCustom-buttons">
<HelpLink article={HELP_ARTICLE.ADDING_NEW_TOKENS} className="AddCustom-buttons-help">
{translate('Need help? Learn how to add custom tokens.')}
</HelpLink>
<button
className="AddCustom-buttons-btn btn btn-primary btn-sm"
disabled={!this.isValid()}
>
{translate('x_Save')}
</button>
<div className="AddCustom-buttons">
<button
className="AddCustom-buttons-btn btn btn-sm btn-default"
onClick={this.props.toggleForm}
>
{translate('x_Cancel')}
</button>
<button
className="AddCustom-buttons-btn btn btn-primary btn-sm"
disabled={!this.isValid()}
>
{translate('x_Save')}
</button>
</div>
</form>
);

View File

@ -54,6 +54,7 @@ export default class TokenRow extends React.PureComponent<Props, State> {
{!!custom && (
<img
src={removeIcon}
alt="Remove"
className="TokenRow-symbol-remove"
title="Remove Token"
onClick={this.onRemove}

View File

@ -1,4 +1,4 @@
@import "common/sass/variables";
@import 'common/sass/variables';
.TokenBalances {
&-title {
@ -34,6 +34,12 @@
}
&-buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
& > &-btn {
margin: 0.25rem 0.5rem;
}
&-help {
padding-top: 10px;
text-align: center;

View File

@ -1,5 +1,5 @@
@import "common/sass/variables";
@import "common/sass/mixins";
@import 'common/sass/variables';
@import 'common/sass/mixins';
.BetaAgreement {
@include cover-message;
@ -20,7 +20,6 @@
margin: 0 auto;
border: none;
padding: 0;
outline: none;
transition: $transition;
&.is-continue {

View File

@ -55,13 +55,13 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
return (
<Modal
title={translate('Generate Keystore File')}
title={translateRaw('Generate Keystore File')}
isOpen={this.props.isOpen}
handleClose={this.handleClose}
>
<form className="GenKeystore" onSubmit={this.handleSubmit}>
<div className="input-group-wrapper GenKeystore-field">
<label className="input-group input-group-inline-dropdown">
<label className="input-group input-group-inline">
<div className="input-group-header">Private Key</div>
<TogglablePassword
name="privateKey"
@ -74,7 +74,7 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
</label>
</div>
<div className="input-group-wrapper GenKeystore-field">
<label className="input-group input-group-inline-dropdown">
<label className="input-group input-group-inline">
<div className="input-group-header">Password</div>
<TogglablePassword
name="password"

View File

@ -0,0 +1,15 @@
.CustomNodeModal {
.flex-wrapper {
margin: 0px -8px;
> .input-group {
margin: 0px 8px;
> .input-group-input {
width: 100%;
}
}
}
input[type='checkbox'] {
margin-right: 1rem;
margin-bottom: 1rem;
}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations';
import translate, { translateRaw } from 'translations';
import { CustomNetworkConfig } from 'types/network';
import { CustomNodeConfig } from 'types/node';
import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config';
@ -13,19 +13,13 @@ import {
} from 'selectors/config';
import { CustomNode } from 'libs/nodes';
import { Input } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
import './CustomNodeModal.scss';
const CUSTOM = 'custom';
interface InputProps {
name: keyof Omit<State, 'hasAuth'>;
placeholder?: string;
type?: string;
autoComplete?: 'off';
onFocus?(): void;
onBlur?(): void;
}
const CUSTOM = { label: 'Custom', value: 'custom' };
interface OwnProps {
isOpen: boolean;
addCustomNode(payload: AddCustomNodeAction['payload']): void;
handleClose(): void;
}
@ -55,7 +49,7 @@ interface State {
type Props = OwnProps & StateProps & DispatchProps;
class CustomNodeModal extends React.Component<Props, State> {
public state: State = {
public INITIAL_STATE = {
name: '',
url: '',
network: Object.keys(this.props.staticNetworks)[0],
@ -66,9 +60,17 @@ class CustomNodeModal extends React.Component<Props, State> {
username: '',
password: ''
};
public state: State = this.INITIAL_STATE;
public componentDidUpdate(prevProps: Props) {
// Reset state when modal opens
if (!prevProps.isOpen && prevProps.isOpen !== this.props.isOpen) {
this.setState(this.INITIAL_STATE);
}
}
public render() {
const { customNetworks, handleClose, staticNetworks } = this.props;
const { customNetworks, handleClose, staticNetworks, isOpen } = this.props;
const { network } = this.state;
const isHttps = window.location.protocol.includes('https');
const invalids = this.getInvalids();
@ -88,16 +90,21 @@ class CustomNodeModal extends React.Component<Props, State> {
];
const conflictedNode = this.getConflictedNode();
const staticNetwrks = Object.keys(staticNetworks).map(net => {
return { label: net, value: net };
});
const customNetwrks = Object.entries(customNetworks).map(([id, net]) => {
return { label: net.name + ' (Custom)', value: id };
});
const options = [...staticNetwrks, ...customNetwrks, CUSTOM];
return (
<Modal
title={translate('NODE_Title')}
isOpen={true}
title={translateRaw('NODE_Title')}
isOpen={isOpen}
buttons={buttons}
handleClose={handleClose}
maxWidth={580}
>
<div>
{isHttps && <div className="alert alert-warning small">{translate('NODE_Warning')}</div>}
{conflictedNode && (
@ -107,139 +114,128 @@ class CustomNodeModal extends React.Component<Props, State> {
</div>
)}
<form>
<div className="row">
<div className="col-sm-7">
<label>{translate('NODE_Name')}</label>
{this.renderInput(
{
name: 'name',
placeholder: 'My Node'
},
invalids
)}
</div>
<div className="col-sm-5">
<label>Network</label>
<select
className="form-control"
name="network"
<form className="CustomNodeModal">
<div className="flex-wrapper">
<label className="col-sm-9 input-group flex-grow-1">
<div className="input-group-header">Node Name</div>
<Input
className={`input-group-input ${this.state.name && invalids.name ? 'invalid' : ''}`}
type="text"
placeholder="My Node"
value={this.state.name}
onChange={e => this.setState({ name: e.currentTarget.value })}
/>
</label>
<label className="col-sm-3 input-group">
<div className="input-group-header">Network</div>
<Dropdown
className="input-group-dropdown"
value={network}
onChange={this.handleChange}
>
{Object.keys(staticNetworks).map(net => (
<option key={net} value={net}>
{net}
</option>
))}
{Object.entries(customNetworks).map(([id, net]) => (
<option key={id} value={id}>
{net.name} (Custom)
</option>
))}
<option value={CUSTOM}>Custom...</option>
</select>
</div>
options={options}
clearable={false}
onChange={(e: { label: string; value: string }) =>
this.setState({ network: e.value })
}
/>
</label>
</div>
{network === CUSTOM && (
<div className="row">
<div className="col-sm-6">
<label className="is-required">Network Name</label>
{this.renderInput(
{
name: 'customNetworkId',
placeholder: 'My Custom Network'
},
invalids
)}
</div>
<div className="col-sm-3">
<label className="is-required">Currency</label>
{this.renderInput(
{
name: 'customNetworkUnit',
placeholder: 'ETH'
},
invalids
)}
</div>
<div className="col-sm-3">
<label>Chain ID</label>
{this.renderInput(
{
name: 'customNetworkChainId',
placeholder: 'e.g. 1'
},
invalids
)}
</div>
{network === CUSTOM.value && (
<div className="flex-wrapper">
<label className="col-sm-6 input-group input-group-inline">
<div className="input-group-header">Network Name</div>
<Input
className={`input-group-input ${
this.state.customNetworkId && invalids.customNetworkId ? 'invalid' : ''
}`}
type="text"
placeholder="My Custom Network"
value={this.state.customNetworkId}
onChange={e => this.setState({ customNetworkId: e.currentTarget.value })}
/>
</label>
<label className="col-sm-3 input-group input-group-inline">
<div className="input-group-header">Currency</div>
<Input
className={`input-group-input ${
this.state.customNetworkUnit && invalids.customNetworkUnit ? 'invalid' : ''
}`}
type="text"
placeholder="ETH"
value={this.state.customNetworkUnit}
onChange={e => this.setState({ customNetworkUnit: e.currentTarget.value })}
/>
</label>
<label className="col-sm-3 input-group input-group-inline">
<div className="input-group-header">Chain ID</div>
<Input
className={`input-group-input ${
this.state.customNetworkChainId && invalids.customNetworkChainId
? 'invalid'
: ''
}`}
type="text"
placeholder="1"
value={this.state.customNetworkChainId}
onChange={e => this.setState({ customNetworkChainId: e.currentTarget.value })}
/>
</label>
</div>
)}
<div className="row">
<div className="col-sm-12">
<label>URL</label>
{this.renderInput(
{
name: 'url',
placeholder: 'e.g. https://127.0.0.1:8545/',
autoComplete: 'off'
},
invalids
)}
</div>
</div>
<label className="input-group input-group-inline">
<div className="input-group-header">URL</div>
<Input
className={`input-group-input ${this.state.url && invalids.url ? 'invalid' : ''}`}
type="text"
placeholder="https://127.0.0.1:8545/"
value={this.state.url}
onChange={e => this.setState({ url: e.currentTarget.value })}
autoComplete="off"
/>
</label>
<div className="row">
<div className="col-sm-12">
<label>
<input
type="checkbox"
name="hasAuth"
checked={this.state.hasAuth}
onChange={this.handleCheckbox}
/>{' '}
onChange={() => this.setState({ hasAuth: !this.state.hasAuth })}
/>
<span>HTTP Basic Authentication</span>
</label>
</div>
</div>
{this.state.hasAuth && (
<div className="row">
<div className="col-sm-6">
<label className="is-required">Username</label>
{this.renderInput({ name: 'username' }, invalids)}
</div>
<div className="col-sm-6">
<label className="is-required">Password</label>
{this.renderInput(
{
name: 'password',
type: 'password'
},
invalids
)}
</div>
<div className="flex-wrapper ">
<label className="col-sm-6 input-group input-group-inline">
<div className="input-group-header">Username</div>
<Input
className={`input-group-input ${
this.state.username && invalids.username ? 'invalid' : ''
}`}
type="text"
value={this.state.username}
onChange={e => this.setState({ username: e.currentTarget.value })}
/>
</label>
<label className="col-sm-6 input-group input-group-inline">
<div className="input-group-header">Password</div>
<Input
className={`input-group-input ${
this.state.password && invalids.password ? 'invalid' : ''
}`}
type="password"
value={this.state.password}
onChange={e => this.setState({ password: e.currentTarget.value })}
/>
</label>
</div>
)}
</form>
</div>
</Modal>
);
}
private renderInput(input: InputProps, invalids: { [key: string]: boolean }) {
return (
<Input
className={`${this.state[input.name] && invalids[input.name] ? 'invalid' : ''}`}
value={this.state[input.name]}
onChange={this.handleChange}
autoComplete="off"
{...input}
/>
);
}
private getInvalids(): { [key: string]: boolean } {
const {
url,
@ -278,7 +274,7 @@ class CustomNodeModal extends React.Component<Props, State> {
}
// If they have a custom network, make sure info is provided
if (network === CUSTOM) {
if (network === CUSTOM.value) {
if (!customNetworkId) {
invalids.customNetworkId = true;
}
@ -315,7 +311,7 @@ class CustomNodeModal extends React.Component<Props, State> {
const { network, url, name, username, password } = this.state;
const networkId =
network === CUSTOM
network === CUSTOM.value
? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
: network;
@ -348,20 +344,10 @@ class CustomNodeModal extends React.Component<Props, State> {
return customNodes[config.id];
}
private handleChange = (ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = ev.currentTarget;
this.setState({ [name as any]: value });
};
private handleCheckbox = (ev: React.FormEvent<HTMLInputElement>) => {
const { name } = ev.currentTarget;
this.setState({ [name as any]: !this.state[name as keyof State] });
};
private saveAndAdd = () => {
const node = this.makeCustomNodeConfigFromState();
if (this.state.network === CUSTOM) {
if (this.state.network === CUSTOM.value) {
const network = this.makeCustomNetworkConfigFromState();
this.props.addCustomNetwork({ config: network, id: node.network });

View File

@ -192,12 +192,11 @@ class Header extends Component<Props, State> {
<Navigation color={!network.isCustom && network.color} />
{isAddingCustomNode && (
<CustomNodeModal
isOpen={isAddingCustomNode}
addCustomNode={this.addCustomNode}
handleClose={this.closeCustomNodeModal}
/>
)}
</div>
);
}

View File

@ -100,8 +100,8 @@ export default class PaperWallet extends React.Component<Props, {}> {
return (
<div style={styles.container}>
<img src={sidebarImg} style={styles.sidebar} />
<img src={ethLogo} style={styles.ethLogo} />
<img src={sidebarImg} style={styles.sidebar} alt="MyCrypto Logo" />
<img src={ethLogo} style={styles.ethLogo} alt="ETH Logo" />
<div style={styles.block}>
<div style={styles.box}>
@ -111,7 +111,7 @@ export default class PaperWallet extends React.Component<Props, {}> {
</div>
<div style={styles.block}>
<img src={notesBg} style={styles.box} />
<img src={notesBg} style={styles.box} aria-hidden={true} />
<p style={styles.blockText}>AMOUNT / NOTES</p>
</div>

View File

@ -18,6 +18,7 @@ interface Props {
isValid?: boolean;
isVisible?: boolean;
validity?: 'valid' | 'invalid' | 'semivalid';
readOnly?: boolean;
// Textarea-only props
isTextareaWhenVisible?: boolean;
@ -61,18 +62,16 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
onChange,
onFocus,
onBlur,
handleToggleVisibility
handleToggleVisibility,
readOnly
} = this.props;
const { isVisible } = this.state;
const validClass = validity
? `is-${validity}`
: isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
return (
<div className={`TogglablePassword input-group input-group-inline-dropdown ${className}`}>
<div className={`TogglablePassword input-group input-group-inline ${className}`}>
{isTextareaWhenVisible && isVisible ? (
<TextArea
className={validClass}
className={validity || !isValid ? 'invalid' : ''}
value={value}
name={name}
disabled={disabled}
@ -83,6 +82,7 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
placeholder={placeholder}
rows={this.props.rows || 3}
aria-label={ariaLabel}
readOnly={readOnly}
/>
) : (
<Input
@ -90,12 +90,13 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
name={name}
disabled={disabled}
type={isVisible ? 'text' : 'password'}
className={`${validClass}`}
className={`${validity || !isValid ? 'invalid' : ''} border-rad-right-0`}
placeholder={placeholder}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
aria-label={ariaLabel}
readOnly={readOnly}
/>
)}
<span

View File

@ -40,7 +40,6 @@ $speed: 500ms;
margin: 0;
}
&:last-child {
margin: 0;
}
@ -80,7 +79,6 @@ $speed: 500ms;
}
&:active {
outline: none;
opacity: 1;
}
@ -106,7 +104,7 @@ $speed: 500ms;
.DecryptContent {
&-enter {
opacity: 0;
transition: opacity $speed * .25 ease $speed * .125;
transition: opacity $speed * 0.25 ease $speed * 0.125;
&-active {
opacity: 1;
@ -119,7 +117,7 @@ $speed: 500ms;
left: 0;
width: 100%;
opacity: 1;
transition: opacity $speed * .25 ease;
transition: opacity $speed * 0.25 ease;
pointer-events: none;
&-active {

View File

@ -29,7 +29,6 @@
transition: transform 150ms ease, box-shadow 150ms ease;
animation: wallet-button-enter 400ms ease 1;
animation-fill-mode: backwards;
outline: none;
@for $i from 0 to 5 {
&:nth-child(#{$i}) {
@ -60,7 +59,6 @@
}
&.is-disabled {
outline: none;
cursor: not-allowed;
@include show-tooltip-on-hover;

View File

@ -28,6 +28,7 @@ interface Icon {
icon: string;
tooltip: string;
href?: string;
arialabel: string;
}
type Props = OwnProps & StateProps;
@ -50,18 +51,21 @@ export class WalletButton extends React.PureComponent<Props> {
if (isReadOnly) {
icons.push({
icon: 'eye',
tooltip: translateRaw('You cannot send using address only')
tooltip: translateRaw('You cannot send using address only'),
arialabel: 'Read Only'
});
} else {
if (isSecure) {
icons.push({
icon: 'shield',
tooltip: translateRaw('This wallet type is secure')
tooltip: translateRaw('This wallet type is secure'),
arialabel: 'Secure wallet type'
});
} else {
icons.push({
icon: 'exclamation-triangle',
tooltip: translateRaw('This wallet type is insecure')
tooltip: translateRaw('This wallet type is insecure'),
arialabel: 'Insecure wallet type'
});
}
}
@ -69,7 +73,8 @@ export class WalletButton extends React.PureComponent<Props> {
icons.push({
icon: 'question-circle',
tooltip: translateRaw('NAV_Help'),
href: helpLink
href: helpLink,
arialabel: 'More info'
});
}
@ -86,22 +91,30 @@ export class WalletButton extends React.PureComponent<Props> {
>
<div className="WalletButton-inner">
<div className="WalletButton-title">
{icon && <img className="WalletButton-title-icon" src={icon} />}
{icon && <img className="WalletButton-title-icon" src={icon} alt={name + ' logo'} />}
<span>{name}</span>
</div>
{description && <div className="WalletButton-description">{description}</div>}
{example && <div className="WalletButton-example">{example}</div>}
{description && (
<div className="WalletButton-description" aria-label="description">
{description}
</div>
)}
{example && (
<div className="WalletButton-example" aria-label="example" aria-hidden={true}>
{example}
</div>
)}
<div className="WalletButton-icons">
{icons.map(i => (
<span className="WalletButton-icons-icon" key={i.icon} onClick={this.stopPropogation}>
{i.href ? (
<NewTabLink href={i.href} onClick={this.stopPropogation}>
<NewTabLink href={i.href} onClick={this.stopPropogation} aria-label={i.arialabel}>
<i className={`fa fa-${i.icon}`} />
</NewTabLink>
) : (
<i className={`fa fa-${i.icon}`} />
<i className={`fa fa-${i.icon}`} aria-label={i.arialabel} />
)}
{!isDisabled && <Tooltip size="sm">{i.tooltip}</Tooltip>}
</span>

View File

@ -92,6 +92,7 @@ export default class ColorDropdown<T> extends PureComponent<Props<T>, {}> {
className="ColorDropdown-item-remove"
onClick={this.onRemove.bind(null, option.onRemove)}
src={removeIcon}
alt="remove"
/>
)}
</a>

View File

@ -12,7 +12,7 @@ interface Props {
const Help = ({ size = 'x1', link }: Props) => {
return (
<a href={link} className={`Help Help-${size}`} target="_blank" rel="noopener noreferrer">
<img src={icon} />
<img src={icon} alt="help" />
</a>
);
};

View File

@ -24,6 +24,7 @@ export default function Identicon(props: Props) {
<React.Fragment>
<img
src={identiconDataUrl}
alt="Unique Address Image"
style={{
height: '100%',
width: '100%',

View File

@ -12,6 +12,22 @@
> .TogglablePassword {
width: 100%;
}
&-inline {
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;
}
}
&-header {
display: flex;
font-size: 1rem;
@ -29,6 +45,9 @@
color: rgba(0, 0, 0, 0.54);
}
}
&-dropdown {
margin-bottom: 1rem;
}
&-input {
width: 100%;
border: 1px solid #e5ecf3;
@ -40,6 +59,17 @@
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
transition: border-color 120ms, box-shadow 120ms;
margin-bottom: 1rem;
&.border-rad-right-0 {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&.border-rad-left-0 {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&-small {
padding: 0.5rem 0.75rem;
}
&::placeholder {
color: rgba(0, 0, 0, 0.3);
}
@ -60,23 +90,6 @@
}
}
.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

@ -1,149 +0,0 @@
import closeIcon from 'assets/images/close.svg';
import React, { PureComponent } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import './Modal.scss';
export interface IButton {
text: string | React.ReactElement<string>;
type?: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'link';
disabled?: boolean;
onClick?(): void;
}
interface Props {
isOpen?: boolean;
title?: string | React.ReactElement<any>;
disableButtons?: boolean;
children: any;
buttons?: IButton[];
maxWidth?: number;
handleClose?(): void;
}
interface ModalStyle {
width?: string;
maxWidth?: string;
}
const Fade = ({ children, ...props }: any) => (
<CSSTransition {...props} timeout={300} classNames="animate-modal">
{children}
</CSSTransition>
);
export default class Modal extends PureComponent<Props, {}> {
private modalContent: HTMLElement | null = null;
public componentDidMount() {
this.updateBodyClass();
document.addEventListener('keydown', this.escapeListner);
}
public componentDidUpdate() {
this.updateBodyClass();
}
public updateBodyClass() {
document.body.classList.toggle('no-scroll', !!this.props.isOpen);
}
public componentWillUnmount() {
document.removeEventListener('keydown', this.escapeListner);
document.body.classList.remove('no-scroll');
}
public render() {
const { isOpen, title, children, buttons, handleClose, maxWidth } = this.props;
const hasButtons = buttons && buttons.length;
const modalStyle: ModalStyle = {};
if (maxWidth) {
modalStyle.width = '100%';
modalStyle.maxWidth = `${maxWidth}px`;
}
return (
<TransitionGroup>
{isOpen && (
<Fade>
<div>
<div className="Modalshade" />
<div className="Modal" style={modalStyle}>
{title && (
<div className="Modal-header flex-wrapper">
<h2 className="Modal-header-title">{title}</h2>
<div className="flex-spacer" />
<button className="Modal-header-close" onClick={handleClose}>
<img className="Modal-header-close-icon" src={closeIcon} />
</button>
</div>
)}
<div className="Modal-content" ref={el => (this.modalContent = el)}>
{isOpen && children}
<div className="Modal-fade" />
</div>
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
</div>
</div>
</Fade>
)}
</TransitionGroup>
);
}
public scrollContentToTop = () => {
if (this.modalContent) {
this.modalContent.scrollTop = 0;
}
};
private escapeListner = (ev: KeyboardEvent) => {
if (!this.props.isOpen) {
return;
}
// Don't trigger if they hit escape while on an input
if (ev.target) {
if (
(ev.target as HTMLElement).tagName === 'INPUT' ||
(ev.target as HTMLElement).tagName === 'SELECT' ||
(ev.target as HTMLElement).tagName === 'TEXTAREA' ||
(ev.target as HTMLElement).isContentEditable
) {
return;
}
}
if (ev.key === 'Escape' || ev.keyCode === 27) {
if (!this.props.handleClose) {
return;
}
this.props.handleClose();
}
};
private renderButtons = () => {
const { disableButtons, buttons } = this.props;
if (!buttons || !buttons.length) {
return;
}
return buttons.map((btn, idx) => {
let btnClass = 'Modal-footer-btn btn';
if (btn.type) {
btnClass += ` btn-${btn.type}`;
}
return (
<button
className={btnClass}
onClick={btn.onClick}
key={idx}
disabled={disableButtons || btn.disabled}
>
{btn.text}
</button>
);
});
};
}

View File

@ -0,0 +1,131 @@
import React, { CSSProperties } from 'react';
import closeIcon from 'assets/images/close.svg';
import { IButton } from 'components/ui/Modal';
interface Props {
title?: string;
children: any;
modalStyle?: CSSProperties;
hasButtons?: number;
buttons?: IButton[];
disableButtons?: any;
handleClose(): void;
}
export default class ModalBody extends React.Component<Props> {
private modal: HTMLElement;
private modalContent: HTMLElement;
private focusedElementBeforeModal: HTMLElement;
private firstTabStop: HTMLElement;
private lastTabStop: HTMLElement;
public componentDidMount() {
this.focusedElementBeforeModal = document.activeElement as HTMLElement;
// Find all focusable children
const focusableElementsString =
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
const focusableElements = Array.prototype.slice.call(
this.modal.querySelectorAll(focusableElementsString)
);
// Convert NodeList to Array
this.firstTabStop = focusableElements[0];
this.lastTabStop = focusableElements[focusableElements.length - 1];
// Focus first child
this.firstTabStop.focus();
this.modal.addEventListener('keydown', this.keyDownListener);
}
public componentWillUnmount() {
document.removeEventListener('keydown', this.keyDownListener);
}
public scrollContentToTop = () => {
this.modalContent.scrollTop = 0;
};
public render() {
const { title, children, modalStyle, hasButtons, handleClose } = this.props;
return (
<div
className="Modal"
style={modalStyle}
role="dialog"
aria-labelledby="Modal-header-title"
ref={div => {
this.modal = div as HTMLElement;
}}
>
{title && (
<div className="Modal-header flex-wrapper">
<h2 className="Modal-header-title">{title}</h2>
<div className="flex-spacer" />
<button className="Modal-header-close" aria-label="Close" onClick={handleClose}>
<img className="Modal-header-close-icon" src={closeIcon} alt="Close" />
</button>
</div>
)}
<div className="Modal-content" ref={div => (this.modalContent = div as HTMLElement)}>
{children}
<div className="Modal-fade" />
</div>
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
</div>
);
}
private renderButtons = () => {
const { disableButtons, buttons } = this.props;
if (!buttons || !buttons.length) {
return;
}
return buttons.map((btn, idx: number) => {
let btnClass = 'Modal-footer-btn btn';
if (btn.type) {
btnClass += ` btn-${btn.type}`;
}
return (
<button
className={btnClass}
onClick={btn.onClick}
key={idx}
disabled={disableButtons || btn.disabled}
>
{btn.text}
</button>
);
});
};
private keyDownListener = (e: KeyboardEvent) => {
// Check for TAB key press
if (e.keyCode === 9) {
// SHIFT + TAB
if (e.shiftKey) {
if (document.activeElement === this.firstTabStop) {
e.preventDefault();
this.lastTabStop.focus();
}
// TAB
} else {
if (document.activeElement === this.lastTabStop) {
e.preventDefault();
this.firstTabStop.focus();
}
}
}
// Check for ESC key press
if (e.keyCode === 27) {
this.focusedElementBeforeModal.focus();
this.props.handleClose();
}
};
}

View File

@ -13,17 +13,6 @@ $m-footer-padding: 0.5rem 2rem 1rem 2rem;
$m-close-size: 26px;
$m-anim-speed: 400ms;
.Modalshade {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(#000, 0.54);
z-index: $zindex-modal-background;
display: block;
}
.Modal {
position: fixed;
top: $m-window-padding-h;
@ -45,6 +34,17 @@ $m-anim-speed: 400ms;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14),
0px 6px 30px 5px rgba(0, 0, 0, 0.12);
&-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(#000, 0.54);
z-index: $zindex-modal-background;
display: block;
}
&-fade {
background: linear-gradient(to bottom, #fff0, #fff);
position: fixed;
@ -112,7 +112,7 @@ $m-anim-speed: 400ms;
}
// Mobile styles
@media(max-width: $screen-sm) {
@media (max-width: $screen-sm) {
top: $m-window-padding-h-mobile;
width: calc(100% - #{$m-window-padding-w-mobile}) !important;
max-width: calc(100% - #{$m-window-padding-w-mobile * 2});

View File

@ -0,0 +1,70 @@
import React, { PureComponent } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import ModalBody from './ModalBody';
import './index.scss';
export interface IButton {
text: string | React.ReactElement<string>;
type?: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'link';
disabled?: boolean;
onClick?(): void;
}
interface Props {
isOpen?: boolean;
title?: string;
disableButtons?: boolean;
children: any;
buttons?: IButton[];
maxWidth?: number;
handleClose(): void;
}
interface ModalStyle {
width?: string;
maxWidth?: string;
}
const Fade = ({ children, ...props }: any) => (
<CSSTransition {...props} timeout={300} classNames="animate-modal">
{children}
</CSSTransition>
);
export default class Modal extends PureComponent<Props, {}> {
public modalBody: ModalBody;
public componentDidUpdate(prevProps: Props) {
if (prevProps.isOpen !== this.props.isOpen) {
document.body.classList.toggle('no-scroll', !!this.props.isOpen);
}
}
public componentWillUnmount() {
document.body.classList.remove('no-scroll');
}
public render() {
const { isOpen, title, children, buttons, handleClose, maxWidth } = this.props;
const hasButtons = buttons && buttons.length;
const modalStyle: ModalStyle = {};
if (maxWidth) {
modalStyle.width = '100%';
modalStyle.maxWidth = `${maxWidth}px`;
}
const modalBodyProps = { title, children, modalStyle, hasButtons, buttons, handleClose };
return (
<TransitionGroup>
{isOpen && (
<Fade>
<div>
<div className="Modal-overlay" onClick={handleClose} />
<ModalBody {...modalBodyProps} ref={div => (this.modalBody = div as ModalBody)} />
</div>
</Fade>
)}
</TransitionGroup>
);
}
}

View File

@ -26,7 +26,7 @@ const OfflineSymbol = ({ offline, size }: OfflineSymbolProps) => {
break;
}
return <img src={offline ? wifiOff : wifiOn} width={width} height={height} />;
return <img src={offline ? wifiOff : wifiOn} alt="wifi status" width={width} height={height} />;
};
export default OfflineSymbol;

View File

@ -35,6 +35,7 @@ export default class QRCode extends React.PureComponent<Props, State> {
return (
<img
src={qr}
alt="QR Code"
style={{
width: '100%',
height: '100%'

View File

@ -11,7 +11,7 @@ interface SpinnerProps {
const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => {
const color = light ? 'Spinner-light' : 'Spinner-dark';
return (
<svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50">
<svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50" aria-busy="true">
<circle className="path" cx="25" cy="25" r="20" fill="none" strokeWidth="5" />
</svg>
);

View File

@ -8,9 +8,6 @@
padding: 0.4rem 1rem;
border-radius: 2px;
height: 2.5rem;
&:focus {
outline: none;
}
&:active,
&:hover {
opacity: 0.8;

View File

@ -18,7 +18,7 @@ interface Props<T> {
const ValueComp: React.SFC = (props: any) => {
return (
<div className={`${props.className} swap-option-wrapper`}>
<img src={props.value.img} className="swap-option-img" />
<img src={props.value.img} className="swap-option-img" alt={props.value.label + ' logo'} />
<span className="swap-option-label">{props.value.label}</span>
</div>
);
@ -46,7 +46,7 @@ const OptionComp: React.SFC = (props: any) => {
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
>
<img src={props.option.img} className="swap-option-img" />
<img src={props.option.img} className="swap-option-img" alt={props.option.label + ' logo'} />
<span className="swap-option-label">{props.option.label}</span>
</div>
);

View File

@ -18,8 +18,9 @@ export const ANNOUNCEMENT_TYPE = '';
export const ANNOUNCEMENT_MESSAGE = (
<React.Fragment>
This is a Beta version of MyCrypto. Please submit any bug reports to our{' '}
<NewTabLink href="https://github.com/MyCryptoHQ/MyCrypto/issues">GitHub</NewTabLink>, and join
the discussion on <NewTabLink href={discordURL}>Discord</NewTabLink>.
<NewTabLink href="https://github.com/MyCryptoHQ/MyCrypto/issues">GitHub</NewTabLink> and use{' '}
<NewTabLink href="https://hackerone.com/mycrypto">HackerOne</NewTabLink> for critical
vulnerabilities. Join the discussion on <NewTabLink href={discordURL}>Discord</NewTabLink>.
</React.Fragment>
);

View File

@ -120,7 +120,11 @@ class OnboardModal extends React.Component<Props, State> {
return (
<div className="OnboardModal">
<Modal isOpen={isOpen} buttons={buttons} ref={el => (this.modal = el)}>
<Modal
isOpen={isOpen}
buttons={buttons}
handleClose={() => (slideNumber === NUMBER_OF_SLIDES ? this.closeModal : null)}
>
<div className="OnboardModal-stepper">
<Stepper
steps={steps}
@ -171,7 +175,7 @@ class OnboardModal extends React.Component<Props, State> {
localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(prevSlideNum));
this.props.decrementSlide();
if (this.modal) {
this.modal.scrollContentToTop();
this.modal.modalBody.scrollContentToTop();
}
};
@ -180,7 +184,7 @@ class OnboardModal extends React.Component<Props, State> {
localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(nextSlideNum));
this.props.incrementSlide();
if (this.modal) {
this.modal.scrollContentToTop();
this.modal.modalBody.scrollContentToTop();
}
};
}

View File

@ -14,7 +14,7 @@ interface Props {
export class Notifications extends React.Component<Props, {}> {
public render() {
return (
<TransitionGroup className="Notifications">
<TransitionGroup className="Notifications" aria-live="polite">
{this.props.notifications.map(n => {
return (
<CSSTransition classNames="NotificationAnimation" timeout={500} key={n.id}>

View File

@ -33,10 +33,12 @@ class NameInput extends Component<Props, State> {
return (
<form className="ENSInput" onSubmit={this.onSubmit}>
<div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown ENSInput-name">
<label className="input-group input-group-inline ENSInput-name">
<Input
value={domainToCheck}
className={!domainToCheck ? '' : isValidDomain ? 'is-valid' : 'is-invalid'}
className={`${
!domainToCheck ? '' : isValidDomain ? '' : 'invalid'
} border-rad-right-0`}
type="text"
placeholder="mycrypto"
onChange={this.onChange}

View File

@ -53,7 +53,11 @@ const CryptoWarning: React.SFC<{}> = () => (
className="CryptoWarning-browsers-browser"
>
<div>
<img className="CryptoWarning-browsers-browser-icon" src={browser.icon} />
<img
className="CryptoWarning-browsers-browser-icon"
src={browser.icon}
alt={browser.name + ' logo'}
/>
<div className="CryptoWarning-browsers-browser-name">{browser.name}</div>
</div>
</NewTabLink>

View File

@ -1,4 +1,4 @@
@import "common/sass/variables";
@import 'common/sass/variables';
.GenPaper {
&-title {
@ -6,8 +6,17 @@
}
&-private {
max-width: 700px;
max-width: 680px;
margin: 0 auto $space * 3;
margin-top: 12px;
> .input-group-header {
margin-bottom: 1rem;
// This selector is an exception, it targets the span returned using `translate`.
> span {
font-size: 2rem;
margin: auto;
}
}
}
&-paper,

View File

@ -1,7 +1,7 @@
import PrintableWallet from 'components/PrintableWallet';
import { IV3Wallet } from 'ethereumjs-wallet';
import React from 'react';
import translate from 'translations';
import translate, { translateRaw } from 'translations';
import { stripHexPrefix } from 'libs/values';
import './PaperWallet.scss';
import Template from '../Template';
@ -17,18 +17,20 @@ const PaperWallet: React.SFC<Props> = props => (
<Template>
<div className="GenPaper">
{/* Private Key */}
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
<label className="input-group GenPaper-private">
{/* translateRaw isn't used here because it wont properly render the ` characters as a string of code in markdown*/}
<h1 className="input-group-header">{translate('GEN_Label_5')}</h1>
<Input
className="GenPaper-private"
value={stripHexPrefix(props.privateKey)}
aria-label={translate('x_PrivKey', true)}
aria-label={translateRaw('x_PrivKey')}
aria-describedby="x_PrivKeyDesc"
type="text"
readOnly={true}
/>
</label>
{/* Download Paper Wallet */}
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
<h2 className="GenPaper-title">{translate('x_Print')}</h2>
<div className="GenPaper-paper">
<PrintableWallet address={props.keystore.address} privateKey={props.privateKey} />
</div>

View File

@ -28,10 +28,10 @@ export default class MnemonicWord extends React.Component<Props, State> {
return (
<div className="input-group-wrapper MnemonicWord">
<label className="input-group input-group-inline-dropdown ENSInput-name">
<label className="input-group input-group-inline ENSInput-name">
<span className="input-group-addon input-group-addon--transparent">{index + 1}.</span>
<Input
className={classnames('MnemonicWord-word-input', word === value && 'valid')}
className={`MnemonicWord-word-input ${!isReadOnly && 'border-rad-right-0'}`}
value={readOnly ? word : value}
onChange={this.handleChange}
readOnly={readOnly}

View File

@ -14,7 +14,6 @@
top: 36px;
left: 30px;
opacity: 0.3;
outline: none;
color: $text-color;
@media (max-width: $screen-sm) {

View File

@ -6,7 +6,7 @@ import { UnlockHeader } from 'components/ui';
import { SideBar } from './components/index';
import { getWalletInst } from 'selectors/wallet';
import { AppState } from 'reducers';
import { RouteComponentProps, Route, Switch } from 'react-router';
import { RouteComponentProps, Route, Switch, Redirect } from 'react-router';
import { RedirectWithQuery } from 'components/RedirectWithQuery';
import {
WalletInfo,
@ -74,7 +74,13 @@ class SendTransaction extends React.Component<Props> {
/>
)}
/>
<Route exact={true} path={`${currentPath}/send`} component={Send} />
<Route
exact={true}
path={`${currentPath}/send`}
render={() => {
return wallet.isReadOnly ? <Redirect to={`${currentPath}/info`} /> : <Send />;
}}
/>
<Route
path={`${currentPath}/info`}
exact={true}

View File

@ -341,7 +341,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
<div className="flex-spacer" />
<div className="input-group-wrapper">
<div className="input-group-header">Deposit</div>
<label className="input-group input-group-inline-dropdown">
<label className="input-group input-group-inline">
<Input
id="origin-swap-input"
className={`input-group-input ${
@ -365,7 +365,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
</div>
<div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown">
<label className="input-group input-group-inline">
<div className="input-group-header">Recieve</div>
<Input
id="destination-swap-input"

View File

@ -132,7 +132,7 @@ class CurrentRates extends PureComponent<Props> {
target="_blank"
rel="noopener noreferrer"
>
<img src={providerLogo} width={120} height={49} />
<img src={providerLogo} width={120} height={49} alt="Shapeshift Logo" />
</a>
</section>
</article>

View File

@ -1,7 +1,7 @@
import { RestartSwapAction } from 'actions/swap';
import bityLogo from 'assets/images/logo-bity.svg';
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
import { bityReferralURL } from 'config';
import { shapeshiftReferralURL, bitboxReferralURL } from 'config';
import React, { PureComponent } from 'react';
import translate from 'translations';
import './SwapInfoHeader.scss';
@ -29,11 +29,11 @@ export default class SwapInfoHeaderTitle extends PureComponent<SwapInfoHeaderTit
<div className="col-xs-3">
<a
className="SwapInfo-top-logo"
href={bityReferralURL}
href={provider === 'shapeshift' ? shapeshiftReferralURL : bitboxReferralURL}
target="_blank"
rel="noopener noreferrer"
>
<img className="SwapInfo-top-logo-img" src={logoToRender} />
<img className="SwapInfo-top-logo-img" src={logoToRender} alt={provider + ' logo'} />
</a>
</div>
</section>

View File

@ -39,3 +39,8 @@
[data-whatintent='mouse'] *:focus {
outline: none;
}
// This is fine because the outline effect is reproduced with border and box-shadow styles on input elements
input {
outline: none;
}

View File

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

View File

@ -8,9 +8,3 @@
.btn-group > .btn-group {
float: left;
}
// On active and open, don't show outline
.btn-group .dropdown-toggle:active,
.btn-group.open .dropdown-toggle {
outline: 0;
}

View File

@ -35,7 +35,6 @@ input[readonly] {
&:focus {
border-color: $input-border-focus;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba($brand-primary, 0.5);
}
}

View File

@ -60,6 +60,9 @@
border-bottom-right-radius: 2px;
border-color: inherit;
}
&-menu {
max-height: 8.625rem;
}
&.invalid.has-blurred {
border-color: $brand-danger;
box-shadow: inset 0px 0px 0px 1px $brand-danger;

View File

@ -56,12 +56,7 @@ const nonStandardTransaction = (state: AppState): boolean => {
const getGasCost = (state: AppState) => {
const gasPrice = getGasPrice(state);
const gasLimit = getGasLimit(state);
if (!gasLimit.value) {
return Wei('0');
}
const cost = gasLimit.value.mul(gasPrice.value);
return cost;
return gasLimit.value ? gasPrice.value.mul(gasLimit.value) : Wei('0');
};
const serializedAndTransactionFieldsMatch = (state: AppState, isLocallySigned: boolean) => {

View File

@ -18,7 +18,7 @@
"electron-updater": "2.21.0",
"ethereum-blockies": "git+https://github.com/MyCryptoHQ/blockies.git",
"ethereumjs-abi": "0.6.5",
"ethereumjs-tx": "1.3.3",
"ethereumjs-tx": "1.3.4",
"ethereumjs-util": "5.1.5",
"ethereumjs-wallet": "0.6.0",
"font-awesome": "4.7.0",
@ -37,7 +37,7 @@
"react": "16.2.0",
"react-copy-to-clipboard": "5.0.1",
"react-dom": "16.2.0",
"react-markdown": "3.2.2",
"react-markdown": "3.3.0",
"react-redux": "5.0.7",
"react-router-dom": "4.2.2",
"react-router-redux": "4.0.8",
@ -49,7 +49,7 @@
"redux-saga": "0.16.0",
"scryptsy": "2.0.0",
"uuid": "3.2.1",
"wallet-address-validator": "0.1.2",
"wallet-address-validator": "0.1.3",
"whatwg-fetch": "2.0.3",
"zxcvbn": "4.4.2"
},
@ -70,7 +70,7 @@
"@types/react-redux": "5.0.15",
"@types/react-router-dom": "4.2.4",
"@types/react-router-redux": "5.0.12",
"@types/react-select": "1.2.3",
"@types/react-select": "1.2.4",
"@types/react-transition-group": "2.0.7",
"@types/redux-logger": "3.0.5",
"@types/uuid": "3.4.3",
@ -83,10 +83,10 @@
"cache-loader": "1.2.2",
"check-node-version": "3.2.0",
"concurrently": "3.5.1",
"copy-webpack-plugin": "4.5.0",
"copy-webpack-plugin": "4.5.1",
"css-loader": "0.28.10",
"electron": "1.8.2",
"electron-builder": "20.2.1",
"electron": "1.8.3",
"electron-builder": "20.4.0",
"empty": "0.10.1",
"enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1",
@ -104,7 +104,7 @@
"jest": "22.1.4",
"klaw-sync": "3.0.2",
"less": "2.7.3",
"less-loader": "4.0.6",
"less-loader": "4.1.0",
"lint-staged": "7.0.0",
"minimist": "1.2.0",
"node-sass": "4.7.2",
@ -119,7 +119,7 @@
"resolve-url-loader": "2.3.0",
"rimraf": "2.6.2",
"sass-loader": "6.0.7",
"style-loader": "0.20.2",
"style-loader": "0.20.3",
"thread-loader": "1.1.5",
"ts-jest": "22.4.1",
"ts-loader": "3.3.1",
@ -130,7 +130,7 @@
"types-rlp": "0.0.1",
"typescript": "2.6.2",
"url-loader": "1.0.1",
"url-search-params-polyfill": "2.0.3",
"url-search-params-polyfill": "3.0.0",
"webpack": "3.11.0",
"webpack-dev-middleware": "2.0.6",
"webpack-hot-middleware": "2.21.0",
@ -147,14 +147,10 @@
"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",
"test:coverage": "jest --config=jest_config/jest.config.json --coverage",
"test": "jest --config=jest_config/jest.config.json",
@ -166,18 +162,14 @@
"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": {

View File

@ -0,0 +1,56 @@
import { INITIAL_STATE } from 'reducers/transaction';
import { broadcast, ITransactionStatus } from 'reducers/transaction/broadcast';
import * as txActions from 'actions/transaction';
const indexingHash = 'testingHash';
describe('broadcast reducer', () => {
const serializedTransaction = new Buffer('testSerialized');
const nextTxStatus: ITransactionStatus = {
broadcastedHash: null,
broadcastSuccessful: false,
isBroadcasting: true,
serializedTransaction
};
const nextState: any = {
...INITIAL_STATE,
[indexingHash]: nextTxStatus
};
it('should handle BROADCAST_TRANSACTION_QUEUED', () => {
expect(
broadcast(
INITIAL_STATE as any,
txActions.broadcastTransactionQueued({ indexingHash, serializedTransaction })
)
).toEqual(nextState);
});
it('should handle BROADCAST_TRANSACTION_SUCCESS', () => {
const broadcastedHash = 'testBroadcastHash';
const broadcastedState = {
...nextState,
[indexingHash]: {
...nextTxStatus,
broadcastedHash,
isBroadcasting: false,
broadcastSuccessful: true
}
};
expect(
broadcast(
nextState,
txActions.broadcastTransactionSucceeded({ indexingHash, broadcastedHash })
)
).toEqual(broadcastedState);
});
it('should handle BROADCAST_TRANSACTION_FAILURE', () => {
const failedBroadcastState = {
...nextState,
[indexingHash]: { ...nextTxStatus, isBroadcasting: false, broadcastSuccessful: false }
};
expect(broadcast(nextState, txActions.broadcastTransactionFailed({ indexingHash }))).toEqual(
failedBroadcastState
);
});
});

View File

@ -0,0 +1,122 @@
import { TypeKeys } from 'actions/transaction/constants';
import { gasPricetoBase } from 'libs/units';
import { fields, State } from 'reducers/transaction/fields';
import * as txActions from 'actions/transaction';
import BN from 'bn.js';
describe('fields reducer', () => {
const INITIAL_STATE: State = {
to: { raw: '', value: null },
data: { raw: '', value: null },
nonce: { raw: '', value: null },
value: { raw: '', value: null },
gasLimit: { raw: '21000', value: new BN(21000) },
gasPrice: { raw: '20', value: gasPricetoBase(20) }
};
const testPayload = { raw: 'test', value: null };
it('should handle TO_FIELD_SET', () => {
expect(fields(INITIAL_STATE, txActions.setToField(testPayload))).toEqual({
...INITIAL_STATE,
to: testPayload
});
});
it('should handle VALUE_FIELD_SET', () => {
expect(fields(INITIAL_STATE, txActions.setValueField(testPayload))).toEqual({
...INITIAL_STATE,
value: testPayload
});
});
it('should handle DATA_FIELD_SET', () => {
expect(fields(INITIAL_STATE, txActions.setDataField(testPayload))).toEqual({
...INITIAL_STATE,
data: testPayload
});
});
it('should handle GAS_LIMIT_FIELD_SET', () => {
expect(fields(INITIAL_STATE, txActions.setGasLimitField(testPayload))).toEqual({
...INITIAL_STATE,
gasLimit: testPayload
});
});
it('should handle NONCE_SET', () => {
expect(fields(INITIAL_STATE, txActions.setNonceField(testPayload))).toEqual({
...INITIAL_STATE,
nonce: testPayload
});
});
it('should handle GAS_PRICE_FIELD_SET', () => {
expect(fields(INITIAL_STATE, txActions.setGasPriceField(testPayload))).toEqual({
...INITIAL_STATE,
gasPrice: testPayload
});
});
it('should handle TOKEN_TO_ETHER_SWAP', () => {
const swapAction: txActions.SwapTokenToEtherAction = {
type: TypeKeys.TOKEN_TO_ETHER_SWAP,
payload: {
to: testPayload,
value: testPayload,
decimal: 1
}
};
expect(fields(INITIAL_STATE, swapAction)).toEqual({
...INITIAL_STATE,
to: testPayload,
value: testPayload
});
});
it('should handle ETHER_TO_TOKEN_SWAP', () => {
const swapAction: txActions.SwapEtherToTokenAction = {
type: TypeKeys.ETHER_TO_TOKEN_SWAP,
payload: {
to: testPayload,
data: testPayload,
tokenTo: testPayload,
tokenValue: testPayload,
decimal: 1
}
};
expect(fields(INITIAL_STATE, swapAction)).toEqual({
...INITIAL_STATE,
to: testPayload,
data: testPayload
});
});
it('should handle TOKEN_TO_TOKEN_SWAP', () => {
const swapAction: txActions.SwapTokenToTokenAction = {
type: TypeKeys.TOKEN_TO_TOKEN_SWAP,
payload: {
to: testPayload,
data: testPayload,
tokenValue: testPayload,
decimal: 1
}
};
expect(fields(INITIAL_STATE, swapAction)).toEqual({
...INITIAL_STATE,
to: testPayload,
data: testPayload
});
});
it('should reset', () => {
const resetAction: txActions.ResetAction = {
type: TypeKeys.RESET,
payload: { include: {}, exclude: {} }
};
const modifiedState: State = {
...INITIAL_STATE,
data: { raw: 'modified', value: null }
};
expect(fields(modifiedState, resetAction)).toEqual(INITIAL_STATE);
});
});

View File

@ -0,0 +1,109 @@
import { TypeKeys } from 'actions/transaction/constants';
import { getDecimalFromEtherUnit } from 'libs/units';
import { State, meta } from 'reducers/transaction/meta';
import * as txActions from 'actions/transaction';
describe('meta reducer', () => {
const INITIAL_STATE: State = {
unit: '',
previousUnit: '',
decimal: getDecimalFromEtherUnit('ether'),
tokenValue: { raw: '', value: null },
tokenTo: { raw: '', value: null },
from: null
};
const testPayload = { raw: 'test', value: null };
it('should handle UNIT_META_SET', () => {
const setUnitMetaAction: txActions.SetUnitMetaAction = {
type: TypeKeys.UNIT_META_SET,
payload: 'test'
};
expect(meta(INITIAL_STATE, setUnitMetaAction));
});
it('should handle TOKEN_VALUE_META_SET', () => {
expect(meta(INITIAL_STATE, txActions.setTokenValue(testPayload))).toEqual({
...INITIAL_STATE,
tokenValue: testPayload
});
});
it('should handle TOKEN_TO_META_SET', () => {
expect(meta(INITIAL_STATE, txActions.setTokenTo(testPayload))).toEqual({
...INITIAL_STATE,
tokenTo: testPayload
});
});
it('should handle GET_FROM_SUCCEEDED', () => {
expect(meta(INITIAL_STATE, txActions.getFromSucceeded('test'))).toEqual({
...INITIAL_STATE,
from: 'test'
});
});
it('should handle TOKEN_TO_ETHER_SWAP', () => {
const swapAction: txActions.SwapTokenToEtherAction = {
type: TypeKeys.TOKEN_TO_ETHER_SWAP,
payload: {
to: testPayload,
value: testPayload,
decimal: 1
}
};
expect(meta(INITIAL_STATE, swapAction)).toEqual({
...INITIAL_STATE,
decimal: swapAction.payload.decimal
});
});
it('should handle ETHER_TO_TOKEN_SWAP', () => {
const swapAction: txActions.SwapEtherToTokenAction = {
type: TypeKeys.ETHER_TO_TOKEN_SWAP,
payload: {
to: testPayload,
data: testPayload,
tokenTo: testPayload,
tokenValue: testPayload,
decimal: 1
}
};
expect(meta(INITIAL_STATE, swapAction)).toEqual({
...INITIAL_STATE,
decimal: swapAction.payload.decimal,
tokenTo: testPayload,
tokenValue: testPayload
});
});
it('should handle TOKEN_TO_TOKEN_SWAP', () => {
const swapAction: txActions.SwapTokenToTokenAction = {
type: TypeKeys.TOKEN_TO_TOKEN_SWAP,
payload: {
to: testPayload,
data: testPayload,
tokenValue: testPayload,
decimal: 1
}
};
expect(meta(INITIAL_STATE, swapAction)).toEqual({
...INITIAL_STATE,
decimal: swapAction.payload.decimal,
tokenValue: testPayload
});
});
it('should reset', () => {
const resetAction: txActions.ResetAction = {
type: TypeKeys.RESET,
payload: { include: {}, exclude: {} }
};
const modifiedState: State = {
...INITIAL_STATE,
unit: 'modified'
};
expect(meta(modifiedState, resetAction)).toEqual(INITIAL_STATE);
});
});

View File

@ -0,0 +1,55 @@
import { State, network } from 'reducers/transaction/network';
import * as txActions from 'actions/transaction';
import { TypeKeys } from 'actions/transaction/constants';
describe('network reducer', () => {
const INITIAL_STATE: State = {
gasEstimationStatus: null,
getFromStatus: null,
getNonceStatus: null,
gasPriceStatus: null
};
it('should handle gas estimation status actions', () => {
const gasEstimationAction: txActions.NetworkAction = {
type: TypeKeys.ESTIMATE_GAS_SUCCEEDED
};
expect(network(INITIAL_STATE, gasEstimationAction)).toEqual({
...INITIAL_STATE,
gasEstimationStatus: 'SUCCESS'
});
});
it('should handle get from status actions', () => {
const getFromAction: txActions.NetworkAction = {
type: TypeKeys.GET_FROM_SUCCEEDED,
payload: 'test'
};
expect(network(INITIAL_STATE, getFromAction)).toEqual({
...INITIAL_STATE,
getFromStatus: 'SUCCESS'
});
});
it('should handle get nonce status actions', () => {
const getNonceAction: txActions.NetworkAction = {
type: TypeKeys.GET_NONCE_SUCCEEDED,
payload: 'test'
};
expect(network(INITIAL_STATE, getNonceAction)).toEqual({
...INITIAL_STATE,
getNonceStatus: 'SUCCESS'
});
});
it('should handle gasPriceIntent', () => {
const gasPriceAction: txActions.InputGasPriceAction = {
type: TypeKeys.GAS_PRICE_INPUT,
payload: 'test'
};
expect(network(INITIAL_STATE, gasPriceAction)).toEqual({
...INITIAL_STATE,
gasPriceStatus: 'SUCCESS'
});
});
});

View File

@ -0,0 +1,62 @@
import EthTx from 'ethereumjs-tx';
import * as txActions from 'actions/transaction';
import { TypeKeys } from 'actions/transaction/constants';
import { State, sign } from 'reducers/transaction/sign';
describe('sign reducer', () => {
const INITIAL_STATE: State = {
local: { signedTransaction: null },
web3: { transaction: null },
indexingHash: null,
pending: false
};
it('should handle SIGN_TRANSACTION_REQUESTED', () => {
const signTxRequestedAction: txActions.SignTransactionRequestedAction = {
type: TypeKeys.SIGN_TRANSACTION_REQUESTED,
payload: {} as EthTx
};
expect(sign(INITIAL_STATE, signTxRequestedAction)).toEqual({ ...INITIAL_STATE, pending: true });
});
it('should handle SIGN_LOCAL_TRANSACTION_SUCCEEDED', () => {
const signedTransaction = new Buffer('test');
const indexingHash = 'test';
const signLocalTxSucceededAction: txActions.SignLocalTransactionSucceededAction = {
type: TypeKeys.SIGN_LOCAL_TRANSACTION_SUCCEEDED,
payload: { signedTransaction, indexingHash }
};
expect(sign(INITIAL_STATE, signLocalTxSucceededAction)).toEqual({
...INITIAL_STATE,
pending: false,
indexingHash,
local: { signedTransaction }
});
});
it('should handle SIGN_WEB3_TRANSACTION_SUCCEEDED', () => {
const transaction = new Buffer('test');
const indexingHash = 'test';
const signWeb3TxSucceededAction: txActions.SignWeb3TransactionSucceededAction = {
type: TypeKeys.SIGN_WEB3_TRANSACTION_SUCCEEDED,
payload: { transaction, indexingHash }
};
expect(sign(INITIAL_STATE, signWeb3TxSucceededAction)).toEqual({
...INITIAL_STATE,
pending: false,
indexingHash,
web3: { transaction }
});
});
it('should reset', () => {
const resetAction: txActions.ResetAction = {
type: TypeKeys.RESET,
payload: { include: {}, exclude: {} }
};
const modifiedState: State = {
...INITIAL_STATE,
pending: true
};
expect(sign(modifiedState, resetAction)).toEqual(INITIAL_STATE);
});
});

View File

@ -1,46 +1,97 @@
import { configuredStore } from 'store';
import { getResolvedAddress } from 'selectors/ens';
import { Address } from 'libs/units';
import { call, select, put } from 'redux-saga/effects';
import { call, select, put, take } from 'redux-saga/effects';
import { isValidETHAddress, isValidENSAddress } from 'libs/validators';
import { setCurrentTo, setField } from 'sagas/transaction/current/currentTo';
import { isEtherTransaction } from 'selectors/transaction';
import { cloneableGenerator } from 'redux-saga/utils';
import { setToField, setTokenTo } from 'actions/transaction';
configuredStore.getState();
const raw = '0xa';
const payload = {
raw,
value: Address(raw)
};
import { resolveDomainRequested, TypeKeys as ENSTypekeys } from 'actions/ens';
describe('setCurrentTo*', () => {
const action: any = {
const data = {} as any;
describe('with valid Ethereum address', () => {
const raw = '0xa';
const ethAddrPayload = {
raw,
value: Address(raw)
};
const ethAddrAction: any = {
payload: raw
};
const validAddress = true;
const validEns = false;
const gen = setCurrentTo(action);
data.validEthGen = setCurrentTo(ethAddrAction);
it('should call isValidETHAddress', () => {
expect(gen.next().value).toEqual(call(isValidETHAddress, raw));
expect(data.validEthGen.next().value).toEqual(call(isValidETHAddress, raw));
});
it('should call isValidENSAddress', () => {
expect(gen.next(validAddress).value).toEqual(call(isValidENSAddress, raw));
expect(data.validEthGen.next(raw).value).toEqual(call(isValidENSAddress, raw));
});
it('should call setField', () => {
expect(gen.next(validEns).value).toEqual(call(setField, payload));
expect(data.validEthGen.next(raw).value).toEqual(call(setField, ethAddrPayload));
});
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
describe('with invalid Ethereum address, valid ENS address', () => {
const raw = 'testing.eth';
const resolvedAddress = '0xa';
const [domain] = raw.split('.');
const ensAddrPayload = {
raw,
value: null
};
const ensAddrAction: any = {
payload: raw
};
data.validEnsGen = setCurrentTo(ensAddrAction);
it('should call isValidETHAddress', () => {
expect(data.validEnsGen.next().value).toEqual(call(isValidETHAddress, raw));
});
it('should call isValidENSAddress', () => {
expect(data.validEnsGen.next(false).value).toEqual(call(isValidENSAddress, raw));
});
it('should call setField', () => {
expect(data.validEnsGen.next(true).value).toEqual(call(setField, ensAddrPayload));
});
it('should put resolveDomainRequested', () => {
expect(data.validEnsGen.next().value).toEqual(put(resolveDomainRequested(domain)));
});
it('should take ENS type keys', () => {
expect(data.validEnsGen.next().value).toEqual(
take([
ENSTypekeys.ENS_RESOLVE_DOMAIN_FAILED,
ENSTypekeys.ENS_RESOLVE_DOMAIN_SUCCEEDED,
ENSTypekeys.ENS_RESOLVE_DOMAIN_CACHED
])
);
});
it('should select getResolvedAddress', () => {
expect(data.validEnsGen.next().value).toEqual(select(getResolvedAddress, true));
});
it('should call setField', () => {
expect(data.validEnsGen.next(resolvedAddress).value).toEqual(
call(setField, { raw, value: Address(resolvedAddress) })
);
});
});
});
describe('setField', () => {
const raw = '0xa';
const payload = {
raw,
value: Address(raw)
};
const etherTransaction = cloneableGenerator(setField)(payload);
it('should select etherTransaction', () => {
expect(etherTransaction.next().value).toEqual(select(isEtherTransaction));

View File

@ -46,7 +46,6 @@ describe('valueHandler', () => {
});
it('should select getUnit', () => {
gen.invalidDecimal = gen.pass.clone();
expect(gen.pass.next(decimal).value).toEqual(select(getUnit));
expect(gen.invalidNumber.next(decimal).value).toEqual(select(getUnit));
expect(gen.invalidDecimal.next(failCases.invalidDecimal).value).toEqual(select(getUnit));

View File

@ -1,5 +1,5 @@
import { configuredStore } from 'store';
import BN from 'bn.js';
import { SagaIterator, delay } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import { setDataField, setGasLimitField, setNonceField } from 'actions/transaction/actionCreators';
import { isValidHex, isValidNonce, gasPriceValidator, gasLimitValidator } from 'libs/validators';
@ -8,12 +8,11 @@ import {
handleDataInput,
handleGasLimitInput,
handleNonceInput,
handleGasPriceInput
handleGasPriceInput,
handleGasPriceInputIntent
} from 'sagas/transaction/fields/fields';
import { cloneableGenerator } from 'redux-saga/utils';
import { setGasPriceField } from 'actions/transaction';
import { SagaIterator } from 'redux-saga';
configuredStore.getState();
import { setGasPriceField, inputGasPrice } from 'actions/transaction';
const itShouldBeDone = (gen: SagaIterator) => {
it('should be done', () => {
@ -142,6 +141,19 @@ describe('handleGasPriceInput*', () => {
});
});
describe('handleGasPriceInputIntent*', () => {
const payload = '100.111';
const action: any = { payload };
const gen = handleGasPriceInputIntent(action);
it('should call delay', () => {
expect(gen.next().value).toEqual(call(delay, 300));
});
it('should put inputGasPrice', () => {
expect(gen.next().value).toEqual(put(inputGasPrice(payload)));
});
});
describe('handleNonceInput*', () => {
const payload = '42';
const action: any = { payload };

View File

@ -0,0 +1,79 @@
import { put, apply, call } from 'redux-saga/effects';
import { signLocalTransactionSucceeded, signWeb3TransactionSucceeded } from 'actions/transaction';
import { computeIndexingHash } from 'libs/transaction';
import {
signLocalTransactionHandler,
signWeb3TransactionHandler
} from 'sagas/transaction/signing/signing';
describe('signLocalTransactionHandler*', () => {
const tx = 'tx';
const wallet = {
signRawTransaction: jest.fn()
};
const action: any = { tx, wallet };
const signedTransaction = new Buffer('signedTransaction');
const indexingHash = 'indexingHash';
const gen = signLocalTransactionHandler(action);
it('should apply wallet.signRawTransaction', () => {
expect(gen.next().value).toEqual(apply(wallet, wallet.signRawTransaction, [tx]));
});
it('should call computeIndexingHash', () => {
expect(gen.next(signedTransaction).value).toEqual(call(computeIndexingHash, signedTransaction));
});
it('should put signLocalTransactionSucceeded', () => {
expect(gen.next(indexingHash).value).toEqual(
put(
signLocalTransactionSucceeded({
signedTransaction,
indexingHash,
noVerify: false
})
)
);
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('signWeb3TransactionHandler*', () => {
const tx = {
serialize: jest.fn
};
const action: any = { tx };
const serializedTransaction = new Buffer('tx');
const indexingHash = 'indexingHash';
const gen = signWeb3TransactionHandler(action);
it('should apply tx.serialize', () => {
expect(gen.next().value).toEqual(apply(tx, tx.serialize));
});
it('should call computeIndexingHash', () => {
expect(gen.next(serializedTransaction).value).toEqual(
call(computeIndexingHash, serializedTransaction)
);
});
it('should put signWeb3TransactionSucceeded', () => {
expect(gen.next(indexingHash).value).toEqual(
put(
signWeb3TransactionSucceeded({
transaction: serializedTransaction,
indexingHash
})
)
);
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});

11
spec/selectors/helpers.ts Normal file
View File

@ -0,0 +1,11 @@
import { configuredStore } from '../../common/store';
export function getInitialState() {
return { ...configuredStore.getState() };
}
export function testShallowlyEqual(oldValue: any, newValue: any) {
it('should be shallowly equal when called again with the same state', () => {
expect(oldValue === newValue).toBeTruthy();
});
}

View File

@ -0,0 +1,63 @@
import {
getTransactionStatus,
currentTransactionFailed,
currentTransactionBroadcasting,
currentTransactionBroadcasted,
getCurrentTransactionStatus
} from 'selectors/transaction';
import { getInitialState } from '../helpers';
describe('broadcast selector', () => {
const state = getInitialState();
state.transaction = {
...state.transaction,
broadcast: {
...state.transaction.broadcast,
testIndexingHash1: {
broadcastedHash: 'testBroadcastedHash',
broadcastSuccessful: true,
isBroadcasting: false,
serializedTransaction: new Buffer([1, 2, 3])
},
testIndexingHash2: {
broadcastedHash: 'testBroadcastedHash',
broadcastSuccessful: true,
isBroadcasting: false,
serializedTransaction: new Buffer([1, 2, 3])
}
},
sign: {
...state.transaction.sign,
indexingHash: 'testIndexingHash1',
pending: false
}
};
it('should check getTransactionState with an indexing hash', () => {
expect(getTransactionStatus(state, 'testIndexingHash1')).toEqual(
state.transaction.broadcast.testIndexingHash1
);
});
it('should check getCurrentTransactionStatus', () => {
expect(getCurrentTransactionStatus(state)).toEqual(
state.transaction.broadcast.testIndexingHash2
);
});
it('should check currentTransactionFailed', () => {
expect(currentTransactionFailed(state)).toEqual(false);
});
it('should check currentTransactionBroadcasting', () => {
expect(currentTransactionBroadcasting(state)).toEqual(false);
});
it('should check currentTransactionBroadcasted', () => {
expect(currentTransactionBroadcasted(state)).toEqual(true);
});
it('should return false on getCurrentTransactionStatus if no index hash present', () => {
state.transaction.sign.indexingHash = null;
expect(getCurrentTransactionStatus(state)).toEqual(false);
});
});

View File

@ -0,0 +1,68 @@
import { Wei } from 'libs/units';
import {
getCurrentValue,
getCurrentTo,
isEtherTransaction,
isValidCurrentTo,
isValidGasPrice,
isValidGasLimit,
getCurrentToAddressMessage
} from 'selectors/transaction';
import { getInitialState } from '../helpers';
describe('current selector', () => {
const state = getInitialState();
state.transaction = {
...state.transaction,
fields: {
...state.transaction.fields,
to: {
raw: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
value: new Buffer([0, 1, 2, 3])
},
gasLimit: {
raw: '21000',
value: Wei('21000')
},
gasPrice: {
raw: '1500',
value: Wei('1500')
}
},
meta: {
...state.transaction.meta,
unit: 'ETH',
previousUnit: 'ETH'
}
};
it('should get stored receiver address on getCurrentTo', () => {
expect(getCurrentTo(state)).toEqual(state.transaction.fields.to);
});
it('should get stored value on getCurrentValue', () => {
expect(getCurrentValue(state)).toEqual(state.transaction.fields.value);
});
it('should get message to the receiver', () => {
expect(getCurrentToAddressMessage(state)).toEqual({
msg: 'Thank you for donating to MyCrypto. TO THE MOON!'
});
});
it('should check isValidGasPrice', () => {
expect(isValidGasPrice(state)).toEqual(true);
});
it('should check isEtherTransaction', () => {
expect(isEtherTransaction(state)).toEqual(true);
});
it('should check isValidGasLimit', () => {
expect(isValidGasLimit(state)).toEqual(true);
});
it('should check isValidCurrentTo', () => {
expect(isValidCurrentTo(state)).toEqual(true);
});
});

View File

@ -0,0 +1,88 @@
import BN from 'bn.js';
import { Wei } from 'libs/units';
import {
getData,
getFields,
getGasLimit,
getValue,
getTo,
getNonce,
getGasPrice,
getDataExists,
getValidGasCost
} from 'selectors/transaction';
import { getInitialState } from '../helpers';
describe('fields selector', () => {
const state = getInitialState();
state.transaction.fields = {
to: {
raw: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
value: new Buffer([0, 1, 2, 3])
},
data: {
raw: '',
value: null
},
nonce: {
raw: '0',
value: new BN('0')
},
value: {
raw: '1000000000',
value: Wei('1000000000')
},
gasLimit: {
raw: '21000',
value: Wei('21000')
},
gasPrice: {
raw: '1500',
value: Wei('1500')
}
};
it('should get fields from fields store', () => {
expect(getFields(state)).toEqual(state.transaction.fields);
});
it('should get data from fields store', () => {
expect(getData(state)).toEqual(state.transaction.fields.data);
});
it('should get gas limit from fields store', () => {
expect(getGasLimit(state)).toEqual(state.transaction.fields.gasLimit);
});
it('should get value from fields store', () => {
expect(getValue(state)).toEqual(state.transaction.fields.value);
});
it('sould get receiver address from fields store', () => {
expect(getTo(state)).toEqual(state.transaction.fields.to);
});
it('should get nonce from fields store', () => {
expect(getNonce(state)).toEqual(state.transaction.fields.nonce);
});
it('should get gas price from fields store', () => {
expect(getGasPrice(state)).toEqual(state.transaction.fields.gasPrice);
});
it('should check getDataExists', () => {
expect(getDataExists(state)).toEqual(false);
});
it('should check when gas cost is valid', () => {
expect(getValidGasCost(state)).toEqual(true);
});
it('should check when gas cost is invalid', () => {
state.wallet.balance = {
wei: Wei('0'),
isPending: false
};
expect(getValidGasCost(state)).toEqual(false);
});
});

View File

@ -0,0 +1,99 @@
import BN from 'bn.js';
import { Wei } from 'libs/units';
import { reduceToValues, isFullTx } from 'selectors/transaction/helpers';
import {
getCurrentTo,
getCurrentValue,
getFields,
getUnit,
getDataExists,
getValidGasCost
} from 'selectors/transaction';
import { getInitialState } from '../helpers';
describe('helpers selector', () => {
const state = getInitialState();
state.transaction = {
...state.transaction,
meta: {
...state.transaction.meta,
unit: 'ETH'
},
fields: {
to: {
raw: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
value: new Buffer([0, 1, 2, 3])
},
data: {
raw: '',
value: null
},
nonce: {
raw: '0',
value: new BN('0')
},
value: {
raw: '1000000000',
value: Wei('1000000000')
},
gasLimit: {
raw: '21000',
value: Wei('21000')
},
gasPrice: {
raw: '1500',
value: Wei('1500')
}
}
};
it('should reduce the fields state to its base values', () => {
const values = {
data: null,
gasLimit: Wei('21000'),
gasPrice: Wei('1500'),
nonce: new BN('0'),
to: new Buffer([0, 1, 2, 3]),
value: Wei('1000000000')
};
expect(reduceToValues(state.transaction.fields)).toEqual(values);
});
it('should check isFullTransaction with full transaction arguments', () => {
const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state);
const transactionFields = getFields(state);
const unit = getUnit(state);
const dataExists = getDataExists(state);
const validGasCost = getValidGasCost(state);
const isFullTransaction = isFullTx(
state,
transactionFields,
currentTo,
currentValue,
dataExists,
validGasCost,
unit
);
expect(isFullTransaction).toEqual(true);
});
it('should check isFullTransaction without full transaction arguments', () => {
const currentTo = { raw: '', value: null };
const currentValue = getCurrentValue(state);
const transactionFields = getFields(state);
const unit = getUnit(state);
const dataExists = getDataExists(state);
const validGasCost = getValidGasCost(state);
const isFullTransaction = isFullTx(
state,
transactionFields,
currentTo,
currentValue,
dataExists,
validGasCost,
unit
);
expect(isFullTransaction).toEqual(false);
});
});

View File

@ -0,0 +1,70 @@
import {
getFrom,
getDecimal,
getTokenValue,
getTokenTo,
getUnit,
getPreviousUnit,
getDecimalFromUnit
} from 'selectors/transaction/meta';
import { getInitialState } from '../helpers';
describe('meta tests', () => {
const state = getInitialState();
(state.transaction.meta = {
unit: 'ETH',
previousUnit: 'ETH',
decimal: 18,
tokenValue: {
raw: '',
value: null
},
tokenTo: {
raw: '',
value: null
},
from: 'fromAddress'
}),
(state.customTokens = [
{
address: '0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7',
symbol: 'UNI',
decimal: 0
}
]);
it('should get the stored sender address', () => {
expect(getFrom(state)).toEqual(state.transaction.meta.from);
});
it('should get the stored decimal', () => {
expect(getDecimal(state)).toEqual(state.transaction.meta.decimal);
});
it('should get the token value', () => {
expect(getTokenValue(state)).toEqual(state.transaction.meta.tokenValue);
});
it('should get the token receiver address', () => {
expect(getTokenTo(state)).toEqual(state.transaction.meta.tokenTo);
});
it('should get the stored unit', () => {
expect(getUnit(state)).toEqual(state.transaction.meta.unit);
});
it('should get the stored previous unit', () => {
expect(getPreviousUnit(state)).toEqual(state.transaction.meta.previousUnit);
});
it('should get the decimal for ether', () => {
expect(getDecimalFromUnit(state, getUnit(state))).toEqual(18);
});
it('should get the decimal for a token', () => {
expect(getDecimalFromUnit(state, 'UNI')).toEqual(0);
});
it('should throw error if the token is not found', () => {
expect(() => getDecimalFromUnit(state, 'ABC')).toThrowError(`Token ABC not found`);
});
});

View File

@ -0,0 +1,48 @@
import { RequestStatus } from 'reducers/transaction/network';
import {
getNetworkStatus,
nonceRequestPending,
nonceRequestFailed,
isNetworkRequestPending,
getGasEstimationPending,
getGasLimitEstimationTimedOut
} from 'selectors/transaction';
import { getInitialState } from '../helpers';
describe('current selector', () => {
const state = getInitialState();
state.transaction.network = {
...state.transaction.network,
gasEstimationStatus: RequestStatus.REQUESTED,
getFromStatus: RequestStatus.SUCCEEDED,
getNonceStatus: RequestStatus.REQUESTED,
gasPriceStatus: RequestStatus.SUCCEEDED
};
it('should get network status', () => {
expect(getNetworkStatus(state)).toEqual(state.transaction.network);
});
it('should check with the store if the nonce request is pending', () => {
expect(nonceRequestPending(state)).toEqual(true);
});
it('should check with the store if the nonce request failed', () => {
state.transaction.network.getNonceStatus = RequestStatus.FAILED;
expect(nonceRequestFailed(state)).toEqual(true);
});
it('should check with the store if the gas estimation is pending', () => {
expect(getGasEstimationPending(state)).toEqual(true);
});
it('should check with the store if gas limit estimation timed out', () => {
state.transaction.network.gasEstimationStatus = RequestStatus.TIMEDOUT;
expect(getGasLimitEstimationTimedOut(state)).toEqual(true);
});
it('should check with the store if network request is pending', () => {
state.transaction.network.gasEstimationStatus = RequestStatus.REQUESTED;
expect(isNetworkRequestPending(state)).toEqual(true);
});
});

View File

@ -0,0 +1,44 @@
import {
signaturePending,
getSignedTx,
getWeb3Tx,
getSignState,
getSerializedTransaction
} from 'selectors/transaction/sign';
import { getInitialState } from '../helpers';
describe('sign tests', () => {
const state = getInitialState();
(state.transaction.sign = {
indexingHash: 'testIndexingHash',
pending: false,
local: {
signedTransaction: new Buffer([4, 5, 6, 7])
},
web3: {
transaction: null
}
}),
it('should return whether the current signature is pending', () => {
expect(signaturePending(state)).toEqual({
isHardwareWallet: false,
isSignaturePending: false
});
});
it('should should get the stored sign state', () => {
expect(getSignState(state)).toEqual(state.transaction.sign);
});
it('should get the signed local transaction state', () => {
expect(getSignedTx(state)).toEqual(state.transaction.sign.local.signedTransaction);
});
it('should get the signed web3 transaction state', () => {
expect(getWeb3Tx(state)).toEqual(state.transaction.sign.web3.transaction);
});
it('should get the serialized transaction state', () => {
expect(getSerializedTransaction(state)).toEqual(new Buffer([4, 5, 6, 7]));
});
});