Merge pull request #2404 from 0xProject/feature/instant/fortmatic-integration

Formatic integration into instant
This commit is contained in:
David Sun 2020-02-18 13:58:10 -06:00 committed by GitHub
commit 282a351859
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 329 additions and 88 deletions

View File

@ -2,6 +2,8 @@ INSTANT_ROLLBAR_PUBLISH_TOKEN=
INSTANT_ROLLBAR_CLIENT_TOKEN=
INSTANT_HEAP_ANALYTICS_ID_PRODUCTION=
INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT=
INSTANT_FORTMATIC_API_KEY_PRODUCTION=
INSTANT_FORTMATIC_API_KEY_DEVELOPMENT=
INSTANT_INFURA_PROJECT_ID_PRODUCTION=
INSTANT_INFURA_PROJECT_ID_DEVELOPMENT=
# if you want to report to heap or rollbar when building in development mode, you can use the following:

View File

@ -53,6 +53,7 @@
"babel-runtime": "^6.26.0",
"bowser": "^1.9.4",
"copy-to-clipboard": "^3.0.8",
"fortmatic": "^1.0.1",
"lodash": "^4.17.11",
"polished": "^1.9.2",
"react": "^16.5.2",

View File

@ -199,7 +199,7 @@
};
const render = renderOptionsOverrides => {
const renderOptionsDefaults = {
orderSource: 'https://api.0x.org/sra/',
orderSource: 'https://api.0x.org/sra',
onClose: () => {
console.log('0x Instant Closed');
},

View File

@ -0,0 +1,3 @@
<svg width="7" height="13" viewBox="0 0 7 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1.5L6 6.5L1 11.5" stroke="#AAAAAA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.55139 1.67194C5.93755 2.05783 6.32503 2.44262 6.70943 2.83027C7.38729 3.51398 7.38861 4.31146 6.71338 4.9899C6.22904 5.4766 5.74755 5.96616 5.25376 6.44298C5.12328 6.56883 5.11296 6.67249 5.17908 6.83107C5.50329 7.60966 5.97204 8.29908 6.49943 8.94743C7.5595 10.2505 8.75839 11.4071 10.1881 12.3045C10.4943 12.4967 10.833 12.6366 11.152 12.8096C11.3161 12.8986 11.4285 12.8703 11.5599 12.7345C12.0429 12.2353 12.5376 11.7471 13.0312 11.2579C13.6776 10.6175 14.489 10.6162 15.1383 11.2593C15.9317 12.0451 16.7216 12.8347 17.5082 13.6271C18.1681 14.2917 18.1628 15.1039 17.4992 15.7733C17.0509 16.2258 16.5771 16.6552 16.1531 17.1291C15.5361 17.819 14.762 18.0423 13.8766 17.9936C12.5896 17.9229 11.4024 17.4959 10.2591 16.9402C7.71765 15.7052 5.54502 13.9937 3.72758 11.8305C2.38372 10.2312 1.27313 8.4906 0.542776 6.52468C0.189567 5.5739 -0.0630384 4.60006 0.0138415 3.56999C0.0612874 2.93438 0.300055 2.39365 0.768583 1.9478C1.2716 1.46878 1.75089 0.964948 2.24643 0.477807C2.8953 -0.16022 3.70693 -0.158902 4.35734 0.479565C4.75887 0.873582 5.15359 1.27419 5.55139 1.67194Z" fill="#3CB34F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -13,7 +13,7 @@ import * as _ from 'lodash';
import * as React from 'react';
import { oc } from 'ts-optchain';
import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants';
import { DEFAULT_AFFILIATE_INFO, WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants';
import { ColorOption } from '../style/theme';
import { AffiliateInfo, Asset, ZeroExInstantError } from '../types';
import { analytics } from '../util/analytics';
@ -77,7 +77,7 @@ export class BuyButton extends React.PureComponent<BuyButtonProps> {
const {
swapQuote,
swapQuoteConsumer,
affiliateInfo,
affiliateInfo = DEFAULT_AFFILIATE_INFO,
accountAddress,
accountEthBalanceInWei,
web3Wrapper,
@ -132,6 +132,14 @@ export class BuyButton extends React.PureComponent<BuyButtonProps> {
this.props.onSignatureDenied(swapQuote);
return;
}
// Fortmatic specific error handling
if (e.message && e.message.includes('Fortmatic:')) {
if (e.message.includes('User denied transaction.')) {
analytics.trackBuySignatureDenied(swapQuote);
this.props.onSignatureDenied(swapQuote);
return;
}
}
throw e;
}
const startTimeUnix = new Date().getTime();

View File

@ -2,10 +2,11 @@ import * as React from 'react';
export interface CoinbaseWalletLogoProps {
width?: number;
height?: number;
}
export const CoinbaseWalletLogo: React.StatelessComponent<CoinbaseWalletLogoProps> = ({ width }) => (
<svg width={width} viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg">
export const CoinbaseWalletLogo: React.StatelessComponent<CoinbaseWalletLogoProps> = ({ width, height }) => (
<svg width={width} height={height} viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="25.5" cy="25.5" r="25.5" fill="#3263E9" />
<path
fillRule="evenodd"
@ -20,4 +21,5 @@ CoinbaseWalletLogo.displayName = 'CoinbaseWalletLogo';
CoinbaseWalletLogo.defaultProps = {
width: 164,
height: 164,
};

View File

@ -6,7 +6,7 @@ import { oc } from 'ts-optchain';
import { BIG_NUMBER_ZERO, DEFAULT_UNKOWN_ASSET_NAME } from '../constants';
import { ColorOption } from '../style/theme';
import { BaseCurrency } from '../types';
import { Account, AccountState, BaseCurrency } from '../types';
import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder';
@ -25,10 +25,15 @@ export interface OrderDetailsProps {
baseCurrency: BaseCurrency;
onBaseCurrencySwitchEth: () => void;
onBaseCurrencySwitchUsd: () => void;
account: Account;
}
export class OrderDetails extends React.PureComponent<OrderDetailsProps> {
public render(): React.ReactNode {
const shouldShowUsdError = this.props.baseCurrency === BaseCurrency.USD && this._hadErrorFetchingUsdPrice();
const { state } = this.props.account;
if (state !== AccountState.Ready) {
return null;
} else {
return (
<Container width="100%" flexGrow={1} padding="20px 20px 0px 20px">
<Container marginBottom="10px">{this._renderHeader()}</Container>
@ -36,6 +41,7 @@ export class OrderDetails extends React.PureComponent<OrderDetailsProps> {
</Container>
);
}
}
private _renderRows(): React.ReactNode {
const { swapQuoteInfo } = this.props;

View File

@ -1,19 +1,16 @@
import * as _ from 'lodash';
import * as React from 'react';
import PhoneIconSvg from '../assets/icons/phone.svg';
import { ColorOption } from '../style/theme';
import { Account, AccountState, Network } from '../types';
import { Account, AccountState, Network, ProviderType } from '../types';
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';
import { Icon } from './ui/icon';
import { Text } from './ui/text';
import { WalletPrompt } from './wallet_prompt';
export interface PaymentMethodProps {
@ -21,20 +18,20 @@ export interface PaymentMethodProps {
network: Network;
walletDisplayName: string;
onInstallWalletClick: () => void;
onUnlockWalletClick: () => void;
onUnlockWalletClick: (providerType: ProviderType) => void;
}
export class PaymentMethod extends React.PureComponent<PaymentMethodProps> {
public render(): React.ReactNode {
const marginBottom = this.props.account.state !== AccountState.Ready ? '77px' : null;
return (
<Container width="100%" height="120px" padding="20px 20px 0px 20px">
<Container width="100%" height="100%" padding="20px 20px 0px 20px" marginBottom={marginBottom}>
<Container marginBottom="12px">
<Flex justify="space-between">
<SectionHeader>{this._renderTitleText()}</SectionHeader>
{this._renderTitleLabel()}
</Flex>
</Container>
{this._renderMainContent()}
<Container>{this._renderMainContent()}</Container>
</Container>
);
}
@ -50,52 +47,88 @@ export class PaymentMethod extends React.PureComponent<PaymentMethodProps> {
return 'payment method';
}
};
private readonly _renderTitleLabel = (): React.ReactNode => {
const { account } = this.props;
if (account.state === AccountState.Ready || account.state === AccountState.Locked) {
const circleColor: ColorOption = account.state === AccountState.Ready ? ColorOption.green : ColorOption.red;
return (
<Flex align="center">
<Circle diameter={8} color={circleColor} />
<Container marginLeft="5px">
<Text fontColor={ColorOption.darkGrey} fontSize="12px" lineHeight="30px">
{this.props.walletDisplayName}
</Text>
</Container>
</Flex>
);
}
return null;
};
private readonly _renderMainContent = (): React.ReactNode => {
const { account, network } = this.props;
const isMobile = envUtil.isMobileOperatingSystem();
const logo = isMobile ? <CoinbaseWalletLogo width={22} /> : <MetaMaskLogo width={19} height={18} />;
const primaryColor = isMobile ? ColorOption.darkBlue : ColorOption.darkOrange;
const secondaryColor = isMobile ? ColorOption.lightBlue : ColorOption.lightOrange;
const metamaskLogo = <MetaMaskLogo width={23} height={22} />;
const logo = isMobile ? <CoinbaseWalletLogo width={22} height={22} /> : metamaskLogo;
const primaryColor = ColorOption.grey;
const secondaryColor = ColorOption.whiteBackground;
const colors = { primaryColor, secondaryColor };
const onUnlockGenericWallet = () => {
this.props.onUnlockWalletClick(ProviderType.MetaMask);
};
const onUnlockFormatic = () => this.props.onUnlockWalletClick(ProviderType.Fortmatic);
switch (account.state) {
case AccountState.Loading:
return null;
case AccountState.Locked:
return (
<Flex direction="column">
<WalletPrompt
onClick={this.props.onUnlockWalletClick}
onClick={onUnlockGenericWallet}
display="flex"
alignText={'flex-start'}
marginLeft="16px"
fontWeight="normal"
padding="15px 18px"
image={
<Container position="relative" top="2px">
<Icon width={13} icon="lock" color={ColorOption.black} />
<Container position="relative" display="flex">
{logo}
</Container>
}
{...colors}
>
Click to Connect {this.props.walletDisplayName}
{isMobile ? 'Coinbase Wallet' : 'MetaMask'}
</WalletPrompt>
<WalletPrompt
onClick={onUnlockFormatic}
marginTop="14px"
marginLeft="19px"
fontWeight="normal"
padding="15px 18px"
image={
<Container position="relative" display="flex">
<PhoneIconSvg />
</Container>
}
display="flex"
{...colors}
>
Use phone number
</WalletPrompt>
</Flex>
);
case AccountState.None:
return (
<WalletPrompt onClick={this.props.onInstallWalletClick} image={logo} {...colors}>
<Flex direction="column" justify="space-between" height="100%">
<WalletPrompt
onClick={this.props.onInstallWalletClick}
image={logo}
{...colors}
fontWeight="normal"
marginLeft="19px"
padding="15px 18px"
>
{isMobile ? 'Install Coinbase Wallet' : 'Install MetaMask'}
</WalletPrompt>
<WalletPrompt
onClick={onUnlockFormatic}
marginTop="14px"
fontWeight="normal"
marginLeft="19px"
padding="15px 18px"
image={
<Container position="relative" display="flex">
<PhoneIconSvg />
</Container>
}
display="flex"
{...colors}
>
Use phone number
</WalletPrompt>
</Flex>
);
case AccountState.Ready:
return (

View File

@ -75,6 +75,7 @@ export const Container = styled.div<ContainerProps>`
${props => cssRuleIfExists(props, 'opacity')}
${props => cssRuleIfExists(props, 'cursor')}
${props => cssRuleIfExists(props, 'overflow')}
${props => cssRuleIfExists(props, 'alignSelf')}
${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)}

View File

@ -19,6 +19,7 @@ interface IconInfoMapping {
failed: IconInfo;
success: IconInfo;
chevron: IconInfo;
chevronRight: IconInfo;
search: IconInfo;
lock: IconInfo;
}
@ -52,6 +53,14 @@ const ICONS: IconInfoMapping = {
strokeLinecap: 'round',
strokeLinejoin: 'round',
},
chevronRight: {
viewBox: '0 0 7 13',
path: 'M1 1.5L6 6.5L1 11.5',
strokeOpacity: 0.5,
strokeWidth: 1.5,
strokeLinecap: 'round',
strokeLinejoin: 'round',
},
search: {
viewBox: '0 0 14 14',
fillRule: 'evenodd',
@ -89,6 +98,7 @@ const PlainIcon: React.StatelessComponent<IconProps> = props => {
viewBox={iconInfo.viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke={props.stroke}
>
<path
d={iconInfo.path}

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import ChevronRightSvg from '../assets/icons/chevronRight.svg';
import { ColorOption } from '../style/theme';
import { Container } from './ui/container';
@ -7,10 +8,16 @@ import { Flex } from './ui/flex';
import { Text } from './ui/text';
export interface WalletPromptProps {
image: React.ReactNode;
image?: React.ReactNode;
onClick?: () => void;
primaryColor: ColorOption;
secondaryColor: ColorOption;
marginTop?: string;
display?: string;
alignText?: string;
marginLeft?: string;
fontWeight?: string;
padding?: string;
}
export const WalletPrompt: React.StatelessComponent<WalletPromptProps> = ({
@ -19,24 +26,36 @@ export const WalletPrompt: React.StatelessComponent<WalletPromptProps> = ({
children,
secondaryColor,
primaryColor,
marginTop,
display,
alignText,
marginLeft = '10px',
fontWeight = '500',
padding = '10px',
}) => (
<Container
padding="10px"
border={`1px solid ${primaryColor}`}
padding={padding}
border={`1px solid`}
borderColor={ColorOption.feintGrey}
backgroundColor={secondaryColor}
width="100%"
borderRadius="4px"
onClick={onClick}
cursor={onClick ? 'pointer' : undefined}
boxShadowOnHover={!!onClick}
marginTop={marginTop}
display={display}
>
<Flex>
<Flex width="100%">
{image}
<Container marginLeft="10px">
<Text fontSize="16px" fontColor={primaryColor} fontWeight="500">
<Container marginLeft={marginLeft} display={display} width="100%" alignSelf={alignText}>
<Text fontSize="16px" fontColor={primaryColor} fontWeight={fontWeight}>
{children}
</Text>
</Container>
<Container position="relative" top="2px" display={display}>
<ChevronRightSvg />
</Container>
</Flex>
</Container>
);

View File

@ -1,6 +1,6 @@
import { BigNumber } from '@0x/utils';
import { AccountNotReady, AccountState, Network, ProviderType } from './types';
import { AccountNotReady, AccountState, AffiliateInfo, Network, ProviderType } from './types';
// TODO(dave4506) until we have /prices endpoint ready, we will use this whitelist for bridge order liquidity assets
export const SUPPORTED_TOKEN_ASSET_DATA_WITH_BRIDGE_ORDERS = [
@ -91,5 +91,13 @@ export const PROVIDER_TYPE_TO_NAME: { [key in ProviderType]: string } = {
[ProviderType.Parity]: 'Parity',
[ProviderType.TrustWallet]: 'Trust Wallet',
[ProviderType.Opera]: 'Opera Wallet',
[ProviderType.Fortmatic]: 'Fortmatic',
[ProviderType.Fallback]: 'Fallback',
};
export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
export const DEFAULT_AFFILIATE_INFO: AffiliateInfo = {
feeRecipient: NULL_ADDRESS,
feePercentage: 0,
};
export const FORTMATIC_API_KEY = process.env.INSTANT_FORTMATIC_API_KEY;

View File

@ -11,9 +11,18 @@ import {
import { Action, actions } from '../redux/actions';
import { asyncData } from '../redux/async_data';
import { State } from '../redux/reducer';
import { Network, Omit, OperatingSystem, ProviderState, StandardSlidingPanelContent, WalletSuggestion } from '../types';
import {
Network,
Omit,
OperatingSystem,
ProviderState,
ProviderType,
StandardSlidingPanelContent,
WalletSuggestion,
} from '../types';
import { analytics } from '../util/analytics';
import { envUtil } from '../util/env';
import { providerStateFactory } from '../util/provider_state_factory';
export interface ConnectedAccountPaymentMethodProps {}
@ -25,7 +34,7 @@ interface ConnectedState {
interface ConnectedDispatch {
openInstallWalletPanel: () => void;
unlockWalletAndDispatchToStore: (providerState: ProviderState) => void;
unlockWalletAndDispatchToStore: (providerState: ProviderState, providerType: ProviderType) => void;
}
type ConnectedProps = Omit<PaymentMethodProps, keyof ConnectedAccountPaymentMethodProps>;
@ -43,10 +52,17 @@ const mapDispatchToProps = (
ownProps: ConnectedAccountPaymentMethodProps,
): ConnectedDispatch => ({
openInstallWalletPanel: () => dispatch(actions.openStandardSlidingPanel(StandardSlidingPanelContent.InstallWallet)),
unlockWalletAndDispatchToStore: (providerState: ProviderState) => {
unlockWalletAndDispatchToStore: (providerState: ProviderState, providerType: ProviderType) => {
const newProviderState: ProviderState = providerStateFactory.getProviderStateBasedOnProviderType(
providerState,
providerType,
);
// Updates provider state
dispatch(actions.setProviderState(newProviderState));
// Unlocks wallet
analytics.trackAccountUnlockRequested();
// tslint:disable-next-line:no-floating-promises
asyncData.fetchAccountInfoAndDispatchToStore(providerState, dispatch, true);
asyncData.fetchAccountInfoAndDispatchToStore(newProviderState, dispatch, true);
},
});
@ -59,7 +75,8 @@ const mergeProps = (
network: connectedState.network,
account: connectedState.providerState.account,
walletDisplayName: connectedState.providerState.displayName,
onUnlockWalletClick: () => connectedDispatch.unlockWalletAndDispatchToStore(connectedState.providerState),
onUnlockWalletClick: (providerType: ProviderType) =>
connectedDispatch.unlockWalletAndDispatchToStore(connectedState.providerState, providerType),
onInstallWalletClick: () => {
const isMobile = envUtil.isMobileOperatingSystem();
const walletSuggestion: WalletSuggestion = isMobile

View File

@ -22,6 +22,7 @@ const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProp
isLoading: state.quoteRequestState === AsyncProcessState.Pending,
assetName: assetUtils.bestNameForAsset(state.selectedAsset),
baseCurrency: state.baseCurrency,
account: state.providerState.account,
});
interface ConnectedDispatch extends Pick<OrderDetailsProps, DispatchProperties> {}

View File

@ -10,3 +10,4 @@ declare module '*.json' {
export default json;
/* tslint:enable */
}
declare module 'fortmatic';

View File

@ -1,7 +1,14 @@
import { MarketBuySwapQuote } from '@0x/asset-swapper';
import { BigNumber } from '@0x/utils';
import { ActionsUnion, AddressAndEthBalanceInWei, Asset, BaseCurrency, StandardSlidingPanelContent } from '../types';
import {
ActionsUnion,
AddressAndEthBalanceInWei,
Asset,
BaseCurrency,
ProviderState,
StandardSlidingPanelContent,
} from '../types';
export interface PlainAction<T extends string> {
type: T;
@ -23,6 +30,7 @@ export enum ActionTypes {
SetAccountStateLoading = 'SET_ACCOUNT_STATE_LOADING',
SetAccountStateLocked = 'SET_ACCOUNT_STATE_LOCKED',
SetAccountStateReady = 'SET_ACCOUNT_STATE_READY',
SetAccountStateNone = 'SET_ACCOUNT_STATE_NONE',
UpdateAccountEthBalance = 'UPDATE_ACCOUNT_ETH_BALANCE',
UpdateEthUsdPrice = 'UPDATE_ETH_USD_PRICE',
UpdateSelectedAssetUnitAmount = 'UPDATE_SELECTED_ASSET_UNIT_AMOUNT',
@ -43,11 +51,13 @@ export enum ActionTypes {
OpenStandardSlidingPanel = 'OPEN_STANDARD_SLIDING_PANEL',
CloseStandardSlidingPanel = 'CLOSE_STANDARD_SLIDING_PANEL',
UpdateBaseCurrency = 'UPDATE_BASE_CURRENCY',
SetProviderState = 'SET_PROVIDER_STATE',
}
export const actions = {
setAccountStateLoading: () => createAction(ActionTypes.SetAccountStateLoading),
setAccountStateLocked: () => createAction(ActionTypes.SetAccountStateLocked),
setAccountStateNone: () => createAction(ActionTypes.SetAccountStateNone),
setAccountStateReady: (address: string) => createAction(ActionTypes.SetAccountStateReady, address),
updateAccountEthBalance: (addressAndBalance: AddressAndEthBalanceInWei) =>
createAction(ActionTypes.UpdateAccountEthBalance, addressAndBalance),
@ -73,4 +83,5 @@ export const actions = {
createAction(ActionTypes.OpenStandardSlidingPanel, content),
closeStandardSlidingPanel: () => createAction(ActionTypes.CloseStandardSlidingPanel),
updateBaseCurrency: (baseCurrency: BaseCurrency) => createAction(ActionTypes.UpdateBaseCurrency, baseCurrency),
setProviderState: (providerState: ProviderState) => createAction(ActionTypes.SetProviderState, providerState),
};

View File

@ -9,6 +9,7 @@ import { assetUtils } from '../util/asset';
import { coinbaseApi } from '../util/coinbase_api';
import { errorFlasher } from '../util/error_flasher';
import { errorReporter } from '../util/error_reporter';
import { providerStateFactory } from '../util/provider_state_factory';
import { swapQuoteUpdater } from '../util/swap_quote_updater';
import { actions } from './actions';
@ -59,24 +60,39 @@ export const asyncData = {
providerState: ProviderState,
dispatch: Dispatch,
shouldAttemptUnlock: boolean = false,
shouldSetToLoading: boolean = false,
) => {
const web3Wrapper = providerState.web3Wrapper;
const provider = providerState.provider;
if (shouldSetToLoading && providerState.account.state !== AccountState.Loading) {
let availableAddresses: string[] = [];
if (shouldAttemptUnlock && providerState.account.state !== AccountState.Loading) {
dispatch(actions.setAccountStateLoading());
}
let availableAddresses: string[];
try {
// HACK: Fortmatic's getAvailableAddressesAsync behaves in ways that default wallet behavior can't handle
if ((provider as any).isFortmatic) {
availableAddresses =
(provider as any).isLoggedIn || shouldAttemptUnlock
? await web3Wrapper.getAvailableAddressesAsync()
: [];
} else {
// TODO(bmillman): Add support at the web3Wrapper level for calling `eth_requestAccounts` instead of calling enable here
const isPrivacyModeEnabled = (provider as any).enable !== undefined;
availableAddresses =
isPrivacyModeEnabled && shouldAttemptUnlock
? await (provider as any).enable()
: await web3Wrapper.getAvailableAddressesAsync();
}
} catch (e) {
analytics.trackAccountUnlockDenied();
if (e.message.includes('Fortmatic: User denied account access.')) {
// If Fortmatic is not used, revert to injected provider
const initialProviderState = providerStateFactory.getInitialProviderStateWithCurrentProviderState(
providerState,
);
dispatch(actions.setProviderState(initialProviderState));
} else {
dispatch(actions.setAccountStateLocked());
}
return;
}
if (!_.isEmpty(availableAddresses)) {
@ -84,7 +100,7 @@ export const asyncData = {
dispatch(actions.setAccountStateReady(activeAddress));
// tslint:disable-next-line:no-floating-promises
asyncData.fetchAccountBalanceAndDispatchToStore(activeAddress, providerState.web3Wrapper, dispatch);
} else {
} else if (providerState.account.state !== AccountState.Loading) {
dispatch(actions.setAccountStateLocked());
}
},

View File

@ -4,7 +4,7 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import { LOADING_ACCOUNT, LOCKED_ACCOUNT } from '../constants';
import { LOADING_ACCOUNT, LOCKED_ACCOUNT, NO_ACCOUNT } from '../constants';
import { assetMetaDataMap } from '../data/asset_meta_data_map';
import {
Account,
@ -77,6 +77,8 @@ export const createReducer = (initialState: State) => {
return reduceStateWithAccount(state, LOADING_ACCOUNT);
case ActionTypes.SetAccountStateLocked:
return reduceStateWithAccount(state, LOCKED_ACCOUNT);
case ActionTypes.SetAccountStateNone:
return reduceStateWithAccount(state, NO_ACCOUNT);
case ActionTypes.SetAccountStateReady: {
const address = action.data;
let newAccount: AccountReady = {
@ -252,6 +254,11 @@ export const createReducer = (initialState: State) => {
...state,
baseCurrency: action.data,
};
case ActionTypes.SetProviderState:
return {
...state,
providerState: action.data,
};
default:
return state;
}
@ -291,7 +298,6 @@ const doesSwapQuoteMatchState = (swapQuote: MarketBuySwapQuote, state: State): b
selectedAssetMetaData.decimals,
);
const doesAssetAmountMatch = selectedAssetAmountBaseUnits.eq(swapQuote.makerAssetFillAmount);
return doesAssetAmountMatch;
} else {
return true;

View File

@ -27,6 +27,7 @@ export enum ColorOption {
red = 'red',
darkBlue = 'darkBlue',
lightBlue = 'lightBlue',
whiteBackground = 'whiteBackground',
}
export const theme: Theme = {
@ -44,6 +45,7 @@ export const theme: Theme = {
red: '#D00000',
darkBlue: '#135df6',
lightBlue: '#F2F7FF',
whiteBackground: '#FFFFFF',
};
export const transparentWhite = 'rgba(255,255,255,0.3)';

View File

@ -114,10 +114,12 @@ export interface ProviderState {
swapQuoteConsumer: SwapQuoteConsumer;
web3Wrapper: Web3Wrapper;
account: Account;
orderSource: OrderSource;
isProviderInjected: boolean;
}
export enum AccountState {
None = 'NONE,',
None = 'NONE',
Loading = 'LOADING',
Ready = 'READY',
Locked = 'LOCKED',
@ -185,6 +187,7 @@ export enum ProviderType {
Cipher = 'CIPHER',
TrustWallet = 'TRUST_WALLET',
Opera = 'OPERA',
Fortmatic = 'Fortmatic',
Fallback = 'FALLBACK',
}

View File

@ -51,6 +51,8 @@ export const envUtil = {
return ProviderType.Parity;
} else if (anyProvider.isMetaMask) {
return ProviderType.MetaMask;
} else if (anyProvider.isFortmatic) {
return ProviderType.Fortmatic;
} else if (_.get(window, 'SOFA') !== undefined) {
return ProviderType.CoinbaseWallet;
} else if (_.get(window, '__CIPHER__') !== undefined) {
@ -58,7 +60,7 @@ export const envUtil = {
} else if (envUtil.getBrowser() === Browser.Opera && !anyProvider.isMetaMask) {
return ProviderType.Opera;
}
return;
return undefined;
},
getProviderName(provider: ZeroExProvider): string {
const providerTypeIfExists = envUtil.getProviderType(provider);

View File

@ -1,10 +1,10 @@
import { providerUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import * as Fortmatic from 'fortmatic';
import { LOADING_ACCOUNT, NO_ACCOUNT } from '../constants';
import { Maybe, Network, OrderSource, ProviderState } from '../types';
import { FORTMATIC_API_KEY, LOCKED_ACCOUNT, NO_ACCOUNT } from '../constants';
import { Maybe, Network, OrderSource, ProviderState, ProviderType } from '../types';
import { envUtil } from '../util/env';
import { assetSwapperFactory } from './asset_swapper_factory';
@ -50,7 +50,9 @@ export const providerStateFactory = {
web3Wrapper: new Web3Wrapper(provider),
swapQuoter: assetSwapperFactory.getSwapQuoter(provider, orderSource, network),
swapQuoteConsumer: assetSwapperFactory.getSwapQuoteConsumer(provider, network),
account: LOADING_ACCOUNT,
account: LOCKED_ACCOUNT,
orderSource,
isProviderInjected: false,
};
return providerState;
},
@ -68,7 +70,9 @@ export const providerStateFactory = {
web3Wrapper: new Web3Wrapper(injectedProviderIfExists),
swapQuoter: assetSwapperFactory.getSwapQuoter(injectedProviderIfExists, orderSource, network),
swapQuoteConsumer: assetSwapperFactory.getSwapQuoteConsumer(injectedProviderIfExists, network),
account: LOADING_ACCOUNT,
account: LOCKED_ACCOUNT,
orderSource,
isProviderInjected: true,
};
return providerState;
} else {
@ -89,7 +93,66 @@ export const providerStateFactory = {
swapQuoter: assetSwapperFactory.getSwapQuoter(provider, orderSource, network),
swapQuoteConsumer: assetSwapperFactory.getSwapQuoteConsumer(provider, network),
account: NO_ACCOUNT,
orderSource,
isProviderInjected: true,
};
return providerState;
},
// function to call getInitialProviderState with parameters retreived from a provided ProviderState
getInitialProviderStateWithCurrentProviderState: (currentProviderState: ProviderState): ProviderState => {
const orderSource = currentProviderState.orderSource;
const chainId = currentProviderState.swapQuoter.chainId;
// If provider is provided to instant, use that and the displayName
if (!currentProviderState.isProviderInjected) {
return providerStateFactory.getInitialProviderState(
orderSource,
chainId,
currentProviderState.provider,
currentProviderState.displayName,
);
}
const newProviderState = providerStateFactory.getInitialProviderState(orderSource, chainId);
newProviderState.account = LOCKED_ACCOUNT;
return newProviderState;
},
getProviderStateBasedOnProviderType: (
currentProviderState: ProviderState,
providerType: ProviderType,
): ProviderState => {
const chainId = currentProviderState.swapQuoter.chainId;
const orderSource = currentProviderState.orderSource;
// Returns current provider if the provider type selected is not found
if (providerType === ProviderType.MetaMask) {
const provider = providerFactory.getInjectedProviderIfExists();
if (provider) {
return {
displayName: envUtil.getProviderDisplayName(provider),
name: envUtil.getProviderName(provider),
provider,
web3Wrapper: new Web3Wrapper(provider),
swapQuoter: assetSwapperFactory.getSwapQuoter(provider, orderSource, chainId),
swapQuoteConsumer: assetSwapperFactory.getSwapQuoteConsumer(provider, chainId),
account: LOCKED_ACCOUNT,
orderSource,
isProviderInjected: true,
};
}
}
if (providerType === ProviderType.Fortmatic) {
const fm = new Fortmatic(FORTMATIC_API_KEY);
const fmProvider = fm.getProvider();
return {
displayName: envUtil.getProviderDisplayName(fmProvider),
name: envUtil.getProviderName(fmProvider),
provider: fmProvider,
web3Wrapper: new Web3Wrapper(fmProvider),
swapQuoter: assetSwapperFactory.getSwapQuoter(fmProvider, orderSource, chainId),
swapQuoteConsumer: assetSwapperFactory.getSwapQuoteConsumer(fmProvider, chainId),
account: LOCKED_ACCOUNT,
orderSource,
isProviderInjected: true,
};
}
return providerStateFactory.getInitialProviderState(orderSource, chainId);
},
};

View File

@ -94,11 +94,17 @@ const generateConfig = (dischargeTarget, heapConfigOptions, rollbarConfigOptions
? process.env.INSTANT_INFURA_PROJECT_ID_PRODUCTION
: process.env.INSTANT_INFURA_PROJECT_ID_DEVELOPMENT;
const fortmaticApiKey =
dischargeTarget === 'production'
? process.env.INSTANT_FORTMATIC_API_KEY_PRODUCTION
: process.env.INSTANT_FORTMATIC_API_KEY_DEVELOPMENT;
const envVars = {
GIT_SHA: JSON.stringify(GIT_SHA),
NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version),
ROLLBAR_ENABLED: rollbarEnabled,
HEAP_ENABLED: heapEnabled,
INSTANT_FORTMATIC_API_KEY: JSON.stringify(fortmaticApiKey),
INSTANT_INFURA_PROJECT_ID: JSON.stringify(infuraProjectId),
};
if (dischargeTarget) {

View File

@ -984,6 +984,12 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/runtime@7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.1.5", "@babel/runtime@^7.3.1":
version "7.5.5"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
@ -7759,6 +7765,13 @@ format-util@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/format-util/-/format-util-1.0.3.tgz#032dca4a116262a12c43f4c3ec8566416c5b2d95"
fortmatic@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fortmatic/-/fortmatic-1.0.1.tgz#ecb2c6777cd25658befe5e86c5eeddcb6c4db472"
integrity sha512-D48g0talOofK6AdwppO2VL/rRjLZb69Qf6fBR1lYUZ2rqMCM8WbBqoEDgFgJyjO1YV7XCJGbm+mW0/y2RxxbEg==
dependencies:
"@babel/runtime" "7.3.4"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -14102,6 +14115,10 @@ regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
regenerator-runtime@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"