-
Welcome to the New MyCrypto Beta!
+
Welcome to the New MyCrypto Beta Release Candidate!
- You are about to use a version of MyCrypto that hasn't been released yet. While we are
- confident that it is close to being production ready, you may want to use the current
- production site for larger or more time-sensitive transactions.
+ 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.
+
We hope to move this version of MyCrypto into production in the near future!
Feedback and bug reports are greatly appreciated. You can file issues on our{' '}
@@ -46,7 +47,7 @@ export default class BetaAgreement extends React.PureComponent<{}, State> {
className="BetaAgreement-content-buttons-btn is-continue"
onClick={this.doContinue}
>
- Yes, continue to the Beta
+ Yes, continue to the Beta RC
No, take me to the production site
diff --git a/common/components/Header/components/CustomNodeModal.scss b/common/components/CustomNodeModal/CustomNodeModal.scss
similarity index 100%
rename from common/components/Header/components/CustomNodeModal.scss
rename to common/components/CustomNodeModal/CustomNodeModal.scss
diff --git a/common/components/Header/components/CustomNodeModal.tsx b/common/components/CustomNodeModal/CustomNodeModal.tsx
similarity index 98%
rename from common/components/Header/components/CustomNodeModal.tsx
rename to common/components/CustomNodeModal/CustomNodeModal.tsx
index 7395b8d6..1eb9235c 100644
--- a/common/components/Header/components/CustomNodeModal.tsx
+++ b/common/components/CustomNodeModal/CustomNodeModal.tsx
@@ -98,7 +98,7 @@ class CustomNodeModal extends React.Component {
const options = [...staticNetwrks, ...customNetwrks, CUSTOM];
return (
{
/>
- Network
+ {translate('CUSTOM_NETWORK')}
| null;
+ isPanelOpen: boolean;
+}
+
+export default class ElectronNav extends React.Component<{}, State> {
+ public state: State = {
+ panelContent: null,
+ isPanelOpen: false
+ };
+
+ public render() {
+ const { panelContent, isPanelOpen } = this.state;
+
+ return (
+
+
+
+
+ {navigationLinks.map(link => (
+
+ ))}
+
+
+
+
+ Change Language
+
+
+
+ Change Network
+
+
+
+
+
+
+
+
+
+
+
+ {translate('MODAL_BACK')}
+
+
{panelContent}
+
+
+ );
+ }
+
+ private openLanguageSelect = () => {
+ const panelContent = ;
+ this.setState({
+ panelContent,
+ isPanelOpen: true
+ });
+ };
+
+ private openNodeSelect = () => {
+ const panelContent = ;
+ this.setState({
+ panelContent,
+ isPanelOpen: true
+ });
+ };
+
+ private closePanel = () => {
+ const { panelContent } = this.state;
+
+ // Start closing panel
+ this.setState({ isPanelOpen: false });
+
+ // Remove content when out of sight
+ setTimeout(() => {
+ if (this.state.panelContent === panelContent) {
+ this.setState({ panelContent: null });
+ }
+ }, 300);
+ };
+}
diff --git a/common/components/ElectronNav/LanguageSelect.scss b/common/components/ElectronNav/LanguageSelect.scss
new file mode 100644
index 00000000..6df85cdf
--- /dev/null
+++ b/common/components/ElectronNav/LanguageSelect.scss
@@ -0,0 +1,25 @@
+@import 'common/sass/variables';
+@import 'common/sass/mixins';
+
+.LanguageSelect {
+ &-language {
+ @include reset-button;
+ display: block;
+ width: 100%;
+ height: 48px;
+ line-height: 48px;
+ padding: 0 10px;
+ color: $text-color;
+ border-bottom: 1px solid $gray-lighter;
+ text-align: left;
+
+ &:hover {
+ color: $link-hover-color;
+ }
+
+ &.is-selected {
+ color: $link-color;
+ background: $gray-lightest;
+ }
+ }
+}
diff --git a/common/components/ElectronNav/LanguageSelect.tsx b/common/components/ElectronNav/LanguageSelect.tsx
new file mode 100644
index 00000000..1d4e336a
--- /dev/null
+++ b/common/components/ElectronNav/LanguageSelect.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import classnames from 'classnames';
+import { connect } from 'react-redux';
+import { languages } from 'config';
+import { TChangeLanguage, changeLanguage } from 'actions/config';
+import { getLanguageSelection } from 'selectors/config';
+import { AppState } from 'reducers';
+import './LanguageSelect.scss';
+
+interface OwnProps {
+ closePanel(): void;
+}
+
+interface StateProps {
+ languageSelection: string;
+}
+
+interface DispatchProps {
+ changeLanguage: TChangeLanguage;
+}
+
+type Props = OwnProps & StateProps & DispatchProps;
+
+class LanguageSelect extends React.Component {
+ public render() {
+ const { languageSelection } = this.props;
+ return (
+
+ {Object.entries(languages).map(lang => (
+ this.handleLanguageSelect(lang[0])}
+ >
+ {lang[1]}
+
+ ))}
+
+ );
+ }
+
+ private handleLanguageSelect = (lang: string) => {
+ this.props.changeLanguage(lang);
+ this.props.closePanel();
+ };
+}
+
+export default connect(
+ (state: AppState): StateProps => ({
+ languageSelection: getLanguageSelection(state)
+ }),
+ {
+ changeLanguage
+ }
+)(LanguageSelect);
diff --git a/common/components/ElectronNav/NetworkStatus.scss b/common/components/ElectronNav/NetworkStatus.scss
new file mode 100644
index 00000000..3f96fb48
--- /dev/null
+++ b/common/components/ElectronNav/NetworkStatus.scss
@@ -0,0 +1,30 @@
+@import 'common/sass/variables';
+
+.NetworkStatus {
+ display: flex;
+ align-items: center;
+
+ &-icon {
+ width: 10px;
+ height: 10px;
+ margin-right: 4px;
+ border-radius: 100%;
+
+ &.is-online {
+ background: $brand-success;
+ }
+
+ &.is-offline {
+ background: $brand-danger;
+ }
+
+ &.is-connecting {
+ background: $brand-warning;
+ }
+ }
+
+ &-text {
+ color: $gray-dark;
+ font-size: 9px;
+ }
+}
diff --git a/common/components/ElectronNav/NetworkStatus.tsx b/common/components/ElectronNav/NetworkStatus.tsx
new file mode 100644
index 00000000..367f5d54
--- /dev/null
+++ b/common/components/ElectronNav/NetworkStatus.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import translate from 'translations';
+import { getNetworkConfig, getOffline, isNodeChanging } from 'selectors/config';
+import { NetworkConfig } from 'types/network';
+import { AppState } from 'reducers';
+import './NetworkStatus.scss';
+
+enum NETWORK_STATUS {
+ CONNECTING = 'NETWORK_STATUS_CONNECTING',
+ OFFLINE = 'NETWORK_STATUS_OFFLINE',
+ ONLINE = 'NETWORK_STATUS_ONLINE'
+}
+
+interface StateProps {
+ network: NetworkConfig;
+ isOffline: boolean;
+ isChangingNode: boolean;
+}
+
+const NetworkStatus: React.SFC = ({ isOffline, isChangingNode, network }) => {
+ let statusClass: string;
+ let statusText: string;
+ const $network = network.isCustom ? network.unit : network.name;
+
+ if (isChangingNode) {
+ statusClass = 'is-connecting';
+ statusText = NETWORK_STATUS.CONNECTING;
+ } else if (isOffline) {
+ statusClass = 'is-offline';
+ statusText = NETWORK_STATUS.OFFLINE;
+ } else {
+ statusClass = 'is-online';
+ statusText = NETWORK_STATUS.ONLINE;
+ }
+
+ return (
+
+
+
{translate(statusText, { $network })}
+
+ );
+};
+
+export default connect((state: AppState): StateProps => ({
+ network: getNetworkConfig(state),
+ isOffline: getOffline(state),
+ isChangingNode: isNodeChanging(state)
+}))(NetworkStatus);
diff --git a/common/components/ElectronNav/NodeSelect.scss b/common/components/ElectronNav/NodeSelect.scss
new file mode 100644
index 00000000..b30e47cd
--- /dev/null
+++ b/common/components/ElectronNav/NodeSelect.scss
@@ -0,0 +1,31 @@
+@import 'common/sass/variables';
+@import 'common/sass/mixins';
+
+.NodeSelect {
+ &-node {
+ @include reset-button;
+ display: block;
+ width: 100%;
+ height: 48px;
+ line-height: 48px;
+ padding: 0 10px;
+ color: $text-color;
+ border-bottom: 1px solid $gray-lighter;
+ border-left: 4px solid;
+ text-align: left;
+ @include ellipsis;
+
+ &:hover {
+ color: $link-hover-color;
+ }
+
+ &.is-active {
+ color: $link-color;
+ background: $gray-lightest;
+ }
+
+ small {
+ font-size: 12px;
+ }
+ }
+}
diff --git a/common/components/ElectronNav/NodeSelect.tsx b/common/components/ElectronNav/NodeSelect.tsx
new file mode 100644
index 00000000..52f24f73
--- /dev/null
+++ b/common/components/ElectronNav/NodeSelect.tsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import classnames from 'classnames';
+import { connect } from 'react-redux';
+import translate from 'translations';
+import CustomNodeModal from 'components/CustomNodeModal';
+import {
+ TChangeNodeIntent,
+ TAddCustomNode,
+ TRemoveCustomNode,
+ changeNodeIntent,
+ addCustomNode,
+ removeCustomNode,
+ AddCustomNodeAction
+} from 'actions/config';
+import {
+ isNodeChanging,
+ getNodeId,
+ CustomNodeOption,
+ NodeOption,
+ getNodeOptions
+} from 'selectors/config';
+import { AppState } from 'reducers';
+import './NodeSelect.scss';
+
+interface OwnProps {
+ closePanel(): void;
+}
+
+interface StateProps {
+ nodeSelection: AppState['config']['nodes']['selectedNode']['nodeId'];
+ isChangingNode: AppState['config']['nodes']['selectedNode']['pending'];
+ nodeOptions: (CustomNodeOption | NodeOption)[];
+}
+
+interface DispatchProps {
+ changeNodeIntent: TChangeNodeIntent;
+ addCustomNode: TAddCustomNode;
+ removeCustomNode: TRemoveCustomNode;
+}
+
+type Props = OwnProps & StateProps & DispatchProps;
+
+interface State {
+ isAddingCustomNode: boolean;
+}
+
+class NodeSelect extends React.Component {
+ public state: State = {
+ isAddingCustomNode: false
+ };
+
+ public render() {
+ const { nodeSelection, nodeOptions } = this.props;
+ const { isAddingCustomNode } = this.state;
+
+ return (
+
+ {nodeOptions.map(node => (
+ this.handleNodeSelect(node.value)}
+ style={{ borderLeftColor: node.color }}
+ >
+ {this.renderNodeLabel(node)}
+
+ ))}
+
+ {translate('NODE_ADD')}
+
+
+
+
+ );
+ }
+
+ private handleNodeSelect = (node: string) => {
+ this.props.changeNodeIntent(node);
+ this.props.closePanel();
+ };
+
+ private renderNodeLabel(node: CustomNodeOption | NodeOption) {
+ return node.isCustom ? (
+
+ {node.label.network} - {node.label.nodeName} (custom)
+
+ ) : (
+
+ {node.label.network} - ({node.label.service})
+
+ );
+ }
+
+ private openCustomNodeModal = () => {
+ this.setState({ isAddingCustomNode: true });
+ };
+
+ private closeCustomNodeModal = () => {
+ this.setState({ isAddingCustomNode: false });
+ };
+
+ private addCustomNode = (payload: AddCustomNodeAction['payload']) => {
+ this.props.addCustomNode(payload);
+ this.closeCustomNodeModal();
+ };
+}
+
+export default connect(
+ (state: AppState): StateProps => ({
+ isChangingNode: isNodeChanging(state),
+ nodeSelection: getNodeId(state),
+ nodeOptions: getNodeOptions(state)
+ }),
+ {
+ changeNodeIntent,
+ addCustomNode,
+ removeCustomNode
+ }
+)(NodeSelect);
diff --git a/common/components/ElectronNav/index.tsx b/common/components/ElectronNav/index.tsx
new file mode 100644
index 00000000..b7fea679
--- /dev/null
+++ b/common/components/ElectronNav/index.tsx
@@ -0,0 +1,2 @@
+import ElectronNav from './ElectronNav';
+export default ElectronNav;
diff --git a/common/components/ExtendedNotifications/TransactionSucceeded.tsx b/common/components/ExtendedNotifications/TransactionSucceeded.tsx
index c2e1e51d..c09c4e4f 100644
--- a/common/components/ExtendedNotifications/TransactionSucceeded.tsx
+++ b/common/components/ExtendedNotifications/TransactionSucceeded.tsx
@@ -3,14 +3,16 @@ import { Link } from 'react-router-dom';
import translate from 'translations';
import { NewTabLink } from 'components/ui';
import { BlockExplorerConfig } from 'types/network';
+import { getTXDetailsCheckURL } from 'libs/scheduling';
import { etherChainExplorerInst } from 'config/data';
export interface TransactionSucceededProps {
txHash: string;
blockExplorer?: BlockExplorerConfig;
+ scheduling?: boolean;
}
-const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededProps) => {
+const TransactionSucceeded = ({ txHash, blockExplorer, scheduling }: TransactionSucceededProps) => {
let verifyBtn: React.ReactElement | undefined;
let altVerifyBtn: React.ReactElement | undefined;
if (blockExplorer) {
@@ -30,11 +32,21 @@ const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededPro
);
}
+ let scheduleDetailsBtn: React.ReactElement | undefined;
+ if (scheduling) {
+ scheduleDetailsBtn = (
+
+ {translate('SCHEDULE_CHECK')}
+
+ );
+ }
+
return (
{translate('SUCCESS_3')} {txHash}
+ {scheduleDetailsBtn}
{verifyBtn}
{altVerifyBtn}
diff --git a/common/components/Footer/index.tsx b/common/components/Footer/index.tsx
index a6776173..ecbda241 100644
--- a/common/components/Footer/index.tsx
+++ b/common/components/Footer/index.tsx
@@ -1,22 +1,22 @@
import logo from 'assets/images/logo-mycrypto.svg';
import {
- ledgerReferralURL,
- trezorReferralURL,
- ethercardReferralURL,
donationAddressMap,
VERSION,
knowledgeBaseURL,
- discordURL
+ socialMediaLinks,
+ productLinks,
+ affiliateLinks,
+ partnerLinks
} from 'config';
import React from 'react';
import PreFooter from './PreFooter';
-import DisclaimerModal from './DisclaimerModal';
+import DisclaimerModal from 'components/DisclaimerModal';
import { NewTabLink } from 'components/ui';
import OnboardModal from 'containers/OnboardModal';
import './index.scss';
import { translateRaw } from 'translations';
-const SocialMediaLink = ({ link, text }: Link) => {
+const SocialMediaLink = ({ link, text }: { link: string; text: string }) => {
return (
@@ -24,101 +24,6 @@ const SocialMediaLink = ({ link, text }: Link) => {
);
};
-const SOCIAL_MEDIA: Link[] = [
- {
- link: 'https://twitter.com/mycrypto',
- text: 'twitter'
- },
- {
- link: 'https://www.facebook.com/MyCrypto/',
- text: 'facebook'
- },
- {
- link: 'https://medium.com/@mycrypto',
- text: 'medium'
- },
- {
- link: 'https://www.linkedin.com/company/mycrypto',
- text: 'linkedin'
- },
- {
- link: 'https://github.com/MyCryptoHQ',
- text: 'github'
- },
- {
- link: 'https://www.reddit.com/r/mycrypto/',
- text: 'reddit'
- },
- {
- link: discordURL,
- text: 'discord'
- }
-];
-
-const PRODUCT_INFO: Link[] = [
- {
- link:
- 'https://chrome.google.com/webstore/detail/etheraddresslookup/pdknmigbbbhmllnmgdfalmedcmcefdfn',
- text: translateRaw('ETHER_ADDRESS_LOOKUP')
- },
- {
- link:
- 'https://chrome.google.com/webstore/detail/ethersecuritylookup/bhhfhgpgmifehjdghlbbijjaimhmcgnf',
- text: translateRaw('ETHER_SECURITY_LOOKUP')
- },
- {
- link: 'https://etherscamdb.info/',
- text: translateRaw('ETHERSCAMDB')
- },
- {
- link: 'https://www.mycrypto.com/helpers.html',
- text: translateRaw('FOOTER_HELP_AND_DEBUGGING')
- },
- {
- link: 'mailto:press@mycrypto.com',
- text: translateRaw('FOOTER_PRESS')
- }
-];
-
-const AFFILIATES: Link[] = [
- {
- link: ledgerReferralURL,
- text: translateRaw('LEDGER_REFERRAL_1')
- },
- {
- link: trezorReferralURL,
- text: translateRaw('TREZOR_REFERAL')
- },
- {
- link: ethercardReferralURL,
- text: translateRaw('ETHERCARD_REFERAL')
- }
-];
-
-const FRIENDS: Link[] = [
- {
- link: 'https://metamask.io/',
- text: 'MetaMask'
- },
- {
- link: 'https://infura.io/',
- text: 'Infura'
- },
- {
- link: 'https://etherscan.io/',
- text: 'Etherscan'
- },
- {
- link: 'https://etherchain.org/',
- text: 'Etherchain'
- }
-];
-
-interface Link {
- link: string;
- text: string;
-}
-
interface Props {
latestBlock: string;
}
@@ -139,7 +44,7 @@ export default class Footer extends React.PureComponent {
-
MyCrypto.com
+
MyCrypto.com
{translateRaw('FOOTER_SUPPORT')}
{translateRaw('FOOTER_TEAM')}
@@ -192,7 +100,7 @@ export default class Footer extends React.PureComponent {
{translateRaw('FOOTER_AFFILIATE_TITLE')}
- {AFFILIATES.map((link, i) => (
+ {affiliateLinks.map((link, i) => (
{link.text}
@@ -214,7 +122,7 @@ export default class Footer extends React.PureComponent
{
- {FRIENDS.map((link, i) => (
+ {partnerLinks.map((link, i) => (
{link.text}
diff --git a/common/components/GasLimitField.tsx b/common/components/GasLimitField.tsx
index 92fbc722..34c6857b 100644
--- a/common/components/GasLimitField.tsx
+++ b/common/components/GasLimitField.tsx
@@ -9,9 +9,14 @@ import { Input } from 'components/ui';
interface Props {
customLabel?: string;
disabled?: boolean;
+ hideGasCalculationSpinner?: boolean;
}
-export const GasLimitField: React.SFC
= ({ customLabel, disabled }) => (
+export const GasLimitField: React.SFC = ({
+ customLabel,
+ disabled,
+ hideGasCalculationSpinner
+}) => (
(
@@ -19,7 +24,10 @@ export const GasLimitField: React.SFC
= ({ customLabel, disabled }) => (
{customLabel ? customLabel : translate('TRANS_GAS')}
-
+
| null;
}
type Props = DispatchProps & OwnProps;
-class GasLimitFieldClass extends Component {
+class GasLimitFieldClass extends Component {
public componentDidMount() {
- const { gasLimit } = this.props;
+ const { gasLimit, scheduling } = this.props;
+
+ if (scheduling) {
+ return;
+ }
+
if (gasLimit) {
this.props.inputGasLimit(gasLimit);
} else {
@@ -45,7 +54,12 @@ class GasLimitFieldClass extends Component {
};
}
-const GasLimitField = connect(null, { inputGasLimit })(GasLimitFieldClass);
+const GasLimitField = connect(
+ (state: AppState) => ({
+ scheduling: getSchedulingToggle(state).value
+ }),
+ { inputGasLimit }
+)(GasLimitFieldClass);
interface DefaultGasLimitFieldProps {
withProps(props: CallBackProps): React.ReactElement | null;
diff --git a/common/components/GenerateTransactionFactory/GenerateTransactionFactory.tsx b/common/components/GenerateTransactionFactory/GenerateTransactionFactory.tsx
index 6503ccff..1e78c0f4 100644
--- a/common/components/GenerateTransactionFactory/GenerateTransactionFactory.tsx
+++ b/common/components/GenerateTransactionFactory/GenerateTransactionFactory.tsx
@@ -42,7 +42,7 @@ interface OwnProps {
type Props = OwnProps & StateProps;
-class GenerateTransactionFactoryClass extends Component {
+export class GenerateTransactionFactoryClass extends Component {
public render() {
const {
walletType,
@@ -61,6 +61,7 @@ class GenerateTransactionFactoryClass extends Component {
const isButtonDisabled =
!isFullTransaction || networkRequestPending || !validGasPrice || !validGasLimit;
+
return (
{
- {tabs.map(link => {
- return ;
- })}
+ {navigationLinks.map(link => (
+
+ ))}
diff --git a/common/components/Header/components/NavigationLink.scss b/common/components/Header/components/NavigationLink.scss
deleted file mode 100644
index 7d68efc2..00000000
--- a/common/components/Header/components/NavigationLink.scss
+++ /dev/null
@@ -1,60 +0,0 @@
-@import 'common/sass/variables';
-
-.NavigationLink {
- display: inline-block;
-
- &-link {
- color: darken($link-color, 15%);
- display: block;
- font-size: 16px;
- font-weight: 300;
- padding: 10px;
- white-space: nowrap;
- position: relative;
- min-height: 2.75rem;
-
- &:after {
- content: '';
- background: $brand-primary;
- height: 2px;
- width: 100%;
- left: 0px;
- position: absolute;
- bottom: -1px;
- transition: all 250ms ease 0s;
- transform: scaleX(0);
- }
-
- &.is-active,
- &:hover,
- &:focus {
- color: $brand-primary;
- text-decoration: none;
- transition: all 250ms ease 0s;
-
- &:after {
- transform: scaleX(1);
- transition: all 250ms ease 0s;
- }
- }
-
- &.is-disabled {
- pointer-events: none;
- opacity: 0.3;
- }
- }
-}
-
-#NAV_SWAP a:before {
- content: '';
- display: inline-block;
- margin-top: -0.1rem;
- width: 1.3rem;
- height: 1.3rem;
- background-image: url('~assets/images/logo-shapeshift-no-text.svg');
- background-position: center;
- background-repeat: no-repeat;
- background-size: contain;
- vertical-align: middle;
- margin-right: 4px;
-}
diff --git a/common/components/Header/index.tsx b/common/components/Header/index.tsx
index 69d6b9e7..a6509e1f 100644
--- a/common/components/Header/index.tsx
+++ b/common/components/Header/index.tsx
@@ -1,12 +1,14 @@
import {
TChangeLanguage,
TChangeNodeIntent,
+ TChangeNodeIntentOneTime,
TAddCustomNode,
TRemoveCustomNode,
TAddCustomNetwork,
AddCustomNodeAction,
changeLanguage,
changeNodeIntent,
+ changeNodeIntentOneTime,
addCustomNode,
removeCustomNode,
addCustomNetwork
@@ -19,8 +21,8 @@ import { Link } from 'react-router-dom';
import { TSetGasPriceField, setGasPriceField } from 'actions/transaction';
import { ANNOUNCEMENT_MESSAGE, ANNOUNCEMENT_TYPE, languages } from 'config';
import Navigation from './components/Navigation';
-import CustomNodeModal from './components/CustomNodeModal';
import OnlineStatus from './components/OnlineStatus';
+import CustomNodeModal from 'components/CustomNodeModal';
import { getKeyByValue } from 'utils/helpers';
import { NodeConfig } from 'types/node';
import './index.scss';
@@ -34,15 +36,21 @@ import {
CustomNodeOption,
NodeOption,
getNodeOptions,
- getNetworkConfig
+ getNetworkConfig,
+ isStaticNodeId
} from 'selectors/config';
import { NetworkConfig } from 'types/network';
-import { connect } from 'react-redux';
-import { stripWeb3Network } from 'libs/nodes';
+import { connect, MapStateToProps } from 'react-redux';
+import translate from 'translations';
+
+interface OwnProps {
+ networkParam: string | null;
+}
interface DispatchProps {
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
+ changeNodeIntentOneTime: TChangeNodeIntentOneTime;
setGasPriceField: TSetGasPriceField;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
@@ -50,6 +58,7 @@ interface DispatchProps {
}
interface StateProps {
+ shouldSetNodeFromQS: boolean;
network: NetworkConfig;
languageSelection: AppState['config']['meta']['languageSelection'];
node: NodeConfig;
@@ -59,7 +68,11 @@ interface StateProps {
nodeOptions: (CustomNodeOption | NodeOption)[];
}
-const mapStateToProps = (state: AppState): StateProps => ({
+const mapStateToProps: MapStateToProps = (
+ state,
+ { networkParam }
+): StateProps => ({
+ shouldSetNodeFromQS: !!(networkParam && isStaticNodeId(state, networkParam)),
isOffline: getOffline(state),
isChangingNode: isNodeChanging(state),
languageSelection: getLanguageSelection(state),
@@ -73,6 +86,7 @@ const mapDispatchToProps: DispatchProps = {
setGasPriceField,
changeLanguage,
changeNodeIntent,
+ changeNodeIntentOneTime,
addCustomNode,
removeCustomNode,
addCustomNetwork
@@ -82,13 +96,17 @@ interface State {
isAddingCustomNode: boolean;
}
-type Props = StateProps & DispatchProps;
+type Props = OwnProps & StateProps & DispatchProps;
class Header extends Component {
public state = {
isAddingCustomNode: false
};
+ public componentDidMount() {
+ this.attemptSetNodeFromQueryParameter();
+ }
+
public render() {
const {
languageSelection,
@@ -120,7 +138,7 @@ class Header extends Component {
...rest,
name: (
- {stripWeb3Network(label.network)} ({label.service})
+ {label.network} ({label.service})
)
};
@@ -161,7 +179,6 @@ class Header extends Component {
color="white"
/>
- {console.log(nodeSelection)}
{
value={nodeSelection || ''}
extra={
- Add Custom Node
+ {translate('NODE_ADD')}
}
disabled={nodeSelection === 'web3'}
@@ -221,6 +238,13 @@ class Header extends Component
{
this.setState({ isAddingCustomNode: false });
this.props.addCustomNode(payload);
};
+
+ private attemptSetNodeFromQueryParameter() {
+ const { shouldSetNodeFromQS, networkParam } = this.props;
+ if (shouldSetNodeFromQS) {
+ this.props.changeNodeIntentOneTime(networkParam!);
+ }
+ }
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
diff --git a/common/components/LogOutPrompt.tsx b/common/components/LogOutPrompt.tsx
index 9ad0daad..09032c7d 100644
--- a/common/components/LogOutPrompt.tsx
+++ b/common/components/LogOutPrompt.tsx
@@ -70,7 +70,6 @@ class LogOutPromptClass extends React.Component {
private onConfirm = () => {
const { nextLocation: next } = this.state;
this.props.resetWallet();
- this.props.web3UnsetNode();
this.setState(
{
openModal: false,
@@ -79,6 +78,7 @@ class LogOutPromptClass extends React.Component {
() => {
if (next) {
this.props.history.push(`${next.pathname}${next.search}${next.hash}`);
+ this.props.web3UnsetNode();
}
}
);
diff --git a/common/components/Header/components/NavigationLink.tsx b/common/components/NavigationLink.tsx
similarity index 76%
rename from common/components/Header/components/NavigationLink.tsx
rename to common/components/NavigationLink.tsx
index 8e841880..313794ba 100644
--- a/common/components/Header/components/NavigationLink.tsx
+++ b/common/components/NavigationLink.tsx
@@ -2,21 +2,20 @@ import classnames from 'classnames';
import React from 'react';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import translate, { translateRaw } from 'translations';
-import { TabLink } from './Navigation';
-import './NavigationLink.scss';
+import { NavigationLink } from 'config';
interface Props extends RouteComponentProps<{}> {
- link: TabLink;
+ link: NavigationLink;
isHomepage: boolean;
+ className: string;
}
-class NavigationLink extends React.PureComponent {
+class NavigationLinkClass extends React.PureComponent {
public render() {
- const { link, location, isHomepage } = this.props;
- const isExternalLink = link.to.includes('http');
+ const { link, location, isHomepage, className } = this.props;
let isActive = false;
- if (!isExternalLink) {
+ if (!link.external) {
// isActive if
// 1) Current path is the same as link
// 2) the first path is the same for both links (/account and /account/send)
@@ -27,7 +26,7 @@ class NavigationLink extends React.PureComponent {
}
const linkClasses = classnames({
- 'NavigationLink-link': true,
+ [`${className}-link`]: true,
'is-disabled': !link.to,
'is-active': isActive
});
@@ -43,6 +42,7 @@ class NavigationLink extends React.PureComponent {
rel="noopener noreferrer"
>
{translate(link.name)}
+
) : (
@@ -51,7 +51,7 @@ class NavigationLink extends React.PureComponent {
);
return (
-
+
{linkEl}
);
@@ -59,4 +59,4 @@ class NavigationLink extends React.PureComponent {
}
// withRouter is a HOC which provides NavigationLink with a react-router location prop
-export default withRouter(NavigationLink);
+export default withRouter(NavigationLinkClass);
diff --git a/common/components/SendButtonFactory/SendButtonFactory.tsx b/common/components/SendButtonFactory/SendButtonFactory.tsx
index a4bf908e..5bed56d6 100644
--- a/common/components/SendButtonFactory/SendButtonFactory.tsx
+++ b/common/components/SendButtonFactory/SendButtonFactory.tsx
@@ -41,7 +41,7 @@ interface OwnProps {
type Props = StateProps & OwnProps;
-class SendButtonFactoryClass extends Component {
+export class SendButtonFactoryClass extends Component {
public render() {
const {
signing,
diff --git a/common/components/SubTabs/SubTabs.scss b/common/components/SubTabs/SubTabs.scss
index 2b8ddb75..e60693c7 100644
--- a/common/components/SubTabs/SubTabs.scss
+++ b/common/components/SubTabs/SubTabs.scss
@@ -3,6 +3,10 @@
.SubTabs {
margin-top: $space-xs;
+ .is-electron & {
+ margin-top: 0;
+ }
+
&-tabs {
display: inline-block;
white-space: nowrap;
diff --git a/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx b/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx
index 36dad255..e7f07e57 100644
--- a/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx
+++ b/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx
@@ -50,6 +50,7 @@ interface OwnProps {
disableToggle?: boolean;
advancedGasOptions?: AdvancedOptions;
className?: string;
+ scheduling?: boolean;
}
type Props = DispatchProps & OwnProps & StateProps;
@@ -90,16 +91,19 @@ class TXMetaDataPanel extends React.Component {
}
public render() {
- const { offline, disableToggle, advancedGasOptions, className = '' } = this.props;
+ const { offline, disableToggle, advancedGasOptions, className = '', scheduling } = this.props;
const { gasPrice } = this.state;
const showAdvanced = this.state.sliderState === 'advanced' || offline;
+
return (
+
{showAdvanced ? (
) : (
{
};
public render() {
- const { autoGasLimitEnabled, gasPrice, validGasPrice } = this.props;
- const { gasPriceField, gasLimitField, nonceField, dataField, feeSummary } = this.state.options;
+ const { autoGasLimitEnabled, gasPrice, scheduling, validGasPrice } = this.props;
+ const { gasPriceField, gasLimitField, nonceField, dataField } = this.state.options;
+
return (
@@ -78,7 +84,7 @@ class AdvancedGas extends React.Component
{
{translateRaw('OFFLINE_STEP2_LABEL_3')} (gwei)
{
{gasLimitField && (
-
+
)}
{nonceField && (
@@ -101,24 +111,62 @@ class AdvancedGas extends React.Component
{
)}
- {dataField && (
-
-
-
- )}
+ {!scheduling &&
+ dataField && (
+
+
+
+ )}
- {feeSummary && (
-
- (
-
- {gasPriceWei} * {gasLimit} = {fee} {usd && ~= ${usd} USD }
-
- )}
- />
-
- )}
+ {this.renderFee()}
+
+ );
+ }
+
+ private renderFee() {
+ const { gasPrice, scheduleGasPrice } = this.props;
+ const { feeSummary } = this.state.options;
+
+ if (!feeSummary) {
+ return;
+ }
+
+ return (
+
+ this.printFeeFormula(data)}
+ />
+
+ );
+ }
+
+ private printFeeFormula(data: RenderData) {
+ if (this.props.scheduling) {
+ return this.getScheduleFeeFormula(data);
+ }
+
+ return this.getStandardFeeFormula(data);
+ }
+
+ private getStandardFeeFormula({ gasPriceWei, gasLimit, fee, usd }: RenderData) {
+ return (
+
+ {gasPriceWei} * {gasLimit} = {fee} {usd && ~= ${usd} USD }
+
+ );
+ }
+
+ private getScheduleFeeFormula({ gasPriceWei, scheduleGasLimit, fee, usd }: RenderData) {
+ const { scheduleGasPrice, timeBounty } = this.props;
+
+ return (
+
+ {timeBounty && timeBounty.value && timeBounty.value.toString()} + {gasPriceWei} *{' '}
+ {EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT.toString()} +{' '}
+ {scheduleGasPrice && scheduleGasPrice.value && scheduleGasPrice.value.toString()} * ({EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST.toString()}{' '}
+ + {scheduleGasLimit}) = {fee} {usd && ~= ${usd} USD }
);
}
@@ -136,6 +184,8 @@ class AdvancedGas extends React.Component {
export default connect(
(state: AppState) => ({
autoGasLimitEnabled: getAutoGasLimitEnabled(state),
+ scheduleGasPrice: getScheduleGasPrice(state),
+ timeBounty: getTimeBounty(state),
validGasPrice: isValidGasPrice(state)
}),
{ toggleAutoGasLimit }
diff --git a/common/components/TXMetaDataPanel/components/FeeSummary.scss b/common/components/TXMetaDataPanel/components/FeeSummary.scss
index 3910465e..00eab85a 100644
--- a/common/components/TXMetaDataPanel/components/FeeSummary.scss
+++ b/common/components/TXMetaDataPanel/components/FeeSummary.scss
@@ -9,3 +9,9 @@
text-align: center;
font-size: 14px;
}
+
+.SchedulingFeeSummary {
+ font-size: 12px;
+ height: auto;
+ min-height: 42px;
+}
diff --git a/common/components/TXMetaDataPanel/components/FeeSummary.tsx b/common/components/TXMetaDataPanel/components/FeeSummary.tsx
index 615162a2..fe6ac65a 100644
--- a/common/components/TXMetaDataPanel/components/FeeSummary.tsx
+++ b/common/components/TXMetaDataPanel/components/FeeSummary.tsx
@@ -2,17 +2,21 @@ import React from 'react';
import BN from 'bn.js';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
+import classNames from 'classnames';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { getIsEstimating } from 'selectors/gas';
import { getGasLimit } from 'selectors/transaction';
import { UnitDisplay, Spinner } from 'components/ui';
import { NetworkConfig } from 'types/network';
import './FeeSummary.scss';
+import { getScheduleGasLimit, getTimeBounty, getSchedulingToggle } from 'selectors/schedule';
+import { calcEACTotalCost } from 'libs/scheduling';
-interface RenderData {
+export interface RenderData {
gasPriceWei: string;
gasPriceGwei: string;
gasLimit: string;
+ scheduleGasLimit: string;
fee: React.ReactElement;
usd: React.ReactElement | null;
}
@@ -23,10 +27,15 @@ interface ReduxStateProps {
network: NetworkConfig;
isOffline: AppState['config']['meta']['offline'];
isGasEstimating: AppState['gas']['isEstimating'];
+ scheduleGasLimit: AppState['schedule']['scheduleGasLimit'];
+ timeBounty: AppState['schedule']['timeBounty'];
+ scheduling: AppState['schedule']['schedulingToggle']['value'];
}
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
+ scheduleGasPrice: AppState['schedule']['scheduleGasPrice'];
+
render(data: RenderData): React.ReactElement | string;
}
@@ -34,7 +43,16 @@ type Props = OwnProps & ReduxStateProps;
class FeeSummary extends React.Component {
public render() {
- const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props;
+ const {
+ gasPrice,
+ gasLimit,
+ rates,
+ network,
+ isOffline,
+ isGasEstimating,
+ scheduling,
+ scheduleGasLimit
+ } = this.props;
if (isGasEstimating) {
return (
@@ -44,7 +62,7 @@ class FeeSummary extends React.Component {
);
}
- const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
+ const feeBig = this.getFee();
const fee = (
{
/>
);
+ const feeSummaryClasses = classNames({
+ FeeSummary: true,
+ SchedulingFeeSummary: scheduling
+ });
+
return (
-
+
{this.props.render({
gasPriceWei: gasPrice.value.toString(),
gasPriceGwei: gasPrice.raw,
fee,
usd,
- gasLimit: gasLimit.raw
+ gasLimit: gasLimit.raw,
+ scheduleGasLimit: scheduleGasLimit.raw
})}
);
}
+
+ private getFee() {
+ const { scheduling } = this.props;
+
+ if (scheduling) {
+ return this.calculateSchedulingFee();
+ }
+
+ return this.calculateStandardFee();
+ }
+
+ private calculateStandardFee() {
+ const { gasPrice, gasLimit } = this.props;
+
+ return gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
+ }
+
+ private calculateSchedulingFee() {
+ const { gasPrice, scheduleGasLimit, scheduleGasPrice, timeBounty } = this.props;
+
+ return (
+ gasPrice.value &&
+ scheduleGasLimit.value &&
+ timeBounty.value &&
+ calcEACTotalCost(
+ scheduleGasLimit.value,
+ gasPrice.value,
+ scheduleGasPrice.value,
+ timeBounty.value
+ )
+ );
+ }
}
function mapStateToProps(state: AppState): ReduxStateProps {
@@ -88,7 +144,10 @@ function mapStateToProps(state: AppState): ReduxStateProps {
rates: state.rates.rates,
network: getNetworkConfig(state),
isOffline: getOffline(state),
- isGasEstimating: getIsEstimating(state)
+ isGasEstimating: getIsEstimating(state),
+ scheduling: getSchedulingToggle(state).value,
+ scheduleGasLimit: getScheduleGasLimit(state),
+ timeBounty: getTimeBounty(state)
};
}
diff --git a/common/components/TXMetaDataPanel/components/SimpleGas.tsx b/common/components/TXMetaDataPanel/components/SimpleGas.tsx
index 593aab76..e386af59 100644
--- a/common/components/TXMetaDataPanel/components/SimpleGas.tsx
+++ b/common/components/TXMetaDataPanel/components/SimpleGas.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import Slider, { createSliderWithTooltip } from 'rc-slider';
import translate from 'translations';
-import FeeSummary from './FeeSummary';
import './SimpleGas.scss';
import { AppState } from 'reducers';
import {
@@ -17,11 +16,15 @@ import { Wei, fromWei } from 'libs/units';
import { gasPriceDefaults } from 'config';
import { InlineSpinner } from 'components/ui/InlineSpinner';
import { TInputGasPrice } from 'actions/transaction';
+import FeeSummary from './FeeSummary';
+import { getScheduleGasPrice } from 'selectors/schedule';
+
const SliderWithTooltip = createSliderWithTooltip(Slider);
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
setGasPrice: TInputGasPrice;
+
inputGasPrice(rawGas: string): void;
}
@@ -32,6 +35,7 @@ interface StateProps {
gasLimitPending: boolean;
isWeb3Node: boolean;
gasLimitEstimationTimedOut: boolean;
+ scheduleGasPrice: AppState['schedule']['scheduleGasPrice'];
}
interface ActionProps {
@@ -68,7 +72,8 @@ class SimpleGas extends React.Component
{
gasLimitEstimationTimedOut,
isWeb3Node,
noncePending,
- gasLimitPending
+ gasLimitPending,
+ scheduleGasPrice
} = this.props;
const bounds = {
@@ -114,6 +119,7 @@ class SimpleGas extends React.Component {
(
{fee} {usd && / ${usd} }
@@ -151,7 +157,8 @@ export default connect(
noncePending: nonceRequestPending(state),
gasLimitPending: getGasEstimationPending(state),
gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state),
- isWeb3Node: getIsWeb3Node(state)
+ isWeb3Node: getIsWeb3Node(state),
+ scheduleGasPrice: getScheduleGasPrice(state)
}),
{
fetchGasEstimates
diff --git a/common/components/Translate.tsx b/common/components/Translate.tsx
index fc579fe1..ca35d3c8 100644
--- a/common/components/Translate.tsx
+++ b/common/components/Translate.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import Markdown from 'react-markdown';
+import NewTabLink from 'components/ui/NewTabLink';
interface Props {
source: string;
@@ -11,7 +12,10 @@ const TranslateMarkdown = ({ source }: Props) => {
escapeHtml={true}
unwrapDisallowed={true}
allowedTypes={['link', 'emphasis', 'strong', 'code', 'root', 'inlineCode']}
- renderers={{ root: React.Fragment }}
+ renderers={{
+ root: React.Fragment,
+ link: NewTabLink
+ }}
source={source}
/>
);
diff --git a/common/components/WalletDecrypt/WalletDecrypt.scss b/common/components/WalletDecrypt/WalletDecrypt.scss
index f97ca1d5..186a6c92 100644
--- a/common/components/WalletDecrypt/WalletDecrypt.scss
+++ b/common/components/WalletDecrypt/WalletDecrypt.scss
@@ -26,6 +26,8 @@ $speed: 500ms;
position: relative;
&-wallets {
+ margin: 0 -$space-md;
+
&-title {
@include decrypt-title;
}
diff --git a/common/components/WalletDecrypt/components/DeprecationWarning.tsx b/common/components/WalletDecrypt/components/DeprecationWarning.tsx
new file mode 100644
index 00000000..82eab917
--- /dev/null
+++ b/common/components/WalletDecrypt/components/DeprecationWarning.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import translate from 'translations';
+
+const DeprecationWarning: React.SFC<{}> = () => {
+ if (process.env.BUILD_DOWNLOADABLE) {
+ return null;
+ }
+
+ return {translate('INSECURE_WALLET_DEPRECATION')}
;
+};
+
+export default DeprecationWarning;
diff --git a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx
index d22bd713..c12d6c78 100644
--- a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx
+++ b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx
@@ -24,7 +24,6 @@ const WALLETS_PER_PAGE = 5;
interface Props {
// Passed props
isOpen?: boolean;
- walletType?: string;
dPath: string;
dPaths: DPath[];
publicKey?: string;
@@ -87,16 +86,7 @@ class DeterministicWalletsModalClass extends React.PureComponent {
}
public render() {
- const {
- wallets,
- desiredToken,
- network,
- tokens,
- dPath,
- dPaths,
- onCancel,
- walletType
- } = this.props;
+ const { wallets, desiredToken, network, tokens, dPath, dPaths, onCancel } = this.props;
const { selectedAddress, customPath, page } = this.state;
const buttons: IButton[] = [
@@ -115,7 +105,7 @@ class DeterministicWalletsModalClass extends React.PureComponent {
return (
{
private getAddresses(props: Props = this.props) {
const { dPath, publicKey, chainCode, seed } = props;
- if (dPath && ((publicKey && chainCode) || seed) && isValidPath(dPath)) {
- this.props.getDeterministicWallets({
- seed,
- dPath,
- publicKey,
- chainCode,
- limit: WALLETS_PER_PAGE,
- offset: WALLETS_PER_PAGE * this.state.page
- });
+ if (dPath && ((publicKey && chainCode) || seed)) {
+ if (isValidPath(dPath)) {
+ this.props.getDeterministicWallets({
+ seed,
+ dPath,
+ publicKey,
+ chainCode,
+ limit: WALLETS_PER_PAGE,
+ offset: WALLETS_PER_PAGE * this.state.page
+ });
+ } else {
+ console.error('Invalid dPath provided', dPath);
+ }
}
}
@@ -274,7 +268,7 @@ class DeterministicWalletsModalClass extends React.PureComponent {
private renderDPathOption(option: Option) {
if (option.value === customDPath.value) {
- return translate('ADD_Radio_5_PathCustom');
+ return translate('X_CUSTOM');
}
return (
diff --git a/common/components/WalletDecrypt/components/Keystore.tsx b/common/components/WalletDecrypt/components/Keystore.tsx
index 974a30e7..7fb12414 100644
--- a/common/components/WalletDecrypt/components/Keystore.tsx
+++ b/common/components/WalletDecrypt/components/Keystore.tsx
@@ -4,6 +4,7 @@ import translate, { translateRaw } from 'translations';
import Spinner from 'components/ui/Spinner';
import { TShowNotification } from 'actions/notifications';
import { Input } from 'components/ui';
+import DeprecationWarning from './DeprecationWarning';
export interface KeystoreValue {
file: string;
@@ -43,7 +44,8 @@ export class KeystoreDecrypt extends PureComponent {
const unlockDisabled = !file || (passReq && !password);
return (
-