Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/website/instant-configurator
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "0x-instant-dogfood",
|
||||
"build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod",
|
||||
"build_command": "WEBPACK_OUTPUT_PATH=public dotenv yarn build --env.discharge_target=dogfood",
|
||||
"upload_directory": "public",
|
||||
"index_key": "index.html",
|
||||
"error_key": "index.html",
|
||||
"trailing_slashes": true,
|
||||
"cache": 3600,
|
||||
"aws_profile": "default",
|
||||
"aws_profile": "0xproject",
|
||||
"aws_region": "us-east-1",
|
||||
"cdn": false,
|
||||
"dns_configured": true
|
||||
|
4
packages/instant/.env_example
Normal file
4
packages/instant/.env_example
Normal file
@@ -0,0 +1,4 @@
|
||||
INSTANT_ROLLBAR_PUBLISH_TOKEN=
|
||||
INSTANT_ROLLBAR_CLIENT_TOKEN=
|
||||
INSTANT_HEAP_ANALYTICS_ID_PRODUCTION=
|
||||
INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT=
|
3
packages/instant/.gitignore
vendored
3
packages/instant/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
public/instant.js
|
||||
public/instant.js.map
|
||||
umd/*
|
||||
umd/*
|
||||
.env
|
@@ -2,4 +2,5 @@
|
||||
*
|
||||
*/
|
||||
!lib/**/*
|
||||
!umd/**/*
|
||||
!umd/**/*
|
||||
.env
|
13
packages/instant/.production.discharge.json
Normal file
13
packages/instant/.production.discharge.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "instant.0xproject.com",
|
||||
"build_command": "dotenv yarn build --env.discharge_target=production",
|
||||
"upload_directory": "umd",
|
||||
"index_key": "instant.js",
|
||||
"error_key": "404.html",
|
||||
"trailing_slashes": true,
|
||||
"cache": 3600,
|
||||
"aws_profile": "0xproject",
|
||||
"aws_region": "us-east-1",
|
||||
"cdn": true,
|
||||
"dns_configured": true
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "0x-instant-staging",
|
||||
"build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod",
|
||||
"build_command": "dotenv WEBPACK_OUTPUT_PATH=public yarn build --env.discharge_target=staging",
|
||||
"upload_directory": "public",
|
||||
"index_key": "index.html",
|
||||
"error_key": "index.html",
|
||||
"trailing_slashes": true,
|
||||
"cache": 3600,
|
||||
"aws_profile": "default",
|
||||
"aws_profile": "0xproject",
|
||||
"aws_region": "us-east-1",
|
||||
"cdn": false,
|
||||
"dns_configured": true
|
||||
|
@@ -2,39 +2,11 @@
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
yarn add @0x/instant
|
||||
```
|
||||
|
||||
**Import**
|
||||
|
||||
**CommonJS module**
|
||||
|
||||
```typescript
|
||||
import { ZeroExInstant } from '@0x/instant';
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```javascript
|
||||
var ZeroExInstant = require('@0x/instant').ZeroExInstant;
|
||||
```
|
||||
|
||||
If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`:
|
||||
|
||||
```json
|
||||
"compilerOptions": {
|
||||
"typeRoots": ["node_modules/@0x/typescript-typings/types", "node_modules/@types"],
|
||||
}
|
||||
```
|
||||
|
||||
**UMD Module**
|
||||
|
||||
The package is also available as a UMD module named `zeroExInstant`.
|
||||
The package is available as a UMD module named `zeroExInstant` at https://instant.0xproject.com/instant.js.
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script type="text/javascript" src="[zeroExInstantUMDPath]" charset="utf-8"></script>
|
||||
<script type="text/javascript" src="https://instant.0xproject.com/instant.js" charset="utf-8"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="zeroExInstantContainer"></div>
|
||||
@@ -48,23 +20,33 @@ The package is also available as a UMD module named `zeroExInstant`.
|
||||
|
||||
## Deploying
|
||||
|
||||
You can deploy a work-in-progress version of 0x Instant at http://0x-instant-dogfood.s3-website-us-east-1.amazonaws.com for easy sharing.
|
||||
To run any of the following commands you need to configure your `.env` file. There is an example `.env_example` file to show you what values are required.
|
||||
|
||||
To build and deploy the site run
|
||||
You can deploy a work-in-progress version of 0x Instant at http://0x-instant-dogfood.s3-website-us-east-1.amazonaws.com/instant.js for easy sharing.
|
||||
|
||||
To build and deploy the bundle run
|
||||
|
||||
```
|
||||
yarn deploy_dogfood
|
||||
```
|
||||
|
||||
We also have a staging bucket that is to be updated less frequently can be used to share instant externally: http://0x-instant-staging.s3-website-us-east-1.amazonaws.com/
|
||||
We also have a staging bucket that is to be updated less frequently can be used to share a beta version of instant externally: http://0x-instant-staging.s3-website-us-east-1.amazonaws.com/instant.js
|
||||
|
||||
To build and deploy to this bucket, run
|
||||
To build and deploy to this bundle, run
|
||||
|
||||
```
|
||||
yarn deploy_staging
|
||||
```
|
||||
|
||||
**NOTE: On deploying the site, it will say the site is available at a non-existent URL. Please ignore and use the (now updated) URL above.**
|
||||
Finally, we have our live production bundle that is only meant to be updated with stable, polished releases: https://instant.0xproject.com/instant.js
|
||||
|
||||
To build and deploy to this bundle, run
|
||||
|
||||
```
|
||||
yarn deploy_production
|
||||
```
|
||||
|
||||
**NOTE: On deploying the site to staging and dogfood, it will say the site is available at a non-existent URL. Please ignore and use the (now updated) URL above.**
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@@ -6,15 +6,10 @@
|
||||
},
|
||||
"private": true,
|
||||
"description": "0x Instant React Component",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"main": "umd/instant.js",
|
||||
"scripts": {
|
||||
"build": "yarn build:all",
|
||||
"build:all": "run-p build:umd:prod build:commonjs",
|
||||
"build:umd:prod": "webpack --mode production",
|
||||
"build:commonjs": "tsc -b",
|
||||
"build": "webpack --mode production",
|
||||
"build:ci": "yarn build",
|
||||
"watch_without_deps": "tsc -w",
|
||||
"dev": "webpack-dev-server --mode development",
|
||||
"lint": "tslint --format stylish --project .",
|
||||
"test": "jest",
|
||||
@@ -24,6 +19,7 @@
|
||||
"clean": "shx rm -rf lib coverage scripts",
|
||||
"deploy_dogfood": "discharge deploy -c .dogfood.discharge.json",
|
||||
"deploy_staging": "discharge deploy -c .staging.discharge.json",
|
||||
"deploy_production": "discharge deploy -c .production.discharge.json",
|
||||
"manual:postpublish": "yarn build; node ./scripts/postpublish.js"
|
||||
},
|
||||
"config": {
|
||||
@@ -64,6 +60,7 @@
|
||||
"react-redux": "^5.0.7",
|
||||
"redux": "^4.0.0",
|
||||
"redux-devtools-extension": "^2.13.5",
|
||||
"rollbar": "^2.5.0",
|
||||
"styled-components": "^4.0.2",
|
||||
"ts-optchain": "^0.1.1"
|
||||
},
|
||||
@@ -81,6 +78,7 @@
|
||||
"@types/redux": "^3.6.0",
|
||||
"@types/styled-components": "^4.0.1",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"dotenv-cli": "^1.4.0",
|
||||
"enzyme": "^3.6.0",
|
||||
"enzyme-adapter-react-16": "^1.5.0",
|
||||
"ip": "^1.1.5",
|
||||
@@ -88,7 +86,9 @@
|
||||
"make-promises-safe": "^1.1.0",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"nyc": "^11.0.1",
|
||||
"rollbar-sourcemap-webpack-plugin": "^2.4.0",
|
||||
"shx": "^0.2.2",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"svg-react-loader": "^0.4.6",
|
||||
"ts-jest": "^23.10.3",
|
||||
"tslint": "5.11.0",
|
||||
@@ -99,6 +99,6 @@
|
||||
"webpack-dev-server": "^3.1.9"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
"access": "private"
|
||||
}
|
||||
}
|
||||
|
@@ -175,6 +175,7 @@
|
||||
defaultSelectedAssetData: queryParams.getQueryParamValue('defaultSelectedAssetData'),
|
||||
affiliateInfo: affiliateInfoOverride,
|
||||
shouldDisablePushToHistory: !!queryParams.getQueryParamValue('shouldDisablePushToHistory'),
|
||||
walletDisplayName: queryParams.getQueryParamValue('walletDisplayName') || undefined,
|
||||
};
|
||||
return renderOptionsOverrides;
|
||||
};
|
||||
|
7
packages/instant/src/assets/icons/zrx.svg
generated
7
packages/instant/src/assets/icons/zrx.svg
generated
@@ -1,3 +1,6 @@
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.6726 18.5002L22.6787 18.5063L22.625 18.5868C22.641 18.558 22.6569 18.5291 22.6726 18.5002V18.5002H22.6726ZM22.7893 18.5002L21.3866 17.0053L17.9692 12.9131L19.6779 11.0919L17.1249 7.89955L18.8337 6.07835L20.0599 4.74999C20.663 5.26419 21.2058 5.83552 21.6882 6.46394C22.1707 7.09242 22.5828 7.77444 22.9245 8.5101C23.2663 9.2457 23.5309 10.0206 23.7186 10.8347C23.9062 11.6489 24 12.4916 24 13.3628C24 14.3055 23.8928 15.216 23.6784 16.0945C23.4703 16.9468 23.174 17.7487 22.7893 18.5L22.7893 18.5002ZM6.74427 15.3604L8.87512 18.1019L7.20654 19.8795L5.94009 21.2502L5.91999 21.2288C5.3169 20.7291 4.77415 20.1651 4.29169 19.5368C3.80923 18.9086 3.39714 18.2268 3.05539 17.4915C2.71365 16.7562 2.45567 15.9816 2.28144 15.1677C2.09382 14.3539 2 13.5114 2 12.6405C2 11.6981 2.10721 10.7913 2.32164 9.92041C2.53608 9.04943 2.83091 8.24276 3.20615 7.50025L4.61334 8.99943L8.45293 13.54L6.7442 15.3605L6.74427 15.3604ZM7.89849 8.87512L6.12088 7.20654L4.75015 5.94009L4.77157 5.91999C5.27132 5.3169 5.83531 4.77415 6.46352 4.29169C7.09178 3.80923 7.77357 3.39714 8.50886 3.05539C9.2442 2.71365 10.0188 2.45567 10.8326 2.28144C11.6465 2.09382 12.489 2 13.3599 2C14.3023 2 15.209 2.10721 16.08 2.32164C16.9509 2.53608 17.7576 2.83091 18.5001 3.20615L17.0866 4.55302L12.9101 8.0307L11.0896 6.32197L7.89835 8.87496L7.89849 8.87512ZM18.1019 17.1252L19.8795 18.7938L21.2503 20.0602L21.2288 20.0803C20.7291 20.6834 20.1651 21.2262 19.5369 21.7086C18.9086 22.1911 18.2268 22.6032 17.4916 22.9449C16.7562 23.2867 15.9817 23.5447 15.1678 23.7189C14.3539 23.9065 13.5114 24.0003 12.6405 24.0003C11.6981 24.0003 10.7914 23.8931 9.92046 23.6787C9.04952 23.4643 8.2428 23.1694 7.50029 22.7942L8.91381 21.4473L13.54 17.5474L15.3606 19.6782L18.1021 17.1252L18.1019 17.1252Z" fill="white"/>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.62099 13.9044L6.3287 12.1375L4.20565 9.27256L1.50251 5.44771C0.547534 7.07749 0 8.97462 0 11C0 14.3552 1.50251 17.3593 3.8722 19.3768L7.30341 16.9518C6.13632 16.2248 5.19614 15.1662 4.62099 13.9044Z" fill="white"/>
|
||||
<path d="M8.09561 4.62099L9.86251 6.3287L12.7274 4.20565L16.5523 1.50251C14.9225 0.547534 13.0254 0 11 0C7.64475 0 4.64072 1.50251 2.62323 3.8722L5.04816 7.30341C5.77525 6.13632 6.83381 5.19614 8.09561 4.62099Z" fill="white"/>
|
||||
<path d="M15.6713 9.86251L17.7943 12.7274L20.4975 16.5523C21.4525 14.9225 22 13.0254 22 11C22 7.64475 20.4975 4.64072 18.1278 2.62323L14.6966 5.04816C15.8637 5.77525 16.8039 6.83381 17.379 8.09561L15.6713 9.86251Z" fill="white"/>
|
||||
<path d="M19.3768 18.1278L16.9518 14.6966C16.2248 15.8637 15.1662 16.8039 13.9044 17.379L12.1375 15.6713L9.27256 17.7943L5.44771 20.4975C7.07749 21.4525 8.97462 22 11 22C14.3552 22 17.3593 20.4975 19.3768 18.1278Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1017 B |
@@ -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,7 @@ 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 { gasPriceEstimator } from '../util/gas_price_estimator';
|
||||
import { util } from '../util/util';
|
||||
@@ -21,6 +22,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;
|
||||
@@ -36,8 +38,12 @@ export class BuyButton extends React.Component<BuyButtonProps> {
|
||||
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%"
|
||||
@@ -45,7 +51,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
|
||||
isDisabled={shouldDisableButton}
|
||||
fontColor={ColorOption.white}
|
||||
>
|
||||
Buy
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -19,12 +19,12 @@ export interface ERC20TokenSelectorProps {
|
||||
}
|
||||
|
||||
export interface ERC20TokenSelectorState {
|
||||
searchQuery?: string;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> {
|
||||
public state: ERC20TokenSelectorState = {
|
||||
searchQuery: undefined,
|
||||
searchQuery: '',
|
||||
};
|
||||
public render(): React.ReactNode {
|
||||
const { tokens, onTokenSelect } = this.props;
|
||||
@@ -62,10 +62,10 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps>
|
||||
};
|
||||
private readonly _isTokenQueryMatch = (token: ERC20Asset): boolean => {
|
||||
const { searchQuery } = this.state;
|
||||
if (_.isUndefined(searchQuery)) {
|
||||
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);
|
||||
|
@@ -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';
|
||||
@@ -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}
|
||||
|
@@ -107,7 +107,14 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
|
||||
|
||||
private readonly _renderEthAmount = (): React.ReactNode => {
|
||||
return (
|
||||
<Text fontSize="16px" textAlign="right" width="100%" fontColor={ColorOption.white} fontWeight={500}>
|
||||
<Text
|
||||
fontSize="16px"
|
||||
textAlign="right"
|
||||
width="100%"
|
||||
fontColor={ColorOption.white}
|
||||
fontWeight={500}
|
||||
noWrap={true}
|
||||
>
|
||||
{format.ethBaseUnitAmount(
|
||||
this.props.totalEthBaseUnitAmount,
|
||||
4,
|
||||
@@ -119,7 +126,7 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
|
||||
|
||||
private readonly _renderDollarAmount = (): React.ReactNode => {
|
||||
return (
|
||||
<Text fontSize="16px" textAlign="right" width="100%" fontColor={ColorOption.white}>
|
||||
<Text fontSize="16px" textAlign="right" width="100%" fontColor={ColorOption.white} noWrap={true}>
|
||||
{format.ethBaseUnitAmountInUsd(
|
||||
this.props.totalEthBaseUnitAmount,
|
||||
this.props.ethUsdPrice,
|
||||
|
@@ -18,7 +18,7 @@ import { WalletPrompt } from './wallet_prompt';
|
||||
export interface PaymentMethodProps {
|
||||
account: Account;
|
||||
network: Network;
|
||||
walletName: string;
|
||||
walletDisplayName: string;
|
||||
onInstallWalletClick: () => void;
|
||||
onUnlockWalletClick: () => void;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export interface PaymentMethodProps {
|
||||
export class PaymentMethod extends React.Component<PaymentMethodProps> {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<Container padding="20px" width="100%">
|
||||
<Container padding="20px" width="100%" height="133px">
|
||||
<Container marginBottom="12px">
|
||||
<Flex justify="space-between">
|
||||
<Text
|
||||
@@ -62,11 +62,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,8 +83,7 @@ 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
|
||||
@@ -92,7 +91,7 @@ export class PaymentMethod extends React.Component<PaymentMethodProps> {
|
||||
image={<Icon width={13} icon="lock" color={ColorOption.black} />}
|
||||
{...colors}
|
||||
>
|
||||
Please Unlock {this.props.walletName}
|
||||
Please Unlock {this.props.walletDisplayName}
|
||||
</WalletPrompt>
|
||||
);
|
||||
case AccountState.None:
|
||||
|
@@ -98,6 +98,12 @@ export class ScalingInput extends React.Component<ScalingInputProps, ScalingInpu
|
||||
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,
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
);
|
||||
|
@@ -27,7 +27,9 @@ export interface ContainerProps {
|
||||
borderBottom?: string;
|
||||
className?: string;
|
||||
backgroundColor?: ColorOption;
|
||||
rawBackgroundColor?: string;
|
||||
hasBoxShadow?: boolean;
|
||||
isHidden?: boolean;
|
||||
zIndex?: number;
|
||||
whiteSpace?: string;
|
||||
opacity?: number;
|
||||
@@ -38,6 +40,16 @@ export interface ContainerProps {
|
||||
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,12 +77,14 @@ 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 =>
|
||||
|
@@ -10,6 +10,7 @@ export interface InputProps {
|
||||
fontSize?: string;
|
||||
fontColor?: ColorOption;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
|
@@ -33,7 +33,7 @@ export const Overlay =
|
||||
|
||||
Overlay.defaultProps = {
|
||||
zIndex: zIndex.overlayDefault,
|
||||
backgroundColor: generateOverlayBlack(0.6),
|
||||
backgroundColor: generateOverlayBlack(0.9),
|
||||
};
|
||||
|
||||
Overlay.displayName = 'Overlay';
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { darken } from 'polished';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ColorOption, styled } from '../../style/theme';
|
||||
@@ -31,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 >
|
||||
@@ -56,8 +55,7 @@ export const StyledText =
|
||||
${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};` : '')};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -11,10 +11,18 @@ 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, ZeroExInstantBaseConfig } from '../types';
|
||||
import {
|
||||
AccountState,
|
||||
AffiliateInfo,
|
||||
AssetMetaData,
|
||||
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';
|
||||
@@ -50,6 +58,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
|
||||
...defaultState,
|
||||
providerState,
|
||||
network: networkId,
|
||||
walletDisplayName: props.walletDisplayName,
|
||||
selectedAsset: _.isUndefined(props.defaultSelectedAssetData)
|
||||
? undefined
|
||||
: assetUtils.createAssetFromAssetDataOrThrow(
|
||||
@@ -70,6 +79,7 @@ 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);
|
||||
@@ -99,7 +109,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
|
||||
|
@@ -22,6 +22,21 @@ export const HEAP_ANALYTICS_ID = process.env.HEAP_ANALYTICS_ID;
|
||||
export const COINBASE_API_BASE_URL = 'https://api.coinbase.com/v2';
|
||||
export const PROGRESS_STALL_AT_WIDTH = '95%';
|
||||
export const PROGRESS_FINISH_ANIMATION_TIME_MS = 200;
|
||||
export const HOST_DOMAINS = [
|
||||
'0x-instant-staging.s3-website-us-east-1.amazonaws.com',
|
||||
'0x-instant-dogfood.s3-website-us-east-1.amazonaws.com',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'0.0.0.0',
|
||||
'instant.0xproject.com',
|
||||
];
|
||||
export const ROLLBAR_CLIENT_TOKEN = process.env.ROLLBAR_CLIENT_TOKEN;
|
||||
export const ROLLBAR_ENABLED = process.env.ROLLBAR_ENABLED;
|
||||
export const INSTANT_DISCHARGE_TARGET = process.env.INSTANT_DISCHARGE_TARGET as
|
||||
| 'production'
|
||||
| 'dogfood'
|
||||
| 'staging'
|
||||
| undefined;
|
||||
export const COINBASE_WALLET_IOS_APP_STORE_URL = 'https://itunes.apple.com/us/app/coinbase-wallet/id1278383455?mt=8';
|
||||
export const COINBASE_WALLET_ANDROID_APP_STORE_URL = 'https://play.google.com/store/apps/details?id=org.toshi&hl=en';
|
||||
export const COINBASE_WALLET_SITE_URL = 'https://wallet.coinbase.com/';
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
import { Action, actions } from '../redux/actions';
|
||||
import { asyncData } from '../redux/async_data';
|
||||
import { State } from '../redux/reducer';
|
||||
import { Network, Omit, OperatingSystem, ProviderState, StandardSlidingPanelContent } from '../types';
|
||||
import { Network, Omit, OperatingSystem, ProviderState, StandardSlidingPanelContent, WalletSuggestion } from '../types';
|
||||
import { analytics } from '../util/analytics';
|
||||
import { envUtil } from '../util/env';
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ConnectedAccountPaymentMethodProps {}
|
||||
interface ConnectedState {
|
||||
network: Network;
|
||||
providerState: ProviderState;
|
||||
walletDisplayName?: string;
|
||||
}
|
||||
|
||||
interface ConnectedDispatch {
|
||||
@@ -34,6 +35,7 @@ type FinalProps = ConnectedProps & ConnectedAccountPaymentMethodProps;
|
||||
const mapStateToProps = (state: State, _ownProps: ConnectedAccountPaymentMethodProps): ConnectedState => ({
|
||||
network: state.network,
|
||||
providerState: state.providerState,
|
||||
walletDisplayName: state.walletDisplayName,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (
|
||||
@@ -56,27 +58,32 @@ const mergeProps = (
|
||||
...ownProps,
|
||||
network: connectedState.network,
|
||||
account: connectedState.providerState.account,
|
||||
walletName: connectedState.providerState.name,
|
||||
walletDisplayName: connectedState.walletDisplayName || connectedState.providerState.name,
|
||||
onUnlockWalletClick: () => connectedDispatch.unlockWalletAndDispatchToStore(connectedState.providerState),
|
||||
onInstallWalletClick: () => {
|
||||
const isMobile = envUtil.isMobileOperatingSystem();
|
||||
if (!isMobile) {
|
||||
const walletSuggestion: WalletSuggestion = isMobile
|
||||
? WalletSuggestion.CoinbaseWallet
|
||||
: WalletSuggestion.MetaMask;
|
||||
|
||||
analytics.trackInstallWalletClicked(walletSuggestion);
|
||||
if (walletSuggestion === WalletSuggestion.MetaMask) {
|
||||
connectedDispatch.openInstallWalletPanel();
|
||||
return;
|
||||
} else {
|
||||
const operatingSystem = envUtil.getOperatingSystem();
|
||||
let url = COINBASE_WALLET_SITE_URL;
|
||||
switch (operatingSystem) {
|
||||
case OperatingSystem.Android:
|
||||
url = COINBASE_WALLET_ANDROID_APP_STORE_URL;
|
||||
break;
|
||||
case OperatingSystem.iOS:
|
||||
url = COINBASE_WALLET_IOS_APP_STORE_URL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
const operatingSystem = envUtil.getOperatingSystem();
|
||||
let url = COINBASE_WALLET_SITE_URL;
|
||||
switch (operatingSystem) {
|
||||
case OperatingSystem.Android:
|
||||
url = COINBASE_WALLET_ANDROID_APP_STORE_URL;
|
||||
break;
|
||||
case OperatingSystem.iOS:
|
||||
url = COINBASE_WALLET_IOS_APP_STORE_URL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { SlidingError } from '../components/sliding_error';
|
||||
import { Container } from '../components/ui/container';
|
||||
import { Overlay } from '../components/ui/overlay';
|
||||
import { Action } from '../redux/actions';
|
||||
import { State } from '../redux/reducer';
|
||||
@@ -23,7 +24,12 @@ export interface LatestErrorComponentProps {
|
||||
|
||||
export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponentProps> = props => {
|
||||
if (!props.latestErrorMessage) {
|
||||
return <div />;
|
||||
// Render a hidden SlidingError such that instant does not move when a real error is rendered.
|
||||
return (
|
||||
<Container isHidden={true}>
|
||||
<SlidingError animationState="slidIn" icon="😢" message="" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@@ -9,7 +9,8 @@ import { Dispatch } from 'redux';
|
||||
import { BuyOrderStateButtons } from '../components/buy_order_state_buttons';
|
||||
import { Action, actions } from '../redux/actions';
|
||||
import { State } from '../redux/reducer';
|
||||
import { AccountState, AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
|
||||
import { AccountState, AffiliateInfo, Asset, OrderProcessState, ZeroExInstantError } from '../types';
|
||||
import { analytics } from '../util/analytics';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
import { etherscanUtil } from '../util/etherscan';
|
||||
|
||||
@@ -21,6 +22,7 @@ interface ConnectedState {
|
||||
assetBuyer: AssetBuyer;
|
||||
web3Wrapper: Web3Wrapper;
|
||||
affiliateInfo?: AffiliateInfo;
|
||||
selectedAsset?: Asset;
|
||||
onViewTransaction: () => void;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt
|
||||
const account = state.providerState.account;
|
||||
const accountAddress = account.state === AccountState.Ready ? account.address : undefined;
|
||||
const accountEthBalanceInWei = account.state === AccountState.Ready ? account.ethBalanceInWei : undefined;
|
||||
const selectedAsset = state.selectedAsset;
|
||||
return {
|
||||
accountAddress,
|
||||
accountEthBalanceInWei,
|
||||
@@ -48,6 +51,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt
|
||||
web3Wrapper,
|
||||
buyQuote: state.latestBuyQuote,
|
||||
affiliateInfo: state.affiliateInfo,
|
||||
selectedAsset,
|
||||
onViewTransaction: () => {
|
||||
if (
|
||||
state.buyOrderState.processState === OrderProcessState.Processing ||
|
||||
@@ -59,6 +63,8 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt
|
||||
assetBuyer.networkId,
|
||||
);
|
||||
if (etherscanUrl) {
|
||||
analytics.trackTransactionViewed(state.buyOrderState.processState);
|
||||
|
||||
window.open(etherscanUrl, '_blank');
|
||||
return;
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ import { ERC20AssetAmountInput, ERC20AssetAmountInputProps } from '../components
|
||||
import { Action, actions } from '../redux/actions';
|
||||
import { State } from '../redux/reducer';
|
||||
import { ColorOption } from '../style/theme';
|
||||
import { AffiliateInfo, ERC20Asset, Omit, OrderProcessState } from '../types';
|
||||
import { AffiliateInfo, ERC20Asset, Omit, OrderProcessState, QuoteFetchOrigin } from '../types';
|
||||
import { buyQuoteUpdater } from '../util/buy_quote_updater';
|
||||
|
||||
export interface SelectedERC20AssetAmountInputProps {
|
||||
@@ -88,7 +88,7 @@ const mapDispatchToProps = (
|
||||
// even if it's debounced, give them the illusion it's loading
|
||||
dispatch(actions.setQuoteRequestStatePending());
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, {
|
||||
debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, QuoteFetchOrigin.Manual, {
|
||||
setPending: true,
|
||||
dispatchErrors: true,
|
||||
affiliateInfo,
|
||||
|
@@ -83,14 +83,14 @@ export const assetMetaDataMap: ObjectMap<AssetMetaData> = {
|
||||
'0xf47261b0000000000000000000000000e0b7927c4af23765cb51314a0e0521a9645f0e2a': {
|
||||
assetProxyId: AssetProxyId.ERC20,
|
||||
decimals: 9,
|
||||
primaryColor: '#DEB564',
|
||||
primaryColor: '#E1AA3E',
|
||||
symbol: 'dgd',
|
||||
name: 'DigixDao',
|
||||
},
|
||||
'0xf47261b00000000000000000000000004f3afec4e5a3f2a6a1a411def7d7dfe50ee057bf': {
|
||||
assetProxyId: AssetProxyId.ERC20,
|
||||
decimals: 9,
|
||||
primaryColor: '#DEB564',
|
||||
primaryColor: '#E1AA3E',
|
||||
symbol: 'dgx',
|
||||
name: 'Digix Gold Token',
|
||||
},
|
||||
|
@@ -4,6 +4,7 @@ import * as ReactDOM from 'react-dom';
|
||||
|
||||
import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR, INJECTED_DIV_CLASS, INJECTED_DIV_ID } from './constants';
|
||||
import { ZeroExInstantOverlay, ZeroExInstantOverlayProps } from './index';
|
||||
import { analytics } from './util/analytics';
|
||||
import { assert } from './util/assert';
|
||||
import { util } from './util/util';
|
||||
|
||||
@@ -38,6 +39,9 @@ const validateInstantRenderConfig = (config: ZeroExInstantConfig, selector: stri
|
||||
if (!_.isUndefined(config.provider)) {
|
||||
assert.isWeb3Provider('provider', config.provider);
|
||||
}
|
||||
if (!_.isUndefined(config.walletDisplayName)) {
|
||||
assert.isString('walletDisplayName', config.walletDisplayName);
|
||||
}
|
||||
if (!_.isUndefined(config.shouldDisablePushToHistory)) {
|
||||
assert.isBoolean('shouldDisablePushToHistory', config.shouldDisablePushToHistory);
|
||||
}
|
||||
@@ -57,6 +61,7 @@ const renderInstant = (config: ZeroExInstantConfig, selector: string) => {
|
||||
injectedDiv.setAttribute('class', INJECTED_DIV_CLASS);
|
||||
appendTo.appendChild(injectedDiv);
|
||||
const closeInstant = () => {
|
||||
analytics.trackInstantClosed();
|
||||
if (!_.isUndefined(config.onClose)) {
|
||||
config.onClose();
|
||||
}
|
||||
@@ -89,12 +94,12 @@ export const render = (config: ZeroExInstantConfig, selector: string = DEFAULT_Z
|
||||
// If the integrator defined a popstate handler, save it to __zeroExInstantIntegratorsPopStateHandler
|
||||
// unless we have already done so on a previous render.
|
||||
const anyWindow = window as any;
|
||||
if (window.onpopstate && !anyWindow.__zeroExInstantIntegratorsPopStateHandler) {
|
||||
anyWindow.__zeroExInstantIntegratorsPopStateHandler = window.onpopstate.bind(window);
|
||||
}
|
||||
const integratorsOnPopStateHandler = anyWindow.__zeroExInstantIntegratorsPopStateHandler || util.boundNoop;
|
||||
const popStateExistsAndNotSetPreviously = window.onpopstate && !anyWindow.__zeroExInstantIntegratorsPopStateHandler;
|
||||
anyWindow.__zeroExInstantIntegratorsPopStateHandler = popStateExistsAndNotSetPreviously
|
||||
? anyWindow.onpopstate.bind(window)
|
||||
: util.boundNoop;
|
||||
const onPopStateHandler = (e: PopStateEvent) => {
|
||||
integratorsOnPopStateHandler(e);
|
||||
anyWindow.__zeroExInstantIntegratorsPopStateHandler(e);
|
||||
const newState = e.state;
|
||||
if (newState && newState.zeroExInstantShowing) {
|
||||
// We have returned to a history state that expects instant to be rendered.
|
||||
@@ -110,3 +115,7 @@ export const render = (config: ZeroExInstantConfig, selector: string = DEFAULT_Z
|
||||
};
|
||||
window.onpopstate = onPopStateHandler;
|
||||
};
|
||||
|
||||
// Write version info to the exported object for debugging
|
||||
export const GIT_SHA = process.env.GIT_SHA;
|
||||
export const NPM_VERSION = process.env.NPM_PACKAGE_VERSION;
|
||||
|
@@ -3,7 +3,7 @@ import * as _ from 'lodash';
|
||||
import { Middleware } from 'redux';
|
||||
|
||||
import { ETH_DECIMALS } from '../constants';
|
||||
import { Account, AccountState } from '../types';
|
||||
import { AccountState, StandardSlidingPanelContent } from '../types';
|
||||
import { analytics } from '../util/analytics';
|
||||
|
||||
import { Action, ActionTypes } from './actions';
|
||||
@@ -77,6 +77,18 @@ export const analyticsMiddleware: Middleware = store => next => middlewareAction
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ActionTypes.OPEN_STANDARD_SLIDING_PANEL:
|
||||
const openSlidingContent = curState.standardSlidingPanelSettings.content;
|
||||
if (openSlidingContent === StandardSlidingPanelContent.InstallWallet) {
|
||||
analytics.trackInstallWalletModalOpened();
|
||||
}
|
||||
break;
|
||||
case ActionTypes.CLOSE_STANDARD_SLIDING_PANEL:
|
||||
const closeSlidingContent = curState.standardSlidingPanelSettings.content;
|
||||
if (closeSlidingContent === StandardSlidingPanelContent.InstallWallet) {
|
||||
analytics.trackInstallWalletModalClosed();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return nextAction;
|
||||
|
@@ -4,12 +4,13 @@ import * as _ from 'lodash';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { BIG_NUMBER_ZERO } from '../constants';
|
||||
import { AccountState, ERC20Asset, OrderProcessState, ProviderState } from '../types';
|
||||
import { AccountState, ERC20Asset, OrderProcessState, ProviderState, QuoteFetchOrigin } from '../types';
|
||||
import { analytics } from '../util/analytics';
|
||||
import { assetUtils } from '../util/asset';
|
||||
import { buyQuoteUpdater } from '../util/buy_quote_updater';
|
||||
import { coinbaseApi } from '../util/coinbase_api';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
import { errorReporter } from '../util/error_reporter';
|
||||
|
||||
import { actions } from './actions';
|
||||
import { State } from './reducer';
|
||||
@@ -23,6 +24,7 @@ export const asyncData = {
|
||||
const errorMessage = 'Error fetching ETH/USD price';
|
||||
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
|
||||
dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO));
|
||||
errorReporter.report(e);
|
||||
}
|
||||
},
|
||||
fetchAvailableAssetDatasAndDispatchToStore: async (state: State, dispatch: Dispatch) => {
|
||||
@@ -37,6 +39,7 @@ export const asyncData = {
|
||||
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
|
||||
// On error, just specify that none are available
|
||||
dispatch(actions.setAvailableAssets([]));
|
||||
errorReporter.report(e);
|
||||
}
|
||||
},
|
||||
fetchAccountInfoAndDispatchToStore: async (
|
||||
@@ -77,6 +80,7 @@ export const asyncData = {
|
||||
const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address);
|
||||
dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei }));
|
||||
} catch (e) {
|
||||
errorReporter.report(e);
|
||||
// leave balance as is
|
||||
return;
|
||||
}
|
||||
@@ -84,6 +88,7 @@ export const asyncData = {
|
||||
fetchCurrentBuyQuoteAndDispatchToStore: async (
|
||||
state: State,
|
||||
dispatch: Dispatch,
|
||||
fetchOrigin: QuoteFetchOrigin,
|
||||
options: { updateSilently: boolean },
|
||||
) => {
|
||||
const { buyOrderState, providerState, selectedAsset, selectedAssetUnitAmount, affiliateInfo } = state;
|
||||
@@ -99,7 +104,12 @@ export const asyncData = {
|
||||
dispatch,
|
||||
selectedAsset as ERC20Asset,
|
||||
selectedAssetUnitAmount,
|
||||
{ setPending: !options.updateSilently, dispatchErrors: !options.updateSilently, affiliateInfo },
|
||||
fetchOrigin,
|
||||
{
|
||||
setPending: !options.updateSilently,
|
||||
dispatchErrors: !options.updateSilently,
|
||||
affiliateInfo,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@@ -49,6 +49,7 @@ interface OptionalState {
|
||||
latestBuyQuote: BuyQuote;
|
||||
latestErrorMessage: string;
|
||||
affiliateInfo: AffiliateInfo;
|
||||
walletDisplayName: string;
|
||||
}
|
||||
|
||||
export type State = DefaultState & PropsDerivedState & Partial<OptionalState>;
|
||||
|
@@ -1,6 +1,14 @@
|
||||
import * as styledComponents from 'styled-components';
|
||||
|
||||
const { default: styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider } = styledComponents;
|
||||
const {
|
||||
default: styled,
|
||||
css,
|
||||
keyframes,
|
||||
withTheme,
|
||||
createGlobalStyle,
|
||||
ThemeConsumer,
|
||||
ThemeProvider,
|
||||
} = styledComponents;
|
||||
|
||||
export type Theme = { [key in ColorOption]: string };
|
||||
|
||||
@@ -45,4 +53,4 @@ export const generateOverlayBlack = (opacity = 0.6) => {
|
||||
return `rgba(0, 0, 0, ${opacity})`;
|
||||
};
|
||||
|
||||
export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider };
|
||||
export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeConsumer, ThemeProvider };
|
||||
|
@@ -21,6 +21,11 @@ export enum OrderProcessState {
|
||||
Failure = 'FAILURE',
|
||||
}
|
||||
|
||||
export enum QuoteFetchOrigin {
|
||||
Manual = 'Manual',
|
||||
Heartbeat = 'Heartbeat',
|
||||
}
|
||||
|
||||
export interface SimulatedProgress {
|
||||
startTimeUnix: number;
|
||||
expectedEndTimeUnix: number;
|
||||
@@ -149,6 +154,11 @@ export enum Browser {
|
||||
Other = 'OTHER',
|
||||
}
|
||||
|
||||
export enum WalletSuggestion {
|
||||
CoinbaseWallet = 'Coinbase Wallet',
|
||||
MetaMask = 'MetaMask',
|
||||
}
|
||||
|
||||
export enum OperatingSystem {
|
||||
Android = 'ANDROID',
|
||||
iOS = 'IOS',
|
||||
@@ -174,6 +184,7 @@ export interface ZeroExInstantRequiredBaseConfig {
|
||||
|
||||
export interface ZeroExInstantOptionalBaseConfig {
|
||||
provider: Provider;
|
||||
walletDisplayName: string;
|
||||
availableAssetDatas: string[];
|
||||
defaultAssetBuyAmount: number;
|
||||
defaultSelectedAssetData: string;
|
||||
|
@@ -1,7 +1,18 @@
|
||||
import { BuyQuote } from '@0x/asset-buyer';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AffiliateInfo, Asset, Network, OrderSource, ProviderState } from '../types';
|
||||
import { INSTANT_DISCHARGE_TARGET } from '../constants';
|
||||
import {
|
||||
AffiliateInfo,
|
||||
Asset,
|
||||
Network,
|
||||
OrderProcessState,
|
||||
OrderSource,
|
||||
ProviderState,
|
||||
QuoteFetchOrigin,
|
||||
WalletSuggestion,
|
||||
} from '../types';
|
||||
|
||||
import { EventProperties, heapUtil } from './heap';
|
||||
|
||||
@@ -18,6 +29,7 @@ export const evaluateIfEnabled = (fnCall: () => void) => {
|
||||
|
||||
enum EventNames {
|
||||
INSTANT_OPENED = 'Instant - Opened',
|
||||
INSTANT_CLOSED = 'Instant - Closed',
|
||||
ACCOUNT_LOCKED = 'Account - Locked',
|
||||
ACCOUNT_READY = 'Account - Ready',
|
||||
ACCOUNT_UNLOCK_REQUESTED = 'Account - Unlock Requested',
|
||||
@@ -33,10 +45,18 @@ enum EventNames {
|
||||
BUY_TX_SUBMITTED = 'Buy - Tx Submitted',
|
||||
BUY_TX_SUCCEEDED = 'Buy - Tx Succeeded',
|
||||
BUY_TX_FAILED = 'Buy - Tx Failed',
|
||||
INSTALL_WALLET_CLICKED = 'Install Wallet - Clicked',
|
||||
INSTALL_WALLET_MODAL_OPENED = 'Install Wallet - Modal - Opened',
|
||||
INSTALL_WALLET_MODAL_CLICKED_EXPLANATION = 'Install Wallet - Modal - Clicked Explanation',
|
||||
INSTALL_WALLET_MODAL_CLICKED_GET = 'Install Wallet - Modal - Clicked Get',
|
||||
INSTALL_WALLET_MODAL_CLOSED = 'Install Wallet - Modal - Closed',
|
||||
TOKEN_SELECTOR_OPENED = 'Token Selector - Opened',
|
||||
TOKEN_SELECTOR_CLOSED = 'Token Selector - Closed',
|
||||
TOKEN_SELECTOR_CHOSE = 'Token Selector - Chose',
|
||||
TOKEN_SELECTOR_SEARCHED = 'Token Selector - Searched',
|
||||
TRANSACTION_VIEWED = 'Transaction - Viewed',
|
||||
QUOTE_FETCHED = 'Quote - Fetched',
|
||||
QUOTE_ERROR = 'Quote - Error',
|
||||
}
|
||||
|
||||
const track = (eventName: EventNames, eventProperties: EventProperties = {}): void => {
|
||||
@@ -84,6 +104,7 @@ export interface AnalyticsEventOptions {
|
||||
providerName?: string;
|
||||
gitSha?: string;
|
||||
npmVersion?: string;
|
||||
instantEnvironment?: string;
|
||||
orderSource?: string;
|
||||
affiliateAddress?: string;
|
||||
affiliateFeePercent?: number;
|
||||
@@ -129,10 +150,12 @@ export const analytics = {
|
||||
affiliateFeePercent,
|
||||
selectedAssetName: selectedAsset ? selectedAsset.metaData.name : 'none',
|
||||
selectedAssetData: selectedAsset ? selectedAsset.assetData : 'none',
|
||||
instantEnvironment: INSTANT_DISCHARGE_TARGET || `Local ${process.env.NODE_ENV}`,
|
||||
};
|
||||
return eventOptions;
|
||||
},
|
||||
trackInstantOpened: trackingEventFnWithoutPayload(EventNames.INSTANT_OPENED),
|
||||
trackInstantClosed: trackingEventFnWithoutPayload(EventNames.INSTANT_CLOSED),
|
||||
trackAccountLocked: trackingEventFnWithoutPayload(EventNames.ACCOUNT_LOCKED),
|
||||
trackAccountReady: (address: string) => trackingEventFnWithPayload(EventNames.ACCOUNT_READY)({ address }),
|
||||
trackAccountUnlockRequested: trackingEventFnWithoutPayload(EventNames.ACCOUNT_UNLOCK_REQUESTED),
|
||||
@@ -170,6 +193,14 @@ export const analytics = {
|
||||
expectedTxTimeMs: expectedEndTimeUnix - startTimeUnix,
|
||||
actualTxTimeMs: new Date().getTime() - startTimeUnix,
|
||||
}),
|
||||
trackInstallWalletClicked: (walletSuggestion: WalletSuggestion) =>
|
||||
trackingEventFnWithPayload(EventNames.INSTALL_WALLET_CLICKED)({ walletSuggestion }),
|
||||
trackInstallWalletModalClickedExplanation: trackingEventFnWithoutPayload(
|
||||
EventNames.INSTALL_WALLET_MODAL_CLICKED_EXPLANATION,
|
||||
),
|
||||
trackInstallWalletModalClickedGet: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLICKED_GET),
|
||||
trackInstallWalletModalOpened: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_OPENED),
|
||||
trackInstallWalletModalClosed: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLOSED),
|
||||
trackTokenSelectorOpened: trackingEventFnWithoutPayload(EventNames.TOKEN_SELECTOR_OPENED),
|
||||
trackTokenSelectorClosed: (closedVia: TokenSelectorClosedVia) =>
|
||||
trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CLOSED)({ closedVia }),
|
||||
@@ -177,4 +208,18 @@ export const analytics = {
|
||||
trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CHOSE)(payload),
|
||||
trackTokenSelectorSearched: (searchText: string) =>
|
||||
trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_SEARCHED)({ searchText }),
|
||||
trackTransactionViewed: (orderProcesState: OrderProcessState) =>
|
||||
trackingEventFnWithPayload(EventNames.TRANSACTION_VIEWED)({ orderState: orderProcesState }),
|
||||
trackQuoteFetched: (buyQuote: BuyQuote, fetchOrigin: QuoteFetchOrigin) =>
|
||||
trackingEventFnWithPayload(EventNames.QUOTE_FETCHED)({
|
||||
...buyQuoteEventProperties(buyQuote),
|
||||
fetchOrigin,
|
||||
}),
|
||||
trackQuoteError: (errorMessage: string, assetBuyAmount: BigNumber, fetchOrigin: QuoteFetchOrigin) => {
|
||||
trackingEventFnWithPayload(EventNames.QUOTE_ERROR)({
|
||||
errorMessage,
|
||||
assetBuyAmount: assetBuyAmount.toString(),
|
||||
fetchOrigin,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { AssetBuyerError } from '@0x/asset-buyer';
|
||||
import { AssetProxyId, ObjectMap } from '@0x/types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
@@ -106,4 +107,20 @@ export const assetUtils = {
|
||||
);
|
||||
return _.compact(erc20sOrUndefined);
|
||||
},
|
||||
assetBuyerErrorMessage: (asset: ERC20Asset, error: Error): string | undefined => {
|
||||
if (error.message === AssetBuyerError.InsufficientAssetLiquidity) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
|
||||
return `Not enough ${assetName} available`;
|
||||
} else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) {
|
||||
return 'Not enough ZRX available';
|
||||
} else if (
|
||||
error.message === AssetBuyerError.StandardRelayerApiError ||
|
||||
error.message.startsWith(AssetBuyerError.AssetUnavailable)
|
||||
) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
|
||||
return `${assetName} is currently unavailable`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
|
||||
import { AssetBuyer, BuyQuote } from '@0x/asset-buyer';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as _ from 'lodash';
|
||||
@@ -6,9 +6,11 @@ import { Dispatch } from 'redux';
|
||||
import { oc } from 'ts-optchain';
|
||||
|
||||
import { Action, actions } from '../redux/actions';
|
||||
import { AffiliateInfo, ERC20Asset } from '../types';
|
||||
import { AffiliateInfo, ERC20Asset, QuoteFetchOrigin } from '../types';
|
||||
import { analytics } from '../util/analytics';
|
||||
import { assetUtils } from '../util/asset';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
import { errorReporter } from '../util/error_reporter';
|
||||
|
||||
export const buyQuoteUpdater = {
|
||||
updateBuyQuoteAsync: async (
|
||||
@@ -16,7 +18,12 @@ export const buyQuoteUpdater = {
|
||||
dispatch: Dispatch<Action>,
|
||||
asset: ERC20Asset,
|
||||
assetUnitAmount: BigNumber,
|
||||
options: { setPending: boolean; dispatchErrors: boolean; affiliateInfo?: AffiliateInfo },
|
||||
fetchOrigin: QuoteFetchOrigin,
|
||||
options: {
|
||||
setPending: boolean;
|
||||
dispatchErrors: boolean;
|
||||
affiliateInfo?: AffiliateInfo;
|
||||
},
|
||||
): Promise<void> => {
|
||||
// get a new buy quote.
|
||||
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetUnitAmount, asset.metaData.decimals);
|
||||
@@ -29,34 +36,24 @@ export const buyQuoteUpdater = {
|
||||
try {
|
||||
newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage });
|
||||
} catch (error) {
|
||||
const errorMessage = assetUtils.assetBuyerErrorMessage(asset, error);
|
||||
|
||||
if (_.isUndefined(errorMessage)) {
|
||||
// This is an unknown error, report it to rollbar
|
||||
errorReporter.report(error);
|
||||
}
|
||||
|
||||
if (options.dispatchErrors) {
|
||||
dispatch(actions.setQuoteRequestStateFailure());
|
||||
let errorMessage;
|
||||
if (error.message === AssetBuyerError.InsufficientAssetLiquidity) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
|
||||
errorMessage = `Not enough ${assetName} available`;
|
||||
} else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) {
|
||||
errorMessage = 'Not enough ZRX available';
|
||||
} else if (
|
||||
error.message === AssetBuyerError.StandardRelayerApiError ||
|
||||
error.message.startsWith(AssetBuyerError.AssetUnavailable)
|
||||
) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
|
||||
errorMessage = `${assetName} is currently unavailable`;
|
||||
}
|
||||
if (!_.isUndefined(errorMessage)) {
|
||||
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
analytics.trackQuoteError(error.message ? error.message : 'other', baseUnitValue, fetchOrigin);
|
||||
errorFlasher.flashNewErrorMessage(dispatch, errorMessage || 'Error fetching price, please try again');
|
||||
}
|
||||
// TODO: report to error reporter on else
|
||||
|
||||
return;
|
||||
}
|
||||
// We have a successful new buy quote
|
||||
errorFlasher.clearError(dispatch);
|
||||
// invalidate the last buy quote.
|
||||
dispatch(actions.updateLatestBuyQuote(newBuyQuote));
|
||||
analytics.trackQuoteFetched(newBuyQuote, fetchOrigin);
|
||||
},
|
||||
};
|
||||
|
62
packages/instant/src/util/error_reporter.ts
Normal file
62
packages/instant/src/util/error_reporter.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { logUtils } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { HOST_DOMAINS, INSTANT_DISCHARGE_TARGET, ROLLBAR_CLIENT_TOKEN, ROLLBAR_ENABLED } from '../constants';
|
||||
|
||||
// Import version of Rollbar designed for embedded components
|
||||
// See https://docs.rollbar.com/docs/using-rollbarjs-inside-an-embedded-component
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const Rollbar = require('rollbar/dist/rollbar.noconflict.umd');
|
||||
|
||||
let rollbar: any;
|
||||
// Configures rollbar and sets up error catching
|
||||
export const setupRollbar = (): any => {
|
||||
if (_.isUndefined(rollbar) && ROLLBAR_CLIENT_TOKEN && ROLLBAR_ENABLED) {
|
||||
rollbar = new Rollbar({
|
||||
accessToken: ROLLBAR_CLIENT_TOKEN,
|
||||
captureUncaught: true,
|
||||
captureUnhandledRejections: true,
|
||||
enabled: true,
|
||||
itemsPerMinute: 10,
|
||||
maxItems: 500,
|
||||
payload: {
|
||||
environment: INSTANT_DISCHARGE_TARGET || `Local ${process.env.NODE_ENV}`,
|
||||
client: {
|
||||
javascript: {
|
||||
source_map_enabled: true,
|
||||
code_version: process.env.GIT_SHA,
|
||||
guess_uncaught_frames: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
hostWhiteList: HOST_DOMAINS,
|
||||
uncaughtErrorLevel: 'error',
|
||||
ignoredMessages: [
|
||||
// Errors from the third-party scripts
|
||||
'Script error',
|
||||
// Network errors or ad-blockers
|
||||
'TypeError: Failed to fetch',
|
||||
'Exchange has not been deployed to detected network (network/artifact mismatch)',
|
||||
// Source: https://groups.google.com/a/chromium.org/forum/#!topic/chromium-discuss/7VU0_VvC7mE
|
||||
"undefined is not an object (evaluating '__gCrWeb.autofill.extractForms')",
|
||||
// Source: http://stackoverflow.com/questions/43399818/securityerror-from-facebook-and-cross-domain-messaging
|
||||
'SecurityError (DOM Exception 18)',
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const errorReporter = {
|
||||
report(err: Error): void {
|
||||
if (!rollbar) {
|
||||
logUtils.log('Not reporting to rollbar because not configured', err);
|
||||
return;
|
||||
}
|
||||
|
||||
rollbar.error(err, (rollbarErr: Error) => {
|
||||
if (rollbarErr) {
|
||||
logUtils.log(`Error reporting to rollbar, ignoring: ${rollbarErr}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
@@ -7,6 +7,8 @@ import {
|
||||
GWEI_IN_WEI,
|
||||
} from '../constants';
|
||||
|
||||
import { errorReporter } from './error_reporter';
|
||||
|
||||
interface EthGasStationResult {
|
||||
average: number;
|
||||
fastestWait: number;
|
||||
@@ -42,8 +44,9 @@ export class GasPriceEstimator {
|
||||
let fetchedAmount: GasInfo | undefined;
|
||||
try {
|
||||
fetchedAmount = await fetchFastAmountInWeiAsync();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
fetchedAmount = undefined;
|
||||
errorReporter.report(e);
|
||||
}
|
||||
|
||||
if (fetchedAmount) {
|
||||
|
@@ -5,6 +5,7 @@ import * as _ from 'lodash';
|
||||
import { HEAP_ANALYTICS_ID } from '../constants';
|
||||
|
||||
import { AnalyticsEventOptions, AnalyticsUserOptions } from './analytics';
|
||||
import { errorReporter } from './error_reporter';
|
||||
|
||||
export type EventProperties = ObjectMap<string | number>;
|
||||
|
||||
@@ -107,8 +108,8 @@ export const heapUtil = {
|
||||
heapFunctionCall(curHeap);
|
||||
} catch (e) {
|
||||
// We never want analytics to crash our React component
|
||||
// TODO(sk): error reporter here
|
||||
logUtils.log('Analytics error', e);
|
||||
errorReporter.report(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { asyncData } from '../redux/async_data';
|
||||
import { Store } from '../redux/store';
|
||||
import { QuoteFetchOrigin } from '../types';
|
||||
|
||||
import { Heartbeater } from './heartbeater';
|
||||
|
||||
@@ -17,8 +18,13 @@ export const generateAccountHeartbeater = (options: HeartbeatFactoryOptions): He
|
||||
export const generateBuyQuoteHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => {
|
||||
const { store, shouldPerformImmediatelyOnStart } = options;
|
||||
return new Heartbeater(async () => {
|
||||
await asyncData.fetchCurrentBuyQuoteAndDispatchToStore(store.getState(), store.dispatch, {
|
||||
updateSilently: true,
|
||||
});
|
||||
await asyncData.fetchCurrentBuyQuoteAndDispatchToStore(
|
||||
store.getState(),
|
||||
store.dispatch,
|
||||
QuoteFetchOrigin.Heartbeat,
|
||||
{
|
||||
updateSilently: true,
|
||||
},
|
||||
);
|
||||
}, shouldPerformImmediatelyOnStart);
|
||||
};
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { AssetBuyerError } from '@0x/asset-buyer';
|
||||
import { AssetProxyId, ObjectMap } from '@0x/types';
|
||||
|
||||
import { Asset, AssetMetaData, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types';
|
||||
import { Asset, AssetMetaData, ERC20Asset, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types';
|
||||
import { assetUtils } from '../../src/util/asset';
|
||||
|
||||
const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498';
|
||||
@@ -11,7 +12,7 @@ const ZRX_META_DATA: ERC20AssetMetaData = {
|
||||
decimals: 18,
|
||||
name: '0x',
|
||||
};
|
||||
const ZRX_ASSET: Asset = {
|
||||
const ZRX_ASSET: ERC20Asset = {
|
||||
assetData: ZRX_ASSET_DATA,
|
||||
metaData: ZRX_META_DATA,
|
||||
};
|
||||
@@ -45,4 +46,32 @@ describe('assetDataUtil', () => {
|
||||
).toThrowError(ZeroExInstantError.AssetMetaDataNotAvailable);
|
||||
});
|
||||
});
|
||||
describe('assetBuyerErrorMessage', () => {
|
||||
it('should return message for InsufficientAssetLiquidity', () => {
|
||||
const insufficientAssetError = new Error(AssetBuyerError.InsufficientAssetLiquidity);
|
||||
expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, insufficientAssetError)).toEqual(
|
||||
'Not enough ZRX available',
|
||||
);
|
||||
});
|
||||
it('should return message for InsufficientAssetLiquidity', () => {
|
||||
const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity);
|
||||
expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, insufficientZrxError)).toEqual(
|
||||
'Not enough ZRX available',
|
||||
);
|
||||
});
|
||||
it('should message for StandardRelayerApiError', () => {
|
||||
const standardRelayerError = new Error(AssetBuyerError.StandardRelayerApiError);
|
||||
expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, standardRelayerError)).toEqual(
|
||||
'ZRX is currently unavailable',
|
||||
);
|
||||
});
|
||||
it('should return error for AssetUnavailable error', () => {
|
||||
const assetUnavailableError = new Error(
|
||||
`${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET_DATA}`,
|
||||
);
|
||||
expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, assetUnavailableError)).toEqual(
|
||||
'ZRX is currently unavailable',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -5,8 +5,10 @@
|
||||
"rootDir": "src",
|
||||
"jsx": "react",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"composite": false
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./src/index.umd.ts"]
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
|
@@ -1,32 +1,117 @@
|
||||
const childProcess = require('child_process');
|
||||
const ip = require('ip');
|
||||
const path = require('path');
|
||||
const RollbarSourceMapPlugin = require('rollbar-sourcemap-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// The common js bundle (not this one) is built using tsc.
|
||||
// The umd bundle (this one) has a different entrypoint.
|
||||
|
||||
const GIT_SHA = childProcess
|
||||
.execSync('git rev-parse HEAD')
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
const HEAP_PRODUCTION_ENV_VAR_NAME = 'INSTANT_HEAP_ANALYTICS_ID_PRODUCTION';
|
||||
const HEAP_DEVELOPMENT_ENV_VAR_NAME = 'INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT';
|
||||
const getHeapAnalyticsId = modeName => {
|
||||
if (modeName === 'production') {
|
||||
return process.env[HEAP_PRODUCTION_ENV_VAR_NAME];
|
||||
}
|
||||
|
||||
if (modeName === 'development') {
|
||||
return process.env[HEAP_DEVELOPMENT_ENV_VAR_NAME];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const DISCHARGE_TARGETS_THAT_REQUIRED_HEAP = ['production', 'staging', 'dogfood'];
|
||||
const getHeapConfigForDischargeTarget = dischargeTarget => {
|
||||
return {
|
||||
heapAnalyticsIdEnvName:
|
||||
dischargeTarget === 'production'
|
||||
? 'INSTANT_HEAP_ANALYTICS_ID_PRODUCTION'
|
||||
: 'INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT',
|
||||
heapAnalyticsIdRequired: DISCHARGE_TARGETS_THAT_REQUIRED_HEAP.includes(dischargeTarget),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const DISCHARGE_TARGETS_THAT_REQUIRE_ROLLBAR = ['production', 'staging', 'dogfood'];
|
||||
const getRollbarConfigForDischargeTarget = dischargeTarget => {
|
||||
if (DISCHARGE_TARGETS_THAT_REQUIRE_ROLLBAR.includes(dischargeTarget)) {
|
||||
const rollbarSourceMapPublicPath =
|
||||
dischargeTarget === 'production'
|
||||
? 'https://instant.0xproject.com'
|
||||
: `http://0x-instant-${dischargeTarget}.s3-website-us-east-1.amazonaws.com`;
|
||||
|
||||
return {
|
||||
rollbarSourceMapPublicPath,
|
||||
rollbarRequired: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rollbarRequired: false,
|
||||
};
|
||||
};
|
||||
|
||||
const ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME = 'INSTANT_ROLLBAR_CLIENT_TOKEN';
|
||||
const ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME = 'INSTANT_ROLLBAR_PUBLISH_TOKEN';
|
||||
const getRollbarTokens = (dischargeTarget, rollbarRequired) => {
|
||||
const clientToken = process.env[ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME];
|
||||
const publishToken = process.env[ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME];
|
||||
|
||||
if (rollbarRequired) {
|
||||
if (!clientToken) {
|
||||
throw new Error(
|
||||
`Rollbar client token required for ${dischargeTarget}, please set env var ${ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME}`,
|
||||
);
|
||||
}
|
||||
if (!publishToken) {
|
||||
throw new Error(
|
||||
`Rollbar publish token required for ${dischargeTarget}, please set env var ${ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { clientToken, publishToken };
|
||||
};
|
||||
|
||||
const generateConfig = (dischargeTarget, heapConfigOptions, rollbarConfigOptions, nodeEnv) => {
|
||||
const outputPath = process.env.WEBPACK_OUTPUT_PATH || 'umd';
|
||||
|
||||
const { heapAnalyticsIdEnvName, heapAnalyticsIdRequired } = heapConfigOptions;
|
||||
const heapAnalyticsId = process.env[heapAnalyticsIdEnvName];
|
||||
if (heapAnalyticsIdRequired && !heapAnalyticsId) {
|
||||
throw new Error(
|
||||
`Must define heap analytics id in ENV var ${heapAnalyticsIdEnvName} when building for ${dischargeTarget}`,
|
||||
);
|
||||
}
|
||||
|
||||
const rollbarTokens = getRollbarTokens(dischargeTarget, rollbarConfigOptions.rollbarRequired);
|
||||
const rollbarEnabled =
|
||||
rollbarTokens.clientToken && (nodeEnv !== 'development' || process.env.INSTANT_ROLLBAR_FORCE_DEVELOPMENT);
|
||||
|
||||
let rollbarPlugin;
|
||||
if (rollbarConfigOptions.rollbarRequired) {
|
||||
if (!rollbarEnabled || !rollbarTokens.publishToken || !rollbarConfigOptions.rollbarSourceMapPublicPath) {
|
||||
throw new Error(`Rollbar required for ${dischargeTarget} but not configured`);
|
||||
}
|
||||
rollbarPlugin = new RollbarSourceMapPlugin({
|
||||
accessToken: rollbarTokens.publishToken,
|
||||
version: GIT_SHA,
|
||||
publicPath: rollbarConfigOptions.rollbarSourceMapPublicPath,
|
||||
});
|
||||
}
|
||||
|
||||
const envVars = {
|
||||
GIT_SHA: JSON.stringify(GIT_SHA),
|
||||
NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
ROLLBAR_ENABLED: rollbarEnabled,
|
||||
};
|
||||
if (dischargeTarget) {
|
||||
envVars.INSTANT_DISCHARGE_TARGET = JSON.stringify(dischargeTarget);
|
||||
}
|
||||
if (heapAnalyticsId) {
|
||||
envVars.HEAP_ANALYTICS_ID = JSON.stringify(heapAnalyticsId);
|
||||
}
|
||||
if (rollbarTokens.clientToken) {
|
||||
envVars.ROLLBAR_CLIENT_TOKEN = JSON.stringify(rollbarTokens.clientToken);
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': envVars,
|
||||
}),
|
||||
];
|
||||
if (rollbarPlugin) {
|
||||
plugins.push(rollbarPlugin);
|
||||
}
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
instant: './src/index.umd.ts',
|
||||
@@ -37,15 +122,7 @@ module.exports = (env, argv) => {
|
||||
library: 'zeroExInstant',
|
||||
libraryTarget: 'umd',
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
GIT_SHA: JSON.stringify(GIT_SHA),
|
||||
HEAP_ANALYTICS_ID: getHeapAnalyticsId(argv.mode),
|
||||
NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins,
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.ts', '.tsx'],
|
||||
@@ -60,6 +137,15 @@ module.exports = (env, argv) => {
|
||||
test: /\.svg$/,
|
||||
loader: 'svg-react-loader',
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'source-map-loader',
|
||||
exclude: [
|
||||
// instead of /\/node_modules\//
|
||||
path.join(process.cwd(), 'node_modules'),
|
||||
path.join(process.cwd(), '../..', 'node_modules'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
@@ -79,3 +165,10 @@ module.exports = (env, argv) => {
|
||||
};
|
||||
return config;
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const dischargeTarget = env ? env.discharge_target : undefined;
|
||||
const heapConfigOptions = getHeapConfigForDischargeTarget(dischargeTarget);
|
||||
const rollbarConfigOptions = getRollbarConfigForDischargeTarget(dischargeTarget);
|
||||
return generateConfig(dischargeTarget, heapConfigOptions, rollbarConfigOptions, argv.mode);
|
||||
};
|
||||
|
Reference in New Issue
Block a user