Skip to content

Commit

Permalink
Add PasswordProtect for SeedPhrase and PrivateKey pages
Browse files Browse the repository at this point in the history
Fixed Header and ChangePasscode styles
  • Loading branch information
Ivan-Mahda committed Oct 29, 2024
1 parent f9f2c7c commit c43caef
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
margin: rem(1px);
width: rem(106px);
height: rem(106px);
background-color: $color-main-bg;

&.wide {
width: rem(160px);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ export default function Header({
accountOpen,
setAccountOpen,
}: HeaderProps) {
if (menuOpen) {
setAccountOpen(false);
}
useEffect(() => {
if (menuOpen) {
setAccountOpen(false);
}
if (menuOpen || accountOpen) {
background?.classList.add('fade-bg');
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
.change-passcode-x {
.divider {
display: block;
margin: rem(16px) 0 rem(24px) 0;
border-bottom: 1px solid rgba($color-white, 0.1);
.change-passcode-page__form {
display: flex;
flex-direction: column;
gap: rem(8px);

.divider {
display: block;
margin: rem(8px) 0 rem(16px) 0;
border-bottom: 1px solid rgba($color-white, 0.1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function ChangePasscode() {
<Page.Top heading={t('changePasscode')} />
<Page.Main>
<Form
id="test-form"
id="change-password-form"
onSubmit={handleSubmit}
className="change-passcode-page__form"
formMethods={form}
Expand Down Expand Up @@ -98,7 +98,7 @@ export default function ChangePasscode() {
</Page.Main>
<Page.Footer>
<Button.Main
form="test-form"
form="change-password-form"
type="submit"
label={t('changePasscode')}
disabled={form.formState.isSubmitting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { NetworkConfiguration } from '@shared/storage/types';
import { getNet } from '@shared/utils/network-helpers';
import { copyToClipboard } from '@popup/popupX/shared/utils/helpers';
import { Navigate, useParams } from 'react-router-dom';
import { withPasswordProtected } from '@popup/popupX/shared/utils/hoc';

type CredentialKeys = {
threshold: number;
Expand Down Expand Up @@ -123,11 +124,17 @@ function PrivateKey({ address }: Props) {
);
}

export default function Loader() {
function Loader() {
const params = useParams();
if (!('account' in params) || params.account === undefined) {
// No account address passed in the url.
return <Navigate to="../" />;
}
return <PrivateKey address={params.account} />;
}

export default withPasswordProtected(Loader, {
headingKey: 'privateKey.accountPrivateKey',
pageInfoKey: 'privateKey.passwordDescription',
submitKey: 'privateKey.showPrivateKey',
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const t = {
'Your account private key is the access key to all the funds in your account. Copy it and keep it safe. To avoid mistakes, do not write it down manually.',
export: 'Export',
copyKey: 'Copy account private key',
passwordDescription: 'Please enter your passcode to show the private key',
showPrivateKey: 'Show private key',
};

export default t;
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,52 @@ import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import Card from '@popup/popupX/shared/Card';
import Copy from '@assets/svgX/copy.svg';
import { useAsyncMemo } from 'wallet-common-helpers';
import { decrypt } from '@shared/utils/crypto';
import { useAtomValue } from 'jotai';
import { encryptedSeedPhraseAtom, sessionPasscodeAtom } from '@popup/store/settings';
import { copyToClipboard } from '@popup/popupX/shared/utils/helpers';
import { withPasswordProtected } from '@popup/popupX/shared/utils/hoc';

const RECOVERY_PHRASE =
'meadow salad weather rural next promote fence mass leopard mail regret mushroom love coral viable layer lumber soft setup radar oppose miracle rural agree'.split(
' '
function SeedPhrase() {
const { t } = useTranslation('x', { keyPrefix: 'seedPhrase' });
const passcode = useAtomValue(sessionPasscodeAtom);
const encryptedSeed = useAtomValue(encryptedSeedPhraseAtom);

const seedPhrase = useAsyncMemo(
async () => {
if (encryptedSeed.loading || passcode.loading) {
return undefined;
}
if (encryptedSeed.value && passcode.value) {
return decrypt(encryptedSeed.value, passcode.value);
}
throw new Error('SeedPhrase should not be retrieved without unlocking the wallet.');
},
undefined,
[encryptedSeed.loading, passcode.loading]
);

export default function SeedPhrase() {
const { t } = useTranslation('x', { keyPrefix: 'seedPhrase' });
if (!seedPhrase) return null;

return (
<Page className="seed-phrase-x">
<Page.Top heading={t('seedPhrase')} />
<Page.Main>
<Text.Capture>{t('seedPhraseDescription')}</Text.Capture>
<Card>
{RECOVERY_PHRASE.map((word) => (
<Text.LabelRegular>{word}</Text.LabelRegular>
{seedPhrase.split(' ').map((word) => (
<Text.LabelRegular key={word}>{word}</Text.LabelRegular>
))}
</Card>
<Button.IconText icon={<Copy />} label={t('copy')} />
<Button.IconText icon={<Copy />} label={t('copy')} onClick={() => copyToClipboard(seedPhrase)} />
</Page.Main>
</Page>
);
}

export default withPasswordProtected(SeedPhrase, {
headingKey: 'seedPhrase.seedPhrase',
pageInfoKey: 'seedPhrase.passwordDescription',
submitKey: 'seedPhrase.showSeedPhrase',
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const t = {
seedPhraseDescription:
'Your seed phrase is the access key to all the funds in your wallet. If you forget it you will lose access to your wallet(s). Keep it somewhere safe.',
copy: 'Copy seed phrase',
passwordDescription: 'Please enter your passcode to show the seed phrase.',
showSeedPhrase: 'Show secret recovery phrase',
};

export default t;
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const Form = forwardRef(
const internal = useForm<V>({ defaultValues });
const methods = external ?? internal;

const submit = () => (onSubmit === undefined ? noOp : methods.handleSubmit(onSubmit));
const submit = onSubmit === undefined ? () => noOp : methods.handleSubmit(onSubmit);

return (
<FormProvider {...methods}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.confirm-password-x {
#confirm-password-form {
margin-top: rem(30px);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import { Validate } from 'react-hook-form';
import { useAtomValue } from 'jotai';
import { useTranslation } from 'react-i18next';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import Button from '@popup/popupX/shared/Button';
import FormPassword from '@popup/popupX/shared/Form/Password';
import Form from '@popup/popupX/shared/Form/Form';
import { sessionPasscodeAtom } from '@popup/store/settings';
import { useForm } from '@popup/shared/Form';
import { TranslationKeyX } from '@popup/shell/i18n/i18n';

type FormValues = {
currentPasscode: string;
};

export type PasswordProtectConfigType = {
headingKey: TranslationKeyX;
pageInfoKey: TranslationKeyX;
submitKey: TranslationKeyX;
};

type PasswordProtectProps = {
setPasswordConfirmed: (passwordConfirmed: boolean) => void;
config: PasswordProtectConfigType;
};

export default function PasswordProtect({
setPasswordConfirmed,
config: { headingKey, pageInfoKey, submitKey },
}: PasswordProtectProps) {
const { t: tUse } = useTranslation('x');
const t = (key: TranslationKeyX) => tUse(key) as unknown as string;
const { t: tPasscode } = useTranslation('x', { keyPrefix: 'sharedX.form.password' });
const passcode = useAtomValue(sessionPasscodeAtom);
const form = useForm<FormValues>();

const handleSubmit = () => {
setPasswordConfirmed(true);
};

function validateCurrentPasscode(): Validate<string> {
return (currentPasscode) => (currentPasscode !== passcode.value ? tPasscode('incorrectPasscode') : undefined);
}

return (
<Page className="confirm-password-x">
<Page.Top heading={t(headingKey)} />
<Page.Main>
<Text.MainRegular>{t(pageInfoKey)}</Text.MainRegular>
<Form id="confirm-password-form" onSubmit={handleSubmit} formMethods={form}>
{(f) => {
return (
<FormPassword
control={f.control}
name="currentPasscode"
label={tPasscode('currentPasscode')}
className="m-t-10"
rules={{
required: tPasscode('passcodeRequired'),
validate: validateCurrentPasscode(),
}}
/>
);
}}
</Form>
</Page.Main>
<Page.Footer>
<Button.Main
form="confirm-password-form"
type="submit"
label={t(submitKey)}
disabled={form.formState.isSubmitting}
/>
</Page.Footer>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './PasswordProtect';
export type { PasswordProtectConfigType } from './PasswordProtect';
3 changes: 3 additions & 0 deletions packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const t = {
weak: 'Weak',
medium: 'Medium',
strong: 'Strong',
incorrectPasscode: 'Incorrect passcode',
currentPasscode: 'Enter current passcode',
passcodeRequired: 'A passcode must be entered',
},
tokenAmount: {
token: {
Expand Down
16 changes: 15 additions & 1 deletion packages/browser-wallet/src/popup/popupX/shared/utils/hoc.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, { useState } from 'react';
import { useSelectedCredential } from '@popup/shared/utils/account-helpers';
import Loader from '@popup/popupX/shared/Loader';
import PasswordProtect, { PasswordProtectConfigType } from '@popup/popupX/shared/PasswordProtect';

export function withSelectedCredential<P extends object>(
Component: React.ComponentType<P>
Expand All @@ -16,3 +17,16 @@ export function withSelectedCredential<P extends object>(
}
return NewComponent;
}

export function withPasswordProtected(Component: React.ComponentType, config: PasswordProtectConfigType) {
function NewComponent() {
const [passwordConfirmed, setPasswordConfirmed] = useState(false);

if (!passwordConfirmed) {
return <PasswordProtect setPasswordConfirmed={setPasswordConfirmed} config={config} />;
}

return <Component />;
}
return NewComponent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generic type for iterating through nested object keys
export type ObjectPath<T extends object, D extends string = ''> = {
[K in keyof T]: `${D}${Exclude<K, symbol>}${'' | (T[K] extends object ? ObjectPath<T[K], '.'> : '')}`;
}[keyof T];
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
@import '../shared/Text/Text';
@import '../shared/Loader/Loader';
@import '../shared/IdCard/IdCard';
@import '../shared/PasswordProtect/PasswordProtect';
@import '../shared/Web3IdCard/Web3IdCard';
@import '../shared/Button/Button';
@import '../shared/ExternalLink/ExternalLink';
Expand Down
4 changes: 1 addition & 3 deletions packages/browser-wallet/src/popup/shell/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,7 @@ export default function Root() {

return (
<Provider>
<MemoryRouter
initialEntries={[uiStyle.value === UiStyle.Old ? '/account' : absoluteRoutes.settings.earn.path]}
>
<MemoryRouter initialEntries={[uiStyle.value === UiStyle.Old ? '/account' : absoluteRoutes.home.path]}>
<Scaling>
<Network>
<Theme>
Expand Down
2 changes: 2 additions & 0 deletions packages/browser-wallet/src/popup/shell/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import countries from 'i18n-iso-countries';
import { ObjectPath } from '@popup/popupX/shared/utils/typescriptHelpers';

import en from './locales/en';
import da from './locales/da';

countries.registerLocale(require('i18n-iso-countries/langs/en.json'));
countries.registerLocale(require('i18n-iso-countries/langs/da.json'));

export type TranslationKeyX = ObjectPath<typeof en.x>;
export const defaultNS: keyof typeof en = 'shared';
const ns: Array<keyof typeof en> = Object.keys(en) as Array<keyof typeof en>;

Expand Down

0 comments on commit c43caef

Please sign in to comment.