1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 03:12:42 +02:00

UX: Multichain: Implement Account Details Popover (#18811)

* UX: Multichain: Implement Account Details Popover

* Styling account details popover

* using ButtonSecondary with variant, removing Text

* adding account-details jest test

* Close popover when outside area clicked

* Move all export functionality into the popover

* Improve jest tests

* Implement new design for export key screens

* Hide warning when popover is closed

* Vertically align the copy button

* Move AccountDetailsDisplay to its own file

* Move authentication to its own file

* Move private key to its own component

* Fix misalignment of avatar on display screen

* Move private key to its own component

* Update ui/components/multichain/account-details/account-details-authenticate.js

Co-authored-by: Nidhi Kumari <nidhi.kumari@consensys.net>

* Update ui/components/multichain/account-details/account-details.test.js

Co-authored-by: Nidhi Kumari <nidhi.kumari@consensys.net>

* Prevent account name overflow, update text size

* Use FormTextField

* Add analytics

* Move location of accountDetailsAddress

* Ensure passsword input is used

---------

Co-authored-by: Victor Thomas <10986371+vthomas13@users.noreply.github.com>
Co-authored-by: Nidhi Kumari <nidhi.kumari@consensys.net>
This commit is contained in:
David Walsh 2023-05-03 12:09:13 -05:00 committed by GitHub
parent c078807c72
commit ab4843f06b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 595 additions and 13 deletions

View File

@ -1412,6 +1412,9 @@
"enterPasswordContinue": {
"message": "Enter password to continue"
},
"enterYourPassword": {
"message": "Enter your password"
},
"errorCode": {
"message": "Code: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
@ -3167,6 +3170,10 @@
"message": "Private Key",
"description": "select this type of file to use to import an account"
},
"privateKeyCopyWarning": {
"message": "Private key for $1",
"description": "$1 represents the account name"
},
"privateKeyWarning": {
"message": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account."
},
@ -3673,6 +3680,9 @@
"showPermissions": {
"message": "Show permissions"
},
"showPrivateKey": {
"message": "Show private key"
},
"showPrivateKeys": {
"message": "Show Private Keys"
},

View File

@ -483,6 +483,7 @@ export enum MetaMetricsEventName {
AccountAddFailed = 'Account Add Failed',
AccountPasswordCreated = 'Account Password Created',
AccountReset = 'Account Reset',
AccountRenamed = 'Account Renamed',
AppInstalled = 'App Installed',
AppUnlocked = 'App Unlocked',
AppUnlockedFailed = 'App Unlocked Failed',

View File

@ -1,4 +1,4 @@
import React, { useRef, useLayoutEffect, useMemo, useState } from 'react';
import React, { useRef, useEffect, useMemo, useState } from 'react';
import classnames from 'classnames';
import {
BLOCK_SIZES,
@ -23,7 +23,7 @@ export const HeaderBase: React.FC<HeaderBaseProps> = ({
const endAccessoryRef = useRef<HTMLDivElement>(null);
const [accessoryMinWidth, setAccessoryMinWidth] = useState<number>();
useLayoutEffect(() => {
useEffect(() => {
function handleResize() {
if (startAccessoryRef.current && endAccessoryRef.current) {
const accMinWidth = Math.max(

View File

@ -0,0 +1,90 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import {
DISPLAY,
SEVERITIES,
TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
import {
BannerAlert,
ButtonPrimary,
ButtonSecondary,
FormTextField,
Text,
} from '../../component-library';
import Box from '../../ui/box/box';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { exportAccount, hideWarning } from '../../../store/actions';
export const AccountDetailsAuthenticate = ({ address, onCancel }) => {
const t = useI18nContext();
const dispatch = useDispatch();
const [password, setPassword] = useState('');
// Password error would result from appState
const warning = useSelector((state) => state.appState.warning);
const onSubmit = useCallback(() => {
dispatch(exportAccount(password, address)).then((res) => {
dispatch(hideWarning());
return res;
});
}, [dispatch, password, address]);
const handleKeyPress = useCallback(
(e) => {
if (e.key === 'Enter') {
onSubmit();
}
},
[onSubmit],
);
return (
<>
<FormTextField
marginTop={6}
id="account-details-authenticate"
label={t('enterYourPassword')}
placeholder={t('password')}
error={warning}
helpText={warning}
onChange={(e) => setPassword(e.target.value)}
value={password}
variant={TextVariant.bodySm}
type="password"
inputProps={{
onKeyPress: handleKeyPress,
}}
/>
{warning ? (
<Text
marginTop={1}
color={TextColor.errorDefault}
variant={TextVariant.bodySm}
>
{warning}
</Text>
) : null}
<BannerAlert marginTop={6} severity={SEVERITIES.DANGER}>
<Text variant={TextVariant.bodySm}>{t('privateKeyWarning')}</Text>
</BannerAlert>
<Box display={DISPLAY.FLEX} marginTop={6} gap={2}>
<ButtonSecondary onClick={onCancel} block>
{t('cancel')}
</ButtonSecondary>
<ButtonPrimary onClick={onSubmit} disabled={password === ''} block>
{t('confirm')}
</ButtonPrimary>
</Box>
</>
);
};
AccountDetailsAuthenticate.propTypes = {
address: PropTypes.string.isRequired,
onCancel: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,103 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import QrView from '../../ui/qr-code';
import EditableLabel from '../../ui/editable-label/editable-label';
import { setAccountLabel } from '../../../store/actions';
import {
getCurrentChainId,
getHardwareWalletType,
getMetaMaskKeyrings,
} from '../../../selectors';
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
import {
BUTTON_SECONDARY_SIZES,
ButtonSecondary,
} from '../../component-library';
import {
AlignItems,
DISPLAY,
FLEX_DIRECTION,
TextVariant,
} from '../../../helpers/constants/design-system';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
MetaMetricsEventCategory,
MetaMetricsEventKeyType,
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Box from '../../ui/box/box';
export const AccountDetailsDisplay = ({
accounts,
accountName,
address,
onExportClick,
}) => {
const dispatch = useDispatch();
const trackEvent = useContext(MetaMetricsContext);
const t = useI18nContext();
const keyrings = useSelector(getMetaMaskKeyrings);
const keyring = keyrings.find((kr) => kr.accounts.includes(address));
const exportPrivateKeyFeatureEnabled = !isHardwareKeyring(keyring?.type);
const chainId = useSelector(getCurrentChainId);
const deviceName = useSelector(getHardwareWalletType);
return (
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<EditableLabel
defaultValue={accountName}
onSubmit={(label) => {
dispatch(setAccountLabel(address, label));
trackEvent({
category: MetaMetricsEventCategory.Accounts,
event: MetaMetricsEventName.AccountRenamed,
properties: {
location: 'Account Details Modal',
chain_id: chainId,
account_hardware_type: deviceName,
},
});
}}
accounts={accounts}
/>
<QrView Qr={{ data: address }} />
{exportPrivateKeyFeatureEnabled ? (
<ButtonSecondary
block
size={BUTTON_SECONDARY_SIZES.LG}
variant={TextVariant.bodyMd}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Accounts,
event: MetaMetricsEventName.KeyExportSelected,
properties: {
key_type: MetaMetricsEventKeyType.Pkey,
location: 'Account Details Modal',
},
});
onExportClick();
}}
>
{t('showPrivateKey')}
</ButtonSecondary>
) : null}
</Box>
);
};
AccountDetailsDisplay.propTypes = {
accounts: PropTypes.array.isRequired,
accountName: PropTypes.string.isRequired,
address: PropTypes.string.isRequired,
onExportClick: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
BannerAlert,
ButtonIcon,
ButtonPrimary,
IconName,
Text,
} from '../../component-library';
import {
AlignItems,
BorderColor,
BorderRadius,
DISPLAY,
FLEX_DIRECTION,
SEVERITIES,
TextVariant,
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Box from '../../ui/box/box';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
export const AccountDetailsKey = ({ accountName, onClose, privateKey }) => {
const t = useI18nContext();
const [privateKeyCopied, handlePrivateKeyCopy] = useCopyToClipboard();
return (
<>
<Text
marginTop={6}
variant={TextVariant.bodySm}
style={{ wordBreak: 'break-word' }}
>
{t('privateKeyCopyWarning', [accountName])}
</Text>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
alignItems={AlignItems.center}
borderRadius={BorderRadius.SM}
borderWidth={1}
borderColor={BorderColor.default}
padding={4}
gap={4}
>
<Text variant={TextVariant.bodySm} style={{ wordBreak: 'break-word' }}>
{privateKey}
</Text>
<ButtonIcon
onClick={() => handlePrivateKeyCopy(privateKey)}
iconName={privateKeyCopied ? IconName.CopySuccess : IconName.Copy}
/>
</Box>
<BannerAlert severity={SEVERITIES.DANGER} marginTop={4}>
<Text variant={TextVariant.bodySm}>{t('privateKeyWarning')}</Text>
</BannerAlert>
<ButtonPrimary marginTop={6} onClick={onClose}>
{t('done')}
</ButtonPrimary>
</>
);
};
AccountDetailsKey.propTypes = {
accountName: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
privateKey: PropTypes.string.isRequired,
};

View File

@ -0,0 +1,145 @@
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import Popover from '../../ui/popover/popover.component';
import {
setAccountDetailsAddress,
clearAccountDetails,
hideWarning,
} from '../../../store/actions';
import {
AvatarAccount,
AvatarAccountSize,
AvatarAccountVariant,
ButtonIcon,
IconName,
PopoverHeader,
Text,
} from '../../component-library';
import Box from '../../ui/box/box';
import { getMetaMaskAccountsOrdered } from '../../../selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import {
AlignItems,
DISPLAY,
FLEX_DIRECTION,
JustifyContent,
TextVariant,
Size,
} from '../../../helpers/constants/design-system';
import { AddressCopyButton } from '../address-copy-button';
import { AccountDetailsDisplay } from './account-details-display';
import { AccountDetailsAuthenticate } from './account-details-authenticate';
import { AccountDetailsKey } from './account-details-key';
export const AccountDetails = ({ address }) => {
const dispatch = useDispatch();
const t = useI18nContext();
const useBlockie = useSelector((state) => state.metamask.useBlockie);
const accounts = useSelector(getMetaMaskAccountsOrdered);
const { name } = accounts.find((account) => account.address === address);
const [attemptingExport, setAttemptingExport] = useState(false);
// This is only populated when the user properly authenticates
const privateKey = useSelector(
(state) => state.appState.accountDetail.privateKey,
);
const onClose = useCallback(() => {
dispatch(setAccountDetailsAddress(''));
dispatch(clearAccountDetails());
dispatch(hideWarning());
}, [dispatch]);
const avatar = (
<AvatarAccount
variant={
useBlockie
? AvatarAccountVariant.Blockies
: AvatarAccountVariant.Jazzicon
}
address={address}
size={AvatarAccountSize.Lg}
/>
);
return (
<Popover
headerProps={{
paddingBottom: 1,
}}
contentProps={{
paddingLeft: 4,
paddingRight: 4,
paddingBottom: 4,
}}
title={
attemptingExport ? (
<PopoverHeader
startAccessory={
<ButtonIcon
onClick={() => setAttemptingExport(false)}
iconName={IconName.ArrowLeft}
size={Size.SM}
/>
}
>
{t('showPrivateKey')}
</PopoverHeader>
) : (
<PopoverHeader
childrenWrapperProps={{
display: DISPLAY.FLEX,
justifyContent: JustifyContent.center,
}}
>
<Box paddingLeft={6}>{avatar}</Box>
</PopoverHeader>
)
}
onClose={onClose}
>
{attemptingExport ? (
<>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN}
>
{avatar}
<Text
marginTop={2}
marginBottom={2}
variant={TextVariant.bodyLgMedium}
style={{ wordBreak: 'break-word' }}
>
{name}
</Text>
<AddressCopyButton address={address} shorten />
</Box>
{privateKey ? (
<AccountDetailsKey
accountName={name}
onClose={onClose}
privateKey={privateKey}
/>
) : (
<AccountDetailsAuthenticate address={address} onCancel={onClose} />
)}
</>
) : (
<AccountDetailsDisplay
accounts={accounts}
accountName={name}
address={address}
onExportClick={() => setAttemptingExport(true)}
/>
)}
</Popover>
);
};
AccountDetails.propTypes = {
address: PropTypes.string,
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import testData from '../../../../.storybook/test-data';
import { AccountDetails } from './account-details';
const [, address] = Object.keys(testData.metamask.identities);
export default {
title: 'Components/Multichain/AccountDetails',
component: AccountDetails,
argTypes: {
address: {
control: 'text',
},
},
args: {
address,
},
};
export const DefaultStory = (args) => <AccountDetails {...args} />;

View File

@ -0,0 +1,91 @@
import React from 'react';
import { screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import mockState from '../../../../test/data/mock-state.json';
import { showPrivateKey } from '../../../../app/_locales/en/messages.json';
import {
setAccountDetailsAddress,
exportAccount,
hideWarning,
} from '../../../store/actions';
import { AccountDetails } from '.';
jest.mock('../../../store/actions.ts');
describe('AccountDetails', () => {
const address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc';
const mockSetAccountDetailsAddress = jest.fn();
const mockExportAccount = jest.fn().mockResolvedValue(true);
const mockHideWarning = jest.fn();
beforeEach(() => {
setAccountDetailsAddress.mockReturnValue(mockSetAccountDetailsAddress);
exportAccount.mockReturnValue(mockExportAccount);
hideWarning.mockReturnValue(mockHideWarning);
});
afterEach(() => jest.clearAllMocks());
function render(props = {}, storeModifications = {}) {
const store = configureStore({
metamask: {
...mockState.metamask,
},
...storeModifications,
});
const allProps = { address, ...props };
return renderWithProvider(<AccountDetails {...allProps} />, store);
}
it('should set account label when changing default account label', () => {
render();
const editButton = screen.getByTestId('editable-label-button');
fireEvent.click(editButton);
const editableInput = screen.getByTestId('editable-input');
const newAccountLabel = 'New Label';
fireEvent.change(editableInput, { target: { value: newAccountLabel } });
expect(editableInput).toHaveAttribute('value', newAccountLabel);
});
it('shows export private key contents and password field when clicked', () => {
const { queryByText, queryByPlaceholderText } = render();
const exportPrivateKeyButton = queryByText(showPrivateKey.message);
fireEvent.click(exportPrivateKeyButton);
expect(queryByText('Show private key')).toBeInTheDocument();
expect(queryByPlaceholderText('Password')).toBeInTheDocument();
});
it('attempts to validate password when submitted', async () => {
const password = 'password';
const { queryByPlaceholderText, queryByText } = render();
const exportPrivateKeyButton = queryByText(showPrivateKey.message);
fireEvent.click(exportPrivateKeyButton);
queryByPlaceholderText('Password').focus();
await userEvent.keyboard(password);
fireEvent.click(queryByText('Confirm'));
expect(exportAccount).toHaveBeenCalledWith(password, address);
});
it('displays the private key when exposed in state', () => {
const samplePrivateKey = '8675309';
const { queryByText } = render(
{},
{ appState: { accountDetail: { privateKey: samplePrivateKey } } },
);
const exportPrivateKeyButton = queryByText(showPrivateKey.message);
fireEvent.click(exportPrivateKeyButton);
expect(queryByText(samplePrivateKey)).toBeInTheDocument();
});
});

View File

@ -0,0 +1 @@
export { AccountDetails } from './account-details';

View File

@ -22,7 +22,7 @@ import {
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import { getURLHostName } from '../../../helpers/utils/util';
import { showModal } from '../../../store/actions';
import { setAccountDetailsAddress, showModal } from '../../../store/actions';
import { TextVariant } from '../../../helpers/constants/design-system';
import { formatAccountType } from '../../../helpers/utils/metrics';
@ -101,7 +101,7 @@ export const AccountListItemMenu = ({
</MenuItem>
<MenuItem
onClick={() => {
dispatch(showModal({ name: 'ACCOUNT_DETAILS' }));
dispatch(setAccountDetailsAddress(identity.address));
trackEvent({
event: MetaMetricsEventName.NavAccountDetailsOpened,
category: MetaMetricsEventCategory.Navigation,

View File

@ -21,6 +21,7 @@ export const AddressCopyButton = ({
address,
shorten = false,
wrap = false,
onClick,
}) => {
const displayAddress = shorten ? shortenAddress(address) : address;
const [copied, handleCopy] = useCopyToClipboard();
@ -30,7 +31,10 @@ export const AddressCopyButton = ({
<Tooltip position="bottom" title={copied ? t('copiedExclamation') : null}>
<ButtonBase
backgroundColor={BackgroundColor.primaryMuted}
onClick={() => handleCopy(address)}
onClick={() => {
handleCopy(address);
onClick?.();
}}
paddingRight={4}
paddingLeft={4}
size={Size.SM}
@ -63,4 +67,8 @@ AddressCopyButton.propTypes = {
* Represents if the element should wrap to multiple lines
*/
wrap: PropTypes.bool,
/**
* Fires when the button is clicked
*/
onClick: PropTypes.func,
};

View File

@ -12,3 +12,4 @@ export { MultichainConnectedSiteMenu } from './multichain-connected-site-menu';
export { NetworkListItem } from './network-list-item';
export { NetworkListMenu } from './network-list-menu';
export { ProductTour } from './product-tour-popover';
export { AccountDetails } from './account-details';

View File

@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, { useContext } from 'react';
import qrCode from 'qrcode-generator';
import { connect } from 'react-redux';
import { isHexPrefixed } from 'ethereumjs-util';
@ -10,6 +10,11 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
import { AddressCopyButton } from '../../multichain/address-copy-button';
import Box from '../box/box';
import { Icon, IconName, IconSize } from '../../component-library';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import { MetaMetricsContext } from '../../../contexts/metametrics';
export default connect(mapStateToProps)(QrCodeView);
@ -22,14 +27,14 @@ function mapStateToProps(state) {
};
}
function QrCodeView(props) {
const { Qr, warning } = props;
function QrCodeView({ Qr, warning }) {
const [copied, handleCopy] = useCopyToClipboard();
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const { message, data } = Qr;
const address = `${
isHexPrefixed(data) ? 'ethereum:' : ''
}${toChecksumHexAddress(data)}`;
const [copied, handleCopy] = useCopyToClipboard();
const t = useI18nContext();
const qrImage = qrCode(4, 'M');
qrImage.addData(address);
qrImage.make();
@ -55,12 +60,26 @@ function QrCodeView(props) {
<div
className="qr-code__wrapper"
dangerouslySetInnerHTML={{
__html: qrImage.createTableTag(4),
__html: process.env.MULTICHAIN
? qrImage.createTableTag(5, 24)
: qrImage.createTableTag(4),
}}
/>
{process.env.MULTICHAIN ? (
<Box marginLeft={2} marginRight={2}>
<AddressCopyButton wrap address={toChecksumHexAddress(data)} />
<Box marginBottom={6}>
<AddressCopyButton
wrap
address={toChecksumHexAddress(data)}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Accounts,
event: MetaMetricsEventName.PublicAddressCopied,
properties: {
location: 'Account Details Modal',
},
});
}}
/>
</Box>
) : (
<Tooltip

View File

@ -66,6 +66,7 @@ interface AppState {
onboardedInThisUISession: boolean;
customTokenAmount: string;
txId: number | null;
accountDetailsAddress: string;
}
interface AppSliceState {
@ -130,6 +131,7 @@ const initialState: AppState = {
customTokenAmount: '',
scrollToBottom: true,
txId: null,
accountDetailsAddress: '',
};
export default function reduceApp(
@ -170,6 +172,13 @@ export default function reduceApp(
alertMessage: null,
};
case actionConstants.SET_ACCOUNT_DETAILS_ADDRESS: {
return {
...appState,
accountDetailsAddress: action.payload,
};
}
// qr scanner methods
case actionConstants.QR_CODE_DETECTED:
return {

View File

@ -34,6 +34,7 @@ import {
AppHeader as MultichainAppHeader,
AccountListMenu,
NetworkListMenu,
AccountDetails,
} from '../../components/multichain';
import UnlockPage from '../unlock-page';
import Alerts from '../../components/app/alerts';
@ -152,6 +153,7 @@ export default class Routes extends Component {
toggleAccountMenu: PropTypes.func,
isNetworkMenuOpen: PropTypes.bool,
toggleNetworkMenu: PropTypes.func,
accountDetailsAddress: PropTypes.string,
};
static contextTypes = {
@ -495,6 +497,7 @@ export default class Routes extends Component {
toggleAccountMenu,
isNetworkMenuOpen,
toggleNetworkMenu,
accountDetailsAddress,
} = this.props;
const loadMessage =
loadingMessage || isNetworkLoading
@ -566,6 +569,9 @@ export default class Routes extends Component {
{process.env.MULTICHAIN && isNetworkMenuOpen ? (
<NetworkListMenu onClose={() => toggleNetworkMenu()} />
) : null}
{process.env.MULTICHAIN && accountDetailsAddress ? (
<AccountDetails address={accountDetailsAddress} />
) : null}
<div className="main-container-wrapper">
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}

View File

@ -60,6 +60,7 @@ function mapStateToProps(state) {
completedOnboarding,
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
accountDetailsAddress: state.appState.accountDetailsAddress,
};
}

View File

@ -35,6 +35,7 @@ export const SHOW_SEND_TOKEN_PAGE = 'SHOW_SEND_TOKEN_PAGE';
export const SHOW_PRIVATE_KEY = 'SHOW_PRIVATE_KEY';
export const SET_ACCOUNT_LABEL = 'SET_ACCOUNT_LABEL';
export const CLEAR_ACCOUNT_DETAILS = 'CLEAR_ACCOUNT_DETAILS';
export const SET_ACCOUNT_DETAILS_ADDRESS = 'SET_ACCOUNT_DETAILS_ADDRESS';
// tx conf screen
export const COMPLETED_TX = 'COMPLETED_TX';
export const TRANSACTION_ERROR = 'TRANSACTION_ERROR';

View File

@ -3115,6 +3115,13 @@ export function toggleNetworkMenu() {
};
}
export function setAccountDetailsAddress(address: string) {
return {
type: actionConstants.SET_ACCOUNT_DETAILS_ADDRESS,
payload: address,
};
}
export function setParticipateInMetaMetrics(
participationPreference: boolean,
): ThunkAction<