aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/instant/src/components')
-rw-r--r--packages/instant/src/components/amount_placeholder.tsx2
-rw-r--r--packages/instant/src/components/animations/slide_animation.tsx10
-rw-r--r--packages/instant/src/components/buy_button.tsx34
-rw-r--r--packages/instant/src/components/buy_order_progress.tsx4
-rw-r--r--packages/instant/src/components/buy_order_state_buttons.tsx6
-rw-r--r--packages/instant/src/components/erc20_asset_amount_input.tsx13
-rw-r--r--packages/instant/src/components/erc20_token_selector.tsx96
-rw-r--r--packages/instant/src/components/install_wallet_panel_content.tsx11
-rw-r--r--packages/instant/src/components/instant_heading.tsx47
-rw-r--r--packages/instant/src/components/order_details.tsx269
-rw-r--r--packages/instant/src/components/payment_method.tsx36
-rw-r--r--packages/instant/src/components/payment_method_dropdown.tsx18
-rw-r--r--packages/instant/src/components/placing_order_button.tsx2
-rw-r--r--packages/instant/src/components/scaling_amount_input.tsx11
-rw-r--r--packages/instant/src/components/scaling_input.tsx100
-rw-r--r--packages/instant/src/components/search_input.tsx8
-rw-r--r--packages/instant/src/components/secondary_button.tsx1
-rw-r--r--packages/instant/src/components/section_header.tsx21
-rw-r--r--packages/instant/src/components/sliding_error.tsx4
-rw-r--r--packages/instant/src/components/sliding_panel.tsx74
-rw-r--r--packages/instant/src/components/standard_panel_content.tsx15
-rw-r--r--packages/instant/src/components/standard_sliding_panel.tsx4
-rw-r--r--packages/instant/src/components/time_counter.tsx2
-rw-r--r--packages/instant/src/components/timed_progress_bar.tsx19
-rw-r--r--packages/instant/src/components/ui/container.tsx18
-rw-r--r--packages/instant/src/components/ui/dropdown.tsx51
-rw-r--r--packages/instant/src/components/ui/input.tsx11
-rw-r--r--packages/instant/src/components/ui/overlay.tsx2
-rw-r--r--packages/instant/src/components/ui/text.tsx10
-rw-r--r--packages/instant/src/components/wallet_prompt.tsx6
-rw-r--r--packages/instant/src/components/zero_ex_instant.tsx2
-rw-r--r--packages/instant/src/components/zero_ex_instant_container.tsx36
-rw-r--r--packages/instant/src/components/zero_ex_instant_overlay.tsx19
-rw-r--r--packages/instant/src/components/zero_ex_instant_provider.tsx57
34 files changed, 675 insertions, 344 deletions
diff --git a/packages/instant/src/components/amount_placeholder.tsx b/packages/instant/src/components/amount_placeholder.tsx
index 29ce8fafb..290e34a07 100644
--- a/packages/instant/src/components/amount_placeholder.tsx
+++ b/packages/instant/src/components/amount_placeholder.tsx
@@ -30,3 +30,5 @@ export const AmountPlaceholder: React.StatelessComponent<AmountPlaceholderProps>
return <PlainPlaceholder color={props.color} />;
}
};
+
+AmountPlaceholder.displayName = 'AmountPlaceholder';
diff --git a/packages/instant/src/components/animations/slide_animation.tsx b/packages/instant/src/components/animations/slide_animation.tsx
index 5992bcba7..6ac47e9a6 100644
--- a/packages/instant/src/components/animations/slide_animation.tsx
+++ b/packages/instant/src/components/animations/slide_animation.tsx
@@ -11,6 +11,7 @@ export interface SlideAnimationProps {
slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
zIndex?: OptionallyScreenSpecific<number>;
height?: string;
+ onAnimationEnd?: () => void;
}
export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => {
@@ -19,8 +20,15 @@ export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = pro
}
const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings;
return (
- <PositionAnimation height={props.height} positionSettings={positionSettings} zIndex={props.zIndex}>
+ <PositionAnimation
+ onAnimationEnd={props.onAnimationEnd}
+ height={props.height}
+ positionSettings={positionSettings}
+ zIndex={props.zIndex}
+ >
{props.children}
</PositionAnimation>
);
};
+
+SlideAnimation.displayName = 'SlideAnimation';
diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx
index 8b6121e43..551e857a5 100644
--- a/packages/instant/src/components/buy_button.tsx
+++ b/packages/instant/src/components/buy_button.tsx
@@ -1,4 +1,5 @@
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
+import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
@@ -7,7 +8,9 @@ import { oc } from 'ts-optchain';
import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants';
import { ColorOption } from '../style/theme';
-import { AffiliateInfo, ZeroExInstantError } from '../types';
+import { AffiliateInfo, Asset, ZeroExInstantError } from '../types';
+import { analytics } from '../util/analytics';
+import { errorReporter } from '../util/error_reporter';
import { gasPriceEstimator } from '../util/gas_price_estimator';
import { util } from '../util/util';
@@ -20,6 +23,7 @@ export interface BuyButtonProps {
assetBuyer: AssetBuyer;
web3Wrapper: Web3Wrapper;
affiliateInfo?: AffiliateInfo;
+ selectedAsset?: Asset;
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
onSignatureDenied: (buyQuote: BuyQuote) => void;
@@ -28,15 +32,19 @@ export interface BuyButtonProps {
onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void;
}
-export class BuyButton extends React.Component<BuyButtonProps> {
+export class BuyButton extends React.PureComponent<BuyButtonProps> {
public static defaultProps = {
onClick: util.boundNoop,
onBuySuccess: util.boundNoop,
onBuyFailure: util.boundNoop,
};
public render(): React.ReactNode {
- const { buyQuote, accountAddress } = this.props;
+ const { buyQuote, accountAddress, selectedAsset } = this.props;
const shouldDisableButton = _.isUndefined(buyQuote) || _.isUndefined(accountAddress);
+ const buttonText =
+ !_.isUndefined(selectedAsset) && selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20
+ ? `Buy ${selectedAsset.metaData.symbol.toUpperCase()}`
+ : 'Buy Now';
return (
<Button
width="100%"
@@ -44,7 +52,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
isDisabled={shouldDisableButton}
fontColor={ColorOption.white}
>
- Buy
+ {buttonText}
</Button>
);
}
@@ -59,6 +67,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
// if we don't have a balance for the user, let the transaction through, it will be handled by the wallet
const hasSufficientEth = _.isUndefined(accountEthBalanceInWei) || accountEthBalanceInWei.gte(ethNeededForBuy);
if (!hasSufficientEth) {
+ analytics.trackBuyNotEnoughEth(buyQuote);
this.props.onValidationFail(buyQuote, ZeroExInstantError.InsufficientETH);
return;
}
@@ -66,6 +75,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
const feeRecipient = oc(affiliateInfo).feeRecipient();
try {
+ analytics.trackBuyStarted(buyQuote);
txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, {
feeRecipient,
takerAddress: accountAddress,
@@ -73,11 +83,18 @@ export class BuyButton extends React.Component<BuyButtonProps> {
});
} catch (e) {
if (e instanceof Error) {
- if (e.message === AssetBuyerError.SignatureRequestDenied) {
+ if (e.message === AssetBuyerError.TransactionValueTooLow) {
+ analytics.trackBuySimulationFailed(buyQuote);
+ this.props.onValidationFail(buyQuote, AssetBuyerError.TransactionValueTooLow);
+ return;
+ } else if (e.message === AssetBuyerError.SignatureRequestDenied) {
+ analytics.trackBuySignatureDenied(buyQuote);
this.props.onSignatureDenied(buyQuote);
return;
- } else if (e.message === AssetBuyerError.TransactionValueTooLow) {
- this.props.onValidationFail(buyQuote, AssetBuyerError.TransactionValueTooLow);
+ } else {
+ errorReporter.report(e);
+ analytics.trackBuyUnknownError(buyQuote, e.message);
+ this.props.onValidationFail(buyQuote, ZeroExInstantError.CouldNotSubmitTransaction);
return;
}
}
@@ -87,14 +104,17 @@ export class BuyButton extends React.Component<BuyButtonProps> {
const expectedEndTimeUnix = startTimeUnix + gasInfo.estimatedTimeMs;
this.props.onBuyProcessing(buyQuote, txHash, startTimeUnix, expectedEndTimeUnix);
try {
+ analytics.trackBuyTxSubmitted(buyQuote, txHash, startTimeUnix, expectedEndTimeUnix);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
} catch (e) {
if (e instanceof Error && e.message.startsWith(WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX)) {
+ analytics.trackBuyTxFailed(buyQuote, txHash, startTimeUnix, expectedEndTimeUnix);
this.props.onBuyFailure(buyQuote, txHash);
return;
}
throw e;
}
+ analytics.trackBuyTxSucceeded(buyQuote, txHash, startTimeUnix, expectedEndTimeUnix);
this.props.onBuySuccess(buyQuote, txHash);
};
}
diff --git a/packages/instant/src/components/buy_order_progress.tsx b/packages/instant/src/components/buy_order_progress.tsx
index 6568de91b..11ac5d5e0 100644
--- a/packages/instant/src/components/buy_order_progress.tsx
+++ b/packages/instant/src/components/buy_order_progress.tsx
@@ -21,7 +21,7 @@ export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> =
const hasEnded = buyOrderState.processState !== OrderProcessState.Processing;
const expectedTimeMs = progress.expectedEndTimeUnix - progress.startTimeUnix;
return (
- <Container padding="20px 20px 0px 20px" width="100%">
+ <Container width="100%" padding="20px 20px 0px 20px">
<Container marginBottom="5px">
<TimeCounter estimatedTimeMs={expectedTimeMs} hasEnded={hasEnded} key={progress.startTimeUnix} />
</Container>
@@ -31,3 +31,5 @@ export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> =
}
return null;
};
+
+BuyOrderProgress.displayName = 'BuyOrderProgress';
diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx
index e563bec73..1214559d1 100644
--- a/packages/instant/src/components/buy_order_state_buttons.tsx
+++ b/packages/instant/src/components/buy_order_state_buttons.tsx
@@ -4,7 +4,7 @@ import { Web3Wrapper } from '@0x/web3-wrapper';
import * as React from 'react';
import { ColorOption } from '../style/theme';
-import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
+import { AffiliateInfo, Asset, OrderProcessState, ZeroExInstantError } from '../types';
import { BuyButton } from './buy_button';
import { PlacingOrderButton } from './placing_order_button';
@@ -21,6 +21,7 @@ export interface BuyOrderStateButtonProps {
assetBuyer: AssetBuyer;
web3Wrapper: Web3Wrapper;
affiliateInfo?: AffiliateInfo;
+ selectedAsset?: Asset;
onViewTransaction: () => void;
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
@@ -60,6 +61,7 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP
assetBuyer={props.assetBuyer}
web3Wrapper={props.web3Wrapper}
affiliateInfo={props.affiliateInfo}
+ selectedAsset={props.selectedAsset}
onValidationPending={props.onValidationPending}
onValidationFail={props.onValidationFail}
onSignatureDenied={props.onSignatureDenied}
@@ -69,3 +71,5 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP
/>
);
};
+
+BuyOrderStateButtons.displayName = 'BuyOrderStateButtons';
diff --git a/packages/instant/src/components/erc20_asset_amount_input.tsx b/packages/instant/src/components/erc20_asset_amount_input.tsx
index b825255c4..0418f9165 100644
--- a/packages/instant/src/components/erc20_asset_amount_input.tsx
+++ b/packages/instant/src/components/erc20_asset_amount_input.tsx
@@ -31,7 +31,7 @@ export interface ERC20AssetAmountInputState {
currentFontSizePx: number;
}
-export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInputProps, ERC20AssetAmountInputState> {
+export class ERC20AssetAmountInput extends React.PureComponent<ERC20AssetAmountInputProps, ERC20AssetAmountInputState> {
public static defaultProps = {
onChange: util.boundNoop,
isDisabled: false,
@@ -64,6 +64,9 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput
maxFontSizePx={this.props.startingFontSizePx}
onAmountChange={this._handleChange}
onFontSizeChange={this._handleFontSizeChange}
+ hasAutofocus={true}
+ /* We send in a key of asset data to force a rerender of this component when the user selects a new asset. We do this so the autofocus attribute will bring focus onto this input */
+ key={asset.assetData}
/>
</Container>
<Container
@@ -110,7 +113,7 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput
);
};
private readonly _renderChevronIcon = (): React.ReactNode => {
- if (!this._areMultipleAssetsAvailable()) {
+ if (!this._areAnyAssetsAvailable()) {
return null;
}
return (
@@ -131,14 +134,14 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput
// We don't want to allow opening the token selection panel if there are no assets.
// Since styles are inferred from the presence of a click handler, we want to return undefined
// instead of providing a noop.
- if (!this._areMultipleAssetsAvailable() || _.isUndefined(this.props.onSelectAssetClick)) {
+ if (!this._areAnyAssetsAvailable() || _.isUndefined(this.props.onSelectAssetClick)) {
return undefined;
}
return this._handleSelectAssetClick;
};
- private readonly _areMultipleAssetsAvailable = (): boolean => {
+ private readonly _areAnyAssetsAvailable = (): boolean => {
const { numberOfAssetsAvailable } = this.props;
- return !_.isUndefined(numberOfAssetsAvailable) && numberOfAssetsAvailable > 1;
+ return !_.isUndefined(numberOfAssetsAvailable) && numberOfAssetsAvailable > 0;
};
private readonly _handleSelectAssetClick = (): void => {
if (this.props.onSelectAssetClick) {
diff --git a/packages/instant/src/components/erc20_token_selector.tsx b/packages/instant/src/components/erc20_token_selector.tsx
index 1b1921acb..a26fb5cf5 100644
--- a/packages/instant/src/components/erc20_token_selector.tsx
+++ b/packages/instant/src/components/erc20_token_selector.tsx
@@ -3,10 +3,10 @@ import * as React from 'react';
import { ColorOption } from '../style/theme';
import { ERC20Asset } from '../types';
+import { analytics } from '../util/analytics';
import { assetUtils } from '../util/asset';
import { SearchInput } from './search_input';
-
import { Circle } from './ui/circle';
import { Container } from './ui/container';
import { Flex } from './ui/flex';
@@ -18,15 +18,15 @@ export interface ERC20TokenSelectorProps {
}
export interface ERC20TokenSelectorState {
- searchQuery?: string;
+ searchQuery: string;
}
-export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> {
+export class ERC20TokenSelector extends React.PureComponent<ERC20TokenSelectorProps> {
public state: ERC20TokenSelectorState = {
- searchQuery: undefined,
+ searchQuery: '',
};
public render(): React.ReactNode {
- const { tokens, onTokenSelect } = this.props;
+ const { tokens } = this.props;
return (
<Container height="100%">
<Container marginBottom="10px">
@@ -42,12 +42,11 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps>
tabIndex={-1}
/>
<Container overflow="scroll" height="calc(100% - 90px)" marginTop="10px">
- {_.map(tokens, token => {
- if (!this._isTokenQueryMatch(token)) {
- return null;
- }
- return <TokenSelectorRow key={token.assetData} token={token} onClick={onTokenSelect} />;
- })}
+ <TokenRowFilter
+ tokens={tokens}
+ onClick={this._handleTokenClick}
+ searchQuery={this.state.searchQuery}
+ />
</Container>
</Container>
);
@@ -57,13 +56,38 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps>
this.setState({
searchQuery,
});
+ analytics.trackTokenSelectorSearched(searchQuery);
+ };
+ private readonly _handleTokenClick = (token: ERC20Asset): void => {
+ this.props.onTokenSelect(token);
};
+}
+
+interface TokenRowFilterProps {
+ tokens: ERC20Asset[];
+ onClick: (token: ERC20Asset) => void;
+ searchQuery: string;
+}
+
+class TokenRowFilter extends React.Component<TokenRowFilterProps> {
+ public render(): React.ReactNode {
+ return _.map(this.props.tokens, token => {
+ if (!this._isTokenQueryMatch(token)) {
+ return null;
+ }
+ return <TokenSelectorRow key={token.assetData} token={token} onClick={this.props.onClick} />;
+ });
+ }
+ public shouldComponentUpdate(nextProps: TokenRowFilterProps): boolean {
+ const arePropsDeeplyEqual = _.isEqual(nextProps, this.props);
+ return !arePropsDeeplyEqual;
+ }
private readonly _isTokenQueryMatch = (token: ERC20Asset): boolean => {
- const { searchQuery } = this.state;
- if (_.isUndefined(searchQuery)) {
+ const { searchQuery } = this.props;
+ const searchQueryLowerCase = searchQuery.toLowerCase().trim();
+ if (searchQueryLowerCase === '') {
return true;
}
- const searchQueryLowerCase = searchQuery.toLowerCase();
const tokenName = token.metaData.name.toLowerCase();
const tokenSymbol = token.metaData.symbol.toLowerCase();
return _.startsWith(tokenSymbol, searchQueryLowerCase) || _.startsWith(tokenName, searchQueryLowerCase);
@@ -75,7 +99,7 @@ interface TokenSelectorRowProps {
onClick: (token: ERC20Asset) => void;
}
-class TokenSelectorRow extends React.Component<TokenSelectorRowProps> {
+class TokenSelectorRow extends React.PureComponent<TokenSelectorRowProps> {
public render(): React.ReactNode {
const { token } = this.props;
const circleColor = token.metaData.primaryColor || 'black';
@@ -121,20 +145,32 @@ interface TokenSelectorRowIconProps {
token: ERC20Asset;
}
-const TokenSelectorRowIcon: React.StatelessComponent<TokenSelectorRowIconProps> = props => {
- const { token } = props;
- const iconUrlIfExists = token.metaData.iconUrl;
- const TokenIcon = require(`../assets/icons/${token.metaData.symbol}.svg`);
- const displaySymbol = assetUtils.bestNameForAsset(token);
- if (!_.isUndefined(iconUrlIfExists)) {
- return <img src={iconUrlIfExists} />;
- } else if (!_.isUndefined(TokenIcon)) {
- return <TokenIcon />;
- } else {
- return (
- <Text fontColor={ColorOption.white} fontSize="8px">
- {displaySymbol}
- </Text>
- );
+const getTokenIcon = (symbol: string): React.StatelessComponent | undefined => {
+ try {
+ return require(`../assets/icons/${symbol}.svg`) as React.StatelessComponent;
+ } catch (e) {
+ // Can't find icon
+ return undefined;
}
};
+
+class TokenSelectorRowIcon extends React.PureComponent<TokenSelectorRowIconProps> {
+ public render(): React.ReactNode {
+ const { token } = this.props;
+ const iconUrlIfExists = token.metaData.iconUrl;
+
+ const TokenIcon = getTokenIcon(token.metaData.symbol);
+ const displaySymbol = assetUtils.bestNameForAsset(token);
+ if (!_.isUndefined(iconUrlIfExists)) {
+ return <img src={iconUrlIfExists} />;
+ } else if (!_.isUndefined(TokenIcon)) {
+ return <TokenIcon />;
+ } else {
+ return (
+ <Text fontColor={ColorOption.white} fontSize="8px">
+ {displaySymbol}
+ </Text>
+ );
+ }
+ }
+}
diff --git a/packages/instant/src/components/install_wallet_panel_content.tsx b/packages/instant/src/components/install_wallet_panel_content.tsx
index 88c26f59c..1af3dafbb 100644
--- a/packages/instant/src/components/install_wallet_panel_content.tsx
+++ b/packages/instant/src/components/install_wallet_panel_content.tsx
@@ -8,7 +8,9 @@ import {
} from '../constants';
import { ColorOption } from '../style/theme';
import { Browser } from '../types';
+import { analytics } from '../util/analytics';
import { envUtil } from '../util/env';
+import { util } from '../util/util';
import { MetaMaskLogo } from './meta_mask_logo';
import { StandardPanelContent, StandardPanelContentProps } from './standard_panel_content';
@@ -16,7 +18,7 @@ import { Button } from './ui/button';
export interface InstallWalletPanelContentProps {}
-export class InstallWalletPanelContent extends React.Component<InstallWalletPanelContentProps> {
+export class InstallWalletPanelContent extends React.PureComponent<InstallWalletPanelContentProps> {
public render(): React.ReactNode {
const panelProps = this._getStandardPanelContentProps();
return <StandardPanelContent {...panelProps} />;
@@ -45,6 +47,10 @@ export class InstallWalletPanelContent extends React.Component<InstallWalletPane
default:
break;
}
+ const onActionClick = () => {
+ analytics.trackInstallWalletModalClickedGet();
+ util.createOpenUrlInNewWindow(actionUrl)();
+ };
return {
image: <MetaMaskLogo width={85} height={80} />,
title: 'Install MetaMask',
@@ -52,10 +58,11 @@ export class InstallWalletPanelContent extends React.Component<InstallWalletPane
moreInfoSettings: {
href: META_MASK_SITE_URL,
text: 'What is MetaMask?',
+ onClick: analytics.trackInstallWalletModalClickedExplanation,
},
action: (
<Button
- href={actionUrl}
+ onClick={onActionClick}
width="100%"
fontColor={ColorOption.white}
backgroundColor={ColorOption.darkOrange}
diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx
index 002695269..e943f68d7 100644
--- a/packages/instant/src/components/instant_heading.tsx
+++ b/packages/instant/src/components/instant_heading.tsx
@@ -28,11 +28,11 @@ const ICON_WIDTH = 34;
const ICON_HEIGHT = 34;
const ICON_COLOR = ColorOption.white;
-export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
+export class InstantHeading extends React.PureComponent<InstantHeadingProps, {}> {
public render(): React.ReactNode {
const iconOrAmounts = this._renderIcon() || this._renderAmountsSection();
return (
- <Container backgroundColor={ColorOption.primaryColor} padding="20px" width="100%">
+ <Container backgroundColor={ColorOption.primaryColor} width="100%" padding="20px">
<Container marginBottom="5px">
<Text
letterSpacing="1px"
@@ -61,12 +61,19 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
}
private _renderAmountsSection(): React.ReactNode {
- return (
- <Container>
- <Container marginBottom="5px">{this._renderPlaceholderOrAmount(this._renderEthAmount)}</Container>
- <Container opacity={0.7}>{this._renderPlaceholderOrAmount(this._renderDollarAmount)}</Container>
- </Container>
- );
+ if (
+ _.isUndefined(this.props.totalEthBaseUnitAmount) &&
+ this.props.quoteRequestState !== AsyncProcessState.Pending
+ ) {
+ return null;
+ } else {
+ return (
+ <Container>
+ <Container marginBottom="5px">{this._renderPlaceholderOrAmount(this._renderEthAmount)}</Container>
+ <Container opacity={0.7}>{this._renderPlaceholderOrAmount(this._renderDollarAmount)}</Container>
+ </Container>
+ );
+ }
}
private _renderIcon(): React.ReactNode {
@@ -106,20 +113,30 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
}
private readonly _renderEthAmount = (): React.ReactNode => {
+ const ethAmount = format.ethBaseUnitAmount(
+ this.props.totalEthBaseUnitAmount,
+ 4,
+ <AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />,
+ );
+
+ const fontSize = _.isString(ethAmount) && ethAmount.length >= 13 ? '14px' : '16px';
return (
- <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}>
- {format.ethBaseUnitAmount(
- this.props.totalEthBaseUnitAmount,
- 4,
- <AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />,
- )}
+ <Text
+ fontSize={fontSize}
+ textAlign="right"
+ width="100%"
+ fontColor={ColorOption.white}
+ fontWeight={500}
+ noWrap={true}
+ >
+ {ethAmount}
</Text>
);
};
private readonly _renderDollarAmount = (): React.ReactNode => {
return (
- <Text fontSize="16px" fontColor={ColorOption.white}>
+ <Text fontSize="16px" textAlign="right" width="100%" fontColor={ColorOption.white} noWrap={true}>
{format.ethBaseUnitAmountInUsd(
this.props.totalEthBaseUnitAmount,
this.props.ethUsdPrice,
diff --git a/packages/instant/src/components/order_details.tsx b/packages/instant/src/components/order_details.tsx
index 5fc956e1c..4db20b13e 100644
--- a/packages/instant/src/components/order_details.tsx
+++ b/packages/instant/src/components/order_details.tsx
@@ -4,124 +4,227 @@ import * as _ from 'lodash';
import * as React from 'react';
import { oc } from 'ts-optchain';
-import { BIG_NUMBER_ZERO } from '../constants';
+import { BIG_NUMBER_ZERO, DEFAULT_UNKOWN_ASSET_NAME } from '../constants';
import { ColorOption } from '../style/theme';
+import { BaseCurrency } from '../types';
import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder';
+import { SectionHeader } from './section_header';
import { Container } from './ui/container';
import { Flex } from './ui/flex';
-import { Text } from './ui/text';
+import { Text, TextProps } from './ui/text';
export interface OrderDetailsProps {
buyQuoteInfo?: BuyQuoteInfo;
selectedAssetUnitAmount?: BigNumber;
ethUsdPrice?: BigNumber;
isLoading: boolean;
+ assetName?: string;
+ baseCurrency: BaseCurrency;
+ onBaseCurrencySwitchEth: () => void;
+ onBaseCurrencySwitchUsd: () => void;
}
-export class OrderDetails extends React.Component<OrderDetailsProps> {
+export class OrderDetails extends React.PureComponent<OrderDetailsProps> {
public render(): React.ReactNode {
- const { buyQuoteInfo, ethUsdPrice, selectedAssetUnitAmount } = this.props;
- const buyQuoteAccessor = oc(buyQuoteInfo);
- const assetEthBaseUnitAmount = buyQuoteAccessor.assetEthAmount();
- const feeEthBaseUnitAmount = buyQuoteAccessor.feeEthAmount();
- const totalEthBaseUnitAmount = buyQuoteAccessor.totalEthAmount();
- const pricePerTokenEth =
- !_.isUndefined(assetEthBaseUnitAmount) &&
- !_.isUndefined(selectedAssetUnitAmount) &&
- !selectedAssetUnitAmount.eq(BIG_NUMBER_ZERO)
- ? assetEthBaseUnitAmount.div(selectedAssetUnitAmount).ceil()
- : undefined;
+ const shouldShowUsdError = this.props.baseCurrency === BaseCurrency.USD && this._hadErrorFetchingUsdPrice();
return (
- <Container padding="20px" width="100%" flexGrow={1}>
- <Container marginBottom="10px">
- <Text
- letterSpacing="1px"
- fontColor={ColorOption.primaryColor}
- fontWeight={600}
- textTransform="uppercase"
- fontSize="14px"
- >
- Order Details
- </Text>
- </Container>
- <EthAmountRow
- rowLabel="Token Price"
- ethAmount={pricePerTokenEth}
- ethUsdPrice={ethUsdPrice}
- isLoading={this.props.isLoading}
+ <Container width="100%" flexGrow={1} padding="20px 20px 0px 20px">
+ <Container marginBottom="10px">{this._renderHeader()}</Container>
+ {shouldShowUsdError ? this._renderErrorFetchingUsdPrice() : this._renderRows()}
+ </Container>
+ );
+ }
+
+ private _renderRows(): React.ReactNode {
+ const { buyQuoteInfo } = this.props;
+ return (
+ <React.Fragment>
+ <OrderDetailsRow
+ labelText={this._assetAmountLabel()}
+ primaryValue={this._displayAmountOrPlaceholder(buyQuoteInfo && buyQuoteInfo.assetEthAmount)}
/>
- <EthAmountRow
- rowLabel="Fee"
- ethAmount={feeEthBaseUnitAmount}
- ethUsdPrice={ethUsdPrice}
- isLoading={this.props.isLoading}
+ <OrderDetailsRow
+ labelText="Fee"
+ primaryValue={this._displayAmountOrPlaceholder(buyQuoteInfo && buyQuoteInfo.feeEthAmount)}
/>
- <EthAmountRow
- rowLabel="Total Cost"
- ethAmount={totalEthBaseUnitAmount}
- ethUsdPrice={ethUsdPrice}
- shouldEmphasize={true}
- isLoading={this.props.isLoading}
+ <OrderDetailsRow
+ labelText="Total Cost"
+ isLabelBold={true}
+ primaryValue={this._displayAmountOrPlaceholder(buyQuoteInfo && buyQuoteInfo.totalEthAmount)}
+ isPrimaryValueBold={true}
+ secondaryValue={this._totalCostSecondaryValue()}
/>
- </Container>
+ </React.Fragment>
);
}
-}
-export interface EthAmountRowProps {
- rowLabel: string;
- ethAmount?: BigNumber;
- isEthAmountInBaseUnits?: boolean;
- ethUsdPrice?: BigNumber;
- shouldEmphasize?: boolean;
- isLoading: boolean;
+ private _renderErrorFetchingUsdPrice(): React.ReactNode {
+ return (
+ <Text>
+ There was an error fetching the USD price.
+ <Text
+ onClick={this.props.onBaseCurrencySwitchEth}
+ fontWeight={700}
+ fontColor={ColorOption.primaryColor}
+ >
+ Click here
+ </Text>
+ {' to view ETH prices'}
+ </Text>
+ );
+ }
+
+ private _hadErrorFetchingUsdPrice(): boolean {
+ return this.props.ethUsdPrice ? this.props.ethUsdPrice.equals(BIG_NUMBER_ZERO) : false;
+ }
+
+ private _totalCostSecondaryValue(): React.ReactNode {
+ const secondaryCurrency = this.props.baseCurrency === BaseCurrency.USD ? BaseCurrency.ETH : BaseCurrency.USD;
+
+ const canDisplayCurrency =
+ secondaryCurrency === BaseCurrency.ETH ||
+ (secondaryCurrency === BaseCurrency.USD && this.props.ethUsdPrice && !this._hadErrorFetchingUsdPrice());
+
+ if (this.props.buyQuoteInfo && canDisplayCurrency) {
+ return this._displayAmount(secondaryCurrency, this.props.buyQuoteInfo.totalEthAmount);
+ } else {
+ return undefined;
+ }
+ }
+
+ private _displayAmountOrPlaceholder(weiAmount?: BigNumber): React.ReactNode {
+ const { baseCurrency, isLoading } = this.props;
+
+ if (_.isUndefined(weiAmount)) {
+ return (
+ <Container opacity={0.5}>
+ <AmountPlaceholder color={ColorOption.lightGrey} isPulsating={isLoading} />
+ </Container>
+ );
+ }
+
+ return this._displayAmount(baseCurrency, weiAmount);
+ }
+
+ private _displayAmount(currency: BaseCurrency, weiAmount: BigNumber): React.ReactNode {
+ switch (currency) {
+ case BaseCurrency.USD:
+ return format.ethBaseUnitAmountInUsd(weiAmount, this.props.ethUsdPrice, 2, '');
+ case BaseCurrency.ETH:
+ return format.ethBaseUnitAmount(weiAmount, 4, '');
+ }
+ }
+
+ private _assetAmountLabel(): React.ReactNode {
+ const { assetName, baseCurrency } = this.props;
+ const numTokens = this.props.selectedAssetUnitAmount;
+
+ // Display as 0 if we have a selected asset
+ const displayNumTokens =
+ assetName && assetName !== DEFAULT_UNKOWN_ASSET_NAME && _.isUndefined(numTokens)
+ ? new BigNumber(0)
+ : numTokens;
+ if (!_.isUndefined(displayNumTokens)) {
+ let numTokensWithSymbol: React.ReactNode = displayNumTokens.toString();
+ if (assetName) {
+ numTokensWithSymbol += ` ${assetName}`;
+ }
+ const pricePerTokenWei = this._pricePerTokenWei();
+ if (pricePerTokenWei) {
+ const atPriceDisplay = (
+ <Text fontColor={ColorOption.lightGrey}>
+ @ {this._displayAmount(baseCurrency, pricePerTokenWei)}
+ </Text>
+ );
+ numTokensWithSymbol = (
+ <React.Fragment>
+ {numTokensWithSymbol} {atPriceDisplay}
+ </React.Fragment>
+ );
+ }
+ return numTokensWithSymbol;
+ }
+ return 'Token Amount';
+ }
+
+ private _pricePerTokenWei(): BigNumber | undefined {
+ const buyQuoteAccessor = oc(this.props.buyQuoteInfo);
+ const assetTotalInWei = buyQuoteAccessor.assetEthAmount();
+ const selectedAssetUnitAmount = this.props.selectedAssetUnitAmount;
+ return !_.isUndefined(assetTotalInWei) &&
+ !_.isUndefined(selectedAssetUnitAmount) &&
+ !selectedAssetUnitAmount.eq(BIG_NUMBER_ZERO)
+ ? assetTotalInWei.div(selectedAssetUnitAmount).ceil()
+ : undefined;
+ }
+
+ private _baseCurrencyChoice(choice: BaseCurrency): React.ReactNode {
+ const onClick =
+ choice === BaseCurrency.ETH ? this.props.onBaseCurrencySwitchEth : this.props.onBaseCurrencySwitchUsd;
+ const isSelected = this.props.baseCurrency === choice;
+
+ const textStyle: TextProps = { onClick, fontSize: '12px' };
+ if (isSelected) {
+ textStyle.fontColor = ColorOption.primaryColor;
+ textStyle.fontWeight = 700;
+ } else {
+ textStyle.fontColor = ColorOption.lightGrey;
+ }
+ return <Text {...textStyle}>{choice}</Text>;
+ }
+
+ private _renderHeader(): React.ReactNode {
+ return (
+ <Flex justify="space-between">
+ <SectionHeader>Order Details</SectionHeader>
+ <Container>
+ {this._baseCurrencyChoice(BaseCurrency.ETH)}
+ <Container marginLeft="5px" marginRight="5px" display="inline">
+ <Text fontSize="12px" fontColor={ColorOption.feintGrey}>
+ /
+ </Text>
+ </Container>
+ {this._baseCurrencyChoice(BaseCurrency.USD)}
+ </Container>
+ </Flex>
+ );
+ }
}
-export class EthAmountRow extends React.Component<EthAmountRowProps> {
- public static defaultProps = {
- shouldEmphasize: false,
- isEthAmountInBaseUnits: true,
- };
+export interface OrderDetailsRowProps {
+ labelText: React.ReactNode;
+ isLabelBold?: boolean;
+ isPrimaryValueBold?: boolean;
+ primaryValue: React.ReactNode;
+ secondaryValue?: React.ReactNode;
+}
+export class OrderDetailsRow extends React.PureComponent<OrderDetailsRowProps, {}> {
public render(): React.ReactNode {
- const { rowLabel, ethAmount, isEthAmountInBaseUnits, shouldEmphasize, isLoading } = this.props;
-
- const fontWeight = shouldEmphasize ? 700 : 400;
- const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseUnitAmount : format.ethUnitAmount;
return (
<Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>
<Flex justify="space-between">
- <Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
- {rowLabel}
+ <Text fontWeight={this.props.isLabelBold ? 700 : 400} fontColor={ColorOption.grey}>
+ {this.props.labelText}
</Text>
- <Container>
- {this._renderUsdSection()}
- <Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
- {ethFormatter(
- ethAmount,
- 4,
- <Container opacity={0.5}>
- <AmountPlaceholder color={ColorOption.lightGrey} isPulsating={isLoading} />
- </Container>,
- )}
- </Text>
- </Container>
+ <Container>{this._renderValues()}</Container>
</Flex>
</Container>
);
}
- private _renderUsdSection(): React.ReactNode {
- const usdFormatter = this.props.isEthAmountInBaseUnits
- ? format.ethBaseUnitAmountInUsd
- : format.ethUnitAmountInUsd;
- const shouldHideUsdPriceSection = _.isUndefined(this.props.ethUsdPrice) || _.isUndefined(this.props.ethAmount);
- return shouldHideUsdPriceSection ? null : (
+
+ private _renderValues(): React.ReactNode {
+ const secondaryValueNode: React.ReactNode = this.props.secondaryValue && (
<Container marginRight="3px" display="inline-block">
- <Text fontColor={ColorOption.lightGrey}>
- ({usdFormatter(this.props.ethAmount, this.props.ethUsdPrice)})
- </Text>
+ <Text fontColor={ColorOption.lightGrey}>({this.props.secondaryValue})</Text>
</Container>
);
+ return (
+ <React.Fragment>
+ {secondaryValueNode}
+ <Text fontWeight={this.props.isPrimaryValueBold ? 700 : 400}>{this.props.primaryValue}</Text>
+ </React.Fragment>
+ );
}
}
diff --git a/packages/instant/src/components/payment_method.tsx b/packages/instant/src/components/payment_method.tsx
index ebcd62f35..ada9f7bab 100644
--- a/packages/instant/src/components/payment_method.tsx
+++ b/packages/instant/src/components/payment_method.tsx
@@ -8,6 +8,7 @@ import { envUtil } from '../util/env';
import { CoinbaseWalletLogo } from './coinbase_wallet_logo';
import { MetaMaskLogo } from './meta_mask_logo';
import { PaymentMethodDropdown } from './payment_method_dropdown';
+import { SectionHeader } from './section_header';
import { Circle } from './ui/circle';
import { Container } from './ui/container';
import { Flex } from './ui/flex';
@@ -18,26 +19,18 @@ import { WalletPrompt } from './wallet_prompt';
export interface PaymentMethodProps {
account: Account;
network: Network;
- walletName: string;
+ walletDisplayName: string;
onInstallWalletClick: () => void;
onUnlockWalletClick: () => void;
}
-export class PaymentMethod extends React.Component<PaymentMethodProps> {
+export class PaymentMethod extends React.PureComponent<PaymentMethodProps> {
public render(): React.ReactNode {
return (
- <Container padding="20px" width="100%">
+ <Container width="100%" height="120px" padding="20px 20px 0px 20px">
<Container marginBottom="12px">
<Flex justify="space-between">
- <Text
- letterSpacing="1px"
- fontColor={ColorOption.primaryColor}
- fontWeight={600}
- textTransform="uppercase"
- fontSize="14px"
- >
- {this._renderTitleText()}
- </Text>
+ <SectionHeader>{this._renderTitleText()}</SectionHeader>
{this._renderTitleLabel()}
</Flex>
</Container>
@@ -62,11 +55,11 @@ export class PaymentMethod extends React.Component<PaymentMethodProps> {
if (account.state === AccountState.Ready || account.state === AccountState.Locked) {
const circleColor: ColorOption = account.state === AccountState.Ready ? ColorOption.green : ColorOption.red;
return (
- <Flex>
+ <Flex align="center">
<Circle diameter={8} color={circleColor} />
- <Container marginLeft="3px">
- <Text fontColor={ColorOption.darkGrey} fontSize="12px">
- {this.props.walletName}
+ <Container marginLeft="5px">
+ <Text fontColor={ColorOption.darkGrey} fontSize="12px" lineHeight="30px">
+ {this.props.walletDisplayName}
</Text>
</Container>
</Flex>
@@ -83,16 +76,19 @@ export class PaymentMethod extends React.Component<PaymentMethodProps> {
const colors = { primaryColor, secondaryColor };
switch (account.state) {
case AccountState.Loading:
- // Just take up the same amount of space as the other states.
- return <Container height="52px" />;
+ return null;
case AccountState.Locked:
return (
<WalletPrompt
onClick={this.props.onUnlockWalletClick}
- image={<Icon width={13} icon="lock" color={ColorOption.black} />}
+ image={
+ <Container position="relative" top="2px">
+ <Icon width={13} icon="lock" color={ColorOption.black} />
+ </Container>
+ }
{...colors}
>
- Please Unlock {this.props.walletName}
+ Click to Connect {this.props.walletDisplayName}
</WalletPrompt>
);
case AccountState.None:
diff --git a/packages/instant/src/components/payment_method_dropdown.tsx b/packages/instant/src/components/payment_method_dropdown.tsx
index b330dbcd6..e463e3eae 100644
--- a/packages/instant/src/components/payment_method_dropdown.tsx
+++ b/packages/instant/src/components/payment_method_dropdown.tsx
@@ -1,8 +1,9 @@
import { BigNumber } from '@0x/utils';
-import copy from 'copy-to-clipboard';
+import * as copy from 'copy-to-clipboard';
import * as React from 'react';
import { Network } from '../types';
+import { analytics } from '../util/analytics';
import { envUtil } from '../util/env';
import { etherscanUtil } from '../util/etherscan';
import { format } from '../util/format';
@@ -15,12 +16,19 @@ export interface PaymentMethodDropdownProps {
network: Network;
}
-export class PaymentMethodDropdown extends React.Component<PaymentMethodDropdownProps> {
+export class PaymentMethodDropdown extends React.PureComponent<PaymentMethodDropdownProps> {
public render(): React.ReactNode {
const { accountAddress, accountEthBalanceInWei } = this.props;
const value = format.ethAddress(accountAddress);
const label = format.ethBaseUnitAmount(accountEthBalanceInWei, 4, '') as string;
- return <Dropdown value={value} label={label} items={this._getDropdownItemConfigs()} />;
+ return (
+ <Dropdown
+ value={value}
+ label={label}
+ items={this._getDropdownItemConfigs()}
+ onOpen={analytics.trackPaymentMethodDropdownOpened}
+ />
+ );
}
private readonly _getDropdownItemConfigs = (): DropdownItemConfig[] => {
if (envUtil.isMobileOperatingSystem()) {
@@ -37,11 +45,15 @@ export class PaymentMethodDropdown extends React.Component<PaymentMethodDropdown
return [viewOnEtherscan, copyAddressToClipboard];
};
private readonly _handleEtherscanClick = (): void => {
+ analytics.trackPaymentMethodOpenedEtherscan();
+
const { accountAddress, network } = this.props;
const etherscanUrl = etherscanUtil.getEtherScanEthAddressIfExists(accountAddress, network);
window.open(etherscanUrl, '_blank');
};
private readonly _handleCopyToClipboardClick = (): void => {
+ analytics.trackPaymentMethodCopiedAddress();
+
const { accountAddress } = this.props;
copy(accountAddress);
};
diff --git a/packages/instant/src/components/placing_order_button.tsx b/packages/instant/src/components/placing_order_button.tsx
index 2516b90b1..528a305dc 100644
--- a/packages/instant/src/components/placing_order_button.tsx
+++ b/packages/instant/src/components/placing_order_button.tsx
@@ -14,3 +14,5 @@ export const PlacingOrderButton: React.StatelessComponent<{}> = props => (
Placing Order&hellip;
</Button>
);
+
+PlacingOrderButton.displayName = 'PlacingOrderButton';
diff --git a/packages/instant/src/components/scaling_amount_input.tsx b/packages/instant/src/components/scaling_amount_input.tsx
index 5dc719293..7dc1fdc0c 100644
--- a/packages/instant/src/components/scaling_amount_input.tsx
+++ b/packages/instant/src/components/scaling_amount_input.tsx
@@ -4,6 +4,7 @@ import * as React from 'react';
import { Maybe } from '../types';
+import { GIT_SHA, MAGIC_TRIGGER_ERROR_INPUT, MAGIC_TRIGGER_ERROR_MESSAGE, NPM_PACKAGE_VERSION } from '../constants';
import { ColorOption } from '../style/theme';
import { maybeBigNumberUtil } from '../util/maybe_big_number';
import { util } from '../util/util';
@@ -18,17 +19,19 @@ export interface ScalingAmountInputProps {
value?: BigNumber;
onAmountChange: (value?: BigNumber) => void;
onFontSizeChange: (fontSizePx: number) => void;
+ hasAutofocus: boolean;
}
interface ScalingAmountInputState {
stringValue: string;
}
const { stringToMaybeBigNumber, areMaybeBigNumbersEqual } = maybeBigNumberUtil;
-export class ScalingAmountInput extends React.Component<ScalingAmountInputProps, ScalingAmountInputState> {
+export class ScalingAmountInput extends React.PureComponent<ScalingAmountInputProps, ScalingAmountInputState> {
public static defaultProps = {
onAmountChange: util.boundNoop,
onFontSizeChange: util.boundNoop,
isDisabled: false,
+ hasAutofocus: false,
};
public constructor(props: ScalingAmountInputProps) {
super(props);
@@ -55,6 +58,7 @@ export class ScalingAmountInput extends React.Component<ScalingAmountInputProps,
const { textLengthThreshold, fontColor, maxFontSizePx, onFontSizeChange } = this.props;
return (
<ScalingInput
+ type="number"
maxFontSizePx={maxFontSizePx}
textLengthThreshold={textLengthThreshold}
onFontSizeChange={onFontSizeChange}
@@ -64,10 +68,15 @@ export class ScalingAmountInput extends React.Component<ScalingAmountInputProps,
placeholder="0.00"
emptyInputWidthCh={3.5}
isDisabled={this.props.isDisabled}
+ hasAutofocus={this.props.hasAutofocus}
/>
);
}
private readonly _handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
+ if (event.target.value === MAGIC_TRIGGER_ERROR_INPUT) {
+ throw new Error(`${MAGIC_TRIGGER_ERROR_MESSAGE} git: ${GIT_SHA}, npm: ${NPM_PACKAGE_VERSION}`);
+ }
+
const sanitizedValue = event.target.value.replace(/[^0-9.]/g, ''); // only allow numbers and "."
this.setState({
stringValue: sanitizedValue,
diff --git a/packages/instant/src/components/scaling_input.tsx b/packages/instant/src/components/scaling_input.tsx
index e1599a316..c31de1fb5 100644
--- a/packages/instant/src/components/scaling_input.tsx
+++ b/packages/instant/src/components/scaling_input.tsx
@@ -1,3 +1,4 @@
+import { ObjectMap } from '@0x/types';
import * as _ from 'lodash';
import * as React from 'react';
@@ -13,10 +14,15 @@ export enum ScalingInputPhase {
export interface ScalingSettings {
percentageToReduceFontSizePerCharacter: number;
- constantPxToIncreaseWidthPerCharacter: number;
+ // 1ch = the width of the 0 chararacter.
+ // Allow to customize 'char' length for different characters.
+ characterWidthOverrides: ObjectMap<number>;
+ // How much room to leave to the right of the scaling input.
+ additionalInputSpaceInCh: number;
}
export interface ScalingInputProps {
+ type?: string;
textLengthThreshold: number;
maxFontSizePx: number;
value: string;
@@ -28,10 +34,7 @@ export interface ScalingInputProps {
maxLength?: number;
scalingSettings: ScalingSettings;
isDisabled: boolean;
-}
-
-export interface ScalingInputState {
- inputWidthPxAtPhaseChange?: number;
+ hasAutofocus: boolean;
}
export interface ScalingInputSnapshot {
@@ -40,20 +43,22 @@ export interface ScalingInputSnapshot {
// These are magic numbers that were determined experimentally.
const defaultScalingSettings: ScalingSettings = {
- percentageToReduceFontSizePerCharacter: 0.125,
- constantPxToIncreaseWidthPerCharacter: 4,
+ percentageToReduceFontSizePerCharacter: 0.1,
+ characterWidthOverrides: {
+ '1': 0.7,
+ '.': 0.4,
+ },
+ additionalInputSpaceInCh: 0.4,
};
-export class ScalingInput extends React.Component<ScalingInputProps, ScalingInputState> {
+export class ScalingInput extends React.PureComponent<ScalingInputProps> {
public static defaultProps = {
onChange: util.boundNoop,
onFontSizeChange: util.boundNoop,
- maxLength: 7,
+ maxLength: 9,
scalingSettings: defaultScalingSettings,
isDisabled: false,
- };
- public state: ScalingInputState = {
- inputWidthPxAtPhaseChange: undefined,
+ hasAutofocus: false,
};
private readonly _inputRef = React.createRef<HTMLInputElement>();
public static getPhase(textLengthThreshold: number, value: string): ScalingInputPhase {
@@ -91,30 +96,15 @@ export class ScalingInput extends React.Component<ScalingInputProps, ScalingInpu
scalingSettings.percentageToReduceFontSizePerCharacter,
);
}
- public getSnapshotBeforeUpdate(): ScalingInputSnapshot {
- return {
- inputWidthPx: this._getInputWidthInPx(),
- };
+ public componentDidMount(): void {
+ // Trigger an initial notification of the calculated fontSize.
+ const currentPhase = ScalingInput.getPhaseFromProps(this.props);
+ const currentFontSize = ScalingInput.calculateFontSizeFromProps(this.props, currentPhase);
+ this.props.onFontSizeChange(currentFontSize);
}
- public componentDidUpdate(
- prevProps: ScalingInputProps,
- prevState: ScalingInputState,
- snapshot: ScalingInputSnapshot,
- ): void {
+ public componentDidUpdate(prevProps: ScalingInputProps): void {
const prevPhase = ScalingInput.getPhaseFromProps(prevProps);
const curPhase = ScalingInput.getPhaseFromProps(this.props);
- // if we went from fixed to scaling, save the width from the transition
- if (prevPhase !== ScalingInputPhase.ScalingFontSize && curPhase === ScalingInputPhase.ScalingFontSize) {
- this.setState({
- inputWidthPxAtPhaseChange: snapshot.inputWidthPx,
- });
- }
- // if we went from scaling to fixed, revert back to scaling using `ch`
- if (prevPhase === ScalingInputPhase.ScalingFontSize && curPhase !== ScalingInputPhase.ScalingFontSize) {
- this.setState({
- inputWidthPxAtPhaseChange: undefined,
- });
- }
const prevFontSize = ScalingInput.calculateFontSizeFromProps(prevProps, prevPhase);
const curFontSize = ScalingInput.calculateFontSizeFromProps(this.props, curPhase);
// If font size has changed, notify.
@@ -123,49 +113,53 @@ export class ScalingInput extends React.Component<ScalingInputProps, ScalingInpu
}
}
public render(): React.ReactNode {
- const { isDisabled, fontColor, onChange, placeholder, value, maxLength } = this.props;
+ const { type, hasAutofocus, isDisabled, fontColor, placeholder, value, maxLength } = this.props;
const phase = ScalingInput.getPhaseFromProps(this.props);
return (
<Input
+ type={type}
ref={this._inputRef as any}
fontColor={fontColor}
- onChange={onChange}
+ onChange={this._handleChange}
value={value}
placeholder={placeholder}
fontSize={`${this._calculateFontSize(phase)}px`}
width={this._calculateWidth(phase)}
maxLength={maxLength}
disabled={isDisabled}
+ autoFocus={hasAutofocus}
/>
);
}
private readonly _calculateWidth = (phase: ScalingInputPhase): string => {
- const { value, textLengthThreshold, scalingSettings } = this.props;
+ const { value, scalingSettings } = this.props;
if (_.isEmpty(value)) {
return `${this.props.emptyInputWidthCh}ch`;
}
- switch (phase) {
- case ScalingInputPhase.FixedFontSize:
- return `${value.length}ch`;
- case ScalingInputPhase.ScalingFontSize:
- const { inputWidthPxAtPhaseChange } = this.state;
- if (!_.isUndefined(inputWidthPxAtPhaseChange)) {
- const charactersOverMax = value.length - textLengthThreshold;
- const scalingAmount = scalingSettings.constantPxToIncreaseWidthPerCharacter * charactersOverMax;
- const width = inputWidthPxAtPhaseChange + scalingAmount;
- return `${width}px`;
+ const lengthInCh = _.reduce(
+ value.split(''),
+ (sum, char) => {
+ const widthOverride = scalingSettings.characterWidthOverrides[char];
+ if (!_.isUndefined(widthOverride)) {
+ // tslint is confused
+ // tslint:disable-next-line:restrict-plus-operands
+ return sum + widthOverride;
}
- return `${textLengthThreshold}ch`;
- }
+ return sum + 1;
+ },
+ scalingSettings.additionalInputSpaceInCh,
+ );
+ return `${lengthInCh}ch`;
};
private readonly _calculateFontSize = (phase: ScalingInputPhase): number => {
return ScalingInput.calculateFontSizeFromProps(this.props, phase);
};
- private readonly _getInputWidthInPx = (): number => {
- const ref = this._inputRef.current;
- if (!ref) {
- return 0;
+ private readonly _handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
+ const value = event.target.value;
+ const { maxLength } = this.props;
+ if (!_.isUndefined(value) && !_.isUndefined(maxLength) && value.length > maxLength) {
+ return;
}
- return ref.getBoundingClientRect().width;
+ this.props.onChange(event);
};
}
diff --git a/packages/instant/src/components/search_input.tsx b/packages/instant/src/components/search_input.tsx
index 3a693b9f8..71bc18915 100644
--- a/packages/instant/src/components/search_input.tsx
+++ b/packages/instant/src/components/search_input.tsx
@@ -13,10 +13,10 @@ export interface SearchInputProps extends InputProps {
}
export const SearchInput: React.StatelessComponent<SearchInputProps> = props => (
- <Container backgroundColor={props.backgroundColor} borderRadius="3px" padding=".5em .3em">
- <Flex justify="flex-start" align="flex-end">
- <Icon width={14} height={14} icon="search" color={ColorOption.lightGrey} padding="0px 12px" />
- <Input {...props} fontSize="14px" fontColor={props.fontColor} />
+ <Container backgroundColor={props.backgroundColor} borderRadius="3px" padding=".5em .5em">
+ <Flex justify="flex-start" align="center">
+ <Icon width={14} height={14} icon="search" color={ColorOption.lightGrey} padding="2px 12px" />
+ <Input {...props} type="search" fontSize="16px" fontColor={props.fontColor} />
</Flex>
</Container>
);
diff --git a/packages/instant/src/components/secondary_button.tsx b/packages/instant/src/components/secondary_button.tsx
index 705390e28..0714ce287 100644
--- a/packages/instant/src/components/secondary_button.tsx
+++ b/packages/instant/src/components/secondary_button.tsx
@@ -24,3 +24,4 @@ export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = p
SecondaryButton.defaultProps = {
width: '100%',
};
+SecondaryButton.displayName = 'SecondaryButton';
diff --git a/packages/instant/src/components/section_header.tsx b/packages/instant/src/components/section_header.tsx
new file mode 100644
index 000000000..2185b67ba
--- /dev/null
+++ b/packages/instant/src/components/section_header.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react';
+
+import { ColorOption } from '../style/theme';
+
+import { Text } from './ui/text';
+
+export interface SectionHeaderProps {}
+export const SectionHeader: React.StatelessComponent<SectionHeaderProps> = props => {
+ return (
+ <Text
+ letterSpacing="1px"
+ fontColor={ColorOption.primaryColor}
+ fontWeight={600}
+ textTransform="uppercase"
+ fontSize="12px"
+ >
+ {props.children}
+ </Text>
+ );
+};
+SectionHeader.displayName = 'SectionHeader';
diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx
index b59e2a905..c7c6732cf 100644
--- a/packages/instant/src/components/sliding_error.tsx
+++ b/packages/instant/src/components/sliding_error.tsx
@@ -38,6 +38,8 @@ export const Error: React.StatelessComponent<ErrorProps> = props => (
</Container>
);
+Error.displayName = 'Error';
+
export interface SlidingErrorProps extends ErrorProps {
animationState: SlideAnimationState;
}
@@ -94,3 +96,5 @@ export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props =
</SlideAnimation>
);
};
+
+SlidingError.displayName = 'SlidingError';
diff --git a/packages/instant/src/components/sliding_panel.tsx b/packages/instant/src/components/sliding_panel.tsx
index 7f9037049..9b09a0d80 100644
--- a/packages/instant/src/components/sliding_panel.tsx
+++ b/packages/instant/src/components/sliding_panel.tsx
@@ -26,42 +26,48 @@ export const Panel: React.StatelessComponent<PanelProps> = ({ children, onClose
</Container>
);
+Panel.displayName = 'Panel';
+
export interface SlidingPanelProps extends PanelProps {
animationState: SlideAnimationState;
+ onAnimationEnd?: () => void;
}
-export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props => {
- if (props.animationState === 'none') {
- return null;
+export class SlidingPanel extends React.PureComponent<SlidingPanelProps> {
+ public render(): React.ReactNode {
+ if (this.props.animationState === 'none') {
+ return null;
+ }
+ const { animationState, onAnimationEnd, ...rest } = this.props;
+ const slideAmount = '100%';
+ const slideUpSettings: PositionAnimationSettings = {
+ duration: '0.3s',
+ timingFunction: 'ease-in-out',
+ top: {
+ from: slideAmount,
+ to: '0px',
+ },
+ position: 'absolute',
+ };
+ const slideDownSettings: PositionAnimationSettings = {
+ duration: '0.3s',
+ timingFunction: 'ease-out',
+ top: {
+ from: '0px',
+ to: slideAmount,
+ },
+ position: 'absolute',
+ };
+ return (
+ <SlideAnimation
+ slideInSettings={slideUpSettings}
+ slideOutSettings={slideDownSettings}
+ animationState={animationState}
+ height="100%"
+ onAnimationEnd={onAnimationEnd}
+ >
+ <Panel {...rest} />
+ </SlideAnimation>
+ );
}
- const { animationState, ...rest } = props;
- const slideAmount = '100%';
- const slideUpSettings: PositionAnimationSettings = {
- duration: '0.3s',
- timingFunction: 'ease-in-out',
- top: {
- from: slideAmount,
- to: '0px',
- },
- position: 'absolute',
- };
- const slideDownSettings: PositionAnimationSettings = {
- duration: '0.3s',
- timingFunction: 'ease-out',
- top: {
- from: '0px',
- to: slideAmount,
- },
- position: 'absolute',
- };
- return (
- <SlideAnimation
- slideInSettings={slideUpSettings}
- slideOutSettings={slideDownSettings}
- animationState={animationState}
- height="100%"
- >
- <Panel {...rest} />
- </SlideAnimation>
- );
-};
+}
diff --git a/packages/instant/src/components/standard_panel_content.tsx b/packages/instant/src/components/standard_panel_content.tsx
index 582b3318e..f2987df82 100644
--- a/packages/instant/src/components/standard_panel_content.tsx
+++ b/packages/instant/src/components/standard_panel_content.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { ColorOption } from '../style/theme';
+import { util } from '../util/util';
import { Container } from './ui/container';
import { Flex } from './ui/flex';
@@ -9,6 +10,7 @@ import { Text } from './ui/text';
export interface MoreInfoSettings {
text: string;
href: string;
+ onClick?: () => void;
}
export interface StandardPanelContentProps {
@@ -21,6 +23,15 @@ export interface StandardPanelContentProps {
const SPACING_BETWEEN_PX = '20px';
+const onMoreInfoClick = (href: string, onClick?: () => void) => {
+ return () => {
+ if (onClick) {
+ onClick();
+ }
+ util.createOpenUrlInNewWindow(href)();
+ };
+};
+
export const StandardPanelContent: React.StatelessComponent<StandardPanelContentProps> = ({
image,
title,
@@ -50,7 +61,7 @@ export const StandardPanelContent: React.StatelessComponent<StandardPanelContent
fontSize="13px"
textDecorationLine="underline"
fontColor={ColorOption.lightGrey}
- href={moreInfoSettings.href}
+ onClick={onMoreInfoClick(moreInfoSettings.href, moreInfoSettings.onClick)}
>
{moreInfoSettings.text}
</Text>
@@ -60,3 +71,5 @@ export const StandardPanelContent: React.StatelessComponent<StandardPanelContent
<Container>{action}</Container>
</Container>
);
+
+StandardPanelContent.displayName = 'StandardPanelContent';
diff --git a/packages/instant/src/components/standard_sliding_panel.tsx b/packages/instant/src/components/standard_sliding_panel.tsx
index f587ff79a..bcc9d3dce 100644
--- a/packages/instant/src/components/standard_sliding_panel.tsx
+++ b/packages/instant/src/components/standard_sliding_panel.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
-import { SlideAnimationState, StandardSlidingPanelContent, StandardSlidingPanelSettings } from '../types';
+import { StandardSlidingPanelContent, StandardSlidingPanelSettings } from '../types';
import { InstallWalletPanelContent } from './install_wallet_panel_content';
import { SlidingPanel } from './sliding_panel';
@@ -9,7 +9,7 @@ export interface StandardSlidingPanelProps extends StandardSlidingPanelSettings
onClose: () => void;
}
-export class StandardSlidingPanel extends React.Component<StandardSlidingPanelProps> {
+export class StandardSlidingPanel extends React.PureComponent<StandardSlidingPanelProps> {
public render(): React.ReactNode {
const { animationState, content, onClose } = this.props;
return (
diff --git a/packages/instant/src/components/time_counter.tsx b/packages/instant/src/components/time_counter.tsx
index f9b68163c..93dc497d5 100644
--- a/packages/instant/src/components/time_counter.tsx
+++ b/packages/instant/src/components/time_counter.tsx
@@ -16,7 +16,7 @@ interface TimeCounterState {
elapsedSeconds: number;
}
-export class TimeCounter extends React.Component<TimeCounterProps, TimeCounterState> {
+export class TimeCounter extends React.PureComponent<TimeCounterProps, TimeCounterState> {
public state = {
elapsedSeconds: 0,
};
diff --git a/packages/instant/src/components/timed_progress_bar.tsx b/packages/instant/src/components/timed_progress_bar.tsx
index 8465b9cd0..b1644b871 100644
--- a/packages/instant/src/components/timed_progress_bar.tsx
+++ b/packages/instant/src/components/timed_progress_bar.tsx
@@ -1,8 +1,9 @@
import * as _ from 'lodash';
+import { transparentize } from 'polished';
import * as React from 'react';
import { PROGRESS_FINISH_ANIMATION_TIME_MS, PROGRESS_STALL_AT_WIDTH } from '../constants';
-import { ColorOption, css, keyframes, styled } from '../style/theme';
+import { ColorOption, css, keyframes, styled, ThemeConsumer } from '../style/theme';
import { Container } from './ui/container';
@@ -16,7 +17,7 @@ export interface TimedProgressBarProps {
* Goes from 0% -> PROGRESS_STALL_AT_WIDTH over time of expectedTimeMs
* When hasEnded set to true, goes to 100% through animation of PROGRESS_FINISH_ANIMATION_TIME_MS length of time
*/
-export class TimedProgressBar extends React.Component<TimedProgressBarProps, {}> {
+export class TimedProgressBar extends React.PureComponent<TimedProgressBarProps, {}> {
private readonly _barRef = React.createRef<HTMLDivElement>();
public render(): React.ReactNode {
@@ -93,8 +94,16 @@ export interface ProgressBarProps extends ProgressProps {}
export const ProgressBar: React.ComponentType<ProgressBarProps & React.ClassAttributes<{}>> = React.forwardRef(
(props, ref) => (
- <Container width="100%" backgroundColor={ColorOption.lightGrey} borderRadius="6px">
- <Progress {...props} ref={ref as any} />
- </Container>
+ <ThemeConsumer>
+ {theme => (
+ <Container
+ width="100%"
+ borderRadius="6px"
+ rawBackgroundColor={transparentize(0.5, theme[ColorOption.primaryColor])}
+ >
+ <Progress {...props} ref={ref as any} />
+ </Container>
+ )}
+ </ThemeConsumer>
),
);
diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx
index 4dafe1386..58d7d5871 100644
--- a/packages/instant/src/components/ui/container.tsx
+++ b/packages/instant/src/components/ui/container.tsx
@@ -27,17 +27,30 @@ export interface ContainerProps {
borderBottom?: string;
className?: string;
backgroundColor?: ColorOption;
+ rawBackgroundColor?: string;
hasBoxShadow?: boolean;
+ isHidden?: boolean;
zIndex?: number;
whiteSpace?: string;
opacity?: number;
cursor?: string;
overflow?: string;
darkenOnHover?: boolean;
+ rawHoverColor?: string;
boxShadowOnHover?: boolean;
flexGrow?: string | number;
}
+const getBackgroundColor = (theme: any, backgroundColor?: ColorOption, rawBackgroundColor?: string): string => {
+ if (backgroundColor) {
+ return theme[backgroundColor] as string;
+ }
+ if (rawBackgroundColor) {
+ return rawBackgroundColor;
+ }
+ return 'none';
+};
+
export const Container =
styled.div <
ContainerProps >
@@ -65,14 +78,17 @@ export const Container =
${props => cssRuleIfExists(props, 'opacity')}
${props => cssRuleIfExists(props, 'cursor')}
${props => cssRuleIfExists(props, 'overflow')}
+ ${props => (props.overflow === 'scroll' ? `-webkit-overflow-scrolling: touch` : '')};
${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')};
${props => props.display && stylesForMedia<string>('display', props.display)}
${props => props.width && stylesForMedia<string>('width', props.width)}
${props => props.height && stylesForMedia<string>('height', props.height)}
${props => props.borderRadius && stylesForMedia<string>('border-radius', props.borderRadius)}
- background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
+ ${props => (props.isHidden ? 'visibility: hidden;' : '')}
+ background-color: ${props => getBackgroundColor(props.theme, props.backgroundColor, props.rawBackgroundColor)};
border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')};
&:hover {
+ ${props => (props.rawHoverColor ? `background-color: ${props.rawHoverColor}` : '')}
${props =>
props.darkenOnHover
? `background-color: ${
diff --git a/packages/instant/src/components/ui/dropdown.tsx b/packages/instant/src/components/ui/dropdown.tsx
index 3a23f456d..8788d3d59 100644
--- a/packages/instant/src/components/ui/dropdown.tsx
+++ b/packages/instant/src/components/ui/dropdown.tsx
@@ -1,7 +1,8 @@
import * as _ from 'lodash';
+import { transparentize } from 'polished';
import * as React from 'react';
-import { ColorOption, completelyTransparent } from '../../style/theme';
+import { ColorOption, completelyTransparent, ThemeConsumer } from '../../style/theme';
import { zIndex } from '../../style/z_index';
import { Container } from './container';
@@ -19,13 +20,14 @@ export interface DropdownProps {
value: string;
label?: string;
items: DropdownItemConfig[];
+ onOpen?: () => void;
}
export interface DropdownState {
isOpen: boolean;
}
-export class Dropdown extends React.Component<DropdownProps, DropdownState> {
+export class Dropdown extends React.PureComponent<DropdownProps, DropdownState> {
public static defaultProps = {
items: [],
};
@@ -97,9 +99,14 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {
if (_.isEmpty(this.props.items)) {
return;
}
+ const isOpen = !this.state.isOpen;
this.setState({
- isOpen: !this.state.isOpen,
+ isOpen,
});
+
+ if (isOpen && this.props.onOpen) {
+ this.props.onOpen();
+ }
};
private readonly _closeDropdown = (): void => {
this.setState({
@@ -115,20 +122,26 @@ export interface DropdownItemProps extends DropdownItemConfig {
}
export const DropdownItem: React.StatelessComponent<DropdownItemProps> = ({ text, onClick, isLast }) => (
- <Container
- onClick={onClick}
- cursor="pointer"
- darkenOnHover={true}
- backgroundColor={ColorOption.white}
- padding="0.8em"
- borderTop="0"
- border="1px solid"
- borderRadius={isLast ? '0px 0px 4px 4px' : undefined}
- width="100%"
- borderColor={ColorOption.feintGrey}
- >
- <Text fontSize="14px" fontColor={ColorOption.darkGrey}>
- {text}
- </Text>
- </Container>
+ <ThemeConsumer>
+ {theme => (
+ <Container
+ onClick={onClick}
+ cursor="pointer"
+ rawHoverColor={transparentize(0.9, theme[ColorOption.primaryColor])}
+ backgroundColor={ColorOption.white}
+ padding="0.8em"
+ borderTop="0"
+ border="1px solid"
+ borderRadius={isLast ? '0px 0px 4px 4px' : undefined}
+ width="100%"
+ borderColor={ColorOption.feintGrey}
+ >
+ <Text fontSize="14px" fontColor={ColorOption.darkGrey}>
+ {text}
+ </Text>
+ </Container>
+ )}
+ </ThemeConsumer>
);
+
+DropdownItem.displayName = 'DropdownItem';
diff --git a/packages/instant/src/components/ui/input.tsx b/packages/instant/src/components/ui/input.tsx
index 1ea5d8fe1..53c43ea0b 100644
--- a/packages/instant/src/components/ui/input.tsx
+++ b/packages/instant/src/components/ui/input.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { ColorOption, styled } from '../../style/theme';
-export interface InputProps {
+export interface InputProps extends React.HTMLAttributes<HTMLInputElement> {
tabIndex?: number;
className?: string;
value?: string;
@@ -10,6 +10,7 @@ export interface InputProps {
fontSize?: string;
fontColor?: ColorOption;
placeholder?: string;
+ type?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
@@ -28,9 +29,13 @@ export const Input =
outline: none;
border: none;
&::placeholder {
- color: ${props => props.theme[props.fontColor || 'white']};
- opacity: 0.5;
+ color: ${props => props.theme[props.fontColor || 'white']} !important;
+ opacity: 0.5 !important;
}
+ &::-webkit-outer-spin-button, &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
}
`;
diff --git a/packages/instant/src/components/ui/overlay.tsx b/packages/instant/src/components/ui/overlay.tsx
index f67d6fb2f..0b5eaf299 100644
--- a/packages/instant/src/components/ui/overlay.tsx
+++ b/packages/instant/src/components/ui/overlay.tsx
@@ -33,7 +33,7 @@ export const Overlay =
Overlay.defaultProps = {
zIndex: zIndex.overlayDefault,
- backgroundColor: generateOverlayBlack(0.6),
+ backgroundColor: generateOverlayBlack(0.7),
};
Overlay.displayName = 'Overlay';
diff --git a/packages/instant/src/components/ui/text.tsx b/packages/instant/src/components/ui/text.tsx
index fd14cc4d1..282477758 100644
--- a/packages/instant/src/components/ui/text.tsx
+++ b/packages/instant/src/components/ui/text.tsx
@@ -1,4 +1,3 @@
-import { darken } from 'polished';
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme';
@@ -11,6 +10,7 @@ export interface TextProps {
fontSize?: string;
opacity?: number;
letterSpacing?: string;
+ textAlign?: string;
textTransform?: string;
lineHeight?: string;
className?: string;
@@ -22,6 +22,7 @@ export interface TextProps {
noWrap?: boolean;
display?: string;
href?: string;
+ width?: string;
}
export const Text: React.StatelessComponent<TextProps> = ({ href, onClick, ...rest }) => {
@@ -29,7 +30,7 @@ export const Text: React.StatelessComponent<TextProps> = ({ href, onClick, ...re
return <StyledText {...rest} onClick={computedOnClick} />;
};
-const darkenOnHoverAmount = 0.3;
+const opacityOnHoverAmount = 0.5;
export const StyledText =
styled.div <
TextProps >
@@ -51,9 +52,10 @@ export const StyledText =
${props => (props.display ? `display: ${props.display}` : '')};
${props => (props.letterSpacing ? `letter-spacing: ${props.letterSpacing}` : '')};
${props => (props.textTransform ? `text-transform: ${props.textTransform}` : '')};
+ ${props => (props.textAlign ? `text-align: ${props.textAlign}` : '')};
+ ${props => (props.width ? `width: ${props.width}` : '')};
&:hover {
- ${props =>
- props.onClick ? `color: ${darken(darkenOnHoverAmount, props.theme[props.fontColor || 'white'])}` : ''};
+ ${props => (props.onClick ? `opacity: ${opacityOnHoverAmount};` : '')};
}
}
`;
diff --git a/packages/instant/src/components/wallet_prompt.tsx b/packages/instant/src/components/wallet_prompt.tsx
index a0b3ae457..10433767f 100644
--- a/packages/instant/src/components/wallet_prompt.tsx
+++ b/packages/instant/src/components/wallet_prompt.tsx
@@ -21,7 +21,7 @@ export const WalletPrompt: React.StatelessComponent<WalletPromptProps> = ({
primaryColor,
}) => (
<Container
- padding="14.5px"
+ padding="10px"
border={`1px solid ${primaryColor}`}
backgroundColor={secondaryColor}
width="100%"
@@ -33,7 +33,7 @@ export const WalletPrompt: React.StatelessComponent<WalletPromptProps> = ({
<Flex>
{image}
<Container marginLeft="10px">
- <Text fontSize="16px" fontColor={primaryColor}>
+ <Text fontSize="16px" fontColor={primaryColor} fontWeight="500">
{children}
</Text>
</Container>
@@ -45,3 +45,5 @@ WalletPrompt.defaultProps = {
primaryColor: ColorOption.darkOrange,
secondaryColor: ColorOption.lightOrange,
};
+
+WalletPrompt.displayName = 'WalletPrompt';
diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx
index 2267b4dbf..e9cb48e61 100644
--- a/packages/instant/src/components/zero_ex_instant.tsx
+++ b/packages/instant/src/components/zero_ex_instant.tsx
@@ -17,3 +17,5 @@ export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = props
</div>
);
};
+
+ZeroExInstant.displayName = 'ZeroExInstant';
diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx
index 47c938472..e8c64d5d1 100644
--- a/packages/instant/src/components/zero_ex_instant_container.tsx
+++ b/packages/instant/src/components/zero_ex_instant_container.tsx
@@ -11,21 +11,23 @@ import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_
import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading';
import { ColorOption } from '../style/theme';
import { zIndex } from '../style/z_index';
-import { OrderProcessState, SlideAnimationState } from '../types';
+import { SlideAnimationState } from '../types';
+import { analytics, TokenSelectorClosedVia } from '../util/analytics';
import { CSSReset } from './css_reset';
import { SlidingPanel } from './sliding_panel';
import { Container } from './ui/container';
import { Flex } from './ui/flex';
-export interface ZeroExInstantContainerProps {
- orderProcessState: OrderProcessState;
-}
+export interface ZeroExInstantContainerProps {}
export interface ZeroExInstantContainerState {
tokenSelectionPanelAnimationState: SlideAnimationState;
}
-export class ZeroExInstantContainer extends React.Component<{}, ZeroExInstantContainerState> {
+export class ZeroExInstantContainer extends React.PureComponent<
+ ZeroExInstantContainerProps,
+ ZeroExInstantContainerState
+> {
public state = {
tokenSelectionPanelAnimationState: 'none' as SlideAnimationState,
};
@@ -60,9 +62,10 @@ export class ZeroExInstantContainer extends React.Component<{}, ZeroExInstantCon
</Flex>
<SlidingPanel
animationState={this.state.tokenSelectionPanelAnimationState}
- onClose={this._handlePanelClose}
+ onClose={this._handlePanelCloseClickedX}
+ onAnimationEnd={this._handleSlidingPanelAnimationEnd}
>
- <AvailableERC20TokenSelector onTokenSelect={this._handlePanelClose} />
+ <AvailableERC20TokenSelector onTokenSelect={this._handlePanelCloseAfterChose} />
</SlidingPanel>
<CurrentStandardSlidingPanel />
</Container>
@@ -71,7 +74,7 @@ export class ZeroExInstantContainer extends React.Component<{}, ZeroExInstantCon
marginTop="10px"
marginLeft="auto"
marginRight="auto"
- width="140px"
+ width="108px"
>
<a href={ZERO_EX_SITE_URL} target="_blank">
<PoweredByLogo />
@@ -82,13 +85,28 @@ export class ZeroExInstantContainer extends React.Component<{}, ZeroExInstantCon
);
}
private readonly _handleSymbolClick = (): void => {
+ analytics.trackTokenSelectorOpened();
this.setState({
tokenSelectionPanelAnimationState: 'slidIn',
});
};
- private readonly _handlePanelClose = (): void => {
+ private readonly _handlePanelCloseClickedX = (): void => {
+ this._handlePanelClose(TokenSelectorClosedVia.ClickedX);
+ };
+ private readonly _handlePanelCloseAfterChose = (): void => {
+ this._handlePanelClose(TokenSelectorClosedVia.TokenChose);
+ };
+ private readonly _handlePanelClose = (closedVia: TokenSelectorClosedVia): void => {
+ analytics.trackTokenSelectorClosed(closedVia);
this.setState({
tokenSelectionPanelAnimationState: 'slidOut',
});
};
+ private readonly _handleSlidingPanelAnimationEnd = (): void => {
+ if (this.state.tokenSelectionPanelAnimationState === 'slidOut') {
+ // When the slidOut animation completes, don't keep the panel mounted.
+ // Performance optimization
+ this.setState({ tokenSelectionPanelAnimationState: 'none' });
+ }
+ };
}
diff --git a/packages/instant/src/components/zero_ex_instant_overlay.tsx b/packages/instant/src/components/zero_ex_instant_overlay.tsx
index 2856ea3e3..38a716091 100644
--- a/packages/instant/src/components/zero_ex_instant_overlay.tsx
+++ b/packages/instant/src/components/zero_ex_instant_overlay.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { ZeroExInstantContainer } from '../components/zero_ex_instant_container';
+import { MAIN_CONTAINER_DIV_CLASS, OVERLAY_CLOSE_BUTTON_DIV_CLASS, OVERLAY_DIV_CLASS } from '../constants';
import { ColorOption } from '../style/theme';
import { Container } from './ui/container';
@@ -18,9 +19,15 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay
const { onClose, zIndex, ...rest } = props;
return (
<ZeroExInstantProvider {...rest}>
- <Overlay zIndex={zIndex}>
+ <Overlay zIndex={zIndex} className={OVERLAY_DIV_CLASS}>
<Flex height="100vh">
- <Container position="absolute" top="0px" right="0px" display={{ default: 'initial', sm: 'none' }}>
+ <Container
+ className={OVERLAY_CLOSE_BUTTON_DIV_CLASS}
+ position="absolute"
+ top="0px"
+ right="0px"
+ display={{ default: 'initial', sm: 'none' }}
+ >
<Icon
height={18}
width={18}
@@ -30,7 +37,11 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay
padding="2em 2em"
/>
</Container>
- <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}>
+ <Container
+ width={{ default: 'auto', sm: '100%' }}
+ height={{ default: 'auto', sm: '100%' }}
+ className={MAIN_CONTAINER_DIV_CLASS}
+ >
<ZeroExInstantContainer />
</Container>
</Flex>
@@ -38,3 +49,5 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay
</ZeroExInstantProvider>
);
};
+
+ZeroExInstantOverlay.displayName = 'ZeroExInstantOverlay';
diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx
index 9435d8c7c..ec8e82ee3 100644
--- a/packages/instant/src/components/zero_ex_instant_provider.tsx
+++ b/packages/instant/src/components/zero_ex_instant_provider.tsx
@@ -1,6 +1,4 @@
-import { ObjectMap } from '@0x/types';
import { BigNumber } from '@0x/utils';
-import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import * as React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
@@ -11,36 +9,19 @@ import { asyncData } from '../redux/async_data';
import { DEFAULT_STATE, DefaultState, State } from '../redux/reducer';
import { store, Store } from '../redux/store';
import { fonts } from '../style/fonts';
-import { AccountState, AffiliateInfo, AssetMetaData, Network, OrderSource } from '../types';
+import { AccountState, Network, QuoteFetchOrigin, ZeroExInstantBaseConfig } from '../types';
import { analytics, disableAnalytics } from '../util/analytics';
import { assetUtils } from '../util/asset';
import { errorFlasher } from '../util/error_flasher';
+import { setupRollbar } from '../util/error_reporter';
import { gasPriceEstimator } from '../util/gas_price_estimator';
import { Heartbeater } from '../util/heartbeater';
import { generateAccountHeartbeater, generateBuyQuoteHeartbeater } from '../util/heartbeater_factory';
import { providerStateFactory } from '../util/provider_state_factory';
-fonts.include();
+export type ZeroExInstantProviderProps = ZeroExInstantBaseConfig;
-export type ZeroExInstantProviderProps = ZeroExInstantProviderRequiredProps &
- Partial<ZeroExInstantProviderOptionalProps>;
-
-export interface ZeroExInstantProviderRequiredProps {
- orderSource: OrderSource;
-}
-
-export interface ZeroExInstantProviderOptionalProps {
- provider: Provider;
- availableAssetDatas: string[];
- defaultAssetBuyAmount: number;
- defaultSelectedAssetData: string;
- additionalAssetMetaDataMap: ObjectMap<AssetMetaData>;
- networkId: Network;
- affiliateInfo: AffiliateInfo;
- shouldDisableAnalyticsTracking: boolean;
-}
-
-export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> {
+export class ZeroExInstantProvider extends React.PureComponent<ZeroExInstantProviderProps> {
private readonly _store: Store;
private _accountUpdateHeartbeat?: Heartbeater;
private _buyQuoteHeartbeat?: Heartbeater;
@@ -57,10 +38,12 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
props.orderSource,
networkId,
props.provider,
+ props.walletDisplayName,
);
// merge the additional additionalAssetMetaDataMap with our default map
const completeAssetMetaDataMap = {
- ...props.additionalAssetMetaDataMap,
+ // Make sure the passed in assetDatas are lower case
+ ..._.mapKeys(props.additionalAssetMetaDataMap || {}, (value, key) => key.toLowerCase()),
...defaultState.assetMetaDataMap,
};
// construct the final state
@@ -68,6 +51,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
...defaultState,
providerState,
network: networkId,
+ walletDisplayName: props.walletDisplayName,
selectedAsset: _.isUndefined(props.defaultSelectedAssetData)
? undefined
: assetUtils.createAssetFromAssetDataOrThrow(
@@ -88,6 +72,8 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
}
constructor(props: ZeroExInstantProviderProps) {
super(props);
+ setupRollbar();
+ fonts.include();
const initialAppState = ZeroExInstantProvider._mergeDefaultStateWithProps(this.props);
this._store = store.create(initialAppState);
}
@@ -116,7 +102,9 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
this._buyQuoteHeartbeat.start(BUY_QUOTE_UPDATE_INTERVAL_TIME_MS);
// Trigger first buyquote fetch
// tslint:disable-next-line:no-floating-promises
- asyncData.fetchCurrentBuyQuoteAndDispatchToStore(state, dispatch, { updateSilently: false });
+ asyncData.fetchCurrentBuyQuoteAndDispatchToStore(state, dispatch, QuoteFetchOrigin.Manual, {
+ updateSilently: false,
+ });
// warm up the gas price estimator cache just in case we can't
// grab the gas price estimate when submitting the transaction
// tslint:disable-next-line:no-floating-promises
@@ -126,14 +114,17 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
// Analytics
disableAnalytics(this.props.shouldDisableAnalyticsTracking || false);
- analytics.addEventProperties({
- embeddedHost: window.location.host,
- embeddedUrl: window.location.href,
- networkId: state.network,
- providerName: state.providerState.name,
- gitSha: process.env.GIT_SHA,
- npmVersion: process.env.NPM_PACKAGE_VERSION,
- });
+ analytics.addEventProperties(
+ analytics.generateEventProperties(
+ state.network,
+ this.props.orderSource,
+ state.providerState,
+ window,
+ state.selectedAsset,
+ this.props.affiliateInfo,
+ state.baseCurrency,
+ ),
+ );
analytics.trackInstantOpened();
}
public componentWillUnmount(): void {