aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website
diff options
context:
space:
mode:
Diffstat (limited to 'packages/website')
-rw-r--r--packages/website/package.json4
-rw-r--r--packages/website/ts/blockchain.ts8
-rw-r--r--packages/website/ts/components/forms/subscribe_form.tsx124
-rw-r--r--packages/website/ts/components/inputs/allowance_toggle.tsx2
-rw-r--r--packages/website/ts/components/onboarding/onboarding_flow.tsx2
-rw-r--r--packages/website/ts/components/onboarding/portal_onboarding_flow.tsx19
-rw-r--r--packages/website/ts/components/portal/back_button.tsx2
-rw-r--r--packages/website/ts/components/portal/drawer_menu.tsx2
-rw-r--r--packages/website/ts/components/portal/menu.tsx2
-rw-r--r--packages/website/ts/components/portal/portal.tsx170
-rw-r--r--packages/website/ts/components/relayer_index/relayer_grid_tile.tsx2
-rw-r--r--packages/website/ts/components/relayer_index/relayer_index.tsx2
-rw-r--r--packages/website/ts/components/token_balances.tsx2
-rw-r--r--packages/website/ts/components/top_bar/provider_display.tsx4
-rw-r--r--packages/website/ts/components/top_bar/top_bar.tsx2
-rw-r--r--packages/website/ts/components/ui/button.tsx79
-rw-r--r--packages/website/ts/components/ui/container.tsx15
-rw-r--r--packages/website/ts/components/ui/input.tsx43
-rw-r--r--packages/website/ts/components/ui/island.tsx2
-rw-r--r--packages/website/ts/components/ui/overlay.tsx2
-rw-r--r--packages/website/ts/components/ui/text.tsx41
-rw-r--r--packages/website/ts/components/wallet/wallet.tsx119
-rw-r--r--packages/website/ts/components/wallet/wallet_disconnected_item.tsx2
-rw-r--r--packages/website/ts/components/wallet/wrap_ether_item.tsx2
-rw-r--r--packages/website/ts/containers/portal_onboarding_flow.ts8
-rw-r--r--packages/website/ts/pages/landing/landing.tsx52
-rw-r--r--packages/website/ts/style/colors.ts (renamed from packages/website/ts/utils/colors.ts)0
-rw-r--r--packages/website/ts/style/theme.ts15
-rw-r--r--packages/website/ts/style/z_index.ts (renamed from packages/website/ts/utils/style.ts)0
-rw-r--r--packages/website/ts/utils/backend_client.ts8
-rw-r--r--packages/website/ts/utils/fetch_utils.ts32
-rw-r--r--packages/website/ts/utils/wallet_item_styles.ts2
32 files changed, 564 insertions, 205 deletions
diff --git a/packages/website/package.json b/packages/website/package.json
index a17964f2b..5287414c7 100644
--- a/packages/website/package.json
+++ b/packages/website/package.json
@@ -10,7 +10,7 @@
"build": "NODE_ENV=production webpack; exit 0;",
"clean": "shx rm -f public/bundle*",
"lint": "tslint --project . 'ts/**/*.ts' 'ts/**/*.tsx'",
- "watch": "webpack-dev-server --content-base public --https",
+ "watch_without_deps": "webpack-dev-server --content-base public --https",
"deploy_dogfood": "npm run build; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_staging": "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_live": "npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers"
@@ -37,6 +37,7 @@
"lodash": "^4.17.4",
"material-ui": "^0.17.1",
"moment": "2.21.0",
+ "polished": "^1.9.2",
"query-string": "^6.0.0",
"react": "15.6.1",
"react-copy-to-clipboard": "^4.2.3",
@@ -52,6 +53,7 @@
"redux": "^3.6.0",
"redux-devtools-extension": "^2.13.2",
"semver-sort": "0.0.4",
+ "styled-components": "^3.3.0",
"thenby": "^1.2.3",
"truffle-contract": "2.0.1",
"web3": "^0.20.0",
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts
index 212e829dd..6e4d03e27 100644
--- a/packages/website/ts/blockchain.ts
+++ b/packages/website/ts/blockchain.ts
@@ -506,7 +506,7 @@ export class Blockchain {
public async getTokenBalanceAndAllowanceAsync(
ownerAddressIfExists: string,
tokenAddress: string,
- ): Promise<BigNumber[]> {
+ ): Promise<[BigNumber, BigNumber]> {
utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.');
if (_.isUndefined(ownerAddressIfExists)) {
@@ -516,8 +516,10 @@ export class Blockchain {
let balance = new BigNumber(0);
let allowance = new BigNumber(0);
if (this._doesUserAddressExist()) {
- balance = await this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists);
- allowance = await this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists);
+ [balance, allowance] = await Promise.all([
+ this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists),
+ this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists),
+ ]);
}
return [balance, allowance];
}
diff --git a/packages/website/ts/components/forms/subscribe_form.tsx b/packages/website/ts/components/forms/subscribe_form.tsx
new file mode 100644
index 000000000..04ef28e70
--- /dev/null
+++ b/packages/website/ts/components/forms/subscribe_form.tsx
@@ -0,0 +1,124 @@
+import { colors } from '@0xproject/react-shared';
+import * as _ from 'lodash';
+import * as React from 'react';
+
+import { Button } from 'ts/components/ui/button';
+import { Container } from 'ts/components/ui/container';
+import { Input } from 'ts/components/ui/input';
+import { Text } from 'ts/components/ui/text';
+import { styled } from 'ts/style/theme';
+import { backendClient } from 'ts/utils/backend_client';
+
+export interface SubscribeFormProps {}
+
+export enum SubscribeFormStatus {
+ None,
+ Error,
+ Success,
+ Loading,
+ Other,
+}
+
+export interface SubscribeFormState {
+ emailText: string;
+ lastSubmittedEmail: string;
+ status: SubscribeFormStatus;
+}
+
+const FORM_FONT_SIZE = '15px';
+
+// TODO: Translate visible strings. https://app.asana.com/0/628666249318202/697485674422001
+export class SubscribeForm extends React.Component<SubscribeFormProps, SubscribeFormState> {
+ public state = {
+ emailText: '',
+ lastSubmittedEmail: '',
+ status: SubscribeFormStatus.None,
+ };
+ public render(): React.ReactNode {
+ return (
+ <Container className="flex flex-column items-center justify-between md-mx2 sm-mx2">
+ <Container marginBottom="15px">
+ <Text fontFamily="Roboto Mono" fontColor={colors.grey} center={true}>
+ Subscribe to our newsletter for 0x relayer and dApp updates
+ </Text>
+ </Container>
+ <form onSubmit={this._handleFormSubmitAsync.bind(this)}>
+ <Container className="flex flex-wrap justify-center items-center">
+ <Container marginTop="15px">
+ <Input
+ placeholder="you@email.com"
+ value={this.state.emailText}
+ fontColor={colors.white}
+ fontSize={FORM_FONT_SIZE}
+ backgroundColor={colors.projectsGrey}
+ width="300px"
+ onChange={this._handleEmailInputChange.bind(this)}
+ />
+ </Container>
+ <Container marginLeft="15px" marginTop="15px">
+ <Button
+ type="submit"
+ backgroundColor={colors.darkestGrey}
+ fontColor={colors.white}
+ fontSize={FORM_FONT_SIZE}
+ >
+ Subscribe
+ </Button>
+ </Container>
+ </Container>
+ </form>
+ {this._renderMessage()}
+ </Container>
+ );
+ }
+ private _renderMessage(): React.ReactNode {
+ let message = null;
+ switch (this.state.status) {
+ case SubscribeFormStatus.Error:
+ message = 'Sorry, something went wrong. Try again later.';
+ break;
+ case SubscribeFormStatus.Loading:
+ message = 'One second...';
+ break;
+ case SubscribeFormStatus.Success:
+ message = `Thanks! ${this.state.lastSubmittedEmail} is now on the mailing list.`;
+ break;
+ case SubscribeFormStatus.None:
+ break;
+ default:
+ throw new Error(
+ 'The SubscribeFormStatus switch statement is not exhaustive when choosing an error message.',
+ );
+ }
+ return (
+ <Container isHidden={!message} marginTop="30px">
+ <Text center={true} fontFamily="Roboto Mono" fontColor={colors.grey}>
+ {message || 'spacer text (never shown to user)'}
+ </Text>
+ </Container>
+ );
+ }
+ private _handleEmailInputChange(event: React.ChangeEvent<HTMLInputElement>): void {
+ this.setState({ emailText: event.target.value });
+ }
+ private async _handleFormSubmitAsync(event: React.FormEvent<HTMLInputElement>): Promise<void> {
+ event.preventDefault();
+ if (_.isUndefined(this.state.emailText) || _.isEmpty(this.state.emailText)) {
+ return;
+ }
+ this.setState({
+ status: SubscribeFormStatus.Loading,
+ lastSubmittedEmail: this.state.emailText,
+ });
+ try {
+ const response = await backendClient.subscribeToNewsletterAsync(this.state.emailText);
+ const status = response.status === 200 ? SubscribeFormStatus.Success : SubscribeFormStatus.Error;
+ this.setState({ status, emailText: '' });
+ } catch (error) {
+ this._setStatus(SubscribeFormStatus.Error);
+ }
+ }
+ private _setStatus(status: SubscribeFormStatus): void {
+ this.setState({ status });
+ }
+}
diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx
index d61dfa87d..a8df4935a 100644
--- a/packages/website/ts/components/inputs/allowance_toggle.tsx
+++ b/packages/website/ts/components/inputs/allowance_toggle.tsx
@@ -5,9 +5,9 @@ import Toggle from 'material-ui/Toggle';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { Dispatcher } from 'ts/redux/dispatcher';
+import { colors } from 'ts/style/colors';
import { BalanceErrs, Token, TokenState } from 'ts/types';
import { analytics } from 'ts/utils/analytics';
-import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';
diff --git a/packages/website/ts/components/onboarding/onboarding_flow.tsx b/packages/website/ts/components/onboarding/onboarding_flow.tsx
index 4066babaf..158ac23ec 100644
--- a/packages/website/ts/components/onboarding/onboarding_flow.tsx
+++ b/packages/website/ts/components/onboarding/onboarding_flow.tsx
@@ -5,7 +5,7 @@ import { Placement, Popper, PopperChildrenProps } from 'react-popper';
import { ContinueButtonDisplay, OnboardingTooltip } from 'ts/components/onboarding/onboarding_tooltip';
import { Container } from 'ts/components/ui/container';
import { Overlay } from 'ts/components/ui/overlay';
-import { zIndex } from 'ts/utils/style';
+import { zIndex } from 'ts/style/z_index';
export interface Step {
target: string;
diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx
index 2014dd7b0..bf52684d7 100644
--- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx
+++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx
@@ -3,7 +3,7 @@ import * as React from 'react';
import { BigNumber } from '@0xproject/utils';
import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow';
-import { ProviderType, TokenByAddress } from 'ts/types';
+import { ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types';
import { utils } from 'ts/utils/utils';
export interface PortalOnboardingFlowProps {
@@ -16,6 +16,7 @@ export interface PortalOnboardingFlowProps {
blockchainIsLoaded: boolean;
userEtherBalanceInWei?: BigNumber;
tokenByAddress: TokenByAddress;
+ trackedTokenStateByAddress: TokenStateByAddress;
updateIsRunning: (isRunning: boolean) => void;
updateOnboardingStep: (stepIndex: number) => void;
}
@@ -68,13 +69,13 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
target: '.eth-row',
content: 'Before you begin you will need to send some ETH to your metamask wallet',
placement: 'right',
- continueButtonDisplay: this._userHasEth() ? 'enabled' : 'disabled',
+ continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled',
},
{
target: '.weth-row',
content: 'You need to convert some of your ETH into tradeable Wrapped ETH (WETH)',
placement: 'right',
- continueButtonDisplay: this._userHasWeth() ? 'enabled' : 'disabled',
+ continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled',
},
];
return steps;
@@ -84,13 +85,17 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
return !_.isEmpty(this.props.userAddress);
}
- private _userHasEth(): boolean {
+ private _userHasVisibleEth(): boolean {
return this.props.userEtherBalanceInWei > new BigNumber(0);
}
- private _userHasWeth(): boolean {
- // TODO: https://app.asana.com/0/681385331277907/690722374136933
- return false;
+ private _userHasVisibleWeth(): boolean {
+ const ethToken = utils.getEthToken(this.props.tokenByAddress);
+ if (!ethToken) {
+ return false;
+ }
+ const wethTokenState = this.props.trackedTokenStateByAddress[ethToken.address];
+ return wethTokenState.balance > new BigNumber(0);
}
private _overrideOnboardingStateIfShould(): void {
diff --git a/packages/website/ts/components/portal/back_button.tsx b/packages/website/ts/components/portal/back_button.tsx
index 48858613c..2d0bbefc3 100644
--- a/packages/website/ts/components/portal/back_button.tsx
+++ b/packages/website/ts/components/portal/back_button.tsx
@@ -2,7 +2,7 @@ import { Styles } from '@0xproject/react-shared';
import * as React from 'react';
import { Link } from 'react-router-dom';
-import { colors } from 'ts/utils/colors';
+import { colors } from 'ts/style/colors';
export interface BackButtonProps {
to: string;
diff --git a/packages/website/ts/components/portal/drawer_menu.tsx b/packages/website/ts/components/portal/drawer_menu.tsx
index ace11639a..8ac2b9091 100644
--- a/packages/website/ts/components/portal/drawer_menu.tsx
+++ b/packages/website/ts/components/portal/drawer_menu.tsx
@@ -4,8 +4,8 @@ import * as React from 'react';
import { defaultMenuItemEntries, Menu } from 'ts/components/portal/menu';
import { Identicon } from 'ts/components/ui/identicon';
+import { colors } from 'ts/style/colors';
import { WebsitePaths } from 'ts/types';
-import { colors } from 'ts/utils/colors';
import { utils } from 'ts/utils/utils';
const IDENTICON_DIAMETER = 45;
diff --git a/packages/website/ts/components/portal/menu.tsx b/packages/website/ts/components/portal/menu.tsx
index 6e97ee37e..f98346101 100644
--- a/packages/website/ts/components/portal/menu.tsx
+++ b/packages/website/ts/components/portal/menu.tsx
@@ -2,8 +2,8 @@ import { Styles } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { MenuItem } from 'ts/components/ui/menu_item';
+import { colors } from 'ts/style/colors';
import { Environments, WebsitePaths } from 'ts/types';
-import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
export interface MenuTheme {
diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx
index 589ad00ad..9420395cf 100644
--- a/packages/website/ts/components/portal/portal.tsx
+++ b/packages/website/ts/components/portal/portal.tsx
@@ -34,13 +34,17 @@ import { Dispatcher } from 'ts/redux/dispatcher';
import {
BlockchainErrs,
HashData,
+ ItemByAddress,
Order,
ProviderType,
ScreenWidths,
+ Token,
TokenByAddress,
+ TokenStateByAddress,
TokenVisibility,
WebsitePaths,
} from 'ts/types';
+import { backendClient } from 'ts/utils/backend_client';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
import { orderParser } from 'ts/utils/order_parser';
@@ -77,6 +81,7 @@ interface PortalState {
isDisclaimerDialogOpen: boolean;
isLedgerDialogOpen: boolean;
tokenManagementState: TokenManagementState;
+ trackedTokenStateByAddress: TokenStateByAddress;
}
interface AccountManagementItem {
@@ -127,6 +132,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER);
const hasAcceptedDisclaimer =
!_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer);
+ const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(
+ this._getCurrentTrackedTokens(),
+ );
this.state = {
prevNetworkId: this.props.networkId,
prevNodeVersion: this.props.nodeVersion,
@@ -135,6 +143,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
tokenManagementState: TokenManagementState.None,
isLedgerDialogOpen: false,
+ trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
};
}
public componentDidMount(): void {
@@ -143,6 +152,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
}
public componentWillMount(): void {
this._blockchain = new Blockchain(this.props.dispatcher);
+ const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
+ // tslint:disable-next-line:no-floating-promises
+ this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
public componentWillUnmount(): void {
this._blockchain.destroy();
@@ -178,6 +190,39 @@ export class Portal extends React.Component<PortalProps, PortalState> {
prevPathname: nextProps.location.pathname,
});
}
+ if (
+ nextProps.userAddress !== this.props.userAddress ||
+ nextProps.networkId !== this.props.networkId ||
+ nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
+ ) {
+ const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
+ // tslint:disable-next-line:no-floating-promises
+ this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
+ }
+
+ const nextTrackedTokens = this._getTrackedTokens(nextProps.tokenByAddress);
+ const trackedTokens = this._getCurrentTrackedTokens();
+
+ if (!_.isEqual(nextTrackedTokens, trackedTokens)) {
+ const newTokens = _.difference(nextTrackedTokens, trackedTokens);
+ const newTokenAddresses = _.map(newTokens, token => token.address);
+ // Add placeholder entry for this token to the state, since fetching the
+ // balance/allowance is asynchronous
+ const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
+ for (const tokenAddress of newTokenAddresses) {
+ trackedTokenStateByAddress[tokenAddress] = {
+ balance: new BigNumber(0),
+ allowance: new BigNumber(0),
+ isLoaded: false,
+ };
+ }
+ this.setState({
+ trackedTokenStateByAddress,
+ });
+ // Fetch the actual balance/allowance.
+ // tslint:disable-next-line:no-floating-promises
+ this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
+ }
}
public render(): React.ReactNode {
const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind(
@@ -190,7 +235,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
: TokenVisibility.TRACKED;
return (
<div style={styles.root}>
- <PortalOnboardingFlow />
+ <PortalOnboardingFlow trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} />
<DocumentTitle title="0x Portal DApp" />
<TopBar
userAddress={this.props.userAddress}
@@ -223,10 +268,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
toggleDialogFn={updateShouldBlockchainErrDialogBeOpen}
networkId={this.props.networkId}
/>
- <PortalDisclaimerDialog
- isOpen={this.state.isDisclaimerDialogOpen}
- onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
- />
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
{this.props.blockchainIsLoaded && (
<LedgerConfigDialog
@@ -283,8 +324,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
);
}
private _renderWallet(): React.ReactNode {
- const allTokens = _.values(this.props.tokenByAddress);
- const trackedTokens = _.filter(allTokens, t => t.isTracked);
return (
<div>
<Wallet
@@ -295,16 +334,18 @@ export class Portal extends React.Component<PortalProps, PortalState> {
blockchainErr={this.props.blockchainErr}
dispatcher={this.props.dispatcher}
tokenByAddress={this.props.tokenByAddress}
- trackedTokens={trackedTokens}
+ trackedTokens={this._getCurrentTrackedTokens()}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
screenWidth={this.props.screenWidth}
location={this.props.location}
+ trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
onAddToken={this._onAddToken.bind(this)}
onRemoveToken={this._onRemoveToken.bind(this)}
+ refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
/>
<Container marginTop="15px">
<Island>
@@ -351,18 +392,24 @@ export class Portal extends React.Component<PortalProps, PortalState> {
},
];
return (
- <Switch>
- {_.map(accountManagementItems, item => {
- return (
- <Route
- key={item.pathName}
- path={item.pathName}
- render={this._renderAccountManagementItem.bind(this, item)}
- />
- );
- })}}
- <Route render={this._renderNotFoundMessage.bind(this)} />
- </Switch>
+ <div>
+ <Switch>
+ {_.map(accountManagementItems, item => {
+ return (
+ <Route
+ key={item.pathName}
+ path={item.pathName}
+ render={this._renderAccountManagementItem.bind(this, item)}
+ />
+ );
+ })}}
+ <Route render={this._renderNotFoundMessage.bind(this)} />
+ </Switch>
+ <PortalDisclaimerDialog
+ isOpen={this.state.isDisclaimerDialogOpen}
+ onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
+ />
+ </div>
);
}
private _renderAccountManagementItem(item: AccountManagementItem): React.ReactNode {
@@ -424,8 +471,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
);
}
private _renderTokenBalances(): React.ReactNode {
- const allTokens = _.values(this.props.tokenByAddress);
- const trackedTokens = _.filter(allTokens, t => t.isTracked);
return (
<TokenBalances
blockchain={this._blockchain}
@@ -434,7 +479,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
dispatcher={this.props.dispatcher}
screenWidth={this.props.screenWidth}
tokenByAddress={this.props.tokenByAddress}
- trackedTokens={trackedTokens}
+ trackedTokens={this._getCurrentTrackedTokens()}
userAddress={this.props.userAddress}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
networkId={this.props.networkId}
@@ -515,6 +560,85 @@ export class Portal extends React.Component<PortalProps, PortalState> {
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
return isSmallScreen;
}
+
+ private _getCurrentTrackedTokens(): Token[] {
+ return this._getTrackedTokens(this.props.tokenByAddress);
+ }
+
+ private _getTrackedTokens(tokenByAddress: TokenByAddress): Token[] {
+ const allTokens = _.values(tokenByAddress);
+ const trackedTokens = _.filter(allTokens, t => t.isTracked);
+ return trackedTokens;
+ }
+
+ private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress {
+ const trackedTokenStateByAddress: TokenStateByAddress = {};
+ _.each(trackedTokens, token => {
+ trackedTokenStateByAddress[token.address] = {
+ balance: new BigNumber(0),
+ allowance: new BigNumber(0),
+ isLoaded: false,
+ };
+ });
+ return trackedTokenStateByAddress;
+ }
+
+ private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> {
+ const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
+ const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
+ const balancesAndAllowances = await Promise.all(
+ tokenAddresses.map(async tokenAddress => {
+ return this._blockchain.getTokenBalanceAndAllowanceAsync(userAddressIfExists, tokenAddress);
+ }),
+ );
+ const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses);
+ for (let i = 0; i < tokenAddresses.length; i++) {
+ // Order is preserved in Promise.all
+ const [balance, allowance] = balancesAndAllowances[i];
+ const tokenAddress = tokenAddresses[i];
+ trackedTokenStateByAddress[tokenAddress] = {
+ balance,
+ allowance,
+ isLoaded: true,
+ price: priceByAddress[tokenAddress],
+ };
+ }
+ this.setState({
+ trackedTokenStateByAddress,
+ });
+ }
+
+ private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> {
+ if (_.isEmpty(tokenAddresses)) {
+ return {};
+ }
+ // for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists
+ // create a mapping from existing symbols -> address
+ const tokenAddressBySymbol: { [symbol: string]: string } = {};
+ _.each(tokenAddresses, address => {
+ const tokenIfExists = _.get(this.props.tokenByAddress, address);
+ if (!_.isUndefined(tokenIfExists)) {
+ const symbol = tokenIfExists.symbol;
+ tokenAddressBySymbol[symbol] = address;
+ }
+ });
+ const tokenSymbols = _.keys(tokenAddressBySymbol);
+ try {
+ const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols);
+ const priceByAddress = _.mapKeys(priceBySymbol, (value, symbol) => _.get(tokenAddressBySymbol, symbol));
+ const result = _.mapValues(priceByAddress, price => {
+ const priceBigNumber = new BigNumber(price);
+ return priceBigNumber;
+ });
+ return result;
+ } catch (err) {
+ return {};
+ }
+ }
+
+ private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
+ await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
+ }
}
interface LargeLayoutProps {
diff --git a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx
index ad6ab3de1..41febebe9 100644
--- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx
+++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx
@@ -7,8 +7,8 @@ import { TopTokens } from 'ts/components/relayer_index/relayer_top_tokens';
import { Container } from 'ts/components/ui/container';
import { Island } from 'ts/components/ui/island';
import { TokenIcon } from 'ts/components/ui/token_icon';
+import { colors } from 'ts/style/colors';
import { Token, WebsiteBackendRelayerInfo } from 'ts/types';
-import { colors } from 'ts/utils/colors';
export interface RelayerGridTileProps {
relayerInfo: WebsiteBackendRelayerInfo;
diff --git a/packages/website/ts/components/relayer_index/relayer_index.tsx b/packages/website/ts/components/relayer_index/relayer_index.tsx
index 9ef6eaf59..683f7084b 100644
--- a/packages/website/ts/components/relayer_index/relayer_index.tsx
+++ b/packages/website/ts/components/relayer_index/relayer_index.tsx
@@ -6,9 +6,9 @@ import { GridList } from 'material-ui/GridList';
import * as React from 'react';
import { RelayerGridTile } from 'ts/components/relayer_index/relayer_grid_tile';
+import { colors } from 'ts/style/colors';
import { ScreenWidths, WebsiteBackendRelayerInfo } from 'ts/types';
import { backendClient } from 'ts/utils/backend_client';
-import { colors } from 'ts/utils/colors';
export interface RelayerIndexProps {
networkId: number;
diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx
index 7a0742bbe..2bc065b0f 100644
--- a/packages/website/ts/components/token_balances.tsx
+++ b/packages/website/ts/components/token_balances.tsx
@@ -77,11 +77,11 @@ interface TokenBalancesProps {
interface TokenBalancesState {
errorType: BalanceErrs;
+ trackedTokenStateByAddress: TokenStateByAddress;
isBalanceSpinnerVisible: boolean;
isZRXSpinnerVisible: boolean;
isTokenPickerOpen: boolean;
isAddingToken: boolean;
- trackedTokenStateByAddress: TokenStateByAddress;
}
export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> {
diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx
index 8a337119a..dba08f85c 100644
--- a/packages/website/ts/components/top_bar/provider_display.tsx
+++ b/packages/website/ts/components/top_bar/provider_display.tsx
@@ -9,10 +9,10 @@ import { ProviderPicker } from 'ts/components/top_bar/provider_picker';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Dispatcher } from 'ts/redux/dispatcher';
+import { colors } from 'ts/style/colors';
+import { zIndex } from 'ts/style/z_index';
import { ProviderType } from 'ts/types';
-import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
-import { zIndex } from 'ts/utils/style';
import { utils } from 'ts/utils/utils';
const ROOT_HEIGHT = 24;
diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx
index 2561da4e6..81ab2c06c 100644
--- a/packages/website/ts/components/top_bar/top_bar.tsx
+++ b/packages/website/ts/components/top_bar/top_bar.tsx
@@ -16,9 +16,9 @@ import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Dispatcher } from 'ts/redux/dispatcher';
+import { zIndex } from 'ts/style/z_index';
import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/types';
import { constants } from 'ts/utils/constants';
-import { zIndex } from 'ts/utils/style';
import { Translate } from 'ts/utils/translate';
import { utils } from 'ts/utils/utils';
diff --git a/packages/website/ts/components/ui/button.tsx b/packages/website/ts/components/ui/button.tsx
new file mode 100644
index 000000000..4c7d59839
--- /dev/null
+++ b/packages/website/ts/components/ui/button.tsx
@@ -0,0 +1,79 @@
+import { colors } from '@0xproject/react-shared';
+import { darken } from 'polished';
+import * as React from 'react';
+import { styled } from 'ts/style/theme';
+
+export interface ButtonProps {
+ className?: string;
+ fontSize?: string;
+ fontColor?: string;
+ backgroundColor?: string;
+ borderColor?: string;
+ width?: string;
+ type?: string;
+ onClick?: (event: React.MouseEvent<HTMLElement>) => void;
+}
+
+const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, onClick, type, className }) => (
+ <button type={type} className={className} onClick={onClick}>
+ {children}
+ </button>
+);
+
+export const Button = styled(PlainButton)`
+ cursor: pointer;
+ font-size: ${props => props.fontSize};
+ color: ${props => props.fontColor};
+ padding: 0.8em 2.2em;
+ border-radius: 6px;
+ box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
+ font-weight: 500;
+ font-family: 'Roboto';
+ width: ${props => props.width};
+ background-color: ${props => props.backgroundColor};
+ border: ${props => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')};
+ &:hover {
+ background-color: ${props => darken(0.1, props.backgroundColor)};
+ }
+ &:active {
+ background-color: ${props => darken(0.2, props.backgroundColor)};
+ }
+`;
+
+Button.defaultProps = {
+ fontSize: '12px',
+ backgroundColor: colors.white,
+ width: 'auto',
+};
+
+Button.displayName = 'Button';
+
+type CallToActionType = 'light' | 'dark';
+
+export interface CallToActionProps {
+ type?: CallToActionType;
+ fontSize?: string;
+ width?: string;
+}
+
+export const CallToAction: React.StatelessComponent<CallToActionProps> = ({ children, type, fontSize, width }) => {
+ const isLight = type === 'light';
+ const backgroundColor = isLight ? colors.white : colors.heroGrey;
+ const fontColor = isLight ? colors.heroGrey : colors.white;
+ const borderColor = isLight ? undefined : colors.white;
+ return (
+ <Button
+ fontSize={fontSize}
+ backgroundColor={backgroundColor}
+ fontColor={fontColor}
+ width={width}
+ borderColor={borderColor}
+ >
+ {children}
+ </Button>
+ );
+};
+
+CallToAction.defaultProps = {
+ type: 'dark',
+};
diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx
index d577447b0..c6a78e181 100644
--- a/packages/website/ts/components/ui/container.tsx
+++ b/packages/website/ts/components/ui/container.tsx
@@ -11,13 +11,20 @@ export interface ContainerProps {
paddingBottom?: StringOrNum;
paddingRight?: StringOrNum;
paddingLeft?: StringOrNum;
+ backgroundColor?: string;
+ borderRadius?: StringOrNum;
maxWidth?: StringOrNum;
- children?: React.ReactNode;
+ isHidden?: boolean;
+ className?: string;
}
-export const Container: React.StatelessComponent<ContainerProps> = (props: ContainerProps) => {
- const { children, ...style } = props;
- return <div style={style}>{children}</div>;
+export const Container: React.StatelessComponent<ContainerProps> = ({ children, className, isHidden, ...style }) => {
+ const visibility = isHidden ? 'hidden' : undefined;
+ return (
+ <div style={{ ...style, visibility }} className={className}>
+ {children}
+ </div>
+ );
};
Container.displayName = 'Container';
diff --git a/packages/website/ts/components/ui/input.tsx b/packages/website/ts/components/ui/input.tsx
new file mode 100644
index 000000000..e01a71a53
--- /dev/null
+++ b/packages/website/ts/components/ui/input.tsx
@@ -0,0 +1,43 @@
+import { colors } from '@0xproject/react-shared';
+import * as React from 'react';
+import { styled } from 'ts/style/theme';
+
+export interface InputProps {
+ className?: string;
+ value?: string;
+ width?: string;
+ fontSize?: string;
+ fontColor?: string;
+ placeholderColor?: string;
+ placeholder?: string;
+ backgroundColor?: string;
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
+}
+
+const PlainInput: React.StatelessComponent<InputProps> = ({ value, className, placeholder, onChange }) => (
+ <input className={className} value={value} onChange={onChange} placeholder={placeholder} />
+);
+
+export const Input = styled(PlainInput)`
+ font-size: ${props => props.fontSize};
+ width: ${props => props.width};
+ padding: 0.8em 1.2em;
+ border-radius: 3px;
+ font-family: 'Roboto Mono';
+ color: ${props => props.fontColor};
+ border: none;
+ background-color: ${props => props.backgroundColor};
+ &::placeholder {
+ color: ${props => props.placeholderColor};
+ }
+`;
+
+Input.defaultProps = {
+ width: 'auto',
+ backgroundColor: colors.white,
+ fontColor: colors.darkestGrey,
+ placeholderColor: colors.darkGrey,
+ fontSize: '12px',
+};
+
+Input.displayName = 'Input';
diff --git a/packages/website/ts/components/ui/island.tsx b/packages/website/ts/components/ui/island.tsx
index f5480c9c9..802a7830a 100644
--- a/packages/website/ts/components/ui/island.tsx
+++ b/packages/website/ts/components/ui/island.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
+import { colors } from 'ts/style/colors';
import { Styleable } from 'ts/types';
-import { colors } from 'ts/utils/colors';
export interface IslandProps {
style?: React.CSSProperties;
diff --git a/packages/website/ts/components/ui/overlay.tsx b/packages/website/ts/components/ui/overlay.tsx
index bb2ed8e59..acca8194f 100644
--- a/packages/website/ts/components/ui/overlay.tsx
+++ b/packages/website/ts/components/ui/overlay.tsx
@@ -2,7 +2,7 @@ import { colors } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
-import { zIndex } from 'ts/utils/style';
+import { zIndex } from 'ts/style/z_index';
export interface OverlayProps {
children?: React.ReactNode;
diff --git a/packages/website/ts/components/ui/text.tsx b/packages/website/ts/components/ui/text.tsx
new file mode 100644
index 000000000..99bf89966
--- /dev/null
+++ b/packages/website/ts/components/ui/text.tsx
@@ -0,0 +1,41 @@
+import { colors } from '@0xproject/react-shared';
+import * as React from 'react';
+import { styled } from 'ts/style/theme';
+import { Deco, Key } from 'ts/types';
+import { Translate } from 'ts/utils/translate';
+
+export type TextTag = 'p' | 'div' | 'span' | 'label';
+
+export interface TextProps {
+ className?: string;
+ Tag?: TextTag;
+ fontSize?: string;
+ fontFamily?: string;
+ fontColor?: string;
+ lineHeight?: string;
+ center?: boolean;
+ fontWeight?: number;
+}
+
+const PlainText: React.StatelessComponent<TextProps> = ({ children, className, Tag }) => (
+ <Tag className={className}>{children}</Tag>
+);
+
+export const Text = styled(PlainText)`
+ font-family: ${props => props.fontFamily};
+ font-weight: ${props => props.fontWeight};
+ font-size: ${props => props.fontSize};
+ ${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')};
+ ${props => (props.center ? 'text-align: center' : '')};
+ color: ${props => props.fontColor};
+`;
+
+Text.defaultProps = {
+ fontFamily: 'Roboto',
+ fontWeight: 400,
+ fontColor: colors.white,
+ fontSize: '14px',
+ Tag: 'div',
+};
+
+Text.displayName = 'Text';
diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx
index 18dada22f..37233930e 100644
--- a/packages/website/ts/components/wallet/wallet.tsx
+++ b/packages/website/ts/components/wallet/wallet.tsx
@@ -32,10 +32,11 @@ import { TokenIcon } from 'ts/components/ui/token_icon';
import { WalletDisconnectedItem } from 'ts/components/wallet/wallet_disconnected_item';
import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
import { Dispatcher } from 'ts/redux/dispatcher';
+import { colors } from 'ts/style/colors';
+import { zIndex } from 'ts/style/z_index';
import {
BalanceErrs,
BlockchainErrs,
- ItemByAddress,
ProviderType,
ScreenWidths,
Side,
@@ -45,10 +46,7 @@ import {
TokenStateByAddress,
WebsitePaths,
} from 'ts/types';
-import { backendClient } from 'ts/utils/backend_client';
-import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
-import { zIndex } from 'ts/utils/style';
import { utils } from 'ts/utils/utils';
import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles';
@@ -67,13 +65,14 @@ export interface WalletProps {
providerType: ProviderType;
screenWidth: ScreenWidths;
location: Location;
+ trackedTokenStateByAddress: TokenStateByAddress;
onToggleLedgerDialog: () => void;
onAddToken: () => void;
onRemoveToken: () => void;
+ refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
}
interface WalletState {
- trackedTokenStateByAddress: TokenStateByAddress;
wrappedEtherDirection?: Side;
isHoveringSidebar: boolean;
}
@@ -151,48 +150,12 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
constructor(props: WalletProps) {
super(props);
this._isUnmounted = false;
- const trackedTokenAddresses = _.map(props.trackedTokens, token => token.address);
- const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(trackedTokenAddresses);
this.state = {
- trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
wrappedEtherDirection: undefined,
isHoveringSidebar: false,
};
}
- public componentWillMount(): void {
- const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
- // tslint:disable-next-line:no-floating-promises
- this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
- }
- public componentWillUnmount(): void {
- this._isUnmounted = true;
- }
- public componentWillReceiveProps(nextProps: WalletProps): void {
- if (
- nextProps.userAddress !== this.props.userAddress ||
- nextProps.networkId !== this.props.networkId ||
- nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
- ) {
- const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
- // tslint:disable-next-line:no-floating-promises
- this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
- }
- if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) {
- const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens);
- const newTokenAddresses = _.map(newTokens, token => token.address);
- // Add placeholder entry for this token to the state, since fetching the
- // balance/allowance is asynchronous
- const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
- const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(newTokenAddresses);
- _.assign(trackedTokenStateByAddress, initialTrackedTokenStateByAddress);
- this.setState({
- trackedTokenStateByAddress,
- });
- // Fetch the actual balance/allowance.
- // tslint:disable-next-line:no-floating-promises
- this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
- }
- }
+
public render(): React.ReactNode {
const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
return (
@@ -342,7 +305,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
_.isUndefined(this.props.userEtherBalanceInWei),
);
const etherToken = this._getEthToken();
- const etherTokenState = this.state.trackedTokenStateByAddress[etherToken.address];
+ const etherTokenState = this.props.trackedTokenStateByAddress[etherToken.address];
const etherPrice = etherTokenState.price;
const secondaryText = this._renderValue(
this.props.userEtherBalanceInWei || new BigNumber(0),
@@ -366,7 +329,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
}
private _renderTokenRow(token: Token, index: number): React.ReactNode {
- const tokenState = this.state.trackedTokenStateByAddress[token.address];
+ const tokenState = this.props.trackedTokenStateByAddress[token.address];
const tokenLink = sharedUtils.getEtherScanLinkIfExists(
token.address,
this.props.networkId,
@@ -438,7 +401,8 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
etherToken={etherToken}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)}
- refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)}
+ // tslint:disable:jsx-no-lambda
+ refetchEthTokenStateAsync={async () => this.props.refetchTokenStateAsync(etherToken.address)}
/>
)}
</div>
@@ -474,7 +438,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
onErrorOccurred={_.noop} // TODO: Error handling
userAddress={this.props.userAddress}
isDisabled={!config.tokenState.isLoaded}
- refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)}
+ refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)}
/>
);
}
@@ -557,69 +521,6 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
});
return trackedTokenStateByAddress;
}
- private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> {
- const balanceAndAllowanceTupleByAddress: ItemByAddress<BigNumber[]> = {};
- const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
- for (const tokenAddress of tokenAddresses) {
- const balanceAndAllowanceTuple = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
- userAddressIfExists,
- tokenAddress,
- );
- balanceAndAllowanceTupleByAddress[tokenAddress] = balanceAndAllowanceTuple;
- }
- const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses);
- const trackedTokenStateByAddress = _.reduce(
- tokenAddresses,
- (acc, address) => {
- const [balance, allowance] = balanceAndAllowanceTupleByAddress[address];
- const priceIfExists = _.get(priceByAddress, address);
- acc[address] = {
- balance,
- allowance,
- price: priceIfExists,
- isLoaded: true,
- };
- return acc;
- },
- this.state.trackedTokenStateByAddress,
- );
-
- if (!this._isUnmounted) {
- this.setState({
- trackedTokenStateByAddress,
- });
- }
- }
- private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
- await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
- }
- private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> {
- if (_.isEmpty(tokenAddresses)) {
- return {};
- }
- // for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists
- // create a mapping from existing symbols -> address
- const tokenAddressBySymbol: { [symbol: string]: string } = {};
- _.each(tokenAddresses, address => {
- const tokenIfExists = _.get(this.props.tokenByAddress, address);
- if (!_.isUndefined(tokenIfExists)) {
- const symbol = tokenIfExists.symbol;
- tokenAddressBySymbol[symbol] = address;
- }
- });
- const tokenSymbols = _.keys(tokenAddressBySymbol);
- try {
- const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols);
- const priceByAddress = _.mapKeys(priceBySymbol, (value, symbol) => _.get(tokenAddressBySymbol, symbol));
- const result = _.mapValues(priceByAddress, price => {
- const priceBigNumber = new BigNumber(price);
- return priceBigNumber;
- });
- return result;
- } catch (err) {
- return {};
- }
- }
private _openWrappedEtherActionRow(wrappedEtherDirection: Side): void {
this.setState({
wrappedEtherDirection,
diff --git a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx
index 17fd8a19e..e3b88dc54 100644
--- a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx
+++ b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx
@@ -3,8 +3,8 @@ import FlatButton from 'material-ui/FlatButton';
import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
import * as React from 'react';
+import { colors } from 'ts/style/colors';
import { ProviderType } from 'ts/types';
-import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx
index 1dfcffadf..50b64d05c 100644
--- a/packages/website/ts/components/wallet/wrap_ether_item.tsx
+++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx
@@ -10,8 +10,8 @@ import { Blockchain } from 'ts/blockchain';
import { EthAmountInput } from 'ts/components/inputs/eth_amount_input';
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
import { Dispatcher } from 'ts/redux/dispatcher';
+import { colors } from 'ts/style/colors';
import { BlockchainCallErrs, Side, Token } from 'ts/types';
-import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';
diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts
index 8202fb2ae..0ad9aef13 100644
--- a/packages/website/ts/containers/portal_onboarding_flow.ts
+++ b/packages/website/ts/containers/portal_onboarding_flow.ts
@@ -2,12 +2,14 @@ import { BigNumber } from '@0xproject/utils';
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
-import { ActionTypes, ProviderType, TokenByAddress } from 'ts/types';
+import { ActionTypes, ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types';
import { PortalOnboardingFlow as PortalOnboardingFlowComponent } from 'ts/components/onboarding/portal_onboarding_flow';
import { State } from 'ts/redux/reducer';
-interface PortalOnboardingFlowProps {}
+interface PortalOnboardingFlowProps {
+ trackedTokenStateByAddress: TokenStateByAddress;
+}
interface ConnectedState {
stepIndex: number;
@@ -26,7 +28,7 @@ interface ConnectedDispatch {
updateOnboardingStep: (stepIndex: number) => void;
}
-const mapStateToProps = (state: State): ConnectedState => ({
+const mapStateToProps = (state: State, ownProps: PortalOnboardingFlowProps): ConnectedState => ({
stepIndex: state.portalOnboardingStep,
isRunning: state.isPortalOnboardingShowing,
userAddress: state.userAddress,
diff --git a/packages/website/ts/pages/landing/landing.tsx b/packages/website/ts/pages/landing/landing.tsx
index 02ecfa117..ed6ec4ec6 100644
--- a/packages/website/ts/pages/landing/landing.tsx
+++ b/packages/website/ts/pages/landing/landing.tsx
@@ -1,11 +1,13 @@
import { colors } from '@0xproject/react-shared';
import * as _ from 'lodash';
-import RaisedButton from 'material-ui/RaisedButton';
import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { Link } from 'react-router-dom';
import { Footer } from 'ts/components/footer';
+import { SubscribeForm } from 'ts/components/forms/subscribe_form';
import { TopBar } from 'ts/components/top_bar/top_bar';
+import { CallToAction } from 'ts/components/ui/button';
+import { Container } from 'ts/components/ui/container';
import { Dispatcher } from 'ts/redux/dispatcher';
import { Deco, Key, Language, ScreenWidths, WebsitePaths } from 'ts/types';
import { constants } from 'ts/utils/constants';
@@ -236,7 +238,7 @@ export class Landing extends React.Component<LandingProps, LandingState> {
<div className="clearfix py4" style={{ backgroundColor: colors.heroGrey }}>
<div className="mx-auto max-width-4 clearfix">
{this._renderWhatsNew()}
- <div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-my4 md-my4 sm-mt2 sm-mb4 clearfix">
+ <div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-mt4 md-mt4 sm-mt2 sm-mb4 clearfix">
<div className="col lg-col-5 md-col-5 col-12 sm-center">
<img src="/images/landing/hero_chip_image.png" height={isSmallScreen ? 300 : 395} />
</div>
@@ -268,40 +270,31 @@ export class Landing extends React.Component<LandingProps, LandingState> {
>
{this.props.translate.get(Key.TopTagline)}
</div>
- <div className="pt3 clearfix sm-mx-auto" style={{ maxWidth: 389 }}>
- <div className="lg-pr2 md-pr2 col col-6 sm-center">
+ <Container className="pt3 clearfix sm-mx-auto" maxWidth="390px">
+ <div className="lg-pr2 md-pr2 lg-col lg-col-6 sm-center sm-col sm-col-12 mb2">
<Link to={WebsitePaths.ZeroExJs} className="text-decoration-none">
- <RaisedButton
- style={{ borderRadius: 6, minWidth: 157.36 }}
- buttonStyle={{ borderRadius: 6 }}
- labelStyle={buttonLabelStyle}
- label={this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
- onClick={_.noop}
- />
+ <CallToAction width="175px" type="light">
+ {this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
+ </CallToAction>
</Link>
</div>
- <div className="col col-6 sm-center">
+ <div className="lg-col lg-col-6 sm-center sm-col sm-col-12">
<a
href={constants.URL_ZEROEX_CHAT}
target="_blank"
className="text-decoration-none"
>
- <RaisedButton
- style={{ borderRadius: 6, minWidth: 150 }}
- buttonStyle={lightButtonStyle}
- labelColor="white"
- backgroundColor={colors.heroGrey}
- labelStyle={buttonLabelStyle}
- label={this.props.translate.get(Key.CommunityCallToAction, Deco.Cap)}
- onClick={_.noop}
- />
+ <CallToAction width="175px">
+ {this.props.translate.get(Key.CommunityCallToAction, Deco.Cap)}
+ </CallToAction>
</a>
</div>
- </div>
+ </Container>
</div>
</div>
</div>
</div>
+ {this.props.translate.getLanguage() === Language.English && <SubscribeForm />}
</div>
);
}
@@ -349,6 +342,9 @@ export class Landing extends React.Component<LandingProps, LandingState> {
case ScreenWidths.Lg:
colWidth = isRelayersOnly ? 2 : 2 - i % 2;
break;
+
+ default:
+ throw new Error(`Encountered unknown ScreenWidths value: ${this.state.screenWidth}`);
}
return (
<div key={`project-${project.logoFileName}`} className={`col col-${colWidth} center`}>
@@ -782,15 +778,9 @@ export class Landing extends React.Component<LandingProps, LandingState> {
</div>
<div className="sm-center sm-pt2 lg-table-cell md-table-cell">
<Link to={WebsitePaths.ZeroExJs} className="text-decoration-none">
- <RaisedButton
- style={{ borderRadius: 6, minWidth: 150 }}
- buttonStyle={lightButtonStyle}
- labelColor={colors.white}
- backgroundColor={colors.heroGrey}
- labelStyle={buttonLabelStyle}
- label={this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
- onClick={_.noop}
- />
+ <CallToAction fontSize="15px">
+ {this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
+ </CallToAction>
</Link>
</div>
</div>
diff --git a/packages/website/ts/utils/colors.ts b/packages/website/ts/style/colors.ts
index 5ffdd6ba7..5ffdd6ba7 100644
--- a/packages/website/ts/utils/colors.ts
+++ b/packages/website/ts/style/colors.ts
diff --git a/packages/website/ts/style/theme.ts b/packages/website/ts/style/theme.ts
new file mode 100644
index 000000000..9e447e7ee
--- /dev/null
+++ b/packages/website/ts/style/theme.ts
@@ -0,0 +1,15 @@
+import * as styledComponents from 'styled-components';
+
+const {
+ default: styled,
+ css,
+ injectGlobal,
+ keyframes,
+ ThemeProvider,
+} = styledComponents as styledComponents.ThemedStyledComponentsModule<IThemeInterface>;
+
+export interface IThemeInterface {}
+
+export const theme = {};
+
+export { styled, css, injectGlobal, keyframes, ThemeProvider };
diff --git a/packages/website/ts/utils/style.ts b/packages/website/ts/style/z_index.ts
index 0411cdd91..0411cdd91 100644
--- a/packages/website/ts/utils/style.ts
+++ b/packages/website/ts/style/z_index.ts
diff --git a/packages/website/ts/utils/backend_client.ts b/packages/website/ts/utils/backend_client.ts
index c440b1604..6b16aea6b 100644
--- a/packages/website/ts/utils/backend_client.ts
+++ b/packages/website/ts/utils/backend_client.ts
@@ -8,6 +8,7 @@ const ETH_GAS_STATION_ENDPOINT = '/eth_gas_station';
const PRICES_ENDPOINT = '/prices';
const RELAYERS_ENDPOINT = '/relayers';
const WIKI_ENDPOINT = '/wiki';
+const SUBSCRIBE_SUBSTACK_NEWSLETTER_ENDPOINT = '/newsletter_subscriber/substack';
export const backendClient = {
async getGasInfoAsync(): Promise<WebsiteBackendGasInfo> {
@@ -33,4 +34,11 @@ export const backendClient = {
const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), WIKI_ENDPOINT);
return result;
},
+ async subscribeToNewsletterAsync(email: string): Promise<Response> {
+ const result = await fetchUtils.postAsync(utils.getBackendBaseUrl(), SUBSCRIBE_SUBSTACK_NEWSLETTER_ENDPOINT, {
+ email,
+ referrer: window.location.href,
+ });
+ return result;
+ },
};
diff --git a/packages/website/ts/utils/fetch_utils.ts b/packages/website/ts/utils/fetch_utils.ts
index d2e902db5..513f7e479 100644
--- a/packages/website/ts/utils/fetch_utils.ts
+++ b/packages/website/ts/utils/fetch_utils.ts
@@ -4,22 +4,38 @@ import * as queryString from 'query-string';
import { errorReporter } from 'ts/utils/error_reporter';
+const logErrorIfPresent = (response: Response, requestedURL: string) => {
+ if (response.status !== 200) {
+ const errorText = `Error requesting url: ${requestedURL}, ${response.status}: ${response.statusText}`;
+ logUtils.log(errorText);
+ const error = Error(errorText);
+ // tslint:disable-next-line:no-floating-promises
+ errorReporter.reportAsync(error);
+ throw error;
+ }
+};
+
export const fetchUtils = {
async requestAsync(baseUrl: string, path: string, queryParams?: object): Promise<any> {
const query = queryStringFromQueryParams(queryParams);
const url = `${baseUrl}${path}${query}`;
const response = await fetch(url);
- if (response.status !== 200) {
- const errorText = `Error requesting url: ${url}, ${response.status}: ${response.statusText}`;
- logUtils.log(errorText);
- const error = Error(errorText);
- // tslint:disable-next-line:no-floating-promises
- errorReporter.reportAsync(error);
- throw error;
- }
+ logErrorIfPresent(response, url);
const result = await response.json();
return result;
},
+ async postAsync(baseUrl: string, path: string, body: object): Promise<Response> {
+ const url = `${baseUrl}${path}`;
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ });
+ logErrorIfPresent(response, url);
+ return response;
+ },
};
function queryStringFromQueryParams(queryParams?: object): string {
diff --git a/packages/website/ts/utils/wallet_item_styles.ts b/packages/website/ts/utils/wallet_item_styles.ts
index 6b038efd2..9d6033d74 100644
--- a/packages/website/ts/utils/wallet_item_styles.ts
+++ b/packages/website/ts/utils/wallet_item_styles.ts
@@ -1,6 +1,6 @@
import { Styles } from '@0xproject/react-shared';
-import { colors } from 'ts/utils/colors';
+import { colors } from 'ts/style/colors';
export const styles: Styles = {
focusedItem: {