Merge develop, resolve conflicts.

This commit is contained in:
Will O'Beirne 2018-05-23 15:01:40 -04:00
commit 2b5b4848a8
No known key found for this signature in database
GPG Key ID: 44C190DB5DEAF9F6
173 changed files with 4394 additions and 1406 deletions

View File

@ -8,6 +8,10 @@
"beta": {
"gitUrl": "git@github.com:MyCryptoHQ/MyCrypto-Beta.git",
"distFolder": "docs"
},
"prod": {
"gitUrl": "git@github.com:MyCryptoHQ/MyCrypto-Production.git",
"distFolder": "docs"
}
}
}

View File

@ -1,35 +1,65 @@
dist: trusty
sudo: required
language: node_js
matrix:
include:
- os: osx
osx_image: xcode9.3
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- os: linux
dist: trusty
sudo: required
services: docker
cache:
yarn: true
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
services:
- docker
before_cache:
- |
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
rm -rf $HOME/.cache/electron-builder/wine
fi
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
# uncomment once integration tests are included in CI
# - docker pull dternyak/eth-priv-to-addr:latest
- sudo apt-get install libusb-1.0
- |
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
export CHROME_BIN=chromium-browser
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
# uncomment once integration tests are included in CI
# docker pull dternyak/eth-priv-to-addr:latest
sudo apt-get install libusb-1.0
fi
install:
- npm install --silent
- yarn --silent
jobs:
include:
- stage: test
script: npm run prettier:diff
- stage: test
script: npm run test:coverage -- --maxWorkers=2 && npm run report-coverage
- stage: test
script: npm run tslint && npm run tscheck && npm run freezer && npm run freezer:validate
script:
- |
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
npm run prettier:diff
npm run test:coverage -- --maxWorkers=2
npm run report-coverage
npm run tslint
npm run tscheck
npm run freezer
npm run freezer:validate
fi
- |
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
npm run build:electron
ls -la dist/electron-builds
fi
notifications:
email:
on_success: never
on_failure: never
on_failure: never

View File

@ -1,8 +1,10 @@
# MyCrypto Beta RC (VISIT [MyCryptoHQ/mycrypto.com](https://github.com/MyCryptoHQ/mycrypto.com) for the current site)<br/>Just looking to download? Grab our [latest release](https://github.com/MyCryptoHQ/MyCrypto/releases)
# MyCrypto Web & Desktop Apps
[![Greenkeeper badge](https://badges.greenkeeper.io/MyCryptoHq/MyCrypto.svg)](https://greenkeeper.io/)
[![Coverage Status](https://coveralls.io/repos/github/MyCryptoHQ/MyCrypto/badge.svg?branch=develop)](https://coveralls.io/github/MyCryptoHQ/MyCrypto?branch=develop)
* **Just looking to download?** Grab our [latest release](https://github.com/MyCryptoHQ/MyCrypto/releases).
* **Looking for the old site?** Check out [https://legacy.mycrypto.com](https://legacy.mycrypto.com) or the source at [MyCryptoHQ/mycrypto.com](https://github.com/MyCryptoHQ/mycrypto.com)
## Running the App
This codebase targets Node 8.9.4 (LTS). After `npm install`ing all dependencies (You may be required to install additional system dependencies, due to some node modules relying on them) you can run various commands depending on what you want to do:
@ -132,3 +134,4 @@ npm run test:int
</a>
Cross browser testing and debugging provided by the very lovely team at BrowserStack.

View File

@ -15,8 +15,9 @@ import ErrorScreen from 'components/ErrorScreen';
import PageNotFound from 'components/PageNotFound';
import LogOutPrompt from 'components/LogOutPrompt';
import QrSignerModal from 'containers/QrSignerModal';
import OnboardModal from 'containers/OnboardModal';
import WelcomeModal from 'components/WelcomeModal';
import NewAppReleaseModal from 'components/NewAppReleaseModal';
import { TitleBar } from 'components/ui';
import { Store } from 'redux';
import { pollOfflineStatus, TPollOfflineStatus } from 'actions/config';
import { AppState } from 'reducers';
@ -104,12 +105,17 @@ class RootClass extends Component<Props, State> {
<Provider store={store} key={Math.random()}>
<Router key={Math.random()}>
<React.Fragment>
{process.env.BUILD_ELECTRON && <TitleBar />}
{routes}
<LegacyRoutes />
<LogOutPrompt />
<QrSignerModal />
{process.env.BUILD_ELECTRON && <NewAppReleaseModal />}
{!process.env.DOWNLOADABLE_BUILD && (
<React.Fragment>
<OnboardModal />
{!process.env.BUILD_ELECTRON && <WelcomeModal />}
</React.Fragment>
)}
</React.Fragment>
</Router>
</Provider>

View File

@ -0,0 +1,68 @@
import { TypeKeys } from './constants';
import {
AddressLabel,
AddressLabelEntry,
SetAddressLabel,
ClearAddressLabel,
SetAddressLabelEntry,
ChangeAddressLabelEntry,
SaveAddressLabelEntry,
ClearAddressLabelEntry,
RemoveAddressLabelEntry
} from './actionTypes';
export type TSetAddressLabel = typeof setAddressLabel;
export function setAddressLabel(payload: AddressLabel): SetAddressLabel {
return {
type: TypeKeys.SET_ADDRESS_LABEL,
payload
};
}
export type TClearAddressLabel = typeof clearAddressLabel;
export function clearAddressLabel(payload: string): ClearAddressLabel {
return {
type: TypeKeys.CLEAR_ADDRESS_LABEL,
payload
};
}
export type TSetAddressLabelEntry = typeof setAddressLabelEntry;
export function setAddressLabelEntry(payload: AddressLabelEntry): SetAddressLabelEntry {
return {
type: TypeKeys.SET_ADDRESS_LABEL_ENTRY,
payload
};
}
export type TChangeAddressLabelEntry = typeof changeAddressLabelEntry;
export function changeAddressLabelEntry(payload: AddressLabelEntry): ChangeAddressLabelEntry {
return {
type: TypeKeys.CHANGE_ADDRESS_LABEL_ENTRY,
payload
};
}
export type TSaveAddressLabelEntry = typeof saveAddressLabelEntry;
export function saveAddressLabelEntry(payload: string): SaveAddressLabelEntry {
return {
type: TypeKeys.SAVE_ADDRESS_LABEL_ENTRY,
payload
};
}
export type TClearAddressLabelEntry = typeof clearAddressLabelEntry;
export function clearAddressLabelEntry(payload: string): ClearAddressLabelEntry {
return {
type: TypeKeys.CLEAR_ADDRESS_LABEL_ENTRY,
payload
};
}
export type TRemoveAddressLabelEntry = typeof removeAddressLabelEntry;
export function removeAddressLabelEntry(payload: string): RemoveAddressLabelEntry {
return {
type: TypeKeys.REMOVE_ADDRESS_LABEL_ENTRY,
payload
};
}

View File

@ -0,0 +1,60 @@
import { TypeKeys } from './constants';
export interface AddressLabel {
address: string;
label: string;
}
export interface AddressLabelEntry extends AddressLabel {
id: string;
temporaryAddress?: string;
addressError?: string;
temporaryLabel?: string;
labelError?: string;
isEditing?: boolean;
overrideValidation?: boolean;
}
export interface SetAddressLabel {
type: TypeKeys.SET_ADDRESS_LABEL;
payload: AddressLabel;
}
export interface ClearAddressLabel {
type: TypeKeys.CLEAR_ADDRESS_LABEL;
payload: string;
}
export interface SetAddressLabelEntry {
type: TypeKeys.SET_ADDRESS_LABEL_ENTRY;
payload: AddressLabelEntry;
}
export interface ChangeAddressLabelEntry {
type: TypeKeys.CHANGE_ADDRESS_LABEL_ENTRY;
payload: AddressLabelEntry;
}
export interface SaveAddressLabelEntry {
type: TypeKeys.SAVE_ADDRESS_LABEL_ENTRY;
payload: string;
}
export interface ClearAddressLabelEntry {
type: TypeKeys.CLEAR_ADDRESS_LABEL_ENTRY;
payload: string;
}
export interface RemoveAddressLabelEntry {
type: TypeKeys.REMOVE_ADDRESS_LABEL_ENTRY;
payload: string;
}
export type AddressBookAction =
| SetAddressLabel
| ClearAddressLabel
| SetAddressLabelEntry
| ChangeAddressLabelEntry
| SaveAddressLabelEntry
| ClearAddressLabelEntry
| RemoveAddressLabelEntry;

View File

@ -0,0 +1,9 @@
export enum TypeKeys {
SET_ADDRESS_LABEL = 'SET_ADDRESS_LABEL',
CLEAR_ADDRESS_LABEL = 'CLEAR_ADDRESS_LABEL',
SET_ADDRESS_LABEL_ENTRY = 'SET_ADDRESS_LABEL_TEMPORARY_ENTRY',
CHANGE_ADDRESS_LABEL_ENTRY = 'CHANGE_ADDRESS_LABEL_ENTRY',
SAVE_ADDRESS_LABEL_ENTRY = 'SAVE_ADDRESS_LABEL_ENTRY',
CLEAR_ADDRESS_LABEL_ENTRY = 'CLEAR_ADDRESS_LABEL_ENTRY',
REMOVE_ADDRESS_LABEL_ENTRY = 'REMOVE_ADDRESS_LABEL_ENTRY'
}

View File

@ -0,0 +1,3 @@
export * from './actionTypes';
export * from './actionCreators';
export * from './constants';

View File

@ -47,11 +47,11 @@ export const rateSymbols: IRateSymbols = {
// TODO - internationalize
const ERROR_MESSAGE = 'Could not fetch rate data.';
const CCApi = 'https://min-api.cryptocompare.com';
const CCApi = 'https://proxy.mycryptoapi.com/cc';
const CCRates = (symbols: string[]) => {
const tsyms = rateSymbols.symbols.all.concat(symbols as any).join(',');
return `${CCApi}/data/price?fsym=ETH&tsyms=${tsyms}`;
return `${CCApi}/price?fsym=ETH&tsyms=${tsyms}`;
};
export interface CCResponse {

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,139 @@
@import 'common/sass/variables/spacing';
@import 'common/sass/variables/colors';
.AddressBookTable {
&-row {
display: flex;
align-items: center;
margin-bottom: $space-md;
@media (max-width: 650px) {
flex-direction: column;
align-items: flex-start;
padding: 1rem;
box-shadow: 0 1px rgba(0, 0, 0, 0.1), 0 1px 4px rgba(0, 0, 0, 0.12);
}
&-label {
flex: 1 0;
}
&-error {
align-items: flex-end;
align-self: stretch;
padding: 0;
box-shadow: none;
color: $brand-danger;
&-temporary-input--non-mobile {
flex: 1;
visibility: hidden;
&-address {
align-self: flex-start;
}
&-label {
align-self: flex-end;
}
@media (max-width: 650px) {
display: none;
}
}
&--non-mobile {
justify-content: flex-end;
@media (max-width: 650px) {
display: none;
}
}
&--mobile {
@media (min-width: 650px) {
display: none;
}
}
}
&-identicon {
margin-left: $space-sm;
&-mobile {
width: 30px;
height: 30px;
margin-right: $space-md;
@media (min-width: 650px) {
display: none;
}
}
&-non-mobile {
width: 80px;
height: 80px;
@media (max-width: 650px) {
display: none;
}
}
}
&-labels {
@media (max-width: 650px) {
display: none;
}
}
&-input {
display: flex;
align-items: center;
flex-direction: row;
align-self: stretch;
flex: 1;
margin-right: $space-sm;
@media (max-width: 650px) {
margin-right: 0;
margin-bottom: $space-md;
}
label {
margin-bottom: $space-sm;
}
.btn {
margin-left: $space-sm;
height: $space-lg * 2;
@media (max-width: 650px) {
align-self: flex-end;
}
}
&-wrapper {
flex: 1;
&-label {
@media (min-width: 651px) {
display: none;
}
}
&-error {
color: $brand-danger;
align-self: end;
}
}
}
}
input {
margin-bottom: 0;
}
.is-visible {
visibility: visible;
}
}

View File

@ -0,0 +1,320 @@
import React from 'react';
import { connect, MapStateToProps } from 'react-redux';
import classnames from 'classnames';
import { AppState } from 'reducers';
import translate, { translateRaw } from 'translations';
import {
changeAddressLabelEntry,
TChangeAddressLabelEntry,
saveAddressLabelEntry,
TSaveAddressLabelEntry,
removeAddressLabelEntry,
TRemoveAddressLabelEntry
} from 'actions/addressBook';
import {
getAddressLabels,
getLabelAddresses,
getAddressLabelRows,
getAddressBookTableEntry
} from 'selectors/addressBook';
import { Input, Identicon } from 'components/ui';
import AddressBookTableRow from './AddressBookTableRow';
import './AddressBookTable.scss';
interface DispatchProps {
changeAddressLabelEntry: TChangeAddressLabelEntry;
saveAddressLabelEntry: TSaveAddressLabelEntry;
removeAddressLabelEntry: TRemoveAddressLabelEntry;
}
interface StateProps {
rows: ReturnType<typeof getAddressLabelRows>;
entry: ReturnType<typeof getAddressBookTableEntry>;
addressLabels: ReturnType<typeof getAddressLabels>;
labelAddresses: ReturnType<typeof getLabelAddresses>;
}
type Props = DispatchProps & StateProps;
interface State {
editingRow: number | null;
addressTouched: boolean;
addressBlurred: boolean;
labelTouched: boolean;
labelBlurred: boolean;
}
export const ADDRESS_BOOK_TABLE_ID: string = 'ADDRESS_BOOK_TABLE_ID';
class AddressBookTable extends React.Component<Props, State> {
public state: State = {
editingRow: null,
addressTouched: false,
addressBlurred: false,
labelTouched: false,
labelBlurred: false
};
private addressInput: HTMLInputElement | null = null;
private labelInput: HTMLInputElement | null = null;
public render() {
const {
entry: { temporaryAddress = '', addressError = '', temporaryLabel = '', labelError = '' },
rows
} = this.props;
const { addressTouched, addressBlurred, labelTouched, labelBlurred } = this.state;
// Classnames
const addressTouchedWithError = addressTouched && addressError;
const labelTouchedWithError = labelTouched && labelError;
const nonMobileTemporaryInputErrorClassName =
'AddressBookTable-row-error-temporary-input--non-mobile';
const nonMobileTemporaryAddressErrorClassName = classnames({
[nonMobileTemporaryInputErrorClassName]: true,
[`${nonMobileTemporaryInputErrorClassName}-address`]: true,
'is-visible': !!addressTouchedWithError
});
const nonMobileTemporaryLabelErrorClassName = classnames({
[nonMobileTemporaryInputErrorClassName]: true,
[`${nonMobileTemporaryInputErrorClassName}-label`]: true,
'is-visible': !!labelTouchedWithError
});
return (
<section className="AddressBookTable" onKeyDown={this.handleKeyDown}>
<div className="AddressBookTable-row AddressBookTable-row-labels">
<label className="AddressBookTable-row-label" htmlFor="temporaryAddress">
{translate('ADDRESS')}
</label>
<label className="AddressBookTable-row-label" htmlFor="temporaryLabel">
{translate('LABEL')}
</label>
</div>
<div className="AddressBookTable-row AddressBookTable-row-inputs">
<div className="AddressBookTable-row-input">
<div className="AddressBookTable-row-input-wrapper">
<label
className="AddressBookTable-row-input-wrapper-label"
htmlFor="temporaryAddress"
>
{translate('ADDRESS')}
</label>
<Input
name="temporaryAddress"
placeholder={translateRaw('NEW_ADDRESS')}
value={temporaryAddress}
onChange={this.handleAddressChange}
onFocus={this.setAddressTouched}
onBlur={this.setAddressBlurred}
setInnerRef={this.setAddressInputRef}
isValid={!addressTouchedWithError}
/>
</div>
<div className="AddressBookTable-row-identicon AddressBookTable-row-identicon-non-mobile">
<Identicon address={temporaryAddress} />
</div>
<div className="AddressBookTable-row-identicon AddressBookTable-row-identicon-mobile">
<Identicon address={temporaryAddress} size="3rem" />
</div>
</div>
<div className="AddressBookTable-row AddressBookTable-row-error AddressBookTable-row-error--mobile">
<label className="AddressBookTable-row-input-wrapper-error">
{addressBlurred && addressError}
</label>
</div>
<div className="AddressBookTable-row-input">
<div className="AddressBookTable-row-input-wrapper">
<label className="AddressBookTable-row-input-wrapper-label" htmlFor="temporaryLabel">
{translate('LABEL')}
</label>
<Input
name="temporaryLabel"
placeholder={translateRaw('NEW_LABEL')}
value={temporaryLabel}
onChange={this.handleLabelChange}
onFocus={this.setLabelTouched}
onBlur={this.setLabelBlurred}
setInnerRef={this.setLabelInputRef}
isValid={!labelTouchedWithError}
/>
</div>
<button
title={translateRaw('ADD_LABEL')}
className="btn btn-sm btn-success"
onClick={this.handleAddEntry}
>
<i className="fa fa-plus" />
</button>
</div>
<div className="AddressBookTable-row AddressBookTable-row-error AddressBookTable-row-error--mobile">
<label className="AddressBookTable-row-input-wrapper-error">
{labelBlurred && labelError}
</label>
</div>
</div>
<div className="AddressBookTable-row AddressBookTable-row-error">
<label className={nonMobileTemporaryAddressErrorClassName}>
{addressBlurred && addressError}
</label>
<label className={nonMobileTemporaryLabelErrorClassName}>
{labelBlurred && labelError}
</label>
</div>
{rows.map(this.makeLabelRow)}
</section>
);
}
private handleAddEntry = () => {
const { entry: { temporaryAddress, addressError, labelError } } = this.props;
if (!temporaryAddress || addressError || temporaryAddress.length === 0) {
return this.addressInput && this.addressInput.focus();
}
if (labelError && this.labelInput) {
this.labelInput.focus();
}
this.props.saveAddressLabelEntry(ADDRESS_BOOK_TABLE_ID);
if (!addressError && !labelError) {
this.clearFieldStatuses();
this.setEditingRow(null);
}
};
private handleKeyDown = (e: React.KeyboardEvent<HTMLTableElement>) => {
if (e.key === 'Enter') {
this.handleAddEntry();
}
};
private setEditingRow = (editingRow: number | null) => this.setState({ editingRow });
private clearEditingRow = () => this.setEditingRow(null);
private makeLabelRow = (row: any, index: number) => {
const { editingRow } = this.state;
const { id, address, label, temporaryLabel, labelError } = row;
const isEditing = index === editingRow;
const onChange = (newLabel: string) =>
this.props.changeAddressLabelEntry({
id,
address,
label: newLabel,
isEditing: true
});
const onSave = () => {
this.props.saveAddressLabelEntry(id);
this.setEditingRow(null);
};
const onLabelInputBlur = () => {
// If the new changes aren't valid, undo them.
if (labelError) {
this.props.changeAddressLabelEntry({
id,
address,
temporaryAddress: address,
label,
temporaryLabel: label,
overrideValidation: true
});
}
this.clearEditingRow();
};
return (
<AddressBookTableRow
key={address}
index={index}
address={address}
label={label}
temporaryLabel={temporaryLabel}
labelError={labelError}
isEditing={isEditing}
onChange={onChange}
onSave={onSave}
onLabelInputBlur={onLabelInputBlur}
onEditClick={() => this.setEditingRow(index)}
onRemoveClick={() => this.props.removeAddressLabelEntry(id)}
/>
);
};
private setAddressInputRef = (node: HTMLInputElement) => (this.addressInput = node);
private setAddressTouched = () =>
!this.state.addressTouched && this.setState({ addressTouched: true });
private clearAddressTouched = () => this.setState({ addressTouched: false });
private setAddressBlurred = () => this.setState({ addressBlurred: true });
private handleAddressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { entry } = this.props;
const address = e.target.value;
const label = entry.temporaryLabel || '';
this.props.changeAddressLabelEntry({
id: ADDRESS_BOOK_TABLE_ID,
address,
label
});
this.setState(
{ addressTouched: true },
() => address.length === 0 && this.clearAddressTouched()
);
};
private setLabelInputRef = (node: HTMLInputElement) => (this.labelInput = node);
private setLabelTouched = () => !this.state.labelTouched && this.setState({ labelTouched: true });
private clearLabelTouched = () => this.setState({ labelTouched: false });
private setLabelBlurred = () => this.setState({ labelBlurred: true });
private handleLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { entry } = this.props;
const address = entry.temporaryAddress || '';
const label = e.target.value;
this.props.changeAddressLabelEntry({
id: ADDRESS_BOOK_TABLE_ID,
address,
label
});
this.setState({ labelTouched: true }, () => label.length === 0 && this.clearLabelTouched());
};
private clearFieldStatuses = () =>
this.setState({
addressTouched: false,
addressBlurred: false,
labelTouched: false,
labelBlurred: false
});
}
const mapStateToProps: MapStateToProps<StateProps, {}, AppState> = state => ({
rows: getAddressLabelRows(state),
entry: getAddressBookTableEntry(state),
addressLabels: getAddressLabels(state),
labelAddresses: getLabelAddresses(state)
});
const mapDispatchToProps: DispatchProps = {
changeAddressLabelEntry,
saveAddressLabelEntry,
removeAddressLabelEntry
};
export default connect(mapStateToProps, mapDispatchToProps)(AddressBookTable);

View File

@ -0,0 +1,150 @@
import React from 'react';
import translate, { translateRaw } from 'translations';
import noop from 'lodash/noop';
import { Input, Identicon } from 'components/ui';
interface Props {
index: number;
address: string;
label: string;
temporaryLabel: string;
labelError?: string;
isEditing: boolean;
onChange(label: string): void;
onSave(): void;
onLabelInputBlur(): void;
onEditClick(): void;
onRemoveClick(): void;
}
interface State {
labelInputTouched: boolean;
}
class AddressBookTableRow extends React.Component<Props> {
public state: State = {
labelInputTouched: false
};
private labelInput: HTMLInputElement | null = null;
public componentWillReceiveProps(nextProps: Props) {
this.setState({ label: nextProps.label, mostRecentValidLabel: nextProps.label });
}
public render() {
const {
address,
temporaryLabel,
labelError,
isEditing,
onEditClick,
onRemoveClick
} = this.props;
const { labelInputTouched } = this.state;
const trOnClick = isEditing ? noop : onEditClick;
const hashName = `${address}-hash`;
const labelName = `${address}-label`;
return (
<React.Fragment>
<div className="AddressBookTable-row" onClick={trOnClick}>
<div className="AddressBookTable-row-input">
<div className="AddressBookTable-row-input-wrapper">
<label htmlFor={hashName} className="AddressBookTable-row-input-wrapper-label">
{translate('ADDRESS')}
</label>
<Input
name={hashName}
title={address}
value={address}
readOnly={true}
isValid={true}
/>
</div>
<div className="AddressBookTable-row-identicon AddressBookTable-row-identicon-non-mobile">
<Identicon address={address} />
</div>
<div className="AddressBookTable-row-identicon AddressBookTable-row-identicon-mobile">
<Identicon address={address} size="3rem" />
</div>
</div>
<div className="AddressBookTable-row-input">
<div className="AddressBookTable-row-input-wrapper">
<label htmlFor={labelName} className="AddressBookTable-row-input-wrapper-label">
{translate('LABEL')}
</label>
<Input
name={labelName}
title={`${translateRaw('EDIT_LABEL_FOR')} ${address}`}
value={temporaryLabel}
onChange={this.handleLabelChange}
onKeyDown={this.handleKeyDown}
onFocus={this.setLabelTouched}
onBlur={this.handleBlur}
showInvalidBeforeBlur={true}
setInnerRef={this.setLabelInputRef}
isValid={!(labelInputTouched && labelError)}
/>
</div>
<button
title={translateRaw('REMOVE_LABEL')}
className="btn btn-sm btn-danger"
onClick={onRemoveClick}
>
<i className="fa fa-close" />
</button>
</div>
{labelError && (
<div className="AddressBookTable-row AddressBookTable-row-error AddressBookTable-row-error--mobile">
<label className="AddressBookTable-row-input-wrapper-error">{labelError}</label>
</div>
)}
</div>
{labelError && (
<div className="AddressBookTable-row AddressBookTable-row-error AddressBookTable-row-error--non-mobile">
<label className="AddressBookTable-row-input-wrapper-error">{labelError}</label>
</div>
)}
</React.Fragment>
);
}
private handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const { labelInputTouched } = this.state;
e.stopPropagation();
if (e.key === 'Enter' && this.labelInput) {
this.labelInput.blur();
} else if (!labelInputTouched) {
this.setLabelTouched();
}
};
private handleBlur = () => {
this.clearLabelTouched();
this.props.onSave();
this.props.onLabelInputBlur();
};
private setLabelInputRef = (node: HTMLInputElement) => (this.labelInput = node);
private setLabelTouched = () =>
!this.state.labelInputTouched && this.setState({ labelInputTouched: true });
private clearLabelTouched = () => this.setState({ labelInputTouched: false });
private handleLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const label = e.target.value;
this.props.onChange(label);
this.setState(
{ labelInputTouched: true },
() => label.length === 0 && this.clearLabelTouched()
);
};
}
export default AddressBookTableRow;

View File

@ -9,25 +9,35 @@ interface Props {
isReadOnly?: boolean;
isSelfAddress?: boolean;
isCheckSummed?: boolean;
showLabelMatch?: boolean;
}
export const AddressField: React.SFC<Props> = ({ isReadOnly, isSelfAddress, isCheckSummed }) => (
export const AddressField: React.SFC<Props> = ({
isReadOnly,
isSelfAddress,
isCheckSummed,
showLabelMatch
}) => (
<AddressFieldFactory
isSelfAddress={isSelfAddress}
withProps={({ currentTo, isValid, onChange, readOnly }) => (
showLabelMatch={showLabelMatch}
withProps={({ currentTo, isValid, isLabelEntry, onChange, onFocus, onBlur, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">
{translate(isSelfAddress ? 'X_ADDRESS' : 'SEND_ADDR')}
</div>
<Input
className={`input-group-input ${isValid ? '' : 'invalid'}`}
className={`input-group-input ${!isValid && !isLabelEntry ? 'invalid' : ''}`}
isValid={isValid}
type="text"
value={isCheckSummed ? toChecksumAddress(currentTo.raw) : currentTo.raw}
placeholder={donationAddressMap.ETH}
readOnly={!!(isReadOnly || readOnly)}
spellCheck={false}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</label>
</div>

View File

@ -0,0 +1,76 @@
@import 'common/sass/variables';
@import 'common/sass/variables/colors';
@import 'common/sass/variables/spacing';
.AddressFieldDropdown {
position: absolute;
top: calc(100% - #{$space});
width: 100%;
border: 1px solid $gray-lighter;
border-top: none;
background: $component-active-color;
z-index: 1;
list-style-type: none;
margin: 0;
padding: 0;
box-shadow: $tab-box-shadow;
text-align: center;
&-dropdown-item {
margin: 0;
padding: $space-md $space;
display: flex;
align-items: center;
&:hover {
cursor: pointer;
background: $gray-lightest;
}
&-identicon {
width: 40px;
height: 40px;
margin-right: $space-sm;
.Identicon {
width: 40px !important;
height: 40px !important;
}
@media (max-width: 380px) {
display: none;
}
}
&-label,
&-address {
overflow: hidden;
text-overflow: ellipsis;
}
&-label {
flex: 1 0 0;
}
&-address {
flex: 3 0 0;
}
&-no-match {
word-break: break-word;
&:hover {
cursor: not-allowed;
background: inherit;
}
i {
margin-right: $space-sm;
}
}
&--active {
background: $gray-lighter;
}
}
}

View File

@ -0,0 +1,165 @@
import React from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import translate, { translateRaw } from 'translations';
import { setCurrentTo, TSetCurrentTo } from 'actions/transaction';
import { getLabelAddresses } from 'selectors/addressBook';
import { getToRaw } from 'selectors/transaction/fields';
import { Address, Identicon } from 'components/ui';
import './AddressFieldDropdown.scss';
interface StateProps {
labelAddresses: ReturnType<typeof getLabelAddresses>;
currentTo: ReturnType<typeof getToRaw>;
}
interface DispatchProps {
setCurrentTo: TSetCurrentTo;
}
type Props = StateProps & DispatchProps;
interface State {
activeIndex: number | null;
}
class AddressFieldDropdown extends React.Component<Props> {
public state: State = {
activeIndex: null
};
public componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
}
public componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
}
public render() {
const { currentTo } = this.props;
const noMatchContent = currentTo.startsWith('0x') ? null : (
<li className="AddressFieldDropdown-dropdown-item AddressFieldDropdown-dropdown-item-no-match">
<i className="fa fa-warning" /> {translate('NO_LABEL_FOUND_CONTAINING')} "{currentTo}".
</li>
);
return this.props.currentTo.length > 1 ? (
<ul className="AddressFieldDropdown" role="listbox">
{this.getFilteredLabels().length > 0 ? this.renderDropdownItems() : noMatchContent}
</ul>
) : null;
}
private renderDropdownItems = () =>
this.getFilteredLabels().map((filteredLabel, index: number) => {
const { activeIndex } = this.state;
const { address, label } = filteredLabel;
const isActive = activeIndex === index;
const className = `AddressFieldDropdown-dropdown-item ${
isActive ? 'AddressFieldDropdown-dropdown-item--active' : ''
}`;
return (
<li
key={address}
className={className}
onClick={() => this.props.setCurrentTo(address)}
role="option"
title={`${translateRaw('SEND_TO')}${label}`}
>
<div className="AddressFieldDropdown-dropdown-item-identicon">
<Identicon address={address} />
</div>
<strong className="AddressFieldDropdown-dropdown-item-label">{label}</strong>
<em className="AddressFieldDropdown-dropdown-item-address">
<Address address={address} />
</em>
</li>
);
});
private getFilteredLabels = () =>
Object.keys(this.props.labelAddresses)
.filter(label => label.toLowerCase().includes(this.props.currentTo.toLowerCase()))
.map(label => ({ address: this.props.labelAddresses[label], label }))
.slice(0, 5);
private getIsVisible = () =>
this.props.currentTo.length > 1 && this.getFilteredLabels().length > 0;
private handleKeyDown = (e: KeyboardEvent) => {
if (this.getIsVisible()) {
switch (e.key) {
case 'Enter':
e.preventDefault();
return this.handleEnterKeyDown();
case 'ArrowUp':
e.preventDefault();
return this.handleUpArrowKeyDown();
case 'ArrowDown':
e.preventDefault();
return this.handleDownArrowKeyDown();
default:
return;
}
}
};
private handleEnterKeyDown = () => {
const { activeIndex } = this.state;
if (activeIndex !== null) {
const filteredLabels = this.getFilteredLabels();
filteredLabels.forEach(({ address }, index) => {
if (activeIndex === index) {
this.props.setCurrentTo(address);
}
});
this.clearActiveIndex();
}
};
private handleUpArrowKeyDown = () => {
const { activeIndex: previousActiveIndex } = this.state;
const filteredLabelCount = this.getFilteredLabels().length;
let activeIndex =
previousActiveIndex === null ? filteredLabelCount - 1 : previousActiveIndex - 1;
// Loop back to end
if (activeIndex < 0) {
activeIndex = filteredLabelCount - 1;
}
this.setState({ activeIndex });
};
private handleDownArrowKeyDown = () => {
const { activeIndex: previousActiveIndex } = this.state;
const filteredLabelCount = this.getFilteredLabels().length;
let activeIndex = previousActiveIndex === null ? 0 : previousActiveIndex + 1;
// Loop back to beginning
if (activeIndex >= filteredLabelCount) {
activeIndex = 0;
}
this.setState({ activeIndex });
};
private setActiveIndex = (activeIndex: number | null) => this.setState({ activeIndex });
private clearActiveIndex = () => this.setActiveIndex(null);
}
export default connect(
(state: AppState) => ({
labelAddresses: getLabelAddresses(state),
currentTo: getToRaw(state)
}),
{ setCurrentTo }
)(AddressFieldDropdown);

View File

@ -0,0 +1,3 @@
.AddressField {
position: relative;
}

View File

@ -4,6 +4,7 @@ import { AddressInputFactory } from './AddressInputFactory';
import React from 'react';
import { connect } from 'react-redux';
import { ICurrentTo } from 'selectors/transaction';
import './AddressFieldFactory.scss';
interface DispatchProps {
setCurrentTo: TSetCurrentTo;
@ -12,19 +13,33 @@ interface DispatchProps {
interface OwnProps {
to: string | null;
isSelfAddress?: boolean;
showLabelMatch?: boolean;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
interface State {
isFocused: boolean;
}
export interface CallbackProps {
isValid: boolean;
isLabelEntry: boolean;
readOnly: boolean;
currentTo: ICurrentTo;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
onFocus(ev: React.FormEvent<HTMLInputElement>): void;
onBlur(ev: React.FormEvent<HTMLInputElement>): void;
}
type Props = DispatchProps & OwnProps;
class AddressFieldFactoryClass extends React.Component<Props> {
public state: State = {
isFocused: false
};
private goingToBlur: number | null = null;
public componentDidMount() {
// this 'to' parameter can be either token or actual field related
const { to } = this.props;
@ -33,34 +48,62 @@ class AddressFieldFactoryClass extends React.Component<Props> {
}
}
public componentWillUnmount() {
if (this.goingToBlur) {
window.clearTimeout(this.goingToBlur);
}
}
public render() {
return (
<AddressInputFactory
isSelfAddress={this.props.isSelfAddress}
onChange={this.setAddress}
withProps={this.props.withProps}
/>
<div className="AddressField">
<AddressInputFactory
isSelfAddress={this.props.isSelfAddress}
showLabelMatch={this.props.showLabelMatch}
isFocused={this.state.isFocused}
onChange={this.setAddress}
onFocus={this.focus}
onBlur={this.setBlurTimeout}
withProps={this.props.withProps}
/>
</div>
);
}
private focus = () => this.setState({ isFocused: true });
private blur = () => this.setState({ isFocused: false });
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.setCurrentTo(value);
};
private setBlurTimeout = () => (this.goingToBlur = window.setTimeout(this.blur, 150));
}
const AddressFieldFactory = connect(null, { setCurrentTo })(AddressFieldFactoryClass);
interface DefaultAddressFieldProps {
isSelfAddress?: boolean;
showLabelMatch?: boolean;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ isSelfAddress, withProps }) => (
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({
isSelfAddress,
showLabelMatch,
withProps
}) => (
<Query
params={['to']}
withQuery={({ to }) => (
<AddressFieldFactory to={to} isSelfAddress={isSelfAddress} withProps={withProps} />
<AddressFieldFactory
to={to}
isSelfAddress={isSelfAddress}
showLabelMatch={showLabelMatch}
withProps={withProps}
/>
)}
/>
);

View File

@ -1,4 +1,6 @@
@import 'common/sass/variables';
@import 'common/sass/variables/spacing';
@import 'common/sass/variables/colors';
.AddressInput {
display: flex;
@ -6,11 +8,25 @@
flex-wrap: nowrap;
&-input {
position: relative;
flex-grow: 1;
&-with-label {
margin-bottom: $space-lg;
}
&-label {
position: absolute;
width: 100%;
color: darken($brand-success, 15%);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&-identicon {
padding-top: .75rem;
padding-top: $space-md;
transform: translateX(20%);
@media (max-width: $screen-sm) {
@ -21,5 +37,9 @@
height: 3.4rem !important;
}
}
@media (max-width: 380px) {
display: none;
}
}
}

View File

@ -1,7 +1,14 @@
import React, { Component } from 'react';
import { Identicon, Spinner } from 'components/ui';
import { Query } from 'components/renderCbs';
import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction';
import { translateRaw } from 'translations';
import {
ICurrentTo,
getCurrentTo,
isValidCurrentTo,
isCurrentToLabelEntry
} from 'selectors/transaction';
import { getCurrentToLabel } from 'selectors/addressBook';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallbackProps } from 'components/AddressFieldFactory';
@ -10,17 +17,24 @@ import { getWalletInst } from 'selectors/wallet';
import { getResolvingDomain } from 'selectors/ens';
import { isValidENSAddress } from 'libs/validators';
import { Address } from 'libs/units';
import AddressFieldDropdown from './AddressFieldDropdown';
import './AddressInputFactory.scss';
interface StateProps {
currentTo: ICurrentTo;
label: string | null;
isValid: boolean;
isLabelEntry: boolean;
isResolving: boolean;
}
interface OwnProps {
isSelfAddress?: boolean;
showLabelMatch?: boolean;
isFocused?: boolean;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
onFocus(ev: React.FormEvent<HTMLInputElement>): void;
onBlur(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
@ -46,24 +60,51 @@ type Props = OwnProps & StateProps;
class AddressInputFactoryClass extends Component<Props> {
public render() {
const { currentTo, onChange, isValid, withProps, isSelfAddress, isResolving } = this.props;
const {
label,
currentTo,
onChange,
onFocus,
onBlur,
isValid,
isLabelEntry,
withProps,
showLabelMatch,
isSelfAddress,
isResolving,
isFocused
} = this.props;
const { value } = currentTo;
const addr = addHexPrefix(value ? value.toString('hex') : '0');
const inputClassName = `AddressInput-input ${label ? 'AddressInput-input-with-label' : ''}`;
const sendingTo = `${translateRaw('SENDING_TO')} ${label}`;
const isENSAddress = currentTo.raw.includes('.eth');
return (
<div className="AddressInput form-group">
<div className="AddressInput-input">
<div className={inputClassName}>
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
withProps({
currentTo,
isValid,
isLabelEntry,
onChange,
onFocus,
onBlur,
readOnly: !!(readOnly || this.props.isResolving || isSelfAddress)
})
}
/>
<ENSStatus ensAddress={currentTo.raw} isLoading={isResolving} rawAddress={addr} />
{isFocused && !isENSAddress && <AddressFieldDropdown />}
{showLabelMatch &&
label && (
<div title={sendingTo} className="AddressInput-input-label">
<i className="fa fa-check" /> {sendingTo}
</div>
)}
</div>
<div className="AddressInput-identicon">
<Identicon address={addr} />
@ -88,7 +129,9 @@ export const AddressInputFactory = connect((state: AppState, ownProps: OwnProps)
return {
currentTo,
label: getCurrentToLabel(state),
isResolving: getResolvingDomain(state),
isValid: isValidCurrentTo(state)
isValid: isValidCurrentTo(state),
isLabelEntry: isCurrentToLabelEntry(state)
};
})(AddressInputFactoryClass);

View File

@ -23,9 +23,7 @@ export const AmountField: React.SFC<Props> = ({
<label className="AmountField-group input-group input-group-inline">
<div className="input-group-header">{translate('SEND_AMOUNT_SHORT')}</div>
<Input
className={`input-group-input ${
isAmountValid(raw, customValidator, isValid) ? '' : 'invalid'
}`}
isValid={isAmountValid(raw, customValidator, isValid)}
type="number"
placeholder="1"
value={raw}

View File

@ -0,0 +1,272 @@
import React from 'react';
import { connect, MapStateToProps } from 'react-redux';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import translate, { translateRaw } from 'translations';
import { AppState } from 'reducers';
import {
changeAddressLabelEntry,
TChangeAddressLabelEntry,
saveAddressLabelEntry,
TSaveAddressLabelEntry,
removeAddressLabelEntry,
TRemoveAddressLabelEntry
} from 'actions/addressBook';
import { getAccountAddressEntry, getAddressLabels } from 'selectors/addressBook';
import { Address, Identicon, Input } from 'components/ui';
interface StateProps {
entry: ReturnType<typeof getAccountAddressEntry>;
addressLabels: ReturnType<typeof getAddressLabels>;
}
interface DispatchProps {
changeAddressLabelEntry: TChangeAddressLabelEntry;
saveAddressLabelEntry: TSaveAddressLabelEntry;
removeAddressLabelEntry: TRemoveAddressLabelEntry;
}
interface OwnProps {
address: string;
}
type Props = StateProps & DispatchProps & OwnProps;
interface State {
copied: boolean;
editingLabel: boolean;
labelInputTouched: boolean;
}
export const ACCOUNT_ADDRESS_ID: string = 'ACCOUNT_ADDRESS_ID';
class AccountAddress extends React.Component<Props, State> {
public state = {
copied: false,
editingLabel: false,
labelInputTouched: false
};
private goingToClearCopied: number | null = null;
private labelInput: HTMLInputElement | null = null;
public handleCopy = () =>
this.setState(
(prevState: State) => ({
copied: !prevState.copied
}),
this.clearCopied
);
public componentWillUnmount() {
if (this.goingToClearCopied) {
window.clearTimeout(this.goingToClearCopied);
}
}
public render() {
const { address, addressLabels } = this.props;
const { copied } = this.state;
const label = addressLabels[address];
const labelContent = this.generateLabelContent();
const labelButton = this.generateLabelButton();
const addressClassName = `AccountInfo-address-addr ${
label ? 'AccountInfo-address-addr--small' : ''
}`;
return (
<div className="AccountInfo">
<h5 className="AccountInfo-section-header">{translate('SIDEBAR_ACCOUNTADDR')}</h5>
<div className="AccountInfo-section AccountInfo-address-section">
<div className="AccountInfo-address-icon">
<Identicon address={address} size="100%" />
</div>
<div className="AccountInfo-address-wrapper">
{labelContent}
<div className={addressClassName}>
<Address address={address} />
</div>
<CopyToClipboard onCopy={this.handleCopy} text={address}>
<div
className={`AccountInfo-copy ${copied ? 'is-copied' : ''}`}
title="Copy To clipboard"
>
<i className="fa fa-copy" />
<span>{copied ? 'copied!' : 'copy address'}</span>
</div>
</CopyToClipboard>
<div className="AccountInfo-label" title="Edit label">
{labelButton}
</div>
</div>
</div>
</div>
);
}
private clearCopied = () =>
(this.goingToClearCopied = window.setTimeout(() => this.setState({ copied: false }), 2000));
private startEditingLabel = () =>
this.setState({ editingLabel: true }, () => {
if (this.labelInput) {
this.labelInput.focus();
this.labelInput.select();
}
});
private stopEditingLabel = () => this.setState({ editingLabel: false });
private setLabelInputRef = (node: HTMLInputElement) => (this.labelInput = node);
private generateLabelContent = () => {
const { address, addressLabels, entry: { temporaryLabel, labelError } } = this.props;
const { editingLabel, labelInputTouched } = this.state;
const storedLabel = addressLabels[address];
const newLabelSameAsPrevious = temporaryLabel === storedLabel;
const labelInputTouchedWithError = labelInputTouched && !newLabelSameAsPrevious && labelError;
let labelContent = null;
if (editingLabel) {
labelContent = (
<React.Fragment>
<Input
title={translateRaw('ADD_LABEL')}
placeholder={translateRaw('NEW_LABEL')}
defaultValue={storedLabel}
onChange={this.handleLabelChange}
onKeyDown={this.handleKeyDown}
onFocus={this.setTemporaryLabelTouched}
onBlur={this.handleBlur}
showInvalidBeforeBlur={true}
setInnerRef={this.setLabelInputRef}
isValid={!labelInputTouchedWithError}
/>
{labelInputTouchedWithError && (
<label className="AccountInfo-address-wrapper-error">{labelError}</label>
)}
</React.Fragment>
);
} else {
labelContent = (
<label title={storedLabel} className="AccountInfo-address-label">
{storedLabel}
</label>
);
}
return labelContent;
};
private generateLabelButton = () => {
const { address, addressLabels } = this.props;
const { editingLabel } = this.state;
const label = addressLabels[address];
const labelButton = editingLabel ? (
<React.Fragment>
<i className="fa fa-save" />
<span role="button" title={translateRaw('SAVE_LABEL')} onClick={this.stopEditingLabel}>
{translate('SAVE_LABEL')}
</span>
</React.Fragment>
) : (
<React.Fragment>
<i className="fa fa-pencil" />
<span
role="button"
title={label ? translateRaw('EDIT_LABEL') : translateRaw('ADD_LABEL_9')}
onClick={this.startEditingLabel}
>
{label ? translate('EDIT_LABEL') : translate('ADD_LABEL_9')}
</span>
</React.Fragment>
);
return labelButton;
};
private handleBlur = () => {
const { address, addressLabels, entry: { id, label, temporaryLabel, labelError } } = this.props;
const storedLabel = addressLabels[address];
this.clearTemporaryLabelTouched();
this.stopEditingLabel();
if (temporaryLabel === storedLabel) {
return;
}
if (temporaryLabel && temporaryLabel.length > 0) {
this.props.saveAddressLabelEntry(id);
if (labelError) {
// If the new changes aren't valid, undo them.
this.props.changeAddressLabelEntry({
id,
address,
temporaryAddress: address,
label,
temporaryLabel: label,
overrideValidation: true
});
}
} else {
this.props.removeAddressLabelEntry(id);
}
};
private handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case 'Enter':
return this.handleBlur();
case 'Escape':
return this.stopEditingLabel();
}
};
private handleLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { address } = this.props;
const label = e.target.value;
this.props.changeAddressLabelEntry({
id: ACCOUNT_ADDRESS_ID,
address,
label,
isEditing: true
});
this.setState(
{
labelInputTouched: true
},
() => label.length === 0 && this.clearTemporaryLabelTouched()
);
};
private setTemporaryLabelTouched = () => {
const { labelInputTouched } = this.state;
if (!labelInputTouched) {
this.setState({ labelInputTouched: true });
}
};
private clearTemporaryLabelTouched = () => this.setState({ labelInputTouched: false });
}
const mapStateToProps: MapStateToProps<StateProps, {}, AppState> = (state: AppState) => ({
entry: getAccountAddressEntry(state),
addressLabels: getAddressLabels(state)
});
const mapDispatchToProps: DispatchProps = {
changeAddressLabelEntry,
saveAddressLabelEntry,
removeAddressLabelEntry
};
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
mapStateToProps,
mapDispatchToProps
)(AccountAddress);

View File

@ -10,11 +10,11 @@
opacity: 0.5;
transition: $transition;
&:hover{
&:hover {
opacity: 1;
}
&.is-copied{
&.is-copied {
color: $brand-success;
opacity: 1;
}
@ -24,8 +24,20 @@
}
}
&-label {
display: inline-block;
cursor: pointer;
color: $text-color;
font-size: $font-size-xs;
opacity: 0.5;
.fa {
margin: 0 $space-xs 0 $space-sm;
}
}
&-section {
margin-top: $space * 1.5;
margin: $space 0;
&:first-child {
margin-top: 0;
@ -72,10 +84,16 @@
max-width: 100%;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
}
&-wrapper {
max-width: calc(100% - 44px - 24px);
&-error {
color: $brand-danger;
margin-bottom: $space-md;
}
}
&-icon {
@ -88,6 +106,10 @@
margin-top: -$space-xs;
word-wrap: break-word;
@include mono;
&--small {
font-size: 0.8rem;
}
}
&-confirm {
@ -97,6 +119,14 @@
margin-right: 16px;
}
}
&-label {
font-weight: bolder;
font-size: 1.3rem;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
}
}
&-balance {

View File

@ -1,9 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { toChecksumAddress } from 'ethereumjs-util';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Identicon, UnitDisplay, Address, NewTabLink } from 'components/ui';
import { IWallet, Balance, TrezorWallet, LedgerWallet } from 'libs/wallet';
import { UnitDisplay, NewTabLink } from 'components/ui';
import { IWallet, TrezorWallet, LedgerWallet, Balance } from 'libs/wallet';
import translate from 'translations';
import Spinner from 'components/ui/Spinner';
import { getNetworkConfig, getOffline } from 'selectors/config';
@ -12,6 +11,7 @@ import { NetworkConfig } from 'types/network';
import { TRefreshAccountBalance, refreshAccountBalance } from 'actions/wallet';
import { etherChainExplorerInst } from 'config/data';
import './AccountInfo.scss';
import AccountAddress from './AccountAddress';
interface OwnProps {
wallet: IWallet;
@ -19,15 +19,14 @@ interface OwnProps {
interface StateProps {
balance: Balance;
network: NetworkConfig;
isOffline: boolean;
network: ReturnType<typeof getNetworkConfig>;
isOffline: ReturnType<typeof getOffline>;
}
interface State {
showLongBalance: boolean;
address: string;
confirmAddr: boolean;
copied: boolean;
}
interface DispatchProps {
@ -40,8 +39,7 @@ class AccountInfo extends React.Component<Props, State> {
public state = {
showLongBalance: false,
address: '',
confirmAddr: false,
copied: false
confirmAddr: false
};
public setAddressFromWallet() {
@ -74,20 +72,10 @@ class AccountInfo extends React.Component<Props, State> {
});
};
public onCopy = () => {
this.setState(state => {
return {
copied: !state.copied
};
});
setTimeout(() => {
this.setState({ copied: false });
}, 2000);
};
public render() {
const { network, balance, isOffline } = this.props;
const { network, isOffline, balance } = this.props;
const { address, showLongBalance, confirmAddr } = this.state;
let blockExplorer;
let tokenExplorer;
if (!network.isCustom) {
@ -98,27 +86,8 @@ class AccountInfo extends React.Component<Props, State> {
const wallet = this.props.wallet as LedgerWallet | TrezorWallet;
return (
<div className="AccountInfo">
<h5 className="AccountInfo-section-header">{translate('SIDEBAR_ACCOUNTADDR')}</h5>
<div className="AccountInfo-section AccountInfo-address-section">
<div className="AccountInfo-address-icon">
<Identicon address={address} size="100%" />
</div>
<div className="AccountInfo-address-wrapper">
<div className="AccountInfo-address-addr">
<Address address={address} />
</div>
<CopyToClipboard onCopy={this.onCopy} text={toChecksumAddress(address)}>
<div
className={`AccountInfo-copy ${this.state.copied ? 'is-copied' : ''}`}
title="Copy To clipboard"
>
<i className="fa fa-copy" />
<span>{this.state.copied ? 'copied!' : 'copy address'}</span>
</div>
</CopyToClipboard>
</div>
</div>
<div>
<AccountAddress address={toChecksumAddress(address)} />
{typeof wallet.displayAddress === 'function' && (
<div className="AccountInfo-section">
@ -160,7 +129,7 @@ class AccountInfo extends React.Component<Props, State> {
unit={'ether'}
displayShortBalance={!showLongBalance}
checkOffline={true}
symbol={balance.wei ? network.name : null}
symbol={balance.wei ? this.setSymbol(network) : null}
/>
</span>
{balance.wei && (
@ -214,7 +183,15 @@ class AccountInfo extends React.Component<Props, State> {
</div>
);
}
private setSymbol(network: NetworkConfig) {
if (network.isTestnet) {
return network.unit + ' (' + network.name + ')';
}
return network.unit;
}
}
function mapStateToProps(state: AppState): StateProps {
return {
balance: state.wallet.balance,

View File

@ -88,7 +88,7 @@ class EquivalentValues extends React.Component<Props, State> {
};
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
const { balance, tokenBalances, isOffline, network } = this.props;
if (
nextProps.balance !== balance ||

View File

@ -16,8 +16,13 @@ interface IGenerateSymbolLookup {
[tokenSymbol: string]: boolean;
}
interface IGenerateAddressLookup {
[address: string]: boolean;
}
interface State {
tokenSymbolLookup: IGenerateSymbolLookup;
tokenAddressLookup: IGenerateAddressLookup;
address: string;
symbol: string;
decimal: string;
@ -25,20 +30,13 @@ interface State {
export default class AddCustomTokenForm extends React.PureComponent<Props, State> {
public state: State = {
tokenSymbolLookup: {},
tokenSymbolLookup: this.generateSymbolLookup(),
tokenAddressLookup: this.generateAddressMap(),
address: '',
symbol: '',
decimal: ''
};
constructor(props: Props) {
super(props);
this.state = {
...this.state,
tokenSymbolLookup: this.generateSymbolLookup(props.allTokens)
};
}
public render() {
const { address, symbol, decimal } = this.state;
const errors = this.getErrors();
@ -68,15 +66,14 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
<label className="AddCustom-field form-group" key={field.name}>
<div className="input-group-header">{field.label}</div>
<Input
className={`${
errors[field.name] ? 'invalid' : field.value ? 'valid' : ''
} input-group-input-small`}
isValid={!errors[field.name]}
className="input-group-input-small"
type="text"
name={field.name}
value={field.value}
onChange={this.onFieldChange}
/>
{typeof errors[field.name] === 'string' && (
{errors[field.name] && (
<div className="AddCustom-field-error">{errors[field.name]}</div>
)}
</label>
@ -106,14 +103,19 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
public getErrors() {
const { address, symbol, decimal } = this.state;
const errors: { [key: string]: boolean | string } = {};
const errors: { [key: string]: string } = {};
// Formatting errors
if (decimal && !isPositiveIntegerOrZero(parseInt(decimal, 10))) {
errors.decimal = true;
if (decimal && !isPositiveIntegerOrZero(Number(decimal))) {
errors.decimal = 'Invalid decimal';
}
if (address && !isValidETHAddress(address)) {
errors.address = true;
if (address) {
if (!isValidETHAddress(address)) {
errors.address = 'Not a valid address';
}
if (this.state.tokenAddressLookup[address]) {
errors.address = 'A token with this address already exists';
}
}
// Message errors
@ -146,13 +148,19 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
this.props.onSave({ address, symbol, decimal: parseInt(decimal, 10) });
};
private generateSymbolLookup(tokens: Token[]) {
return tokens.reduce(
(prev, tk) => {
prev[tk.symbol] = true;
return prev;
},
{} as IGenerateSymbolLookup
);
private generateSymbolLookup() {
return this.tknArrToMap('symbol');
}
private generateAddressMap() {
return this.tknArrToMap('address');
}
private tknArrToMap(key: Exclude<keyof Token, 'error'>) {
const tokens = this.props.allTokens;
return tokens.reduce<{ [k: string]: boolean }>((prev, tk) => {
prev[tk[key]] = true;
return prev;
}, {});
}
}

View File

@ -20,7 +20,7 @@ interface TrackedTokens {
}
interface State {
trackedTokens: { [symbol: string]: boolean };
trackedTokens: TrackedTokens;
showCustomTokenForm: boolean;
}
export default class TokenBalances extends React.PureComponent<Props, State> {
@ -29,10 +29,10 @@ export default class TokenBalances extends React.PureComponent<Props, State> {
showCustomTokenForm: false
};
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (nextProps.tokenBalances !== this.props.tokenBalances) {
const trackedTokens = nextProps.tokenBalances.reduce<TrackedTokens>((prev, t) => {
prev[t.symbol] = !t.balance.isZero();
prev[t.symbol] = !t.balance.isZero() || t.custom;
return prev;
}, {});
this.setState({ trackedTokens });
@ -45,7 +45,7 @@ export default class TokenBalances extends React.PureComponent<Props, State> {
let bottom;
let help;
if (tokenBalances.length && !hasSavedWalletTokens) {
if (tokenBalances.length && !hasSavedWalletTokens && !this.onlyCustomTokens()) {
help = 'Select which tokens you would like to keep track of';
bottom = (
<div className="TokenBalances-buttons">
@ -134,6 +134,24 @@ export default class TokenBalances extends React.PureComponent<Props, State> {
});
};
/**
*
* @description Checks if all currently tracked tokens are custom
* @private
* @returns
* @memberof TokenBalances
*/
private onlyCustomTokens() {
const tokenMap = this.props.tokenBalances.reduce<{ [key: string]: TokenBalance }>(
(acc, cur) => ({ ...acc, [cur.symbol]: cur }),
{}
);
return Object.keys(this.state.trackedTokens).reduce(
(prev, tokenName) => tokenMap[tokenName].custom && prev,
true
);
}
private addCustomToken = (token: Token) => {
this.props.onAddCustomToken(token);
this.setState({ showCustomTokenForm: false });

View File

@ -30,11 +30,14 @@ export default class TokenRow extends React.PureComponent<Props, State> {
return (
<tr className="TokenRow" onClick={this.handleToggleTracked}>
{this.props.toggleTracked && (
<td className="TokenRow-toggled">
<input type="checkbox" checked={tracked} />
</td>
)}
{/* Only allow to toggle tracking on non custom tokens
because the user can just remove the custom token instead */}
{!this.props.custom &&
this.props.toggleTracked && (
<td className="TokenRow-toggled">
<input type="checkbox" checked={tracked} />
</td>
)}
<td
className="TokenRow-balance"
title={`${balance.toString()} (Double-Click)`}

View File

@ -1,66 +0,0 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.BetaAgreement {
@include cover-message;
background: $brand-info;
&-content {
h2 {
text-align: center;
}
&-buttons {
padding-top: 20px;
&-btn {
display: block;
width: 100%;
max-width: 280px;
margin: 0 auto;
border: none;
padding: 0;
transition: $transition;
&.is-continue {
height: 60px;
line-height: 60px;
font-size: 22px;
background: rgba(#fff, 0.96);
color: $gray-dark;
border-radius: 4px;
margin-bottom: 20px;
&:hover {
background: #fff;
color: $gray-darker;
}
}
&.is-reject {
background: none;
color: #fff;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
}
}
// Fade out
&.is-fading {
pointer-events: none;
opacity: 0;
background: #fff;
transition: all 500ms ease 400ms;
.BetaAgreement-content {
opacity: 0;
transform: translateY(15px);
transition: all 500ms ease;
}
}
}

View File

@ -1,73 +0,0 @@
import React from 'react';
import { NewTabLink } from 'components/ui';
import { discordURL } from 'config';
import './index.scss';
const LS_KEY = 'acknowledged-beta';
interface State {
isFading: boolean;
hasAcknowledged: boolean;
}
export default class BetaAgreement extends React.PureComponent<{}, State> {
public state = {
hasAcknowledged: !!localStorage.getItem(LS_KEY),
isFading: false
};
public render() {
if (this.state.hasAcknowledged) {
return null;
}
const isFading = this.state.isFading ? 'is-fading' : '';
return (
<div className={`BetaAgreement ${isFading}`}>
<div className="BetaAgreement-content">
<h2>Welcome to the New MyCrypto Beta Release Candidate!</h2>
<p>
You are about to use the new MyCrypto Beta Release Candidate. Although this is a release
candidate for production, we encourage caution while using this unreleased version of
MyCrypto.
</p>
<p>We hope to move this version of MyCrypto into production in the near future!</p>
<p>
Feedback and bug reports are greatly appreciated. You can file issues on our{' '}
<NewTabLink href="https://github.com/MyCryptoHQ/MyCrypto/issues">
GitHub repository
</NewTabLink>{' '}
or join our <NewTabLink href={discordURL}>Discord server</NewTabLink> to discuss the
beta.
</p>
<p>Are you sure you would like to continue?</p>
<div className="BetaAgreement-content-buttons">
<button
className="BetaAgreement-content-buttons-btn is-continue"
onClick={this.doContinue}
>
Yes, continue to the Beta RC
</button>
<button className="BetaAgreement-content-buttons-btn is-reject" onClick={this.reject}>
No, take me to the production site
</button>
</div>
</div>
</div>
);
}
private doContinue = () => {
localStorage.setItem(LS_KEY, 'true');
this.setState({ isFading: true });
setTimeout(() => {
this.setState({ hasAcknowledged: true });
}, 1000);
};
private reject = () => {
window.location.assign('https://mycrypto.com');
};
}

View File

@ -20,7 +20,12 @@ class DetailsClass extends Component<StateProps> {
<div className="tx-modal-details">
<label className="input-group">
<div className="input-group-header">Network</div>
<Input readOnly={true} value={`${network} network - provided by ${service}`} />
<Input
isValid={true}
showValidAsPlain={true}
readOnly={true}
value={`${network} network - provided by ${service}`}
/>
</label>
<SerializedTransaction

View File

@ -29,7 +29,7 @@ class CurrentCustomMessageClass extends PureComponent<Props, State> {
this.setAddressState(this.props);
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.wallet !== nextProps.wallet) {
this.setAddressState(nextProps);
}

View File

@ -117,7 +117,7 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="col-sm-9 input-group flex-grow-1">
<div className="input-group-header">{translate('CUSTOM_NODE_NAME')}</div>
<Input
className={`input-group-input ${this.state.name && invalids.name ? 'invalid' : ''}`}
isValid={!(this.state.name && invalids.name)}
type="text"
placeholder="My Node"
value={this.state.name}
@ -142,9 +142,7 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="col-sm-6 input-group input-group-inline">
<div className="input-group-header">{translate('CUSTOM_NETWORK_NAME')}</div>
<Input
className={`input-group-input ${
this.state.customNetworkId && invalids.customNetworkId ? 'invalid' : ''
}`}
isValid={!(this.state.customNetworkId && invalids.customNetworkId)}
type="text"
placeholder="My Custom Network"
value={this.state.customNetworkId}
@ -154,9 +152,7 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="col-sm-3 input-group input-group-inline">
<div className="input-group-header">{translate('CUSTOM_NETWORK_CURRENCY')}</div>
<Input
className={`input-group-input ${
this.state.customNetworkUnit && invalids.customNetworkUnit ? 'invalid' : ''
}`}
isValid={!(this.state.customNetworkUnit && invalids.customNetworkUnit)}
type="text"
placeholder="ETH"
value={this.state.customNetworkUnit}
@ -166,11 +162,7 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="col-sm-3 input-group input-group-inline">
<div className="input-group-header">{translate('CUSTOM_NETWORK_CHAIN_ID')}</div>
<Input
className={`input-group-input ${
this.state.customNetworkChainId && invalids.customNetworkChainId
? 'invalid'
: ''
}`}
isValid={!(this.state.customNetworkChainId && invalids.customNetworkChainId)}
type="text"
placeholder="1"
value={this.state.customNetworkChainId}
@ -183,7 +175,7 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="input-group input-group-inline">
<div className="input-group-header">{translate('CUSTOM_NETWORK_URL')}</div>
<Input
className={`input-group-input ${this.state.url && invalids.url ? 'invalid' : ''}`}
isValid={!(this.state.url && invalids.url)}
type="text"
placeholder="https://127.0.0.1:8545/"
value={this.state.url}
@ -207,9 +199,7 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="col-sm-6 input-group input-group-inline">
<div className="input-group-header">{translate('INPUT_USERNAME_LABEL')}</div>
<Input
className={`input-group-input ${
this.state.username && invalids.username ? 'invalid' : ''
}`}
isValid={!(this.state.username && invalids.username)}
type="text"
value={this.state.username}
onChange={e => this.setState({ username: e.currentTarget.value })}
@ -218,9 +208,7 @@ class CustomNodeModal extends React.Component<Props, State> {
<label className="col-sm-6 input-group input-group-inline">
<div className="input-group-header">{translate('INPUT_PASSWORD_LABEL')}</div>
<Input
className={`input-group-input ${
this.state.password && invalids.password ? 'invalid' : ''
}`}
isValid={!(this.state.password && invalids.password)}
type="password"
value={this.state.password}
onChange={e => this.setState({ password: e.currentTarget.value })}

View File

@ -6,12 +6,12 @@ import { Input } from 'components/ui';
export const DataField: React.SFC<{}> = () => (
<DataFieldFactory
withProps={({ data: { raw }, dataExists, onChange, readOnly }) => (
withProps={({ data: { raw }, validData, onChange, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('OFFLINE_STEP2_LABEL_6')}</div>
<Input
className={dataExists ? 'is-valid' : 'is-invalid'}
isValid={validData}
type="text"
placeholder={donationAddressMap.ETH}
value={raw}

View File

@ -7,7 +7,7 @@ import { isEtherTransaction } from 'selectors/transaction';
import { AppState } from 'reducers';
export interface CallBackProps {
data: AppState['transaction']['fields']['data'];
dataExists: boolean;
validData: boolean;
readOnly: boolean;
onChange(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>): void;
}

View File

@ -1,9 +1,10 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import { getData, getDataExists } from 'selectors/transaction';
import { getData } from 'selectors/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallBackProps } from 'components/DataFieldFactory';
import { isHexString } from 'ethereumjs-util';
interface OwnProps {
withProps(props: CallBackProps): React.ReactElement<any> | null;
@ -11,19 +12,19 @@ interface OwnProps {
}
interface StateProps {
data: AppState['transaction']['fields']['data'];
dataExists: boolean;
validData: boolean;
}
type Props = OwnProps & StateProps;
class DataInputClass extends Component<Props> {
public render() {
const { data, onChange, dataExists } = this.props;
const { data, onChange, validData } = this.props;
return (
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
this.props.withProps({ data, onChange, readOnly: !!readOnly, dataExists })
this.props.withProps({ data, onChange, readOnly: !!readOnly, validData })
}
/>
);
@ -32,5 +33,5 @@ class DataInputClass extends Component<Props> {
export const DataInput = connect((state: AppState) => ({
data: getData(state),
dataExists: getDataExists(state)
validData: getData(state).raw === '' || isHexString(getData(state).raw)
}))(DataInputClass);

View File

@ -12,7 +12,6 @@ import React from 'react';
import PreFooter from './PreFooter';
import DisclaimerModal from 'components/DisclaimerModal';
import { NewTabLink } from 'components/ui';
import OnboardModal from 'containers/OnboardModal';
import './index.scss';
import { translateRaw } from 'translations';
@ -82,6 +81,9 @@ export default class Footer extends React.PureComponent<Props, State> {
<NewTabLink href="https://about.mycrypto.com">
{translateRaw('FOOTER_TEAM')}
</NewTabLink>
<NewTabLink href="https://about.mycrypto.com/privacy/">
{translateRaw('FOOTER_PRIVACY_POLICY')}
</NewTabLink>
</div>
<p className="Footer-about-text">{translateRaw('FOOTER_ABOUT')}</p>
@ -130,8 +132,6 @@ export default class Footer extends React.PureComponent<Props, State> {
</div>
</div>
</footer>
<OnboardModal />
<DisclaimerModal isOpen={this.state.isDisclaimerOpen} handleClose={this.toggleModal} />
</div>
);

View File

@ -30,7 +30,7 @@ export const GasLimitField: React.SFC<Props> = ({
/>
</div>
<Input
className={gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}
isValid={gasLimitValidator(raw)}
type="number"
placeholder="21000"
readOnly={!!readOnly}

View File

@ -41,7 +41,7 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
}
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (nextProps.privateKey !== this.props.privateKey) {
this.setState({ privateKey: nextProps.privateKey || '' });
}

View File

@ -12,7 +12,6 @@
.OnlineStatus {
position: relative;
top: -2px;
width: 12px;
height: 12px;
text-align: center;

View File

@ -7,8 +7,8 @@ interface Props {
}
const OnlineStatus: React.SFC<Props> = ({ isOffline }) => (
<div className={`OnlineStatus fa-stack ${isOffline ? 'is-offline' : 'is-online'}`}>
<Tooltip>{isOffline ? 'Offline' : 'Online'}</Tooltip>
<div className={`OnlineStatus ${isOffline ? 'is-offline' : 'is-online'}`}>
<Tooltip direction="left">{isOffline ? 'Offline' : 'Online'}</Tooltip>
</div>
);

View File

@ -1,27 +1,12 @@
import React from 'react';
import translate, { translateRaw } from 'translations';
import Modal, { IButton } from 'components/ui/Modal';
import { VERSION_RC } from 'config';
import { isNewerVersion } from 'utils/helpers';
interface IGitHubRelease {
tag_name: string;
}
function getLatestGitHubRelease(): Promise<IGitHubRelease> {
return fetch('https://api.github.com/repos/MyCryptoHQ/MyCrypto/releases/latest', {
method: 'GET',
mode: 'cors',
headers: {
'content-type': 'application/json; charset=utf-8'
}
})
.then(res => res.json())
.then(data => data as IGitHubRelease);
}
import { getLatestElectronRelease } from 'utils/versioning';
import { VERSION } from 'config/data';
interface State {
isOpen: boolean;
newRelease?: string;
}
export default class NewAppReleaseModal extends React.Component<{}, State> {
@ -31,10 +16,9 @@ export default class NewAppReleaseModal extends React.Component<{}, State> {
public async componentDidMount() {
try {
const release = await getLatestGitHubRelease();
// TODO: Use VERSION once done with release candidates
if (isNewerVersion(VERSION_RC, release.tag_name)) {
this.setState({ isOpen: true });
const newRelease = await getLatestElectronRelease();
if (newRelease) {
this.setState({ isOpen: true, newRelease });
}
} catch (err) {
console.error('Failed to fetch latest release from GitHub:', err);
@ -64,11 +48,22 @@ export default class NewAppReleaseModal extends React.Component<{}, State> {
handleClose={this.close}
maxWidth={520}
>
<h5>{translateRaw('APP_UPDATE_BODY')}</h5>
<h4>
{translateRaw('APP_UPDATE_BODY')} {this.versionCompareStr()}
</h4>
</Modal>
);
}
private versionCompareStr() {
return (
<>
<h5>Current Version: {VERSION}</h5>
<h5>New Version: {this.state.newRelease}</h5>
</>
);
}
private close = () => {
this.setState({ isOpen: false });
};

View File

@ -42,7 +42,8 @@ class NonceField extends React.Component<Props> {
/>
</div>
<Input
className={`Nonce-field-input ${!!value ? 'is-valid' : 'is-invalid'}`}
isValid={!!value}
className="Nonce-field-input"
type="number"
placeholder="7"
value={raw}

View File

@ -64,7 +64,7 @@ class OnlineSendClass extends Component<Props, State> {
);
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (nextProps.transactionBroadcasted && this.state.showModal) {
this.closeModal();
}

View File

@ -39,7 +39,7 @@ export default class SubTabs extends React.PureComponent<Props, State> {
window.removeEventListener('resize', this.handleResize);
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
// When new tabs come in, we'll need to uncollapse so that they can
// be measured and collapsed again, if needed.
if (this.props.tabs !== nextProps.tabs) {

View File

@ -76,7 +76,7 @@ class TXMetaDataPanel extends React.Component<Props, State> {
}
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (
(this.props.offline && !nextProps.offline) ||
this.props.network.unit !== nextProps.network.unit

View File

@ -9,7 +9,6 @@ import { NonceField, GasLimitField, DataField } from 'components';
import { connect } from 'react-redux';
import { getAutoGasLimitEnabled } from 'selectors/config';
import { isValidGasPrice } from 'selectors/transaction';
import { sanitizeNumericalInput } from 'libs/values';
import { Input } from 'components/ui';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
import { getScheduleGasPrice, getTimeBounty } from 'selectors/schedule';
@ -83,9 +82,11 @@ class AdvancedGas extends React.Component<Props, State> {
<div className="input-group-header">
{translateRaw('OFFLINE_STEP2_LABEL_3')} (gwei)
</div>
{/*We leave type as string instead of number, because things such as multiple decimals
or invalid exponent notation does not fire the onchange handler
so the component will not display as invalid for such things */}
<Input
className={!!gasPrice.raw && !validGasPrice ? 'invalid' : ''}
type="number"
isValid={validGasPrice}
placeholder="40"
value={gasPrice.raw}
onChange={this.handleGasPriceChange}
@ -173,7 +174,7 @@ class AdvancedGas extends React.Component<Props, State> {
private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputGasPrice(sanitizeNumericalInput(value));
this.props.inputGasPrice(value);
};
private handleToggleAutoGasLimit = (_: React.FormEvent<HTMLInputElement>) => {

View File

@ -54,6 +54,9 @@ class FeeSummary extends React.Component<Props> {
scheduleGasLimit
} = this.props;
if (!gasPrice.value || gasPrice.value.eqn(0) || !gasLimit.value || gasLimit.value.eqn(0)) {
return null;
}
if (isGasEstimating) {
return (
<div className="FeeSummary is-loading">

View File

@ -57,7 +57,7 @@ class SimpleGas extends React.Component<Props> {
this.props.fetchGasEstimates();
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (!this.state.hasSetRecommendedGasPrice && nextProps.gasEstimates) {
this.setState({ hasSetRecommendedGasPrice: true });
this.props.setGasPrice(nextProps.gasEstimates.fast.toString());

View File

@ -40,7 +40,7 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
isVisible: !!this.props.isVisible
};
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.isVisible !== nextProps.isVisible) {
this.setState({ isVisible: !!nextProps.isVisible });
}
@ -69,7 +69,8 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
<div className={`TogglablePassword input-group input-group-inline`}>
{isTextareaWhenVisible && isVisible ? (
<TextArea
className={`${className} ${!isValid ? 'invalid' : ''}`}
isValid={!!isValid}
className={className}
value={value}
name={name}
disabled={disabled}
@ -84,11 +85,12 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
/>
) : (
<Input
isValid={!!isValid}
value={value}
name={name}
disabled={disabled}
type={isVisible ? 'text' : 'password'}
className={`${className} ${!isValid ? 'invalid' : ''} border-rad-right-0`}
className={`${className} border-rad-right-0`}
placeholder={placeholder}
onChange={onChange}
onFocus={onFocus}

View File

@ -31,7 +31,7 @@ class TransactionStatus extends React.Component<Props> {
this.props.fetchTransactionData(this.props.txHash);
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.txHash !== nextProps.txHash) {
this.props.fetchTransactionData(nextProps.txHash);
}

View File

@ -59,6 +59,10 @@ $speed: 500ms;
text-align: center;
padding-bottom: $space;
@media (max-width: $screen-md) {
padding-bottom: $space * 2;
}
&-back {
@include reset-button;
position: absolute;

View File

@ -166,7 +166,8 @@ const WalletDecrypt = withRouter<Props>(
component: TrezorDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink: 'https://doc.satoshilabs.com/trezor-apps/mew.html'
helpLink:
'https://support.mycrypto.com/accessing-your-wallet/how-to-use-your-trezor-with-mycrypto.html'
},
[SecureWalletName.PARITY_SIGNER]: {
lid: 'X_PARITYSIGNER',
@ -225,7 +226,7 @@ const WalletDecrypt = withRouter<Props>(
hasAcknowledgedInsecure: false
};
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
// Reset state when unlock is hidden / revealed
if (nextProps.hidden !== this.props.hidden) {
this.setState({

View File

@ -37,16 +37,10 @@
padding-right: $space;
border: none;
}
.form-control {
display: inline-block;
width: auto;
margin: 0 0 0 10px;
}
}
&-addresses {
overflow-y: scroll;
overflow-y: auto;
&-table {
width: 732px;
text-align: center;
@ -61,9 +55,19 @@
font-size: 13px;
text-align: left;
font-family: $font-family-monospace;
display: flex;
align-items: center;
&-label {
font-weight: bold;
}
input {
margin-right: 6px;
margin-right: 10px;
}
&-text {
font-size: 10px;
}
}
@ -74,9 +78,20 @@
background-image: url('~assets/images/icon-external-link.svg');
}
&-na {
font-size: $font-size-xs;
opacity: 0.3;
}
// Specific selectors to override bootstrap
tbody {
tr {
cursor: pointer;
label {
display: block;
font-family: $font-family-sans-serif;
}
}
td {

View File

@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import Select, { Option } from 'react-select';
import { toChecksumAddress } from 'ethereumjs-util';
import translate, { translateRaw } from 'translations';
import {
DeterministicWalletData,
@ -14,39 +15,42 @@ import Modal, { IButton } from 'components/ui/Modal';
import { AppState } from 'reducers';
import { isValidPath } from 'libs/validators';
import { getNetworkConfig } from 'selectors/config';
import { getTokens, MergedToken } from 'selectors/wallet';
import { getTokens } from 'selectors/wallet';
import { getAddressLabels } from 'selectors/addressBook';
import { UnitDisplay, Input } from 'components/ui';
import { StaticNetworkConfig } from 'types/network';
import './DeterministicWalletsModal.scss';
const WALLETS_PER_PAGE = 5;
interface Props {
// Passed props
interface OwnProps {
isOpen?: boolean;
dPath: string;
dPath: DPath;
dPaths: DPath[];
publicKey?: string;
chainCode?: string;
seed?: string;
// Redux state
wallets: AppState['deterministicWallets']['wallets'];
desiredToken: AppState['deterministicWallets']['desiredToken'];
network: StaticNetworkConfig;
tokens: MergedToken[];
// Redux actions
getDeterministicWallets(args: GetDeterministicWalletsArgs): GetDeterministicWalletsAction;
setDesiredToken(tkn: string | undefined): SetDesiredTokenAction;
onCancel(): void;
onConfirmAddress(address: string, addressIndex: number): void;
onPathChange(path: string): void;
}
interface StateProps {
addressLabels: ReturnType<typeof getAddressLabels>;
wallets: AppState['deterministicWallets']['wallets'];
desiredToken: AppState['deterministicWallets']['desiredToken'];
network: ReturnType<typeof getNetworkConfig>;
tokens: ReturnType<typeof getTokens>;
}
interface DispatchProps {
getDeterministicWallets(args: GetDeterministicWalletsArgs): GetDeterministicWalletsAction;
setDesiredToken(tkn: string | undefined): SetDesiredTokenAction;
onCancel(): void;
onConfirmAddress(address: string, addressIndex: number): void;
onPathChange(dPath: DPath): void;
}
type Props = OwnProps & StateProps & DispatchProps;
interface State {
currentLabel: string;
currentDPath: DPath;
selectedAddress: string;
selectedAddrIndex: number;
isCustomPath: boolean;
@ -65,7 +69,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
selectedAddrIndex: 0,
isCustomPath: false,
customPath: '',
currentLabel: '',
currentDPath: this.props.dPath,
page: 0
};
@ -73,7 +77,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
this.getAddresses();
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
const { publicKey, chainCode, seed, dPath } = this.props;
if (
nextProps.publicKey !== publicKey ||
@ -86,7 +90,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
}
public render() {
const { wallets, desiredToken, network, tokens, dPath, dPaths, onCancel } = this.props;
const { wallets, desiredToken, network, tokens, dPaths, onCancel } = this.props;
const { selectedAddress, customPath, page } = this.state;
const buttons: IButton[] = [
@ -119,7 +123,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
<div className="DWModal-path-select">
<Select
name="fieldDPath"
value={this.state.currentLabel || this.findDPath('value', dPath).value}
value={this.state.currentDPath}
onChange={this.handleChangePath}
options={dPaths.concat([customDPath])}
optionRenderer={this.renderDPathOption}
@ -128,11 +132,11 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
searchable={false}
/>
</div>
{this.state.currentLabel === customDPath.label && (
{this.state.currentDPath.label === customDPath.label && (
<React.Fragment>
<div className="DWModal-path-custom">
<Input
className={customPath ? (isValidPath(customPath) ? 'valid' : 'invalid') : ''}
isValid={customPath ? isValidPath(customPath) : true}
value={customPath}
placeholder="m/44'/60'/0'/0"
onChange={this.handleChangeCustomPath}
@ -199,12 +203,12 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
const { dPath, publicKey, chainCode, seed } = props;
if (dPath && ((publicKey && chainCode) || seed)) {
if (isValidPath(dPath)) {
if (isValidPath(dPath.value)) {
this.props.getDeterministicWallets({
seed,
dPath,
publicKey,
chainCode,
dPath: dPath.value,
limit: WALLETS_PER_PAGE,
offset: WALLETS_PER_PAGE * this.state.page
});
@ -214,19 +218,12 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
}
}
private findDPath = (prop: keyof DPath, cmp: string) => {
return this.props.dPaths.find(d => d[prop] === cmp) || customDPath;
};
private handleChangePath = (newPath: DPath) => {
const { value: dPathLabel } = newPath;
const { value } = this.findDPath('value', dPathLabel);
if (value === customDPath.value) {
this.setState({ isCustomPath: true, currentLabel: dPathLabel });
if (newPath.value === customDPath.value) {
this.setState({ isCustomPath: true, currentDPath: newPath });
} else {
this.setState({ isCustomPath: false, currentLabel: dPathLabel });
this.props.onPathChange(value);
this.setState({ isCustomPath: false, currentDPath: newPath });
this.props.onPathChange(newPath);
}
};
@ -235,11 +232,14 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
};
private handleSubmitCustomPath = (ev: React.FormEvent<HTMLFormElement>) => {
const { customPath, currentLabel } = this.state;
const { customPath, currentDPath } = this.state;
ev.preventDefault();
if (currentLabel === customDPath.label && isValidPath(customPath)) {
this.props.onPathChange(customPath);
if (currentDPath.value === customDPath.value && isValidPath(customPath)) {
this.props.onPathChange({
label: customDPath.label,
value: customPath
});
}
};
@ -278,8 +278,10 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
}
private renderWalletRow(wallet: DeterministicWalletData) {
const { desiredToken, network } = this.props;
const { desiredToken, network, addressLabels } = this.props;
const { selectedAddress } = this.state;
const label = addressLabels[toChecksumAddress(wallet.address)];
const spanClassName = label ? 'DWModal-addresses-table-address-text' : '';
// Get renderable values, but keep 'em short
const token = desiredToken ? wallet.tokenValues[desiredToken] : null;
@ -297,7 +299,10 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
checked={selectedAddress === wallet.address}
value={wallet.address}
/>
{wallet.address}
<div>
{label && <label className="DWModal-addresses-table-address-label">{label}</label>}
<span className={spanClassName}>{wallet.address}</span>
</div>
</td>
<td>
<UnitDisplay
@ -309,16 +314,16 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
/>
</td>
<td>
{token ? (
{desiredToken ? (
<UnitDisplay
decimal={token.decimal}
value={token.value}
decimal={token ? token.decimal : 0}
value={token ? token.value : null}
symbol={desiredToken}
displayShortBalance={true}
checkOffline={true}
/>
) : (
'???'
<span className="DWModal-addresses-table-na">N/A</span>
)}
</td>
<td>
@ -335,8 +340,9 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
}
}
function mapStateToProps(state: AppState) {
function mapStateToProps(state: AppState): StateProps {
return {
addressLabels: getAddressLabels(state),
wallets: state.deterministicWallets.wallets,
desiredToken: state.deterministicWallets.desiredToken,
network: getNetworkConfig(state),

View File

@ -65,9 +65,8 @@ export class KeystoreDecrypt extends PureComponent {
{isWalletPending ? <Spinner /> : ''}
<Input
className={`${password.length > 0 ? 'is-valid' : 'is-invalid'} ${
file.length && isWalletPending ? 'hidden' : ''
}`}
isValid={password.length > 0}
className={`${file.length && isWalletPending ? 'hidden' : ''}`}
disabled={!file}
value={password}
onChange={this.onPasswordChange}

View File

@ -25,7 +25,7 @@ interface StateProps {
interface State {
publicKey: string;
chainCode: string;
dPath: string;
dPath: DPath;
error: string | null;
isLoading: boolean;
showTip: boolean;
@ -37,15 +37,15 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : '',
dPath: this.props.dPath || this.props.dPaths[0],
error: null,
isLoading: false,
showTip: false
};
public componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath) {
this.setState({ dPath: nextProps.dPath ? nextProps.dPath.value : '' });
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath && nextProps.dPath) {
this.setState({ dPath: nextProps.dPath });
}
}
@ -116,18 +116,18 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
);
}
private handlePathChange = (dPath: string) => {
private handlePathChange = (dPath: DPath) => {
this.handleConnect(dPath);
};
private handleConnect = (dPath: string = this.state.dPath) => {
private handleConnect = (dPath: DPath) => {
this.setState({
isLoading: true,
error: null,
showTip: false
});
LedgerWallet.getChainCode(dPath)
LedgerWallet.getChainCode(dPath.value)
.then(res => {
this.setState({
publicKey: res.publicKey,
@ -149,19 +149,19 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
};
private handleUnlock = (address: string, index: number) => {
this.props.onUnlock(new LedgerWallet(address, this.state.dPath, index));
this.props.onUnlock(new LedgerWallet(address, this.state.dPath.value, index));
this.reset();
};
private handleNullConnect = (): void => {
return this.handleConnect();
return this.handleConnect(this.state.dPath);
};
private reset() {
this.setState({
publicKey: '',
chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : ''
dPath: this.props.dPath || this.props.dPaths[0]
});
}
}

View File

@ -27,7 +27,7 @@ interface State {
formattedPhrase: string;
pass: string;
seed: string;
dPath: string;
dPath: DPath;
}
class MnemonicDecryptClass extends PureComponent<Props, State> {
@ -36,12 +36,12 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
formattedPhrase: '',
pass: '',
seed: '',
dPath: this.props.dPath.value
dPath: this.props.dPath
};
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath) {
this.setState({ dPath: nextProps.dPath.value });
this.setState({ dPath: nextProps.dPath });
}
}
@ -67,6 +67,8 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
<div className="form-group">
<p>{translate('ADD_LABEL_8')}</p>
<Input
isValid={true}
showValidAsPlain={true}
value={pass}
onChange={this.onPasswordChange}
placeholder={translateRaw('INPUT_PASSWORD_LABEL')}
@ -131,7 +133,7 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
this.setState({ seed: '' });
};
private handlePathChange = (dPath: string) => {
private handlePathChange = (dPath: DPath) => {
this.setState({ dPath });
};
@ -139,7 +141,7 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
const { formattedPhrase, pass, dPath } = this.state;
this.props.onUnlock({
path: `${dPath}/${index}`,
path: `${dPath.value}/${index}`,
pass,
phrase: formattedPhrase,
address

View File

@ -74,7 +74,7 @@ export class PrivateKeyDecrypt extends PureComponent<Props> {
<label className="input-group">
<div className="input-group-header">{translate('ADD_LABEL_3')}</div>
<Input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
isValid={password.length > 0}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}

View File

@ -24,7 +24,7 @@ interface StateProps {
interface State {
publicKey: string;
chainCode: string;
dPath: string;
dPath: DPath;
error: string | null;
isLoading: boolean;
}
@ -35,14 +35,14 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : '',
dPath: this.props.dPath || this.props.dPaths[0],
error: null,
isLoading: false
};
public componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath) {
this.setState({ dPath: nextProps.dPath ? nextProps.dPath.value : '' });
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath && nextProps.dPath) {
this.setState({ dPath: nextProps.dPath });
}
}
@ -78,7 +78,7 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
<div className={`TrezorDecrypt-error alert alert-danger ${showErr}`}>{error || '-'}</div>
<div className="TrezorDecrypt-help">
<NewTabLink href="https://blog.trezor.io/trezor-integration-with-myetherwallet-3e217a652e08">
<NewTabLink href="https://support.mycrypto.com/accessing-your-wallet/how-to-use-your-trezor-with-mycrypto.html">
How to use TREZOR with MyCrypto
</NewTabLink>
</div>
@ -97,18 +97,18 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
);
}
private handlePathChange = (dPath: string) => {
private handlePathChange = (dPath: DPath) => {
this.setState({ dPath });
this.handleConnect(dPath);
};
private handleConnect = (dPath: string = this.state.dPath): void => {
private handleConnect = (dPath: DPath): void => {
this.setState({
isLoading: true,
error: null
});
TrezorWallet.getChainCode(dPath)
TrezorWallet.getChainCode(dPath.value)
.then(res => {
this.setState({
dPath,
@ -130,17 +130,19 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
};
private handleUnlock = (address: string, index: number) => {
this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index));
this.props.onUnlock(new TrezorWallet(address, this.state.dPath.value, index));
this.reset();
};
private handleNullConnect = (): void => this.handleConnect();
private handleNullConnect = (): void => {
this.handleConnect(this.state.dPath);
};
private reset() {
this.setState({
publicKey: '',
chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : ''
dPath: this.props.dPath || this.props.dPaths[0]
});
}
}

View File

@ -60,7 +60,8 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
)}
<Input
className={`ViewOnly-input ${isValid ? 'is-valid' : 'is-invalid'}`}
isValid={isValid}
className="ViewOnly-input"
value={address}
onChange={this.changeAddress}
placeholder={translateRaw('VIEW_ONLY_ENTER')}

View File

@ -0,0 +1,26 @@
@import 'common/sass/variables';
.WelcomeModal {
font-size: $font-size-bump-more;
&-logo {
display: block;
max-width: 380px;
margin: $space auto $space * 2;
}
&-beta {
margin-top: -$space-md;
font-size: $font-size-base;
text-align: center;
}
&-continue {
display: block;
margin: $space * 2 auto 0;
}
p, ul {
margin-bottom: $space;
}
}

View File

@ -0,0 +1,62 @@
import React from 'react';
import translate from 'translations';
import { Modal, NewTabLink } from 'components/ui';
import { isLegacyUser, isBetaUser } from 'utils/localStorage';
import Logo from 'assets/images/logo-mycrypto-transparent.svg';
import './WelcomeModal.scss';
const LS_KEY = 'acknowledged-welcome';
interface State {
isOpen: boolean;
}
export default class WelcomeModal extends React.Component<{}, State> {
public state: State = {
isOpen: false
};
public componentDidMount() {
if (isLegacyUser() && !localStorage.getItem(LS_KEY)) {
this.setState({ isOpen: true });
}
}
public render() {
return (
<Modal isOpen={this.state.isOpen} handleClose={this.close} maxWidth={660}>
<div className="WelcomeModal">
<img className="WelcomeModal-logo" src={Logo} />
{isBetaUser() && (
<p className="WelcomeModal-beta alert alert-success">
💖 {translate('WELCOME_MODAL_BETA')} 🚀
</p>
)}
<p>{translate('WELCOME_MODAL_INTRO')}</p>
<ul>
<li>{translate('WELCOME_MODAL_FEATURE_1')}</li>
<li>{translate('WELCOME_MODAL_FEATURE_2')}</li>
<li>{translate('WELCOME_MODAL_FEATURE_3')}</li>
<li>{translate('WELCOME_MODAL_FEATURE_4')}</li>
<li>
<NewTabLink href="https://download.mycrypto.com/">
{translate('WELCOME_MODAL_FEATURE_5')}
</NewTabLink>
</li>
<li>{translate('WELCOME_MODAL_FEATURE_MORE')}</li>
</ul>
<p>{translate('WELCOME_MODAL_LINKS')}</p>
<button className="WelcomeModal-continue btn btn-lg btn-primary" onClick={this.close}>
{translate('WELCOME_MODAL_CONTINUE')}
</button>
</div>
</Modal>
);
}
private close = () => {
this.setState({ isOpen: false });
localStorage.setItem(LS_KEY, 'true');
};
}

View File

@ -1,25 +0,0 @@
import React, { Component } from 'react';
interface RequiredProps {
condition: boolean;
conditionalProps: {
[key: string]: any;
};
}
/**
* Optional
*/
export const withConditional = <WrappedComponentProps extends {}>(
PassedComponent: React.ComponentType<WrappedComponentProps>
) =>
class extends Component<WrappedComponentProps & RequiredProps, {}> {
public render() {
const { condition, conditionalProps, ...passedProps } = this.props as any;
return condition ? (
<PassedComponent {...{ ...passedProps, ...(conditionalProps as object) }} />
) : (
<PassedComponent {...passedProps} />
);
}
};

View File

@ -1 +0,0 @@
export * from './Conditional';

View File

@ -14,7 +14,6 @@ export { default as Header } from './Header';
export { default as Footer } from './Footer';
export { default as BalanceSidebar } from './BalanceSidebar';
export { default as PaperWallet } from './PaperWallet';
export { default as BetaAgreement } from './BetaAgreement';
export { default as TXMetaDataPanel } from './TXMetaDataPanel';
export { default as WalletDecrypt } from './WalletDecrypt';
export { default as TogglablePassword } from './TogglablePassword';
@ -22,5 +21,6 @@ export { default as GenerateKeystoreModal } from './GenerateKeystoreModal';
export { default as TransactionStatus } from './TransactionStatus';
export { default as ParityQrSigner } from './ParityQrSigner';
export { default as ElectronNav } from './ElectronNav';
export { default as AddressBookTable } from './AddressBookTable';
export { default as Errorable } from './Errorable';
export { default as AppAlphaNotice } from './AppAlphaNotice';

View File

@ -29,7 +29,7 @@ const initialState = { userInput: '' };
class UnitConverterClass extends Component<Props, State> {
public state: State = initialState;
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
const { userInput } = this.state;
if (this.props.decimal !== nextProps.decimal) {

View File

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

View File

@ -10,7 +10,7 @@ interface Props {
export default function Identicon(props: Props) {
const size = props.size || '4rem';
const { address, className } = props;
const { address, className = '' } = props;
// FIXME breaks on failed checksums
const identiconDataUrl = isValidETHAddress(address) ? makeBlockie(address) : '';
return (

View File

@ -85,13 +85,13 @@
border-color: $brand-danger;
box-shadow: inset 0px 0px 0px 1px $brand-danger;
}
&.valid.has-value {
border-color: #8dd17b;
box-shadow: inset 0px 0px 0px 1px #8dd17b;
}
&:focus {
border-color: #4295bc;
box-shadow: inset 0px 0px 0px 1px #4295bc;
&.valid {
border-color: #8dd17b;
box-shadow: inset 0px 0px 0px 1px #8dd17b;
}
}
}
}

View File

@ -1,33 +1,74 @@
import React, { HTMLProps } from 'react';
import classnames from 'classnames';
import './Input.scss';
interface OwnProps extends HTMLProps<HTMLInputElement> {
showInvalidBeforeBlur?: boolean;
setInnerRef?(ref: HTMLInputElement | null): void;
}
interface State {
hasBlurred: boolean;
/**
* @description when the input has not had any values inputted yet
* e.g. "Pristine" condition
*/
isStateless: boolean;
}
class Input extends React.Component<HTMLProps<HTMLInputElement>, State> {
interface OwnProps extends HTMLProps<HTMLInputElement> {
isValid: boolean;
showValidAsPlain?: boolean;
}
class Input extends React.Component<OwnProps, State> {
public state: State = {
hasBlurred: false
hasBlurred: false,
isStateless: true
};
public render() {
const {
setInnerRef,
showInvalidBeforeBlur,
showValidAsPlain,
isValid,
...htmlProps
} = this.props;
const hasValue = !!this.props.value && this.props.value.toString().length > 0;
const classname = classnames(
this.props.className,
'input-group-input',
this.state.isStateless ? '' : isValid ? (showValidAsPlain ? '' : '') : `invalid`,
(showInvalidBeforeBlur || this.state.hasBlurred) && 'has-blurred',
hasValue && 'has-value'
);
return (
<input
{...this.props}
{...htmlProps}
ref={node => setInnerRef && setInnerRef(node)}
onBlur={e => {
this.setState({ hasBlurred: true });
if (this.props && this.props.onBlur) {
this.props.onBlur(e);
}
}}
onChange={this.handleOnChange}
onWheel={this.props.type === 'number' ? this.preventNumberScroll : undefined}
className={`input-group-input ${this.props.className} ${
this.state.hasBlurred ? 'has-blurred' : ''
} ${!!this.props.value && this.props.value.toString().length > 0 ? 'has-value' : ''}`}
className={classname}
/>
);
}
private handleOnChange = (args: React.FormEvent<HTMLInputElement>) => {
if (this.state.isStateless) {
this.setState({ isStateless: false });
}
if (this.props.onChange) {
this.props.onChange(args);
}
};
// When number inputs are scrolled on while in focus, the number changes. So we blur
// it if it's focused to prevent that behavior, without preventing the scroll.
private preventNumberScroll(ev: React.WheelEvent<HTMLInputElement>) {

View File

@ -3,8 +3,8 @@ import closeIcon from 'assets/images/close.svg';
import { IButton } from 'components/ui/Modal';
interface Props {
title?: string;
children: any;
title?: React.ReactNode;
children: React.ReactNode;
modalStyle?: CSSProperties;
hasButtons?: number;
buttons?: IButton[];
@ -67,7 +67,7 @@ export default class ModalBody extends React.Component<Props> {
<div className="Modal-content" ref={div => (this.modalContent = div as HTMLElement)}>
{children}
<div className="Modal-fade" />
<div className={`Modal-fade ${!hasButtons ? 'has-no-footer' : ''}`} />
</div>
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
</div>

View File

@ -53,6 +53,10 @@ $m-anim-speed: 400ms;
bottom: 4.5rem;
left: 50%;
transform: translateX(-50%);
&.has-no-footer {
bottom: 0;
}
}
&-header {

View File

@ -12,9 +12,9 @@ export interface IButton {
}
interface Props {
isOpen?: boolean;
title?: string;
title?: React.ReactNode;
disableButtons?: boolean;
children: any;
children: React.ReactNode;
buttons?: IButton[];
maxWidth?: number;
handleClose(): void;

View File

@ -70,14 +70,7 @@ export default class DropdownComponent<T> extends PureComponent<Props<T>, State>
return (
<ul className={menuClass} style={searchable ? searchableStyle : undefined}>
{searchable && (
<input
className="form-control"
placeholder={'Search'}
onChange={onSearchChange}
value={search}
/>
)}
{searchable && <input placeholder={'Search'} onChange={onSearchChange} value={search} />}
{options
.filter(option => {

View File

@ -15,12 +15,12 @@ interface State {
export default class QRCode extends React.PureComponent<Props, State> {
public state: State = {};
public componentWillMount() {
public UNSAFE_componentWillMount() {
// Start generating QR codes immediately
this.generateQrCode(this.props.data);
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
// Regenerate QR codes if props change
if (nextProps.data !== this.props.data) {
this.generateQrCode(nextProps.data);

View File

@ -1,27 +0,0 @@
import React, { PureComponent } from 'react';
interface Props {
value?: string;
options: string[];
onChange(event: React.FormEvent<HTMLSpanElement>): void;
}
export default class SimpleSelect extends PureComponent<Props, {}> {
public render() {
return (
<select
value={this.props.value || this.props.options[0]}
className={'form-control'}
onChange={this.props.onChange}
>
{this.props.options.map((obj, i) => {
return (
<option value={obj} key={i}>
{obj}
</option>
);
})}
</select>
);
}
}

View File

@ -34,7 +34,7 @@ class SwapDropdown extends PureComponent<Props, State> {
public dropdown: HTMLDivElement | null;
public componentWillMount() {
public UNSAFE_componentWillMount() {
this.buildOptions(this.props.options);
document.addEventListener('click', this.handleBodyClick);
}
@ -43,7 +43,7 @@ class SwapDropdown extends PureComponent<Props, State> {
document.removeEventListener('click', this.handleBodyClick);
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.options !== nextProps.options) {
this.buildOptions(nextProps.options);
}

View File

@ -1,30 +1,59 @@
import React, { HTMLProps } from 'react';
import classnames from 'classnames';
import './Input.scss';
interface State {
hasBlurred: boolean;
/**
* @description when the input has not had any values inputted yet
* e.g. "Pristine" condition
*/
isStateless: boolean;
}
class TextArea extends React.Component<HTMLProps<HTMLTextAreaElement>, State> {
interface OwnProps extends HTMLProps<HTMLTextAreaElement> {
isValid: boolean;
showValidAsPlain?: boolean;
}
class TextArea extends React.Component<OwnProps, State> {
public state: State = {
hasBlurred: false
hasBlurred: false,
isStateless: true
};
public render() {
const { showValidAsPlain, isValid, ...htmlProps } = this.props;
const classname = classnames(
this.props.className,
'input-group-input',
this.state.isStateless ? '' : isValid ? (showValidAsPlain ? '' : '') : `invalid`,
this.state.hasBlurred && 'has-blurred'
);
return (
<textarea
{...this.props}
{...htmlProps}
onBlur={e => {
this.setState({ hasBlurred: true });
if (this.props && this.props.onBlur) {
this.props.onBlur(e);
}
}}
className={`input-group-input ${this.props.className} ${
this.state.hasBlurred ? 'has-blurred' : ''
}`}
onChange={this.handleOnChange}
className={classname}
/>
);
}
private handleOnChange = (args: React.FormEvent<HTMLTextAreaElement>) => {
if (this.state.isStateless) {
this.setState({ isStateless: false });
}
if (this.props.onChange) {
this.props.onChange(args);
}
};
}
export default TextArea;

View File

@ -1,27 +0,0 @@
@import 'common/sass/variables';
$height: 22px;
// TODO - Implement styles for custom title bar on all platforms
.TitleBar,
.TitleBarPlaceholder {
display: none;
}
.TitleBar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: $height;
line-height: $height;
-webkit-user-select: none;
-webkit-app-region: drag;
background: $body-bg;
z-index: $zindex-top;
box-shadow: 0 1px 1px rgba(#000, 0.08);
}
.TitleBarPlaceholder {
height: $height;
}

View File

@ -1,11 +0,0 @@
import React from 'react';
import './TitleBar.scss';
const TitleBar: React.SFC<{}> = () => (
<React.Fragment>
<div className="TitleBar" />
<div className="TitleBarPlaceholder" />
</React.Fragment>
);
export default TitleBar;

View File

@ -1,9 +1,9 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
$tooltip-bg: rgba(#222, 0.95);
.Tooltip {
display: flex;
justify-content: center;
position: absolute;
top: 0;
left: 50%;
@ -14,7 +14,7 @@ $tooltip-bg: rgba(#222, 0.95);
pointer-events: none;
opacity: 0;
visibility: hidden;
transform: translate(-50%, -100%) translateY(-5px);
transform: translate(-50%, -100%) translateY(-$tooltip-start-distance);
transition-property: opacity, transform, visibility;
transition-duration: 100ms, 100ms, 0ms;
transition-delay: 0ms, 0ms, 100ms;
@ -32,7 +32,7 @@ $tooltip-bg: rgba(#222, 0.95);
bottom: 0;
left: 50%;
transform: translate(-50%, 100%);
@include triangle(10px, $tooltip-bg, down);
@include triangle($tooltip-arrow-size * 2, $tooltip-bg, down);
}
}
@ -46,7 +46,7 @@ $tooltip-bg: rgba(#222, 0.95);
border-radius: 2px;
&:after {
@include triangle(8px, $tooltip-bg, down);
border-width: $tooltip-arrow-size - 1;
}
}
}
@ -60,8 +60,55 @@ $tooltip-bg: rgba(#222, 0.95);
border-radius: 4px;
&:after {
@include triangle(12px, $tooltip-bg, down);
border-width: $tooltip-arrow-size + 1;
}
}
}
// Direction, top is default
&.is-direction-left {
left: 0;
top: 50%;
justify-content: flex-end;
transform: translate(-100%, -50%) translateX(-$tooltip-start-distance);
> span:after {
bottom: 50%;
right: 0;
left: auto;
transform: translate(100%, 50%);
border-top-color: transparent;
border-left-color: $tooltip-bg;
}
}
&.is-direction-right {
left: auto;
right: 0;
top: 50%;
justify-content: flex-start;
transform: translate(100%, -50%) translateX($tooltip-start-distance);
> span:after {
bottom: 50%;
left: 0;
transform: translate(-100%, 50%);
border-top-color: transparent;
border-right-color: $tooltip-bg;
}
}
&.is-direction-bottom {
top: auto;
bottom: 0;
transform: translate(-50%, 100%) translateY($tooltip-start-distance);
> span:after {
bottom: auto;
top: 0;
transform: translate(-50%, -100%);
border-top-color: transparent;
border-bottom-color: $tooltip-bg;
}
}
}

View File

@ -5,13 +5,15 @@ import './Tooltip.scss';
interface Props {
children: React.ReactElement<string> | string;
size?: 'sm' | 'md' | 'lg';
direction?: 'top' | 'bottom' | 'left' | 'right';
}
const Tooltip: React.SFC<Props> = ({ size, children }) => (
const Tooltip: React.SFC<Props> = ({ size, direction, children }) => (
<div
className={classnames({
Tooltip: true,
[`is-size-${size}`]: !!size
[`is-size-${size}`]: !!size,
[`is-direction-${direction}`]: !!direction
})}
>
<span className="Tooltip-text">{children}</span>

View File

@ -11,13 +11,11 @@ export { default as UnitDisplay } from './UnitDisplay';
export { default as Spinner } from './Spinner';
export { default as SwapDropdown } from './SwapDropdown';
export { default as Tooltip } from './Tooltip';
export { default as TitleBar } from './TitleBar';
export { default as HelpLink } from './HelpLink';
export { default as Input } from './Input';
export { default as TextArea } from './TextArea';
export { default as Address } from './Address';
export { default as CodeBlock } from './CodeBlock';
export { default as Toggle } from './Toggle';
export * from './ConditionalInput';
export * from './Expandable';
export * from './InlineSpinner';

View File

@ -1,16 +1,17 @@
import React from 'react'; // For ANNOUNCEMENT_MESSAGE jsx
import NewTabLink from 'components/ui/NewTabLink';
import { getValues } from '../utils/helpers';
import packageJson from '../../package.json';
import { GasPriceSetting } from 'types/network';
import { makeExplorer } from 'utils/helpers';
import NewTabLink from 'components/ui/NewTabLink';
export const languages = require('./languages.json');
export const discordURL = 'https://discord.gg/VSaTXEA';
// Displays in the footer
export const VERSION_RAW = packageJson.version;
export const VERSION = `${VERSION_RAW} (Release Candidate)`;
const VERSION_ELECTRON = packageJson['electron-version'];
const VERSION_WEB = packageJson.version;
export const VERSION = process.env.BUILD_ELECTRON ? VERSION_ELECTRON : VERSION_WEB;
export const N_FACTOR = 8192;
// Bricks the app once this date has been exceeded. Remember to update these 2
@ -18,7 +19,6 @@ export const N_FACTOR = 8192;
// It is currently set to: 05/25/2018 @ 12:00am (UTC)
// TODO: Remove me once app alpha / release candidates are done
export const APP_ALPHA_EXPIRATION = 1527206400000;
export const VERSION_RC = `${packageJson.version}-RC.0`;
// Displays at the top of the site, make message empty string to remove.
// Type can be primary, warning, danger, success, info, or blank for grey.
@ -26,10 +26,8 @@ export const VERSION_RC = `${packageJson.version}-RC.0`;
export const ANNOUNCEMENT_TYPE = '';
export const ANNOUNCEMENT_MESSAGE = (
<React.Fragment>
This is a Beta Release Candidate of the new MyCrypto. Please submit any bug reports to our{' '}
<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>.
Welcome to the new MyCrypto. We hope you like it! If it's urgent and you need the old site, you
can still use <NewTabLink href="https://legacy.mycrypto.com">MyCrypto Legacy</NewTabLink>
</React.Fragment>
);

View File

@ -47,6 +47,10 @@ export const socialMediaLinks: Link[] = [
];
export const productLinks: Link[] = [
{
link: 'https://legacy.mycrypto.com/',
text: translateRaw('OLD_MYCRYPTO')
},
{
link:
'https://chrome.google.com/webstore/detail/etheraddresslookup/pdknmigbbbhmllnmgdfalmedcmcefdfn',
@ -62,8 +66,12 @@ export const productLinks: Link[] = [
text: translateRaw('ETHERSCAMDB')
},
{
link: 'https://www.mycrypto.com/helpers.html',
link: 'https://legacy.mycrypto.com/helpers.html',
text: translateRaw('FOOTER_HELP_AND_DEBUGGING')
},
{
link: 'https://hackerone.com/mycrypto',
text: translateRaw('FOOTER_HACKERONE')
}
];

View File

@ -24,6 +24,16 @@
"symbol": "REN",
"decimal": 18
},
{
"address": "0x37427576324fE1f3625c9102674772d7CF71377d",
"symbol": "SGT (SelfieYo Gold Token)",
"decimal": 18
},
{
"address": "0xd248B0D48E44aaF9c49aea0312be7E13a6dc1468",
"symbol": "SGT (SGT)",
"decimal": 1
},
{
"address": "0x78B7FADA55A64dD895D8c8c35779DD8b67fA8a05",
"symbol": "ATL",
@ -49,6 +59,11 @@
"symbol": "SNGLS",
"decimal": 0
},
{
"address": "0x515669d308f887Fd83a471C7764F5d084886D34D",
"symbol": "MUXE",
"decimal": 18
},
{
"address": "0x025abAD9e518516fdaAFBDcdB9701b37fb7eF0FA",
"symbol": "GTKT",
@ -64,6 +79,11 @@
"symbol": "STRC",
"decimal": 8
},
{
"address": "0x2a8E98e256f32259b5E5Cb55Dd63C8e891950666",
"symbol": "PTC",
"decimal": 18
},
{
"address": "0x41e5560054824eA6B0732E656E3Ad64E20e94E45",
"symbol": "CVC",
@ -139,6 +159,11 @@
"symbol": "FAM",
"decimal": 12
},
{
"address": "0x105d97ef2E723f1cfb24519Bc6fF15a6D091a3F1",
"symbol": "UMKA",
"decimal": 4
},
{
"address": "0x694404595e3075A942397F466AAcD462FF1a7BD0",
"symbol": "PATENTS",
@ -184,6 +209,11 @@
"symbol": "GANA",
"decimal": 18
},
{
"address": "0xC5bBaE50781Be1669306b9e001EFF57a2957b09d",
"symbol": "GTO",
"decimal": 5
},
{
"address": "0x9e88613418cF03dCa54D6a2cf6Ad934A78C7A17A",
"symbol": "SWM",
@ -199,6 +229,31 @@
"symbol": "OMG",
"decimal": 18
},
{
"address": "0xf9F7c29CFdf19FCf1f2AA6B84aA367Bcf1bD1676",
"symbol": "DTT",
"decimal": 18
},
{
"address": "0x78Eb8DC641077F049f910659b6d580E80dC4d237",
"symbol": "SMT (Social Media Market)",
"decimal": 8
},
{
"address": "0x55F93985431Fc9304077687a35A1BA103dC1e081",
"symbol": "SMT (SmartMesh)",
"decimal": 18
},
{
"address": "0x2dCFAAc11c9EebD8C6C42103Fe9e2a6AD237aF27",
"symbol": "SMT (Smart Node)",
"decimal": 18
},
{
"address": "0x82125AFe01819Dff1535D0D6276d57045291B6c0",
"symbol": "MRL",
"decimal": 18
},
{
"address": "0x7DD7F56D697Cc0f2b52bD55C057f378F1fE6Ab4b",
"symbol": "$TEAK",
@ -219,11 +274,26 @@
"symbol": "ARN",
"decimal": 8
},
{
"address": "0x464eBE77c293E473B48cFe96dDCf88fcF7bFDAC0",
"symbol": "KRL",
"decimal": 18
},
{
"address": "0x1014613E2B3CBc4d575054D4982E580d9b99d7B1",
"symbol": "BCV",
"decimal": 8
},
{
"address": "0xd94F2778e2B3913C53637Ae60647598bE588c570",
"symbol": "PRPS (1)",
"decimal": 18
},
{
"address": "0x7641b2Ca9DDD58adDf6e3381c1F994Aac5f1A32f",
"symbol": "PRPS (2)",
"decimal": 18
},
{
"address": "0xdd94De9cFE063577051A5eb7465D08317d8808B6",
"symbol": "Devcon2 Token",
@ -284,6 +354,16 @@
"symbol": "DIVX",
"decimal": 18
},
{
"address": "0xe25bCec5D3801cE3a794079BF94adF1B8cCD802D",
"symbol": "MAN",
"decimal": 18
},
{
"address": "0x5adc961D6AC3f7062D2eA45FEFB8D8167d44b190",
"symbol": "DTH",
"decimal": 18
},
{
"address": "0x86Fa049857E0209aa7D9e616F7eb3b3B78ECfdb0",
"symbol": "EOS",
@ -405,13 +485,8 @@
"decimal": 18
},
{
"address": "0x55F93985431Fc9304077687a35A1BA103dC1e081",
"symbol": "SMT (SmartMesh)",
"decimal": 18
},
{
"address": "0x2dCFAAc11c9EebD8C6C42103Fe9e2a6AD237aF27",
"symbol": "SMT (Smart Node)",
"address": "0x38c87AA89B2B8cD9B95b736e1Fa7b612EA972169",
"symbol": "AMO",
"decimal": 18
},
{
@ -479,6 +554,11 @@
"symbol": "XID",
"decimal": 8
},
{
"address": "0xCd4b4b0F3284a33AC49C67961EC6e111708318Cf",
"symbol": "AX1",
"decimal": 5
},
{
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"symbol": "USDT",
@ -524,6 +604,21 @@
"symbol": "BTT",
"decimal": 0
},
{
"address": "0x9fC0583220eB44fAeE9e2dc1E63F39204DDD9090",
"symbol": "2DC",
"decimal": 18
},
{
"address": "0x0E8d6b471e332F140e7d9dbB99E5E3822F728DA6",
"symbol": "ABYSS",
"decimal": 18
},
{
"address": "0xC87c5dD86A3d567fF28701886fB0745aaa898da4",
"symbol": "CTG",
"decimal": 18
},
{
"address": "0xfbd0d1c77B501796A35D86cF91d65D9778EeE695",
"symbol": "TWNKL",
@ -626,7 +721,12 @@
},
{
"address": "0xD4CffeeF10F60eCA581b5E1146B5Aca4194a4C3b",
"symbol": "DUBI",
"symbol": "DUBI (1)",
"decimal": 18
},
{
"address": "0x9c6Fa42209169bCeA032e401188a6fc3e9C9f59c",
"symbol": "DUBI (2)",
"decimal": 18
},
{
@ -709,6 +809,11 @@
"symbol": "DCN",
"decimal": 0
},
{
"address": "0x6e2050CBFB3eD8A4d39b64cC9f47E711a03a5a89",
"symbol": "SSH",
"decimal": 18
},
{
"address": "0x59416A25628A76b4730eC51486114c32E0B582A1",
"symbol": "PLASMA",
@ -729,6 +834,11 @@
"symbol": "CCC (ICONOMI)",
"decimal": 18
},
{
"address": "0x075c60EE2cD308ff47873b38Bd9A0Fa5853382c4",
"symbol": "DEEZ",
"decimal": 18
},
{
"address": "0x638AC149eA8EF9a1286C41B977017AA7359E6Cfa",
"symbol": "ALTS",
@ -739,6 +849,11 @@
"symbol": "WCN",
"decimal": 18
},
{
"address": "0xFA1a856Cfa3409CFa145Fa4e20Eb270dF3EB21ab",
"symbol": "IOST",
"decimal": 18
},
{
"address": "0x5BC7e5f0Ab8b2E10D2D0a3F21739FCe62459aeF3",
"symbol": "ENTRP",
@ -824,6 +939,11 @@
"symbol": "NBAI",
"decimal": 18
},
{
"address": "0x6710c63432A2De02954fc0f851db07146a6c0312",
"symbol": "MFG",
"decimal": 18
},
{
"address": "0x65A15014964F2102Ff58647e16a16a6B9E14bCF6",
"symbol": "Ox Fina",
@ -914,6 +1034,11 @@
"symbol": "LA",
"decimal": 18
},
{
"address": "0x7e9e431a0B8c4D532C745B1043c7FA29a48D4fBa",
"symbol": "eosDAC",
"decimal": 18
},
{
"address": "0xb5C33F965C8899D255c34CDD2A3efA8AbCbB3DeA",
"symbol": "KPR",
@ -1069,6 +1194,11 @@
"symbol": "SIG",
"decimal": 18
},
{
"address": "0xDF347911910b6c9A4286bA8E2EE5ea4a39eB2134",
"symbol": "BOB",
"decimal": 18
},
{
"address": "0x4F4f0Db4de903B88f2B1a2847971E231D54F8fd3",
"symbol": "GEE",
@ -1089,6 +1219,11 @@
"symbol": "BTL (Bitlle)",
"decimal": 4
},
{
"address": "0xdD41fBd1Ae95C5D9B198174A28e04Be6b3d1aa27",
"symbol": "LYS",
"decimal": 8
},
{
"address": "0xf3Db5Fa2C66B7aF3Eb0C0b782510816cbe4813b8",
"symbol": "EVX",
@ -1134,6 +1269,11 @@
"symbol": "LGO",
"decimal": 8
},
{
"address": "0xAd8DD4c725dE1D31b9E8F8D146089e9DC6882093",
"symbol": "MIT (Mychatcoin)",
"decimal": 6
},
{
"address": "0xA89b5934863447f6E4Fc53B315a93e873bdA69a3",
"symbol": "LUM",
@ -1174,6 +1314,11 @@
"symbol": "FLX",
"decimal": 18
},
{
"address": "0x554FFc77F4251a9fB3c0E3590a6a205f8d4e067D",
"symbol": "ZMN",
"decimal": 18
},
{
"address": "0x888666CA69E0f178DED6D75b5726Cee99A87D698",
"symbol": "ICN",
@ -1239,6 +1384,11 @@
"symbol": "EDC",
"decimal": 6
},
{
"address": "0xDDe12a12A6f67156e0DA672be05c374e1B0a3e57",
"symbol": "JOY",
"decimal": 6
},
{
"address": "0xEF68e7C694F40c8202821eDF525dE3782458639f",
"symbol": "LRC",
@ -1259,6 +1409,11 @@
"symbol": "RVL",
"decimal": 18
},
{
"address": "0x7f6715c3FC4740A02F70De85B9FD50ac6001fEd9",
"symbol": "FANX",
"decimal": 18
},
{
"address": "0xa74476443119A942dE498590Fe1f2454d7D4aC0d",
"symbol": "GNT",
@ -1339,6 +1494,11 @@
"symbol": "CC3",
"decimal": 18
},
{
"address": "0x0C91B015AbA6f7B4738dcD36E7410138b29ADC29",
"symbol": "COIL",
"decimal": 8
},
{
"address": "0x85e076361cc813A908Ff672F9BAd1541474402b2",
"symbol": "TEL",
@ -1369,6 +1529,16 @@
"symbol": "AIX",
"decimal": 18
},
{
"address": "0x6c6EE5e31d828De241282B9606C8e98Ea48526E2",
"symbol": "HOT (HoloToken)",
"decimal": 18
},
{
"address": "0x9AF839687F6C94542ac5ece2e317dAAE355493A1",
"symbol": "HOT (Hydro Protocol)",
"decimal": 18
},
{
"address": "0x6927C69fb4daf2043fbB1Cb7b86c5661416bea29",
"symbol": "ETR",
@ -1414,6 +1584,11 @@
"symbol": "CTX",
"decimal": 18
},
{
"address": "0xf0Ee6b27b759C9893Ce4f094b49ad28fd15A23e4",
"symbol": "ENG",
"decimal": 8
},
{
"address": "0x1844b21593262668B7248d0f57a220CaaBA46ab9",
"symbol": "PRL",
@ -1444,6 +1619,11 @@
"symbol": "S-A-PAT",
"decimal": 18
},
{
"address": "0xf6b6AA0Ef0f5Edc2C1c5d925477F97eAF66303e7",
"symbol": "XGG",
"decimal": 8
},
{
"address": "0x3618516F45CD3c913F81F9987AF41077932Bc40d",
"symbol": "PCL",
@ -1474,6 +1654,11 @@
"symbol": "onG",
"decimal": 18
},
{
"address": "0x0a9A9ce600D08BF9b76F49FA4e7b38A67EBEB1E6",
"symbol": "GROW",
"decimal": 8
},
{
"address": "0xF26ef5E0545384b7Dcc0f297F2674189586830DF",
"symbol": "BSDC",
@ -1549,6 +1734,11 @@
"symbol": "REBL",
"decimal": 18
},
{
"address": "0xC64500DD7B0f1794807e67802F8Abbf5F8Ffb054",
"symbol": "LOCUS",
"decimal": 18
},
{
"address": "0x83eEA00D838f92dEC4D1475697B9f4D3537b56E3",
"symbol": "VOISE",
@ -1594,6 +1784,11 @@
"symbol": "DCA",
"decimal": 18
},
{
"address": "0x8a77e40936BbC27e80E9a3F526368C967869c86D",
"symbol": "MVP",
"decimal": 18
},
{
"address": "0x9E46A38F5DaaBe8683E10793b06749EEF7D733d1",
"symbol": "NCT",
@ -1645,9 +1840,14 @@
"decimal": 18
},
{
"address": "0xd248B0D48E44aaF9c49aea0312be7E13a6dc1468",
"symbol": "SGT",
"decimal": 1
"address": "0xFcD862985628b254061F7A918035B80340D045d3",
"symbol": "GIF",
"decimal": 18
},
{
"address": "0x922aC473A3cC241fD3a0049Ed14536452D58D73c",
"symbol": "VLD",
"decimal": 18
},
{
"address": "0xfeDAE5642668f8636A11987Ff386bfd215F942EE",
@ -1689,6 +1889,11 @@
"symbol": "DAXT",
"decimal": 18
},
{
"address": "0x523630976eB6147621B5c31c781eBe2Ec2a806E0",
"symbol": "eUSD",
"decimal": 18
},
{
"address": "0xF433089366899D83a9f26A773D59ec7eCF30355e",
"symbol": "MTL",
@ -1739,6 +1944,11 @@
"symbol": "HKY",
"decimal": 18
},
{
"address": "0x2467AA6B5A2351416fD4C3DeF8462d841feeecEC",
"symbol": "QBX",
"decimal": 18
},
{
"address": "0x5dbe296F97B23C4A6AA6183D73e574D02bA5c719",
"symbol": "LUC",
@ -1819,6 +2029,11 @@
"symbol": "CK",
"decimal": 0
},
{
"address": "0x48e5413b73add2434e47504E2a22d14940dBFe78",
"symbol": "INRM",
"decimal": 3
},
{
"address": "0x1e49fF77c355A3e38D6651ce8404AF0E48c5395f",
"symbol": "MTRc",
@ -1844,16 +2059,21 @@
"symbol": "DICE",
"decimal": 16
},
{
"address": "0x9AF839687F6C94542ac5ece2e317dAAE355493A1",
"symbol": "HOT",
"decimal": 18
},
{
"address": "0x744d70FDBE2Ba4CF95131626614a1763DF805B9E",
"symbol": "SNT",
"decimal": 18
},
{
"address": "0x69c4BB240cF05D51eeab6985Bab35527d04a8C64",
"symbol": "OPEN (1)",
"decimal": 8
},
{
"address": "0xe9dE1C630753A15d7021Cc563429c21d4887506F",
"symbol": "OPEN (2)",
"decimal": 8
},
{
"address": "0x1a7a8BD9106F2B8D977E08582DC7d24c723ab0DB",
"symbol": "APPC",
@ -1929,6 +2149,11 @@
"symbol": "SAN",
"decimal": 18
},
{
"address": "0x82fdedfB7635441aA5A92791D001fA7388da8025",
"symbol": "DTx",
"decimal": 18
},
{
"address": "0x8eFFd494eB698cc399AF6231fCcd39E08fd20B15",
"symbol": "PIX",
@ -2024,6 +2249,11 @@
"symbol": "GXVC",
"decimal": 10
},
{
"address": "0x8a854288a5976036A725879164Ca3e91d30c6A1B",
"symbol": "GET",
"decimal": 18
},
{
"address": "0x4c382F8E09615AC86E08CE58266CC227e7d4D913",
"symbol": "SKR",
@ -2109,6 +2339,11 @@
"symbol": "STAC",
"decimal": 18
},
{
"address": "0xfe7B915A0bAA0E79f85c5553266513F7C1c03Ed0",
"symbol": "THUG",
"decimal": 18
},
{
"address": "0xC0Eb85285d83217CD7c891702bcbC0FC401E2D9D",
"symbol": "HVN",
@ -2209,11 +2444,6 @@
"symbol": "GELD",
"decimal": 18
},
{
"address": "0x7641b2Ca9DDD58adDf6e3381c1F994Aac5f1A32f",
"symbol": "PRPS",
"decimal": 18
},
{
"address": "0x0AfFa06e7Fbe5bC9a764C979aA66E8256A631f02",
"symbol": "PLBT",
@ -2269,6 +2499,11 @@
"symbol": "YUPIE",
"decimal": 18
},
{
"address": "0x558EC3152e2eb2174905cd19AeA4e34A23DE9aD6",
"symbol": "BRD",
"decimal": 18
},
{
"address": "0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a",
"symbol": "TKN",
@ -2284,6 +2519,11 @@
"symbol": "BCPT",
"decimal": 18
},
{
"address": "0x9aeFBE0b3C3ba9Eab262CB9856E8157AB7648e09",
"symbol": "FLR",
"decimal": 18
},
{
"address": "0x0AF44e2784637218dD1D32A322D44e603A8f0c6A",
"symbol": "MTX",
@ -2399,6 +2639,11 @@
"symbol": "DROP",
"decimal": 0
},
{
"address": "0xF03f8D65BaFA598611C3495124093c56e8F638f0",
"symbol": "VIEW",
"decimal": 18
},
{
"address": "0x43F6a1BE992deE408721748490772B15143CE0a7",
"symbol": "POIN",
@ -2604,6 +2849,16 @@
"symbol": "WPR",
"decimal": 18
},
{
"address": "0x543Ff227F64Aa17eA132Bf9886cAb5DB55DCAddf",
"symbol": "GEN",
"decimal": 18
},
{
"address": "0xD760ADdFb24D9C01Fe4Bfea7475C5e3636684058",
"symbol": "USDM",
"decimal": 2
},
{
"address": "0xF4134146AF2d511Dd5EA8cDB1C4AC88C57D60404",
"symbol": "SNC",
@ -2644,11 +2899,6 @@
"symbol": "EMONT",
"decimal": 8
},
{
"address": "0xe9dE1C630753A15d7021Cc563429c21d4887506F",
"symbol": "OPEN",
"decimal": 8
},
{
"address": "0x514910771AF9Ca656af840dff83E8264EcF986CA",
"symbol": "LINK (Chainlink)",
@ -2899,6 +3149,11 @@
"symbol": "AIR",
"decimal": 8
},
{
"address": "0xF244176246168F24e3187f7288EdbCA29267739b",
"symbol": "HAV",
"decimal": 18
},
{
"address": "0xc8C6A31A4A806d3710A7B38b7B296D2fABCCDBA8",
"symbol": "ELIX",

View File

@ -28,9 +28,7 @@ import {
SecureSlideThree,
FinalSlide
} from './components';
const ONBOARD_LOCAL_STORAGE_KEY = 'onboardStatus';
const NUMBER_OF_SLIDES = 10;
import { ONBOARD_LOCAL_STORAGE_KEY, NUMBER_OF_ONBOARD_SLIDES } from 'utils/localStorage';
interface State {
isOpen: boolean;
@ -58,7 +56,6 @@ class OnboardModal extends React.Component<Props, State> {
public componentDidMount() {
const { sessionStarted } = this.props;
const currentSlide = Number(localStorage.getItem(ONBOARD_LOCAL_STORAGE_KEY)) || 0;
if (!sessionStarted) {
@ -68,7 +65,7 @@ class OnboardModal extends React.Component<Props, State> {
isOpen: true
});
}
if (currentSlide > 0 && currentSlide < NUMBER_OF_SLIDES) {
if (currentSlide > 0 && currentSlide < NUMBER_OF_ONBOARD_SLIDES) {
this.props.resumeSlide(currentSlide);
this.setState({
isOpen: true
@ -90,7 +87,7 @@ class OnboardModal extends React.Component<Props, State> {
const firstButtons: IButton[] = [
{
disabled: slideNumber === NUMBER_OF_SLIDES,
disabled: slideNumber === NUMBER_OF_ONBOARD_SLIDES,
text: translate('ACTION_6'),
type: 'primary',
onClick: this.handleNextSlide
@ -115,8 +112,8 @@ class OnboardModal extends React.Component<Props, State> {
}
];
const buttons = slideNumber === NUMBER_OF_SLIDES ? lastButtons : firstButtons;
const steps = new Array(NUMBER_OF_SLIDES).fill({});
const buttons = slideNumber === NUMBER_OF_ONBOARD_SLIDES ? lastButtons : firstButtons;
const steps = new Array(NUMBER_OF_ONBOARD_SLIDES).fill({});
return (
<div className="OnboardModal">
@ -158,8 +155,8 @@ class OnboardModal extends React.Component<Props, State> {
<FinalSlide key={10} closeModal={this.closeModal} />
];
if (slides.length !== NUMBER_OF_SLIDES) {
console.log('Slides length do not match const NUMBER_OF_SLIDES');
if (slides.length !== NUMBER_OF_ONBOARD_SLIDES) {
console.log('Slides length do not match const NUMBER_OF_ONBOARD_SLIDES');
}
const currentSlideIndex = this.props.slideNumber - 1;

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { BetaAgreement, Footer, Header } from 'components';
import { Footer, Header } from 'components';
import { AppState } from 'reducers';
import Notifications from './Notifications';
import OfflineTab from './OfflineTab';
@ -38,7 +38,6 @@ class WebTemplate extends Component<Props, {}> {
<div className="WebTemplate-spacer" />
<Footer latestBlock={latestBlock} />
<Notifications />
<BetaAgreement />
</div>
);
}

View File

@ -65,7 +65,7 @@ class BroadcastTx extends Component<Props> {
<Input
type="text"
placeholder="0xf86b0284ee6b2800825208944bbeeb066ed09b7aed07bf39eee0460dfa26152088016345785d8a00008029a03ba7a0cc6d1756cd771f2119cf688b6d4dc9d37096089f0331fe0de0d1cc1254a02f7bcd19854c8d46f8de09e457aec25b127ab4328e1c0d24bfbff8702ee1f474"
className={stateTransaction ? '' : 'invalid'}
isValid={!!stateTransaction}
value={userInput}
onChange={this.handleChange}
/>

View File

@ -33,7 +33,7 @@ class TxHashInput extends React.Component<Props, State> {
this.state = { hash: props.hash || '' };
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.hash !== nextProps.hash && nextProps.hash) {
this.setState({ hash: nextProps.hash });
}
@ -42,7 +42,7 @@ class TxHashInput extends React.Component<Props, State> {
public render() {
const { recentTxs } = this.props;
const { hash } = this.state;
const validClass = hash ? (isValidTxHash(hash) ? 'is-valid' : 'is-invalid') : '';
let selectOptions: Option[] = [];
if (recentTxs && recentTxs.length) {
@ -75,8 +75,9 @@ class TxHashInput extends React.Component<Props, State> {
<Input
value={hash}
isValid={hash ? isValidTxHash(hash) : true}
placeholder="0x16e521..."
className={`TxHashInput-field ${validClass}`}
className="TxHashInput-field"
onChange={this.handleChange}
/>

View File

@ -34,7 +34,7 @@ class CheckTransaction extends React.Component<Props, State> {
}
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
const { network } = this.props;
if (network.chainId !== nextProps.network.chainId) {
this.setState({ hash: '' });

View File

@ -1,5 +1,4 @@
import translate from 'translations';
import classnames from 'classnames';
import { DataFieldFactory } from 'components/DataFieldFactory';
import { SendButtonFactory } from 'components/SendButtonFactory';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
@ -31,16 +30,15 @@ class DeployClass extends Component<DispatchProps> {
<label className="input-group">
<div className="input-group-header">{translate('CONTRACT_BYTECODE')}</div>
<DataFieldFactory
withProps={({ data: { raw, value }, onChange, readOnly }) => (
withProps={({ data: { raw }, onChange, readOnly, validData }) => (
<TextArea
isValid={validData && !!raw}
name="byteCode"
placeholder="0x8f87a973e..."
rows={6}
onChange={onChange}
disabled={readOnly}
className={classnames('Deploy-field-input', {
'is-valid': value && value.length > 0
})}
className="Deploy-field-input"
value={raw}
/>
)}

View File

@ -1,6 +1,6 @@
import { AmountFieldFactory } from 'components/AmountFieldFactory';
import React from 'react';
import classnames from 'classnames';
import { Input } from 'components/ui';
export const AmountField: React.SFC = () => (
@ -12,11 +12,10 @@ export const AmountField: React.SFC = () => (
<Input
name="value"
value={raw}
isValid={isValid || raw === ''}
onChange={onChange}
readOnly={readOnly}
className={classnames('InteractExplorer-field-input', 'form-control', {
'is-invalid': !(isValid || raw === '')
})}
className="InteractExplorer-field-input"
/>
)}
/>

View File

@ -116,6 +116,7 @@ class InteractExplorerClass extends Component<Props, State> {
<div className="input-group-header">{name + ' ' + type}</div>
<Input
className="InteractExplorer-func-in-input"
isValid={!!(inputs[name] && inputs[name].rawData)}
name={name}
value={(inputs[name] && inputs[name].rawData) || ''}
onChange={this.handleInputChange}
@ -138,7 +139,8 @@ class InteractExplorerClass extends Component<Props, State> {
<label className="input-group">
<div className="input-group-header"> {name + ' ' + type}</div>
<Input
className="InteractExplorer-func-out-input "
className="InteractExplorer-func-out-input"
isValid={!!decodedFieldValue}
value={decodedFieldValue}
disabled={true}
/>

View File

@ -4,7 +4,6 @@ import { getNetworkContracts } from 'selectors/config';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import classnames from 'classnames';
import { NetworkContract } from 'types/network';
import { donationAddressMap } from 'config';
import { Input, TextArea, CodeBlock, Dropdown } from 'components/ui';
@ -63,7 +62,7 @@ class InteractForm extends Component<Props, State> {
};
}
public componentWillReceiveProps(nextProps: Props) {
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
const prevProps = this.props;
if (nextProps.currentTo.raw !== prevProps.currentTo.raw) {
nextProps.resetState();
@ -126,9 +125,8 @@ class InteractForm extends Component<Props, State> {
name="contract_address"
autoComplete="off"
value={currentTo.raw}
className={classnames('InteractForm-address-field-input', {
invalid: !isValid
})}
isValid={isValid}
className="InteractForm-address-field-input"
spellCheck={false}
onChange={onChange}
/>
@ -144,7 +142,8 @@ class InteractForm extends Component<Props, State> {
contract.name === 'Custom' ? (
<TextArea
placeholder={this.abiJsonPlaceholder}
className={`InteractForm-interface-field-input ${validAbiJson ? '' : 'invalid'}`}
isValid={!!validAbiJson}
className="InteractForm-interface-field-input"
onChange={this.handleInput('abiJson')}
value={abiJson}
rows={6}
@ -155,7 +154,8 @@ class InteractForm extends Component<Props, State> {
) : (
<TextArea
placeholder={this.abiJsonPlaceholder}
className={`InteractForm-interface-field-input ${validAbiJson ? '' : 'invalid'}`}
isValid={!!validAbiJson}
className="InteractForm-interface-field-input"
onChange={this.handleInput('abiJson')}
value={abiJson}
rows={6}

View File

@ -37,9 +37,8 @@ class NameInput extends Component<Props, State> {
<label className="input-group input-group-inline ENSInput-name">
<Input
value={domainToCheck}
className={`${
!domainToCheck ? '' : isValidDomain ? '' : 'invalid'
} border-rad-right-0`}
isValid={!!domainToCheck && isValidDomain}
className="border-rad-right-0"
type="text"
placeholder="mycrypto"
onChange={this.onChange}

View File

@ -21,6 +21,8 @@ const PaperWallet: React.SFC<Props> = props => (
<h1 className="GenPaper-title">{translate('GEN_LABEL_5')}</h1>
<Input
value={stripHexPrefix(props.privateKey)}
showValidAsPlain={true}
isValid={true}
aria-label={translateRaw('X_PRIVKEY')}
aria-describedby="x_PrivKeyDesc"
type="text"

View File

@ -61,7 +61,13 @@ export default class MnemonicWord extends React.Component<Props, State> {
{word}
</button>
) : (
<Input className="MnemonicWord-word-input" value={word} readOnly={true} />
<Input
className="MnemonicWord-word-input"
value={word}
readOnly={true}
showValidAsPlain={true}
isValid={true}
/>
)}
</label>
</div>

View File

@ -38,7 +38,7 @@ class ScheduleDepositFieldClass extends Component<Props> {
</span>
</div>
<Input
className={!!scheduleDeposit.raw && !validScheduleDeposit ? 'invalid' : ''}
isValid={scheduleDeposit.raw && validScheduleDeposit}
type="number"
placeholder="0.00001"
value={scheduleDeposit.raw}

Some files were not shown because too many files have changed in this diff Show More