Merge pull request #644 from 0xProject/feature/website/refactor-token-state-fetching
Move TokenState fetching logic up into Portal.tsx so it can be used by Wallet and PortalOnboardingFlow
This commit is contained in:
@@ -506,7 +506,7 @@ export class Blockchain {
|
||||
public async getTokenBalanceAndAllowanceAsync(
|
||||
ownerAddressIfExists: string,
|
||||
tokenAddress: string,
|
||||
): Promise<BigNumber[]> {
|
||||
): Promise<[BigNumber, BigNumber]> {
|
||||
utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.');
|
||||
|
||||
if (_.isUndefined(ownerAddressIfExists)) {
|
||||
@@ -516,8 +516,10 @@ export class Blockchain {
|
||||
let balance = new BigNumber(0);
|
||||
let allowance = new BigNumber(0);
|
||||
if (this._doesUserAddressExist()) {
|
||||
balance = await this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists);
|
||||
allowance = await this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists);
|
||||
[balance, allowance] = await Promise.all([
|
||||
this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists),
|
||||
this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists),
|
||||
]);
|
||||
}
|
||||
return [balance, allowance];
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow';
|
||||
import { ProviderType, TokenByAddress } from 'ts/types';
|
||||
import { ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
export interface PortalOnboardingFlowProps {
|
||||
@@ -16,6 +16,7 @@ export interface PortalOnboardingFlowProps {
|
||||
blockchainIsLoaded: boolean;
|
||||
userEtherBalanceInWei?: BigNumber;
|
||||
tokenByAddress: TokenByAddress;
|
||||
trackedTokenStateByAddress: TokenStateByAddress;
|
||||
updateIsRunning: (isRunning: boolean) => void;
|
||||
updateOnboardingStep: (stepIndex: number) => void;
|
||||
}
|
||||
@@ -68,13 +69,13 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
|
||||
target: '.eth-row',
|
||||
content: 'Before you begin you will need to send some ETH to your metamask wallet',
|
||||
placement: 'right',
|
||||
continueButtonDisplay: this._userHasEth() ? 'enabled' : 'disabled',
|
||||
continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled',
|
||||
},
|
||||
{
|
||||
target: '.weth-row',
|
||||
content: 'You need to convert some of your ETH into tradeable Wrapped ETH (WETH)',
|
||||
placement: 'right',
|
||||
continueButtonDisplay: this._userHasWeth() ? 'enabled' : 'disabled',
|
||||
continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled',
|
||||
},
|
||||
];
|
||||
return steps;
|
||||
@@ -84,13 +85,17 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
|
||||
return !_.isEmpty(this.props.userAddress);
|
||||
}
|
||||
|
||||
private _userHasEth(): boolean {
|
||||
private _userHasVisibleEth(): boolean {
|
||||
return this.props.userEtherBalanceInWei > new BigNumber(0);
|
||||
}
|
||||
|
||||
private _userHasWeth(): boolean {
|
||||
// TODO: https://app.asana.com/0/681385331277907/690722374136933
|
||||
return false;
|
||||
private _userHasVisibleWeth(): boolean {
|
||||
const ethToken = utils.getEthToken(this.props.tokenByAddress);
|
||||
if (!ethToken) {
|
||||
return false;
|
||||
}
|
||||
const wethTokenState = this.props.trackedTokenStateByAddress[ethToken.address];
|
||||
return wethTokenState.balance > new BigNumber(0);
|
||||
}
|
||||
|
||||
private _overrideOnboardingStateIfShould(): void {
|
||||
|
@@ -37,7 +37,9 @@ import {
|
||||
Order,
|
||||
ProviderType,
|
||||
ScreenWidths,
|
||||
Token,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
TokenVisibility,
|
||||
WebsitePaths,
|
||||
} from 'ts/types';
|
||||
@@ -77,6 +79,7 @@ interface PortalState {
|
||||
isDisclaimerDialogOpen: boolean;
|
||||
isLedgerDialogOpen: boolean;
|
||||
tokenManagementState: TokenManagementState;
|
||||
trackedTokenStateByAddress: TokenStateByAddress;
|
||||
}
|
||||
|
||||
interface AccountManagementItem {
|
||||
@@ -127,6 +130,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER);
|
||||
const hasAcceptedDisclaimer =
|
||||
!_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer);
|
||||
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(
|
||||
this._getCurrentTrackedTokens(),
|
||||
);
|
||||
this.state = {
|
||||
prevNetworkId: this.props.networkId,
|
||||
prevNodeVersion: this.props.nodeVersion,
|
||||
@@ -135,6 +141,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
|
||||
tokenManagementState: TokenManagementState.None,
|
||||
isLedgerDialogOpen: false,
|
||||
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
|
||||
};
|
||||
}
|
||||
public componentDidMount(): void {
|
||||
@@ -143,6 +150,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
}
|
||||
public componentWillMount(): void {
|
||||
this._blockchain = new Blockchain(this.props.dispatcher);
|
||||
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
|
||||
}
|
||||
public componentWillUnmount(): void {
|
||||
this._blockchain.destroy();
|
||||
@@ -178,6 +188,39 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
prevPathname: nextProps.location.pathname,
|
||||
});
|
||||
}
|
||||
if (
|
||||
nextProps.userAddress !== this.props.userAddress ||
|
||||
nextProps.networkId !== this.props.networkId ||
|
||||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
|
||||
) {
|
||||
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
|
||||
}
|
||||
|
||||
const nextTrackedTokens = this._getTrackedTokens(nextProps.tokenByAddress);
|
||||
const trackedTokens = this._getCurrentTrackedTokens();
|
||||
|
||||
if (!_.isEqual(nextTrackedTokens, trackedTokens)) {
|
||||
const newTokens = _.difference(nextTrackedTokens, trackedTokens);
|
||||
const newTokenAddresses = _.map(newTokens, token => token.address);
|
||||
// Add placeholder entry for this token to the state, since fetching the
|
||||
// balance/allowance is asynchronous
|
||||
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
|
||||
for (const tokenAddress of newTokenAddresses) {
|
||||
trackedTokenStateByAddress[tokenAddress] = {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
isLoaded: false,
|
||||
};
|
||||
}
|
||||
this.setState({
|
||||
trackedTokenStateByAddress,
|
||||
});
|
||||
// Fetch the actual balance/allowance.
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
|
||||
}
|
||||
}
|
||||
public render(): React.ReactNode {
|
||||
const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind(
|
||||
@@ -190,7 +233,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
: TokenVisibility.TRACKED;
|
||||
return (
|
||||
<div style={styles.root}>
|
||||
<PortalOnboardingFlow />
|
||||
<PortalOnboardingFlow trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} />
|
||||
<DocumentTitle title="0x Portal DApp" />
|
||||
<TopBar
|
||||
userAddress={this.props.userAddress}
|
||||
@@ -283,8 +326,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
);
|
||||
}
|
||||
private _renderWallet(): React.ReactNode {
|
||||
const allTokens = _.values(this.props.tokenByAddress);
|
||||
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
||||
return (
|
||||
<div>
|
||||
<Wallet
|
||||
@@ -295,16 +336,18 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
blockchainErr={this.props.blockchainErr}
|
||||
dispatcher={this.props.dispatcher}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
trackedTokens={trackedTokens}
|
||||
trackedTokens={this._getCurrentTrackedTokens()}
|
||||
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
injectedProviderName={this.props.injectedProviderName}
|
||||
providerType={this.props.providerType}
|
||||
screenWidth={this.props.screenWidth}
|
||||
location={this.props.location}
|
||||
trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
|
||||
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
|
||||
onAddToken={this._onAddToken.bind(this)}
|
||||
onRemoveToken={this._onRemoveToken.bind(this)}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
|
||||
/>
|
||||
<Container marginTop="15px">
|
||||
<Island>
|
||||
@@ -424,8 +467,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
);
|
||||
}
|
||||
private _renderTokenBalances(): React.ReactNode {
|
||||
const allTokens = _.values(this.props.tokenByAddress);
|
||||
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
||||
return (
|
||||
<TokenBalances
|
||||
blockchain={this._blockchain}
|
||||
@@ -434,7 +475,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
dispatcher={this.props.dispatcher}
|
||||
screenWidth={this.props.screenWidth}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
trackedTokens={trackedTokens}
|
||||
trackedTokens={this._getCurrentTrackedTokens()}
|
||||
userAddress={this.props.userAddress}
|
||||
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
||||
networkId={this.props.networkId}
|
||||
@@ -515,6 +556,55 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
|
||||
return isSmallScreen;
|
||||
}
|
||||
|
||||
private _getCurrentTrackedTokens(): Token[] {
|
||||
return this._getTrackedTokens(this.props.tokenByAddress);
|
||||
}
|
||||
|
||||
private _getTrackedTokens(tokenByAddress: TokenByAddress): Token[] {
|
||||
const allTokens = _.values(tokenByAddress);
|
||||
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
||||
return trackedTokens;
|
||||
}
|
||||
|
||||
private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress {
|
||||
const trackedTokenStateByAddress: TokenStateByAddress = {};
|
||||
_.each(trackedTokens, token => {
|
||||
trackedTokenStateByAddress[token.address] = {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
isLoaded: false,
|
||||
};
|
||||
});
|
||||
return trackedTokenStateByAddress;
|
||||
}
|
||||
|
||||
private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> {
|
||||
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
|
||||
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
|
||||
const balancesAndAllowances = await Promise.all(
|
||||
tokenAddresses.map(async tokenAddress => {
|
||||
return this._blockchain.getTokenBalanceAndAllowanceAsync(userAddressIfExists, tokenAddress);
|
||||
}),
|
||||
);
|
||||
for (let i = 0; i < tokenAddresses.length; i++) {
|
||||
// Order is preserved in Promise.all
|
||||
const [balance, allowance] = balancesAndAllowances[i];
|
||||
const tokenAddress = tokenAddresses[i];
|
||||
trackedTokenStateByAddress[tokenAddress] = {
|
||||
balance,
|
||||
allowance,
|
||||
isLoaded: true,
|
||||
};
|
||||
}
|
||||
this.setState({
|
||||
trackedTokenStateByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
|
||||
await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
|
||||
}
|
||||
}
|
||||
|
||||
interface LargeLayoutProps {
|
||||
|
@@ -67,13 +67,14 @@ export interface WalletProps {
|
||||
providerType: ProviderType;
|
||||
screenWidth: ScreenWidths;
|
||||
location: Location;
|
||||
trackedTokenStateByAddress: TokenStateByAddress;
|
||||
onToggleLedgerDialog: () => void;
|
||||
onAddToken: () => void;
|
||||
onRemoveToken: () => void;
|
||||
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface WalletState {
|
||||
trackedTokenStateByAddress: TokenStateByAddress;
|
||||
wrappedEtherDirection?: Side;
|
||||
isHoveringSidebar: boolean;
|
||||
}
|
||||
@@ -151,48 +152,12 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
constructor(props: WalletProps) {
|
||||
super(props);
|
||||
this._isUnmounted = false;
|
||||
const trackedTokenAddresses = _.map(props.trackedTokens, token => token.address);
|
||||
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(trackedTokenAddresses);
|
||||
this.state = {
|
||||
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
|
||||
wrappedEtherDirection: undefined,
|
||||
isHoveringSidebar: false,
|
||||
};
|
||||
}
|
||||
public componentWillMount(): void {
|
||||
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
|
||||
}
|
||||
public componentWillUnmount(): void {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: WalletProps): void {
|
||||
if (
|
||||
nextProps.userAddress !== this.props.userAddress ||
|
||||
nextProps.networkId !== this.props.networkId ||
|
||||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
|
||||
) {
|
||||
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
|
||||
}
|
||||
if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) {
|
||||
const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens);
|
||||
const newTokenAddresses = _.map(newTokens, token => token.address);
|
||||
// Add placeholder entry for this token to the state, since fetching the
|
||||
// balance/allowance is asynchronous
|
||||
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
|
||||
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(newTokenAddresses);
|
||||
_.assign(trackedTokenStateByAddress, initialTrackedTokenStateByAddress);
|
||||
this.setState({
|
||||
trackedTokenStateByAddress,
|
||||
});
|
||||
// Fetch the actual balance/allowance.
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
|
||||
return (
|
||||
@@ -342,7 +307,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
_.isUndefined(this.props.userEtherBalanceInWei),
|
||||
);
|
||||
const etherToken = this._getEthToken();
|
||||
const etherTokenState = this.state.trackedTokenStateByAddress[etherToken.address];
|
||||
const etherTokenState = this.props.trackedTokenStateByAddress[etherToken.address];
|
||||
const etherPrice = etherTokenState.price;
|
||||
const secondaryText = this._renderValue(
|
||||
this.props.userEtherBalanceInWei || new BigNumber(0),
|
||||
@@ -366,7 +331,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
|
||||
}
|
||||
private _renderTokenRow(token: Token, index: number): React.ReactNode {
|
||||
const tokenState = this.state.trackedTokenStateByAddress[token.address];
|
||||
const tokenState = this.props.trackedTokenStateByAddress[token.address];
|
||||
const tokenLink = sharedUtils.getEtherScanLinkIfExists(
|
||||
token.address,
|
||||
this.props.networkId,
|
||||
@@ -438,7 +403,8 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
etherToken={etherToken}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)}
|
||||
refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)}
|
||||
// tslint:disable:jsx-no-lambda
|
||||
refetchEthTokenStateAsync={async () => this.props.refetchTokenStateAsync(etherToken.address)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -474,7 +440,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
onErrorOccurred={_.noop} // TODO: Error handling
|
||||
userAddress={this.props.userAddress}
|
||||
isDisabled={!config.tokenState.isLoaded}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)}
|
||||
refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -557,42 +523,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
});
|
||||
return trackedTokenStateByAddress;
|
||||
}
|
||||
private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> {
|
||||
const balanceAndAllowanceTupleByAddress: ItemByAddress<BigNumber[]> = {};
|
||||
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
|
||||
for (const tokenAddress of tokenAddresses) {
|
||||
const balanceAndAllowanceTuple = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
userAddressIfExists,
|
||||
tokenAddress,
|
||||
);
|
||||
balanceAndAllowanceTupleByAddress[tokenAddress] = balanceAndAllowanceTuple;
|
||||
}
|
||||
const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses);
|
||||
const trackedTokenStateByAddress = _.reduce(
|
||||
tokenAddresses,
|
||||
(acc, address) => {
|
||||
const [balance, allowance] = balanceAndAllowanceTupleByAddress[address];
|
||||
const priceIfExists = _.get(priceByAddress, address);
|
||||
acc[address] = {
|
||||
balance,
|
||||
allowance,
|
||||
price: priceIfExists,
|
||||
isLoaded: true,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
this.state.trackedTokenStateByAddress,
|
||||
);
|
||||
|
||||
if (!this._isUnmounted) {
|
||||
this.setState({
|
||||
trackedTokenStateByAddress,
|
||||
});
|
||||
}
|
||||
}
|
||||
private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
|
||||
await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
|
||||
}
|
||||
private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> {
|
||||
if (_.isEmpty(tokenAddresses)) {
|
||||
return {};
|
||||
|
@@ -2,12 +2,14 @@ import { BigNumber } from '@0xproject/utils';
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { ActionTypes, ProviderType, TokenByAddress } from 'ts/types';
|
||||
import { ActionTypes, ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types';
|
||||
|
||||
import { PortalOnboardingFlow as PortalOnboardingFlowComponent } from 'ts/components/onboarding/portal_onboarding_flow';
|
||||
import { State } from 'ts/redux/reducer';
|
||||
|
||||
interface PortalOnboardingFlowProps {}
|
||||
interface PortalOnboardingFlowProps {
|
||||
trackedTokenStateByAddress: TokenStateByAddress;
|
||||
}
|
||||
|
||||
interface ConnectedState {
|
||||
stepIndex: number;
|
||||
@@ -26,7 +28,7 @@ interface ConnectedDispatch {
|
||||
updateOnboardingStep: (stepIndex: number) => void;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State): ConnectedState => ({
|
||||
const mapStateToProps = (state: State, ownProps: PortalOnboardingFlowProps): ConnectedState => ({
|
||||
stepIndex: state.portalOnboardingStep,
|
||||
isRunning: state.isPortalOnboardingShowing,
|
||||
userAddress: state.userAddress,
|
||||
|
Reference in New Issue
Block a user