mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01: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:
parent
c078807c72
commit
ab4843f06b
10
app/_locales/en/messages.json
generated
10
app/_locales/en/messages.json
generated
@ -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"
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
145
ui/components/multichain/account-details/account-details.js
Normal file
145
ui/components/multichain/account-details/account-details.js
Normal 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,
|
||||
};
|
@ -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} />;
|
@ -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();
|
||||
});
|
||||
});
|
1
ui/components/multichain/account-details/index.js
Normal file
1
ui/components/multichain/account-details/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { AccountDetails } from './account-details';
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -60,6 +60,7 @@ function mapStateToProps(state) {
|
||||
completedOnboarding,
|
||||
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
|
||||
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
|
||||
accountDetailsAddress: state.appState.accountDetailsAddress,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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<
|
||||
|
Loading…
Reference in New Issue
Block a user