Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more options to backup wallet #68

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/ui/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"Cannot export QR code image": "",
"Change Password": "",
"Change Wallet Password": "",
"Choose a method to back up your wallet": "",
"Choose a name and enter your password to create a new account": "",
"Choose a new name for your account": "",
"Choose your new password to continue": "",
Expand Down Expand Up @@ -67,6 +68,12 @@
"Done": "",
"Download JSON File": "",
"Download QR Code Image": "",
"Downloads JSON File": "",
"Downloads QR Code": "",
"Downloads an encrypted JSON file and uploads it to the cloud (iCloud, Google Drive, ...)": "",
"Downloads an encrypted QR Code and uploads it to the cloud (iCloud, Google Drive, ...)": "",
"Downloads the below JSON file and upload it to the cloud (iCloud, Google Drive, ...), you can restore your wallet by uploading this JSON file to Coong Wallet later at any time with your wallet password.": "",
"Downloads the below QR code image and upload it to the cloud (iCloud, Google Drive, ...), you can restore your wallet by scanning or uploading this QR Code to Coong Wallet later at any time with your wallet password.": "",
"Drop the file here...": "",
"Enter wallet password of this backup to continue": "",
"Enter wallet password of the backup to import your wallet": "",
Expand All @@ -80,13 +87,13 @@
"Export your {{object}} on a different device and scan the QR code on the screen to transfer your {{object}}.": "",
"File reading was aborted!": "",
"File reading was failed!": "",
"Finally, back up your secret recovery phrase": "",
"Finally, back up your wallet": "",
"Finally, enter your wallet password to complete importing the account": "",
"Finish": "",
"First, choose your wallet password": "",
"First, enter your secret recovery phrase": "",
"Forgot your password?": "",
"I have backed up my recovery phrase": "",
"I have backed up my wallet": "",
"IMPORTED": "",
"If you open this page by accident, it's safe to close it now.": "",
"Import Account": "",
Expand Down Expand Up @@ -200,6 +207,7 @@
"WalletLocked": "The wallet is locked, please unlock it first",
"Welcome back": "",
"Welcome to Coong Wallet": "",
"Write down 12 random words and keep them in a safe place!": "",
"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. Make sure you are in a safe place.": "You are about to reveal the secret recovery phrase which give access to your accounts and funds. <strong>Make sure you are in a safe place.</strong>",
"You are approving a transaction with account": "",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed packages/ui/src/assets/images/coong-logo.png
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { AccountBackup } from '@coong/keyring/types';
import { TabContext, TabList, TabPanel } from '@mui/lab';
import { Dialog, DialogContent, Tab } from '@mui/material';
import DialogTitle from 'components/shared/DialogTitle';
import JsonFile from 'components/shared/export/JsonFile';
import QrCode from 'components/shared/export/QrCode';
import JsonFileWithExportInstruction from 'components/shared/export/JsonFileWithExportInstruction';
import QrCodeWithExportInstruction from 'components/shared/export/QrCodeWithExportInstruction';
import VerifyingPasswordForm from 'components/shared/forms/VerifyingPasswordForm';
import useDialog from 'hooks/useDialog';
import useRegisterEvent from 'hooks/useRegisterEvent';
Expand Down Expand Up @@ -68,10 +68,10 @@ export default function ExportAccountDialog(): JSX.Element {
<Tab label={t<string>(ExportAccountMethod.JSON)} value={ExportAccountMethod.JSON} />
</TabList>
<TabPanel value={ExportAccountMethod.QRCode} className='p-0'>
<QrCode value={backup} object={TransferableObject.Account} />
<QrCodeWithExportInstruction value={backup} object={TransferableObject.Account} />
</TabPanel>
<TabPanel value={ExportAccountMethod.JSON} className='p-0'>
<JsonFile value={backup} object={TransferableObject.Account} />
<JsonFileWithExportInstruction value={backup} object={TransferableObject.Account} />
</TabPanel>
</TabContext>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { encodeAddress } from '@polkadot/util-crypto';
import { Dialog, DialogContent } from '@mui/material';
import CoongLogo from 'assets/images/coong-logo-circle.png';
import AccountAddress from 'components/pages/Accounts/AccountAddress';
import DialogTitle from 'components/shared/DialogTitle';
import NetworksSelection from 'components/shared/NetworksSelection';
Expand Down Expand Up @@ -51,6 +52,12 @@ export default function ShowAddressQrCodeDialog(): JSX.Element {
size={size}
title={t<string>('Account Address QR Code')}
className='p-4'
imageSettings={{
src: CoongLogo,
height: 45,
width: 45,
excavate: false,
}}
/>
<AccountAddress address={address} className='text-sm' />
</DialogContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { QrCode } from '@mui/icons-material';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import KeyIcon from '@mui/icons-material/Key';
import { Button, List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import BackupWallet from 'components/pages/SetupWallet/NewWallet/BackupWallet';
import { setupWalletActions } from 'redux/slices/setup-wallet';
import { NewWalletScreenStep, WalletRecoveryMethod } from 'types';

const BackupWalletOptions = [
{
method: WalletRecoveryMethod.SecretRecoveryPhrase,
icon: <KeyIcon />,
description: 'Write down 12 random words and keep them in a safe place!',
},
{
method: WalletRecoveryMethod.QrCode,
icon: <QrCode />,
description: 'Downloads an encrypted QR Code and uploads it to the cloud (iCloud, Google Drive, ...)',
},
{
method: WalletRecoveryMethod.JsonFile,
icon: <InsertDriveFileIcon />,
description: 'Downloads an encrypted JSON file and uploads it to the cloud (iCloud, Google Drive, ...)',
},
];

export default function BackupMethodSelection(): JSX.Element {
const { t } = useTranslation();
const dispatch = useDispatch();
const [method, setMethod] = useState<WalletRecoveryMethod>();

const doSelectMethod = (method: WalletRecoveryMethod) => {
setMethod(method);
};

const resetMethod = () => {
setMethod(undefined);
};

const goBack = () => {
dispatch(setupWalletActions.setNewWalletScreenStep(NewWalletScreenStep.ChooseWalletPassword));
dispatch(setupWalletActions.setSecretPhrase(undefined));
};

if (method) {
return <BackupWallet method={method} resetMethod={resetMethod} />;
}

return (
<>
<h3>{t<string>('Finally, back up your wallet')}</h3>
<p>{t<string>('Choose a method to back up your wallet')}</p>
<List>
{BackupWalletOptions.map(({ method, icon, description }) => (
<ListItemButton key={method} onClick={() => doSelectMethod(method)}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={t<string>(method)} secondary={t<string>(description)} />
</ListItemButton>
))}
</List>
<Button onClick={goBack} color='gray' variant='text'>
{t<string>('Back')}
</Button>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,63 +1,25 @@
import { ChangeEvent, FC, FormEvent, useState } from 'react';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useEffectOnce } from 'react-use';
import { generateMnemonic } from '@polkadot/util-crypto/mnemonic/bip39';
import { LoadingButton } from '@mui/lab';
import { Button, Checkbox, FormControlLabel, FormGroup } from '@mui/material';
import SecretRecoveryPhrase from 'components/shared/SecretRecoveryPhrase';
import useSetupWallet from 'hooks/wallet/useSetupWallet';
import { setupWalletActions } from 'redux/slices/setup-wallet';
import { RootState } from 'redux/store';
import { Props, NewWalletScreenStep } from 'types';

const BackupSecretRecoveryPhrase: FC<Props> = ({ className = '' }) => {
import { Props } from 'types';

interface BackupSecretRecoveryPhraseProps extends Props {
secretPhrase: string;
title?: string;
}

const BackupSecretRecoveryPhrase: FC<BackupSecretRecoveryPhraseProps> = ({
secretPhrase,
title = '',
className = '',
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const { password } = useSelector((state: RootState) => state.setupWallet);
const [checked, setChecked] = useState<boolean>(false);
const [secretPhrase, setSecretPhrase] = useState<string>('');
const { setup, loading } = useSetupWallet({ secretPhrase, password });

useEffectOnce(() => {
setSecretPhrase(generateMnemonic(12));
});

const doSetupWallet = (e: FormEvent) => {
e.preventDefault();

setup();
};

const back = () => {
dispatch(setupWalletActions.setNewWalletScreenStep(NewWalletScreenStep.ChooseWalletPassword));
};

const handleCheckbox = (event: ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
};

return (
<div className={className}>
<h3>{t<string>('Finally, back up your secret recovery phrase')}</h3>
<h3>{t<string>(title)}</h3>
1cedrus marked this conversation as resolved.
Show resolved Hide resolved
<p className='mb-4'>{t<string>('Write down the below 12 words and keep it in a safe place.')}</p>
<form className='flex flex-col gap-2' noValidate autoComplete='off' onSubmit={doSetupWallet}>
<SecretRecoveryPhrase secretPhrase={secretPhrase} />
<FormGroup>
<FormControlLabel
control={<Checkbox checked={checked} onChange={handleCheckbox} disabled={loading} />}
label={t<string>('I have backed up my recovery phrase')}
/>
</FormGroup>
<div className='flex flex-row gap-4'>
<Button variant='text' onClick={back} disabled={loading}>
{t<string>('Back')}
</Button>
<LoadingButton type='submit' fullWidth disabled={!checked} loading={loading} variant='contained' size='large'>
{t<string>('Finish')}
</LoadingButton>
</div>
</form>
<SecretRecoveryPhrase secretPhrase={secretPhrase} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { ChangeEvent, FormEvent, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useEffectOnce } from 'react-use';
import { generateMnemonic } from '@polkadot/util-crypto/mnemonic/bip39';
import { WalletBackup } from '@coong/keyring/types';
import { LoadingButton } from '@mui/lab';
import { Button, Checkbox, FormControlLabel, FormGroup } from '@mui/material';
import BackupSecretRecoveryPhrase from 'components/pages/SetupWallet/NewWallet/BackupSecretRecoveryPhrase';
import JsonFile from 'components/shared/export/JsonFile';
import QrCode from 'components/shared/export/QrCode';
import CryptoJS from 'crypto-js';
import useSetupWallet from 'hooks/wallet/useSetupWallet';
import { setupWalletActions } from 'redux/slices/setup-wallet';
import { RootState } from 'redux/store';
import { Props, TransferableObject, WalletRecoveryMethod } from 'types';

interface BackupInstructionProps extends Props {
method: WalletRecoveryMethod;
}

function BackupInstruction({ method }: BackupInstructionProps): JSX.Element {
const { t } = useTranslation();
const { password, secretPhrase } = useSelector((state: RootState) => state.setupWallet);

// Preventing re-create the wallet backup when the component is re-rendered
const walletBackup = useMemo(() => {
if (!secretPhrase || !password) return;
1cedrus marked this conversation as resolved.
Show resolved Hide resolved

const encryptedMnemonic = CryptoJS.AES.encrypt(secretPhrase, password).toString();

return {
encryptedMnemonic,
} as WalletBackup;
}, [secretPhrase, password]);

if (!walletBackup) return <></>;

switch (method) {
case WalletRecoveryMethod.SecretRecoveryPhrase:
return <BackupSecretRecoveryPhrase secretPhrase={secretPhrase!} title='Secret Recovery Phrase' />;
case WalletRecoveryMethod.QrCode:
return (
<QrCode
value={walletBackup}
object={TransferableObject.Wallet}
title='Downloads QR Code'
topInstruction={
<p>
{t<string>(
'Downloads the below QR code image and upload it to the cloud (iCloud, Google Drive, ...), you can restore your wallet by scanning or uploading this QR Code to Coong Wallet later at any time with your wallet password.',
)}
</p>
}
/>
);
case WalletRecoveryMethod.JsonFile:
return (
<JsonFile
value={walletBackup}
object={TransferableObject.Wallet}
title='Downloads JSON File'
topInstruction={
<p>
{t<string>(
'Downloads the below JSON file and upload it to the cloud (iCloud, Google Drive, ...), you can restore your wallet by uploading this JSON file to Coong Wallet later at any time with your wallet password.',
)}
</p>
}
/>
);
default:
return <></>;
}
}

interface BackupWalletProps extends Props {
method: WalletRecoveryMethod;
resetMethod: () => void;
}

export default function BackupWallet({ method, resetMethod }: BackupWalletProps): JSX.Element {
const dispatch = useDispatch();
const { t } = useTranslation();
const { password, secretPhrase } = useSelector((state: RootState) => state.setupWallet);
const { setup, loading } = useSetupWallet({ secretPhrase, password });
const [checked, setChecked] = useState<boolean>(false);

useEffectOnce(() => {
if (secretPhrase) return;
1cedrus marked this conversation as resolved.
Show resolved Hide resolved
dispatch(setupWalletActions.setSecretPhrase(generateMnemonic(12)));
});

const handleCheckbox = (event: ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
};

const doSetupWallet = (e: FormEvent) => {
e.preventDefault();

setup();
};

return (
<>
<BackupInstruction method={method} />
<form>
<FormGroup className='my-2'>
<FormControlLabel
control={<Checkbox checked={checked} onChange={handleCheckbox} disabled={loading} />}
label={t<string>('I have backed up my wallet')}
/>
</FormGroup>
<div className='flex flex-row gap-4'>
<Button variant='text' onClick={resetMethod} disabled={loading}>
{t<string>('Back')}
</Button>
<LoadingButton
onClick={doSetupWallet}
disabled={!checked}
fullWidth
loading={loading}
variant='contained'
size='large'>
{t<string>('Finish')}
</LoadingButton>
</div>
</form>
</>
);
}
Loading