Skip to content

Commit

Permalink
Merge branch 'main' into coong/feature/sign-raw-message
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/ui/public/locales/en/translation.json
  • Loading branch information
sinzii committed Apr 8, 2023
2 parents 09560c1 + efb8f5b commit 3e7e0be
Show file tree
Hide file tree
Showing 18 changed files with 439 additions and 35 deletions.
5 changes: 4 additions & 1 deletion packages/keyring/src/Keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ export default class Keyring {
}
}

async getRawMnemonic(password: string): Promise<string> {
return await this.#decryptMnemonic(password);
}

async verifyPassword(password: string) {
await this.ensureWalletInitialized();
await this.#decryptMnemonic(password);
}

Expand Down
12 changes: 12 additions & 0 deletions packages/ui/public/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"15 minutes": "",
"30 minutes": "",
"5 minutes": "",
"A multichain crypto wallet for Polkadot & Kusama ecosystem": "A multichain crypto wallet <br /> for <strong>Polkadot & Kusama</strong> ecosystem",
"AccountNameRequired": "Account name is required",
"AccountNameUsed": "Account name is already picked",
Expand All @@ -8,20 +11,25 @@
"Address format": "",
"An application, self-identifying as request app name is requesting access your wallet from origin.": "An application, self-identifying as <strong>{{appName}}</strong> is requesting access your wallet from <strong>{{origin}}</strong>.",
"Approve Transaction": "",
"Auto-lock wallet after": "",
"Back": "",
"Backup secret recovery phrase": "",
"Cancel": "",
"Choose a name for your new account": "",
"Click to copy address": "",
"Close settings": "",
"Coming soon": "",
"Confirm wallet password": "",
"Connect": "",
"Copied!": "",
"Copy to clipboard": "",
"Create": "",
"Create New Wallet": "",
"Create new account": "",
"Create your first account now!": "",
"Dark": "",
"Deselect all": "",
"Enter your wallet password to continue": "",
"Finally, back up your secret recovery phrase": "",
"Finish": "",
"First, choose your wallet password": "",
Expand All @@ -37,6 +45,7 @@
"Language": "",
"Light": "",
"Lock the wallet": "",
"Make sure you are in a safe place.": "",
"My first account": "",
"New Account": "",
"New account name": "",
Expand Down Expand Up @@ -74,16 +83,19 @@
"UnknownRequestOrigin": "Unknown request origin",
"Unlock": "",
"Unlock your wallet": "",
"View Secret Recovery Phrase": "",
"Wallet Access Request": "",
"Wallet password": "",
"WalletLocked": "The wallet is locked, please unlock it first",
"Welcome back": "",
"Welcome to Coong": "",
"Welcome to Coong Wallet!": "",
"Write down the below 12 words and keep it in a safe place.": "",
"You are about to reveal the secret recovery phrase which give access to your accounts and funds.": "",
"You are approving a transaction with account": "",
"You are signing a message with account": "",
"Your password will be used to encrypt accounts as well as unlock the wallet, make sure to pick a strong & easy-to-remember password": "Your password will be used to <strong>encrypt accounts as well as unlock the wallet</strong>, make sure to pick a <strong>strong & easy-to-remember</strong> password.",
"Your wallet password": "",
"account(s) selected": "",
"bytes": "",
"from": "",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defaultNetwork } from '@coong/base';
import { initializeKeyring, newUser, PASSWORD, render, screen } from '__tests__/testUtils';
import { AutoLockTimerOptions } from 'components/shared/settings/AutoLockSelection';
import { AutoLockTimerOptions } from 'components/shared/settings/SettingsWalletDialog/AutoLockSelection';
import MainScreen from '../MainScreen';

vi.mock('react-router-dom', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useAsync, useCopyToClipboard } from 'react-use';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { Button, DialogContentText } from '@mui/material';
import { useWalletState } from 'providers/WalletStateProvider';
import { settingsDialogActions } from 'redux/slices/settings-dialog';
import { RootState } from 'redux/store';
import { Props } from 'types';

const ShowingSecretPhrase: FC<Props> = () => {
const { keyring } = useWalletState();
const { verifiedPassword } = useSelector((state: RootState) => state.settingsDialog);
const [secretPhrase, setSecretPhrase] = useState('');
const [_, copyToClipboard] = useCopyToClipboard();
const { t } = useTranslation();
const [copyButtonLabel, setCopyButtonLabel] = useState('Copy to clipboard');
const dispatch = useDispatch();

const doBack = () => {
dispatch(settingsDialogActions.resetState());
};

const doCopy = () => {
copyToClipboard(secretPhrase);
setCopyButtonLabel('Copied!');
setTimeout(() => setCopyButtonLabel('Copy to clipboard'), 5e3);
};

useAsync(async () => {
try {
setSecretPhrase(await keyring.getRawMnemonic(verifiedPassword!));
} catch (e: any) {
console.error(e.message);
}
});

return (
<>
<DialogContentText className='my-8 p-4 bg-black/10 dark:bg-white/15'>{secretPhrase}</DialogContentText>
<div className='mt-4 flex gap-4'>
<Button variant='text' onClick={doBack}>
{t<string>('Back')}
</Button>
<Button variant='contained' onClick={doCopy} startIcon={<ContentCopyIcon />} fullWidth>
{t<string>(copyButtonLabel)}
</Button>
</div>
</>
);
};

export default ShowingSecretPhrase;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ChangeEvent, FC, FormEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { Button, DialogContentText, TextField } from '@mui/material';
import EmptySpace from 'components/shared/misc/EmptySpace';
import { useWalletState } from 'providers/WalletStateProvider';
import { settingsDialogActions } from 'redux/slices/settings-dialog';
import { Props } from 'types';

const VerifyingPassword: FC<Props> = () => {
const { keyring } = useWalletState();
const [password, setPassword] = useState('');
const [validation, setValidation] = useState('');
const dispatch = useDispatch();
const { t } = useTranslation();

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setPassword(event.currentTarget.value);
setValidation('');
};

const doVerify = async (event: FormEvent) => {
event.preventDefault();
if (!password) {
return;
}

try {
await keyring.verifyPassword(password);
dispatch(settingsDialogActions.setVerifiedPassword(password));
} catch (e: any) {
setValidation(t<string>(e.message));
}
};

const doBack = () => {
dispatch(settingsDialogActions.resetState());
};

return (
<>
<DialogContentText className='mt-4 mb-4'>{t<string>('Enter your wallet password to continue')}</DialogContentText>
<form onSubmit={doVerify} noValidate>
<TextField
type='password'
value={password}
label={t<string>('Your wallet password')}
fullWidth
autoFocus
error={!!validation}
helperText={validation || <EmptySpace />}
onChange={handleChange}
/>
<div className='mt-2.5 flex gap-4'>
<Button variant='text' onClick={doBack}>
{t<string>('Back')}
</Button>
<Button type='submit' disabled={!password} fullWidth variant='contained'>
{t<string>('View Secret Recovery Phrase')}
</Button>
</div>
</form>
</>
);
};

export default VerifyingPassword;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Keyring from '@coong/keyring';
import { initializeKeyring, newUser, render, screen, UserEvent, waitFor } from '__tests__/testUtils';
import SettingsWalletButton from 'components/shared/settings/SettingsWalletButton';
import { SettingsDialogScreen } from 'types';

let user: UserEvent;
vi.spyOn(window, 'prompt').mockImplementation(() => '');
beforeEach(async () => {
user = newUser();

render(<SettingsWalletButton />, {
preloadedState: {
app: { seedReady: true, ready: true, locked: false },
settingsDialog: { settingsDialogScreen: SettingsDialogScreen.BackupSecretPhrase },
},
});

const settingsButton = screen.getByTitle('Open settings');
await user.click(settingsButton);
});

describe('when verifying password', () => {
it('should show the content of BackupSecretPhraseDialog', async () => {
await waitFor(() => {
expect(screen.getByText(/Backup secret recovery phrase/)).toBeInTheDocument();
expect(screen.getByText(/reveal the secret recovery phrase/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /View Secret Recovery Phrase/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Back/ })).toBeInTheDocument();
});
});

it('should enable `View Secret Recovery Phrase` button when typing password', async () => {
const passwordField = await screen.findByLabelText(/Your wallet password/);
await user.type(passwordField, 'password');

expect(await screen.findByRole('button', { name: /View Secret Recovery Phrase/ })).toBeEnabled();
});

it('should show error message on submitting incorrect password', async () => {
await initializeKeyring();

const passwordField = await screen.findByLabelText(/Your wallet password/);
await user.type(passwordField, 'incorrect-password');

const viewSecretPhraseButton = await screen.findByRole('button', { name: /View Secret Recovery Phrase/ });
await user.click(viewSecretPhraseButton);

expect(await screen.findByText(/Password incorrect/)).toBeInTheDocument();
});
});

describe('when showing secret phrase', () => {
let keyring: Keyring;
beforeEach(async () => {
keyring = await initializeKeyring();

const passwordField = await screen.findByLabelText(/Your wallet password/);
await user.type(passwordField, 'supersecretpassword');

const viewSecretPhraseButton = await screen.findByRole('button', { name: /View Secret Recovery Phrase/ });
await user.click(viewSecretPhraseButton);
});

it('should show `ShowingSecretPhrase` when submitting correct password', async () => {
const rawMnemonic = await keyring.getRawMnemonic('supersecretpassword');

expect(await screen.findByText(rawMnemonic)).toBeInTheDocument();
expect(await screen.findByRole('button', { name: /Copy to clipboard/ })).toBeInTheDocument();
expect(await screen.findByRole('button', { name: /Back/ })).toBeInTheDocument();
});

it('should show change button label to `Copied!` when clicking on `Copy to clipboard` button', async () => {
const copyToClipboardButton = await screen.findByRole('button', { name: /Copy to clipboard/ });
await user.click(copyToClipboardButton);

expect(await screen.findByRole('button', { name: /Copied!/ })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { Breadcrumbs, DialogContent, DialogContentText, Link, Typography } from '@mui/material';
import DialogTitle from 'components/shared/DialogTitle';
import ShowingSecretPhrase from 'components/shared/settings/BackupSecretPhraseDialog/ShowingSecretPhrase';
import VerifyingPassword from 'components/shared/settings/BackupSecretPhraseDialog/VerifyingPassword';
import { settingsDialogActions } from 'redux/slices/settings-dialog';
import { RootState } from 'redux/store';
import { Props } from 'types';

interface BackupSecretPhraseDialogProps extends Props {
onClose: () => void;
}

const BackupSecretPhraseDialog: FC<BackupSecretPhraseDialogProps> = ({ onClose }) => {
const { verifiedPassword } = useSelector((state: RootState) => state.settingsDialog);
const { t } = useTranslation();
const dispatch = useDispatch();

return (
<>
<DialogTitle onClose={onClose}>
<Breadcrumbs>
<Link
className='cursor-pointer'
underline='hover'
color='inherit'
variant='h6'
onClick={() => dispatch(settingsDialogActions.resetState())}>
{t<string>('Settings')}
</Link>
<Typography color='text.primary' variant='h6'>
{t<string>('Backup secret recovery phrase')}
</Typography>
</Breadcrumbs>
</DialogTitle>
<DialogContent className='pb-8'>
<DialogContentText>
{t<string>(
'You are about to reveal the secret recovery phrase which give access to your accounts and funds.',
)}{' '}
<strong>{t<string>('Make sure you are in a safe place.')}</strong>
</DialogContentText>
{verifiedPassword ? <ShowingSecretPhrase /> : <VerifyingPassword />}
</DialogContent>
</>
);
};

export default BackupSecretPhraseDialog;
Loading

0 comments on commit 3e7e0be

Please sign in to comment.