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:
parent
82f01a6b44
commit
0306422bbf
6
app/_locales/de/messages.json
generated
6
app/_locales/de/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/el/messages.json
generated
6
app/_locales/el/messages.json
generated
@ -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": "Αγνόηση όλων"
|
||||
},
|
||||
|
24
app/_locales/en/messages.json
generated
24
app/_locales/en/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/es/messages.json
generated
6
app/_locales/es/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/fr/messages.json
generated
6
app/_locales/fr/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/hi/messages.json
generated
6
app/_locales/hi/messages.json
generated
@ -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": "सभी को अनदेखा करें"
|
||||
},
|
||||
|
6
app/_locales/id/messages.json
generated
6
app/_locales/id/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/ja/messages.json
generated
6
app/_locales/ja/messages.json
generated
@ -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": "すべて無視"
|
||||
},
|
||||
|
6
app/_locales/ko/messages.json
generated
6
app/_locales/ko/messages.json
generated
@ -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": "모두 무시"
|
||||
},
|
||||
|
6
app/_locales/pt/messages.json
generated
6
app/_locales/pt/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/ru/messages.json
generated
6
app/_locales/ru/messages.json
generated
@ -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": "Игнорировать все"
|
||||
},
|
||||
|
6
app/_locales/tl/messages.json
generated
6
app/_locales/tl/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/tr/messages.json
generated
6
app/_locales/tr/messages.json
generated
@ -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"
|
||||
},
|
||||
|
6
app/_locales/vi/messages.json
generated
6
app/_locales/vi/messages.json
generated
@ -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ả"
|
||||
},
|
||||
|
6
app/_locales/zh_CN/messages.json
generated
6
app/_locales/zh_CN/messages.json
generated
@ -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": "忽略所有"
|
||||
},
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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';
|
||||
|
@ -59,7 +59,8 @@ describe('Export PrivateKey Modal', () => {
|
||||
mockStore,
|
||||
);
|
||||
|
||||
const passwordInput = queryByTestId('password-input');
|
||||
const passwordInput =
|
||||
queryByTestId('password-input').querySelector('input');
|
||||
|
||||
const passwordInputEvent = {
|
||||
target: {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
|
@ -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(() => {
|
||||
|
@ -91,6 +91,7 @@ const RevealSeedPage = () => {
|
||||
setCompletedLongPress(true);
|
||||
setScreen(REVEAL_SEED_SCREEN);
|
||||
},
|
||||
holdToRevealType: 'SRP',
|
||||
}),
|
||||
);
|
||||
})
|
||||
|
@ -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(() => {
|
||||
|
Loading…
Reference in New Issue
Block a user