1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Add reveal to export private key (#18170)

Co-authored-by: George Marshall <george.marshall@consensys.net>
Co-authored-by: Brad Decker <bhdecker84@gmail.com>
Co-authored-by: David Walsh <davidwalsh83@gmail.com>
Co-authored-by: Howard Braham <howrad@gmail.com>

* change js to tsx

* update to typescript

* add labels to circle animation

* add willHide prop to hold to reveal modal

* add test

* convert to design system

* fix lint

* fix type

* bump coverage

* rename

* remove comments

* remove ts comment and add fix exhuastive dep check

* update coverage

* add hide modal test

* use banneralert

* update label

* remove unused

* fix text

* update aria label messages

* change exportAccountAndGetPrivateKey to be async

* fix lint

* update coverage target

* update coverage

* update input component

* update coverage

* update coverage

* fix blank line

* use &&

* move plainKey to under !privateKeyInput

* update hold modal to display srp and private key message

* fix styling

* fix lint and test

* fix unused locales

* remove redundent check

* update storybook

* fix text alignment

* fix lint

* update snapshot

* fix test

* update coverage

* fix merge conflict

* refactor

* fix variant

* update snapshot

* fix test after merge

* fix test after merge conflict

* fix label text

* update to use label component
This commit is contained in:
Monte Lai 2023-05-07 05:04:20 +08:00 committed by GitHub
parent 82f01a6b44
commit 0306422bbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 668 additions and 418 deletions

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Verlauf"
},
"holdToReveal": {
"message": "Halten, um GWP anzuzeigen"
},
"holdToRevealContent1": {
"message": "Ihre geheime Wiederherstellungsphrase bietet $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "Betrüger aber schon.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Bewahren Sie Ihre GWP sicher auf"
},
"ignoreAll": {
"message": "Alle ignorieren"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Ιστορικό"
},
"holdToReveal": {
"message": "Κρατήστε πατημένο για αποκάλυψη της ΜΦΑ"
},
"holdToRevealContent1": {
"message": "Η Μυστική σας Φράση Ανάκτησης παρέχει $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "αλλά οι απατεώνες μπορεί να το κάνουν.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Κρατήστε τη ΜΦΑ σας ασφαλή"
},
"ignoreAll": {
"message": "Αγνόηση όλων"
},

View File

@ -1754,9 +1754,6 @@
"history": {
"message": "History"
},
"holdToReveal": {
"message": "Hold to reveal SRP"
},
"holdToRevealContent1": {
"message": "Your Secret Recovery Phrase provides $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1777,9 +1774,28 @@
"message": "but phishers might.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"holdToRevealContentPrivateKey1": {
"message": "Your Private Key provides $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContentPrivateKey2'"
},
"holdToRevealContentPrivateKey2": {
"message": "full access to your wallet and funds.",
"description": "Is the bolded text in 'holdToRevealContentPrivateKey2'"
},
"holdToRevealLockedLabel": { "message": "hold to reveal circle locked" },
"holdToRevealPrivateKey": {
"message": "Hold to reveal Private Key"
},
"holdToRevealPrivateKeyTitle": {
"message": "Keep your private key safe"
},
"holdToRevealSRP": {
"message": "Hold to reveal SRP"
},
"holdToRevealSRPTitle": {
"message": "Keep your SRP safe"
},
"holdToRevealUnlockedLabel": { "message": "hold to reveal circle unlocked" },
"id": {
"message": "Id"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Historial"
},
"holdToReveal": {
"message": "Mantenga presionado para mostrar la SRP"
},
"holdToRevealContent1": {
"message": "Su frase secreta de recuperación proporciona $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "pero los defraudadores sí.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Mantenga segura su SRP"
},
"ignoreAll": {
"message": "Ignorar todo"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Historique"
},
"holdToReveal": {
"message": "Appuyez longuement pour révéler la PSR"
},
"holdToRevealContent1": {
"message": "Votre phrase secrète de récupération donne $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "mais les hameçonneurs pourraient le faire.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Conservez votre PSR en lieu sûr"
},
"ignoreAll": {
"message": "Ignorer tout"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "इतिहास"
},
"holdToReveal": {
"message": "SRP देखने के लिए होल्ड करें"
},
"holdToRevealContent1": {
"message": "आपका सीक्रेट रिकवरी फ्रेज $1 प्रदान करता है",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "लेकिन फिशर कर सकते हैं।",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "अपने SRP को सुरक्षित रखें"
},
"ignoreAll": {
"message": "सभी को अनदेखा करें"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Riwayat"
},
"holdToReveal": {
"message": "Tahan untuk mengungkap FPR"
},
"holdToRevealContent1": {
"message": "Frasa Pemulihan Rahasia memberikan $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "tetapi penipu akan mencoba memintanya.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Jaga keamanan FPR Anda"
},
"ignoreAll": {
"message": "Abaikan semua"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "履歴"
},
"holdToReveal": {
"message": "長押しして SRP を表示"
},
"holdToRevealContent1": {
"message": "秘密のリカバリーフレーズは$1を提供します。",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "もし尋ねられた場合はフィッシング詐欺の可能性があります。",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "SRP は安全に保管してください"
},
"ignoreAll": {
"message": "すべて無視"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "기록"
},
"holdToReveal": {
"message": "눌러서 SRP 확인"
},
"holdToRevealContent1": {
"message": "비밀 복구 구문이 있으면 $1 기능을 사용할 수 있습니다",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "오히려 피싱 사기꾼들이 요구할 수 있으니 주의가 필요합니다.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "SRP를 안전하게 보관하세요"
},
"ignoreAll": {
"message": "모두 무시"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Histórico"
},
"holdToReveal": {
"message": "Segure para revelar a FRS"
},
"holdToRevealContent1": {
"message": "Sua Frase de Recuperação Secreta concede $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "mas os phishers talvez solicitem.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Mantenha sua FRS em segurança"
},
"ignoreAll": {
"message": "Ignorar tudo"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "История"
},
"holdToReveal": {
"message": "Удерживайте для отображения СВФ"
},
"holdToRevealContent1": {
"message": "Ваша секретная фраза для восстановления дает $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "но злоумышленники-фишеры могут.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Обеспечьте безопасность своей СВФ"
},
"ignoreAll": {
"message": "Игнорировать все"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "History"
},
"holdToReveal": {
"message": "I-hold para ipakita ang SRP"
},
"holdToRevealContent1": {
"message": "Ang iyong Secret Recovery Phrase ay nagbibigay ng $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "ngunit maaring hingin ng mga phisher.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Ingatan ang iyong SRP"
},
"ignoreAll": {
"message": "Huwag pansinin ang lahat"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Geçmiş"
},
"holdToReveal": {
"message": "GKİ'yi göstermek için basılı tut"
},
"holdToRevealContent1": {
"message": "Gizli Kurtarma İfadeniz: $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "ancak dolandırıcılar talep edilebilir.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "GKİ'nizi güvende tutun"
},
"ignoreAll": {
"message": "Tümünü yoksay"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "Lịch sử"
},
"holdToReveal": {
"message": "Giữ để hiển thị Cụm từ khôi phục bí mật"
},
"holdToRevealContent1": {
"message": "Cụm từ khôi phục bí mật của bạn cung cấp $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "nhưng những kẻ lừa đảo qua mạng thì có.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "Đảm bảo an toàn cho Cụm từ khôi phục bí mật của bạn"
},
"ignoreAll": {
"message": "Bỏ qua tất cả"
},

View File

@ -1570,9 +1570,6 @@
"history": {
"message": "历史记录"
},
"holdToReveal": {
"message": "按住以显示 SRP"
},
"holdToRevealContent1": {
"message": "您的助记词提供 $1",
"description": "$1 is a bolded text with the message from 'holdToRevealContent2'"
@ -1593,9 +1590,6 @@
"message": "但网络钓鱼者可能会。",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealTitle": {
"message": "确保 SRP 的安全"
},
"ignoreAll": {
"message": "忽略所有"
},

View File

@ -121,7 +121,7 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
<Box className="hold-to-reveal-button__absolute-fill">
<svg className="hold-to-reveal-button__circle-svg">
<circle
aria-label="circle-locked"
aria-label={t('holdToRevealLockedLabel')}
onTransitionEnd={onProgressComplete}
className="hold-to-reveal-button__circle-foreground"
cx={radius}
@ -181,7 +181,7 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
</svg>
</div>
<div
aria-label="circle-unlocked"
aria-label={t('holdToRevealUnlockedLabel')}
className="hold-to-reveal-button__unlock-icon-container"
onAnimationEnd={triggerOnLongPressed}
>

View File

@ -1,20 +1,21 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import configureMockState from 'redux-mock-store';
import thunk from 'redux-thunk';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import mockState from '../../../../test/data/mock-state.json';
import {
MetaMetricsEventCategory,
MetaMetricsEventKeyType,
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import HoldToRevealButton from './hold-to-reveal-button';
const mockTrackEvent = jest.fn();
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: () => mockTrackEvent,
}));
describe('HoldToRevealButton', () => {
const mockStore = configureMockState([thunk])(mockState);
let props = {};
beforeEach(() => {
@ -51,21 +52,24 @@ describe('HoldToRevealButton', () => {
});
it('should show the locked padlock when a button is long pressed and then should show it after it was lifted off before the animation concludes', async () => {
const { getByText, queryByLabelText } = render(
<HoldToRevealButton {...props} />,
const { getByText, queryByLabelText } = renderWithProvider(
<MetaMetricsContext.Provider value={mockTrackEvent}>
<HoldToRevealButton {...props} />
</MetaMetricsContext.Provider>,
mockStore,
);
const button = getByText('Hold to reveal SRP');
fireEvent.mouseDown(button);
const circleLocked = queryByLabelText('circle-locked');
const circleLocked = queryByLabelText('hold to reveal circle locked');
await waitFor(() => {
expect(circleLocked).toBeInTheDocument();
});
fireEvent.mouseUp(button);
const circleUnlocked = queryByLabelText('circle-unlocked');
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
await waitFor(() => {
expect(circleUnlocked).not.toBeInTheDocument();
@ -73,37 +77,40 @@ describe('HoldToRevealButton', () => {
});
it('should show the unlocked padlock when a button is long pressed for the duration of the animation', async () => {
const { getByText, queryByLabelText } = render(
<HoldToRevealButton {...props} />,
const { getByText, queryByLabelText, getByLabelText } = renderWithProvider(
<MetaMetricsContext.Provider value={mockTrackEvent}>
<HoldToRevealButton {...props} />
</MetaMetricsContext.Provider>,
mockStore,
);
const button = getByText('Hold to reveal SRP');
fireEvent.mouseDown(button);
const circleLocked = queryByLabelText('circle-locked');
const circleLocked = getByLabelText('hold to reveal circle locked');
fireEvent.transitionEnd(circleLocked);
const circleUnlocked = queryByLabelText('circle-unlocked');
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
fireEvent.animationEnd(circleUnlocked);
await waitFor(() => {
expect(circleUnlocked).toBeInTheDocument();
expect(mockTrackEvent).toHaveBeenNthCalledWith(2, {
expect(mockTrackEvent).toHaveBeenNthCalledWith(1, {
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.SrpHoldToRevealClickStarted,
properties: {
key_type: MetaMetricsEventKeyType.Srp,
},
});
expect(mockTrackEvent).toHaveBeenNthCalledWith(5, {
expect(mockTrackEvent).toHaveBeenNthCalledWith(2, {
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.SrpHoldToRevealCompleted,
properties: {
key_type: MetaMetricsEventKeyType.Srp,
},
});
expect(mockTrackEvent).toHaveBeenNthCalledWith(6, {
expect(mockTrackEvent).toHaveBeenNthCalledWith(3, {
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.SrpRevealViewed,
properties: {

View File

@ -59,57 +59,76 @@ exports[`Export PrivateKey Modal should match snapshot 1`] = `
class="account-modal__close"
/>
<span
class="export-private-key-modal__account-name"
class="box mm-text mm-text--body-lg-medium mm-text--font-weight-normal box--margin-top-2 box--flex-direction-row box--color-text-default"
>
Test Account
</span>
<div
class="ellip-address-wrapper"
class="box ellip-address-wrapper box--margin-top-2 box--padding-1 box--sm:padding-2 box--md:padding-1 box--lg:padding-2 box--flex-direction-row box--border-style-solid box--border-color-border-default box--border-width-1"
>
0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc
</div>
<div
class="export-private-key-modal__divider"
class="box export-private-key-modal__divider box--margin-5 box--md:margin-3 box--flex-direction-row box--width-full"
/>
<span
class="export-private-key-modal__body-title"
<p
class="box mm-text mm-text--body-lg-medium mm-text--font-weight-normal box--margin-4 box--md:margin-4 box--flex-direction-row box--color-text-default"
>
Show Private Keys
</span>
</p>
<div
class="export-private-key-modal__password"
class="box box--padding-right-5 box--padding-left-5 box--display-flex box--flex-direction-column box--align-items-flex-start box--width-full"
>
<span
class="export-private-key-modal__password-label"
<label
class="box mm-text mm-label mm-text--body-sm mm-text--font-weight-bold box--margin-bottom-2 box--display-inline-flex box--flex-direction-row box--align-items-center box--color-text-default"
>
Type your MetaMask password
</span>
<input
class="export-private-key-modal__password-input"
</label>
<div
class="box mm-text-field mm-text-field--size-md mm-text-field--truncate export-private-key-modal__password-input box--display-inline-flex box--flex-direction-row box--align-items-center box--width-full box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
data-testid="password-input"
type="password"
/>
>
<input
autocomplete="off"
class="box mm-text mm-input mm-input--disable-state-styles mm-text-field__input mm-text--body-md box--padding-right-4 box--padding-left-4 box--flex-direction-row box--color-text-default box--background-color-transparent box--border-style-none"
focused="false"
placeholder="Enter password"
type="password"
value=""
/>
</div>
</div>
<p
class="box mm-text mm-text--body-sm box--flex-direction-row box--color-error-default"
/>
<div
class="export-private-key-modal__password--warning"
class="box mm-banner-base mm-banner-alert mm-banner-alert--severity-danger box--margin-top-4 box--margin-right-5 box--margin-left-5 box--padding-1 box--sm:padding-3 box--lg:padding-3 box--padding-left-2 box--display-flex box--gap-2 box--flex-direction-row box--background-color-error-muted box--rounded-sm"
>
Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account.
<span
class="box mm-icon mm-icon--size-lg box--display-inline-block box--flex-direction-row box--color-error-default"
style="mask-image: url('./images/icons/danger.svg');"
/>
<div>
<p
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account.
</p>
</div>
</div>
<div
class="export-private-key-modal__buttons"
class="box box--margin-top-3 box--padding-5 box--md:padding-5 box--display-flex box--flex-direction-row box--justify-content-space-between box--width-full"
>
<button
class="button btn--rounded btn-secondary btn--large export-private-key-modal__button export-private-key-modal__button--cancel"
role="button"
tabindex="0"
class="box mm-text mm-button-base mm-button-base--size-lg mm-button-primary mm-text--body-md box--margin-right-4 box--padding-right-4 box--padding-left-4 box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--width-1/2 box--color-primary-inverse box--background-color-primary-default box--rounded-pill"
type="secondary"
>
Cancel
</button>
<button
class="button btn--rounded btn-primary btn--large export-private-key-modal__button"
class="box mm-text mm-button-base mm-button-base--size-lg mm-button-base--disabled mm-button-primary mm-button-primary--disabled mm-text--body-md box--padding-right-4 box--padding-left-4 box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--width-1/2 box--color-primary-inverse box--background-color-primary-default box--rounded-pill"
disabled=""
role="button"
tabindex="0"
type="primary"
>
Confirm
</button>

View File

@ -1,139 +1,188 @@
import log from 'loglevel';
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import copyToClipboard from 'copy-to-clipboard';
import Button from '../../../ui/button';
import AccountModalContainer from '../account-modal-container';
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
import Box from '../../../ui/box';
import {
toChecksumHexAddress,
stripHexPrefix,
} from '../../../../../shared/modules/hexstring-utils';
BUTTON_SIZES,
BUTTON_VARIANT,
BannerAlert,
Button,
Text,
} from '../../../component-library';
import AccountModalContainer from '../account-modal-container';
import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils';
import {
MetaMetricsEventCategory,
MetaMetricsEventKeyType,
MetaMetricsEventName,
} from '../../../../../shared/constants/metametrics';
import HoldToRevealModal from '../hold-to-reveal-modal/hold-to-reveal-modal';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
BLOCK_SIZES,
BorderColor,
BorderStyle,
Color,
DISPLAY,
FLEX_DIRECTION,
FONT_WEIGHT,
JustifyContent,
TextVariant,
} from '../../../../helpers/constants/design-system';
import PrivateKeyDisplay from './private-key';
import PasswordInput from './password-input';
export default class ExportPrivateKeyModal extends Component {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
};
const ExportPrivateKeyModal = ({
clearAccountDetails,
hideWarning,
exportAccount,
selectedIdentity,
showAccountDetailModal,
hideModal,
warning = null,
previousModalState,
}) => {
const [password, setPassword] = useState('');
const [privateKey, setPrivateKey] = useState(null);
const [showWarning, setShowWarning] = useState(true);
const [showHoldToReveal, setShowHoldToReveal] = useState(false);
const trackEvent = useContext(MetaMetricsContext);
const t = useI18nContext();
static defaultProps = {
warning: null,
previousModalState: null,
};
useEffect(() => {
return () => {
clearAccountDetails();
hideWarning();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
static propTypes = {
exportAccount: PropTypes.func.isRequired,
selectedIdentity: PropTypes.object.isRequired,
warning: PropTypes.node,
showAccountDetailModal: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired,
clearAccountDetails: PropTypes.func.isRequired,
previousModalState: PropTypes.string,
};
state = {
password: '',
privateKey: null,
showWarning: true,
};
componentWillUnmount() {
this.props.clearAccountDetails();
this.props.hideWarning();
}
exportAccountAndGetPrivateKey = (password, address) => {
const { exportAccount } = this.props;
exportAccount(password, address)
.then((privateKey) => {
this.context.trackEvent({
const exportAccountAndGetPrivateKey = async (passwordInput, address) => {
try {
const privateKeyRetrieved = await exportAccount(passwordInput, address);
trackEvent(
{
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.KeyExportRevealed,
properties: {
key_type: MetaMetricsEventKeyType.Pkey,
},
});
this.setState({
privateKey,
showWarning: false,
});
})
.catch((e) => {
this.context.trackEvent({
},
{},
);
setPrivateKey(privateKeyRetrieved);
setShowWarning(false);
setShowHoldToReveal(true);
} catch (e) {
trackEvent(
{
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.KeyExportFailed,
properties: {
key_type: MetaMetricsEventKeyType.Pkey,
reason: 'incorrect_password',
},
});
},
{},
);
log.error(e);
});
log.error(e);
}
};
renderPasswordLabel(privateKey) {
const { name, address } = selectedIdentity;
if (showHoldToReveal) {
return (
<span className="export-private-key-modal__password-label">
{privateKey
? this.context.t('copyPrivateKey')
: this.context.t('typePassword')}
</span>
);
}
renderPasswordInput(privateKey) {
const plainKey = privateKey && stripHexPrefix(privateKey);
if (!privateKey) {
return (
<input
type="password"
className="export-private-key-modal__password-input"
data-testid="password-input"
onChange={(event) => this.setState({ password: event.target.value })}
/>
);
}
return (
<div
className="export-private-key-modal__private-key-display"
onClick={() => {
copyToClipboard(plainKey);
this.context.trackEvent({
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.KeyExportCopied,
properties: {
key_type: MetaMetricsEventKeyType.Pkey,
copy_method: 'clipboard',
},
});
}}
<AccountModalContainer
className="export-private-key-modal"
selectedIdentity={selectedIdentity}
showBackButton={previousModalState === 'ACCOUNT_DETAILS'}
backButtonAction={() => showAccountDetailModal()}
>
{plainKey}
</div>
<HoldToRevealModal
onLongPressed={() => setShowHoldToReveal(false)}
willHide={false}
holdToRevealType="PrivateKey"
/>
</AccountModalContainer>
);
}
renderButtons(privateKey, address, hideModal) {
return (
<div className="export-private-key-modal__buttons">
return (
<AccountModalContainer
className="export-private-key-modal"
selectedIdentity={selectedIdentity}
showBackButton={previousModalState === 'ACCOUNT_DETAILS'}
backButtonAction={() => showAccountDetailModal()}
>
<Text
as="span"
marginTop={2}
variant={TextVariant.bodyLgMedium}
fontWeight={FONT_WEIGHT.NORMAL}
>
{name}
</Text>
<Box
className="ellip-address-wrapper"
borderStyle={BorderStyle.solid}
borderColor={BorderColor.borderDefault}
borderWidth={1}
marginTop={2}
padding={[1, 2, 1, 2]}
>
{toChecksumHexAddress(address)}
</Box>
<Box
className="export-private-key-modal__divider"
width={BLOCK_SIZES.FULL}
margin={[5, 0, 3, 0]}
/>
<Text
variant={TextVariant.bodyLgMedium}
margin={[4, 0, 4, 0]}
fontWeight={FONT_WEIGHT.NORMAL}
>
{t('showPrivateKeys')}
</Text>
{privateKey ? (
<PrivateKeyDisplay privateKey={privateKey} />
) : (
<PasswordInput setPassword={setPassword} />
)}
{showWarning && (
<Text color={Color.errorDefault} variant={TextVariant.bodySm}>
{warning}
</Text>
)}
<BannerAlert
padding={[1, 3, 0, 3]}
marginLeft={5}
marginRight={5}
marginTop={4}
severity="danger"
>
{t('privateKeyWarning')}
</BannerAlert>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
width={BLOCK_SIZES.FULL}
justifyContent={JustifyContent.spaceBetween}
marginTop={3}
padding={[5, 0, 5, 0]}
>
{!privateKey && (
<Button
type="secondary"
large
className="export-private-key-modal__button export-private-key-modal__button--cancel"
type={BUTTON_VARIANT.SECONDARY}
size={BUTTON_SIZES.LG}
width={BLOCK_SIZES.HALF}
marginRight={4}
onClick={() => {
this.context.trackEvent({
trackEvent({
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.KeyExportCanceled,
properties: {
@ -143,24 +192,27 @@ export default class ExportPrivateKeyModal extends Component {
hideModal();
}}
>
{this.context.t('cancel')}
{t('cancel')}
</Button>
)}
{privateKey ? (
<Button
type={BUTTON_VARIANT.PRIMARY}
size={BUTTON_SIZES.LG}
width={BLOCK_SIZES.FULL}
onClick={() => {
hideModal();
}}
type="primary"
large
className="export-private-key-modal__button"
>
{this.context.t('done')}
{t('done')}
</Button>
) : (
<Button
type={BUTTON_VARIANT.PRIMARY}
size={BUTTON_SIZES.LG}
width={BLOCK_SIZES.HALF}
onClick={() => {
this.context.trackEvent({
trackEvent({
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.KeyExportRequested,
properties: {
@ -168,61 +220,27 @@ export default class ExportPrivateKeyModal extends Component {
},
});
this.exportAccountAndGetPrivateKey(this.state.password, address);
exportAccountAndGetPrivateKey(password, address);
}}
type="primary"
large
className="export-private-key-modal__button"
disabled={!this.state.password}
disabled={!password}
>
{this.context.t('confirm')}
{t('confirm')}
</Button>
)}
</div>
);
}
</Box>
</AccountModalContainer>
);
};
render() {
const {
selectedIdentity,
warning,
showAccountDetailModal,
hideModal,
previousModalState,
} = this.props;
const { name, address } = selectedIdentity;
ExportPrivateKeyModal.propTypes = {
exportAccount: PropTypes.func.isRequired,
selectedIdentity: PropTypes.object.isRequired,
warning: PropTypes.node,
showAccountDetailModal: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired,
clearAccountDetails: PropTypes.func.isRequired,
previousModalState: PropTypes.string,
};
const { privateKey, showWarning } = this.state;
return (
<AccountModalContainer
className="export-private-key-modal"
selectedIdentity={selectedIdentity}
showBackButton={previousModalState === 'ACCOUNT_DETAILS'}
backButtonAction={() => showAccountDetailModal()}
>
<span className="export-private-key-modal__account-name">{name}</span>
<div className="ellip-address-wrapper">
{toChecksumHexAddress(address)}
</div>
<div className="export-private-key-modal__divider" />
<span className="export-private-key-modal__body-title">
{this.context.t('showPrivateKeys')}
</span>
<div className="export-private-key-modal__password">
{this.renderPasswordLabel(privateKey)}
{this.renderPasswordInput(privateKey)}
{showWarning && warning ? (
<span className="export-private-key-modal__password--error">
{warning}
</span>
) : null}
</div>
<div className="export-private-key-modal__password--warning">
{this.context.t('privateKeyWarning')}
</div>
{this.renderButtons(privateKey, address, hideModal)}
</AccountModalContainer>
);
}
}
export default withModalProps(ExportPrivateKeyModal);

View File

@ -0,0 +1,130 @@
import { fireEvent, waitFor } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import React from 'react';
import thunk from 'redux-thunk';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import ExportPrivateKeyModal from '.';
const mockAddress = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc';
const mockPrivateKey = 'mock private key';
const mockExportAccount = jest.fn().mockResolvedValue(mockPrivateKey);
const mockClearAccountDetail = jest.fn();
const mockHideWarning = jest.fn();
jest.mock('../../../../store/actions', () => ({
exportAccount: () => mockExportAccount,
clearAccountDetails: () => mockClearAccountDetail,
hideWarning: () => mockHideWarning,
}));
describe('Export Private Key Modal', () => {
const state = {
metamask: {
selectedAddress: mockAddress,
identities: {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
name: 'Test Account',
},
},
providerConfig: {
type: 'rpc',
chainId: '0x5',
ticker: 'ETH',
id: 'testNetworkConfigurationId',
},
},
appState: {
warning: null,
previousModalState: {
name: null,
},
isLoading: false,
accountDetail: {
privateKey: null,
},
modal: {
modalState: {},
previousModalState: {
name: null,
},
},
},
};
const mockStore = configureMockStore([thunk])(state);
it('renders export private key modal', () => {
const { queryByText } = renderWithProvider(
<ExportPrivateKeyModal />,
mockStore,
);
const title = queryByText('Show Private Keys');
expect(title).toBeInTheDocument();
const warning = queryByText(
'Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account.',
);
expect(warning).toBeInTheDocument();
expect(queryByText(mockPrivateKey)).not.toBeInTheDocument();
});
it('renders hold to reveal after entering password', async () => {
const { queryByText, getByPlaceholderText } = renderWithProvider(
<ExportPrivateKeyModal />,
mockStore,
);
const nextButton = queryByText('Confirm');
expect(nextButton).toBeInTheDocument();
const input = getByPlaceholderText('Enter password');
fireEvent.change(input, {
target: { value: 'password' },
});
fireEvent.click(nextButton);
await waitFor(() => {
expect(mockExportAccount).toHaveBeenCalled();
expect(queryByText('Keep your private key safe')).toBeInTheDocument();
});
});
it('provides password after passing hold to reveal', async () => {
const { queryByText, getByLabelText, getByText, getByPlaceholderText } =
renderWithProvider(<ExportPrivateKeyModal />, mockStore);
const nextButton = queryByText('Confirm');
expect(nextButton).toBeInTheDocument();
const input = getByPlaceholderText('Enter password');
fireEvent.change(input, {
target: { value: 'password' },
});
fireEvent.click(nextButton);
await waitFor(() => {
expect(mockExportAccount).toHaveBeenCalled();
expect(queryByText('Keep your private key safe')).toBeInTheDocument();
});
const holdButton = getByText('Hold to reveal Private Key');
expect(holdButton).toBeInTheDocument();
fireEvent.mouseDown(holdButton);
const circle = getByLabelText('hold to reveal circle locked');
fireEvent.transitionEnd(circle);
const circleUnlocked = getByLabelText('hold to reveal circle unlocked');
fireEvent.animationEnd(circleUnlocked);
await waitFor(() => {
expect(queryByText('Show Private Keys')).toBeInTheDocument();
expect(queryByText('Done')).toBeInTheDocument();
expect(queryByText(mockPrivateKey)).toBeInTheDocument();
});
});
});

View File

@ -1,10 +1,32 @@
import React from 'react';
import ExportPrivateKeyModal from '.';
import { Provider } from 'react-redux';
import testData from '../../../../../.storybook/test-data';
import configureStore from '../../../../store/store';
import ExportPrivateKeyModal from './export-private-key-modal.component';
// Using Test Data For Redux
const store = configureStore(testData);
export default {
title: 'Components/App/Modals/ExportPrivateKeyModal',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
argsTypes: {
exportAccount: { action: 'exportAccount' },
},
};
export const DefaultStory = () => <ExportPrivateKeyModal />;
export const DefaultStory = () => {
return (
<ExportPrivateKeyModal
// mock actions
exportAccount={() => {
return 'mockPrivateKey';
}}
selectedIdentity={
testData.metamask.identities[testData.metamask.selectedAddress]
}
/>
);
};
DefaultStory.storyName = 'Default';

View File

@ -59,7 +59,8 @@ describe('Export PrivateKey Modal', () => {
mockStore,
);
const passwordInput = queryByTestId('password-input');
const passwordInput =
queryByTestId('password-input').querySelector('input');
const passwordInputEvent = {
target: {

View File

@ -1,11 +1,4 @@
.export-private-key-modal {
&__body-title {
@include H4;
margin-top: 16px;
margin-bottom: 16px;
}
&__divider {
width: 100%;
height: 1px;
@ -13,93 +6,18 @@
background-color: var(--color-border-default);
}
&__account-name {
@include H4;
margin-top: 9px;
}
&__password {
display: flex;
flex-direction: column;
}
&__password-label,
&__password--error {
@include H6;
color: var(--color-text-default);
margin-bottom: 10px;
}
&__password--error {
color: var(--color-error-default);
margin-bottom: 0;
}
&__password-input {
@include Paragraph;
padding: 10px 0 13px 17px;
width: 291px;
height: 44px;
background: var(--color-background-default);
color: var(--color-text-default);
border: 1px solid var(--color-border-default);
}
&__password::-webkit-input-placeholder {
color: var(--color-text-muted);
}
&__password--warning {
@include H7;
border-radius: 8px;
background-color: var(--color-error-muted);
font-weight: 500;
color: var(--color-text-default);
border: 1px solid var(--color-error-default);
width: 292px;
padding: 9px 15px;
margin-top: 18px;
}
&__private-key-display {
@include Paragraph;
height: 80px;
width: 291px;
border: 1px solid var(--color-border-default);
border-radius: 2px;
color: var(--color-error-default);
padding: 9px 13px 8px;
overflow: hidden;
overflow-wrap: break-word;
}
&__buttons {
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
padding: 0 25px;
}
&__button {
margin-top: 17px;
width: 141px;
min-width: initial;
}
&__button--cancel {
margin-right: 15px;
}
.ellip-address-wrapper {
border: 1px solid var(--color-border-default);
padding: 5px 10px;
margin-top: 7px;
max-width: 286px;
direction: ltr;
overflow: hidden;

View File

@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
BLOCK_SIZES,
FLEX_DIRECTION,
DISPLAY,
AlignItems,
Color,
TextVariant,
} from '../../../../helpers/constants/design-system';
import Box from '../../../ui/box';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { Label, TEXT_FIELD_TYPES, TextField } from '../../../component-library';
const PasswordInput = ({ setPassword }) => {
const t = useI18nContext();
return (
<Box
width={BLOCK_SIZES.FULL}
flexDirection={FLEX_DIRECTION.COLUMN}
display={DISPLAY.FLEX}
alignItems={AlignItems.flexStart}
paddingLeft={5}
paddingRight={5}
>
<Label
color={Color.textDefault}
marginBottom={2}
variant={TextVariant.bodySm}
>
{t('typePassword')}
</Label>
<TextField
width={BLOCK_SIZES.FULL}
placeholder={t('enterPassword')}
type={TEXT_FIELD_TYPES.PASSWORD}
className="export-private-key-modal__password-input"
onChange={(event) => setPassword(event.target.value)}
data-testid="password-input"
/>
</Box>
);
};
PasswordInput.propTypes = {
setPassword: PropTypes.func.isRequired,
};
export default PasswordInput;

View File

@ -0,0 +1,81 @@
import copyToClipboard from 'copy-to-clipboard';
import { stripHexPrefix } from 'ethereumjs-util';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import Box from '../../../ui/box';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsEventKeyType,
} from '../../../../../shared/constants/metametrics';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
import {
BLOCK_SIZES,
BorderStyle,
BorderColor,
BorderRadius,
AlignItems,
DISPLAY,
Color,
FLEX_DIRECTION,
TextVariant,
} from '../../../../helpers/constants/design-system';
import { Label } from '../../../component-library';
const PrivateKeyDisplay = ({ privateKey }) => {
const trackEvent = useContext(MetaMetricsContext);
const t = useI18nContext();
const plainKey = stripHexPrefix(privateKey);
return (
<Box
width={BLOCK_SIZES.FULL}
flexDirection={FLEX_DIRECTION.COLUMN}
display={DISPLAY.FLEX}
alignItems={AlignItems.flexStart}
paddingLeft={4}
paddingRight={4}
>
<Label
color={Color.textDefault}
marginBottom={2}
variant={TextVariant.bodySm}
>
{t('copyPrivateKey')}
</Label>
<Box
className="export-private-key-modal__private-key-display"
width={BLOCK_SIZES.FULL}
borderStyle={BorderStyle.solid}
borderColor={BorderColor.borderDefault}
borderRadius={BorderRadius.XS}
borderWidth={1}
padding={[2, 3, 2]}
color={Color.errorDefault}
onClick={() => {
copyToClipboard(plainKey);
trackEvent(
{
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.KeyExportCopied,
properties: {
key_type: MetaMetricsEventKeyType.Pkey,
copy_method: 'clipboard',
},
},
{},
);
}}
>
{plainKey}
</Box>
</Box>
);
};
PrivateKeyDisplay.propTypes = {
privateKey: PropTypes.string.isRequired,
};
export default PrivateKeyDisplay;

View File

@ -5,6 +5,7 @@ import Box from '../../../ui/box';
import {
Text,
Button,
BUTTON_SIZES,
BUTTON_VARIANT,
ButtonIcon,
IconName,
@ -27,52 +28,80 @@ import {
MetaMetricsEventName,
} from '../../../../../shared/constants/metametrics';
const HoldToRevealModal = ({ onLongPressed, hideModal }) => {
const HoldToRevealModal = ({
onLongPressed,
hideModal,
willHide = true,
holdToRevealType = 'SRP',
}) => {
const t = useI18nContext();
const holdToRevealTitle =
holdToRevealType === 'SRP'
? 'holdToRevealSRPTitle'
: 'holdToRevealPrivateKeyTitle';
const holdToRevealButton =
holdToRevealType === 'SRP' ? 'holdToRevealSRP' : 'holdToRevealPrivateKey';
const trackEvent = useContext(MetaMetricsContext);
const unlock = () => {
onLongPressed();
hideModal();
if (willHide) {
hideModal();
}
};
const handleCancel = () => {
hideModal();
};
return (
<Box
className="hold-to-reveal-modal"
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JustifyContent.flexStart}
padding={6}
>
const renderHoldToRevealPrivateKeyContent = () => {
return (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
flexDirection={FLEX_DIRECTION.COLUMN}
gap={4}
marginBottom={6}
>
<Text variant={TextVariant.headingSm}>{t('holdToRevealTitle')}</Text>
<ButtonIcon
className="hold-to-reveal-modal__close"
iconName={IconName.Close}
size={Size.SM}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.SrpHoldToRevealCloseClicked,
properties: {
key_type: MetaMetricsEventKeyType.Srp,
},
});
handleCancel();
}}
ariaLabel={t('close')}
/>
<Text variant={TextVariant.bodyMd}>
{t('holdToRevealContentPrivateKey1', [
<Text
key="hold-to-reveal-2"
variant={TextVariant.bodyMdBold}
as="span"
>
{t('holdToRevealContentPrivateKey2')}
</Text>,
])}
</Text>
<Text variant={TextVariant.bodyMdBold}>
{t('holdToRevealContent3', [
<Text
key="hold-to-reveal-4"
variant={TextVariant.bodyMd}
as="span"
display={DISPLAY.INLINE}
>
{t('holdToRevealContent4')}
</Text>,
<Button
key="hold-to-reveal-5"
variant={BUTTON_VARIANT.LINK}
size={BUTTON_SIZES.INHERIT}
href={ZENDESK_URLS.NON_CUSTODIAL_WALLET}
target="_blank"
rel="noopener noreferrer"
>
{t('holdToRevealContent5')}
</Button>,
])}
</Text>
</Box>
);
};
const renderHoldToRevealSRPContent = () => {
return (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
@ -113,8 +142,49 @@ const HoldToRevealModal = ({ onLongPressed, hideModal }) => {
])}
</Text>
</Box>
);
};
return (
<Box
className="hold-to-reveal-modal"
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JustifyContent.flexStart}
padding={6}
>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
marginBottom={6}
>
<Text variant={TextVariant.headingSm}>{t(holdToRevealTitle)}</Text>
{willHide && (
<ButtonIcon
className="hold-to-reveal-modal__close"
iconName={IconName.Close}
size={Size.SM}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.SrpHoldToRevealCloseClicked,
properties: {
key_type: MetaMetricsEventKeyType.Srp,
},
});
handleCancel();
}}
ariaLabel={t('close')}
/>
)}
</Box>
{holdToRevealType === 'SRP'
? renderHoldToRevealSRPContent()
: renderHoldToRevealPrivateKeyContent()}
<HoldToRevealButton
buttonText={t('holdToReveal')}
buttonText={t(holdToRevealButton)}
onLongPressed={unlock}
marginLeft="auto"
marginRight="auto"
@ -127,6 +197,8 @@ HoldToRevealModal.propTypes = {
// The function to be executed after the hold to reveal long press has been completed
onLongPressed: PropTypes.func.isRequired,
hideModal: PropTypes.func,
willHide: PropTypes.bool,
holdToRevealType: PropTypes.oneOf(['SRP', 'PrivateKey']).isRequired,
};
export default withModalProps(HoldToRevealModal);

View File

@ -10,7 +10,7 @@ import {
holdToRevealContent3,
holdToRevealContent4,
holdToRevealContent5,
holdToRevealTitle,
holdToRevealSRPTitle,
} from '../../../../../app/_locales/en/messages.json';
import {
MetaMetricsEventCategory,
@ -25,8 +25,6 @@ describe('Hold to Reveal Modal', () => {
const onLongPressStub = jest.fn();
const hideModalStub = jest.fn();
global.platform = { openTab: jest.fn() };
afterEach(() => {
jest.resetAllMocks();
});
@ -36,6 +34,7 @@ describe('Hold to Reveal Modal', () => {
<HoldToRevealModal
onLongPressed={onLongPressStub}
hideModal={hideModalStub}
holdToRevealType="SRP"
/>,
mockStore,
);
@ -43,7 +42,7 @@ describe('Hold to Reveal Modal', () => {
const holdButton = getByText('Hold to reveal SRP');
expect(holdButton).toBeInTheDocument();
const warningTitle = getByText(holdToRevealTitle.message);
const warningTitle = getByText(holdToRevealSRPTitle.message);
expect(warningTitle).toBeInTheDocument();
const warningText1 = getByText(
holdToRevealContent1.message.replace(' $1', ''),
@ -83,12 +82,12 @@ describe('Hold to Reveal Modal', () => {
);
const holdButton = getByText('Hold to reveal SRP');
const circleLocked = queryByLabelText('circle-locked');
const circleLocked = queryByLabelText('hold to reveal circle locked');
fireEvent.mouseDown(holdButton);
fireEvent.transitionEnd(circleLocked);
const circleUnlocked = queryByLabelText('circle-unlocked');
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
fireEvent.animationEnd(circleUnlocked);
await waitFor(() => {
@ -112,8 +111,8 @@ describe('Hold to Reveal Modal', () => {
fireEvent.click(holdButton);
const circleLocked = queryByLabelText('circle-locked');
const circleUnlocked = queryByLabelText('circle-unlocked');
const circleLocked = queryByLabelText('hold to reveal circle locked');
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
await waitFor(() => {
expect(circleLocked).toBeInTheDocument();
@ -163,12 +162,12 @@ describe('Hold to Reveal Modal', () => {
);
const holdButton = getByText('Hold to reveal SRP');
const circleLocked = queryByLabelText('circle-locked');
const circleLocked = queryByLabelText('hold to reveal circle locked');
fireEvent.mouseDown(holdButton);
fireEvent.transitionEnd(circleLocked);
const circleUnlocked = queryByLabelText('circle-unlocked');
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
fireEvent.animationEnd(circleUnlocked);
await waitFor(() => {

View File

@ -91,6 +91,7 @@ const RevealSeedPage = () => {
setCompletedLongPress(true);
setScreen(REVEAL_SEED_SCREEN);
},
holdToRevealType: 'SRP',
}),
);
})

View File

@ -214,12 +214,12 @@ describe('Reveal Seed Page', () => {
});
const holdButton = getByText('Hold to reveal SRP');
const circleLocked = queryByLabelText('circle-locked');
const circleLocked = queryByLabelText('hold to reveal circle locked');
fireEvent.mouseDown(holdButton);
fireEvent.transitionEnd(circleLocked);
const circleUnlocked = queryByLabelText('circle-unlocked');
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
fireEvent.animationEnd(circleUnlocked);
await waitFor(() => {