diff --git a/packages/page-accounts/src/modals/ExportAll.tsx b/packages/page-accounts/src/modals/ExportAll.tsx index b4b74d138d..d65dbffa0e 100644 --- a/packages/page-accounts/src/modals/ExportAll.tsx +++ b/packages/page-accounts/src/modals/ExportAll.tsx @@ -7,7 +7,7 @@ import type { ModalProps } from '../types.js'; import FileSaver from 'file-saver'; import React, { useCallback, useState } from 'react'; -import { Button, Checkbox, MarkWarning, Modal, styled } from '@polkadot/react-components'; +import { AddressRow, Button, Checkbox, MarkWarning, Modal, Password, styled } from '@polkadot/react-components'; import { keyring } from '@polkadot/ui-keyring'; import { nextTick } from '@polkadot/util'; @@ -20,10 +20,17 @@ interface Props extends ModalProps { onStatusChange: (status: ActionStatus) => void; } +interface TPassword { + account: string, isPassTouched: boolean, password: string +} + +const ALLOWED_CATEGORIES = ['accounts', 'multisig', 'qr']; + function ExportAll ({ accountsByGroup, className, onClose, onStatusChange }: Props): React.ReactElement | null { const { t } = useTranslation(); const [isBusy, setIsBusy] = useState(false); const [selectedTypes, setSelectedTypes] = useState([]); + const [allBrowserAccountPassword, setAllBrowserAccountPassword] = useState([]); // toggle checkboxes const onChangeCheckBox = useCallback((group: string) => { @@ -51,10 +58,12 @@ function ExportAll ({ accountsByGroup, className, onClose, onStatusChange }: Pro const accounts = allAccounts.map((account) => { const keyringAccount = keyring.getPair(account); - return { - address: account, - meta: keyringAccount.meta - }; + // Handle In-Browser Accounts explicitly + if (accountsByGroup.accounts.includes(account)) { + return keyring.backupAccount(keyringAccount, allBrowserAccountPassword.find((a) => a.account === account)?.password || ''); + } else { + return keyringAccount; + } }); const blob = new Blob([JSON.stringify(accounts, null, 2)], { type: 'application/json; charset=utf-8' }); @@ -77,7 +86,7 @@ function ExportAll ({ accountsByGroup, className, onClose, onStatusChange }: Pro onClose(); } }); - }, [accountsByGroup, onClose, onStatusChange, selectedTypes, t]); + }, [accountsByGroup, allBrowserAccountPassword, onClose, onStatusChange, selectedTypes, t]); return ( {Object.keys(accountsByGroup) - .filter((group) => accountsByGroup[group].length > 0) + .filter((group) => ALLOWED_CATEGORIES.includes(group) && accountsByGroup[group].length > 0) .map((group) => { const isSelected = selectedTypes.includes(group); return ( - onChangeCheckBox(group)} - value={isSelected} - /> +
+ onChangeCheckBox(group)} + value={isSelected} + /> + {group === 'accounts' && isSelected && + } +
); })}
@@ -122,10 +138,46 @@ function ExportAll ({ accountsByGroup, className, onClose, onStatusChange }: Pro ); } +const BrowserAccounts = ({ accounts, allPassword, setAllPassword }: {accounts: string[], allPassword: TPassword[], setAllPassword: React.Dispatch>}) => { + const { t } = useTranslation(); + + const _onChangePass = useCallback( + (account: string, password: string): void => { + setAllPassword((prev) => { + return [ + ...prev.filter((e) => e.account !== account), + { account, isPassTouched: true, password } + ]; + }); + }, + [setAllPassword] + ); + + return accounts.map((account) => { + const { isPassTouched, password } = allPassword.find((a) => a.account === account) || { isPassTouched: false, password: '' }; + const isPassValid = keyring.isPassValid(password); + + return + + _onChangePass(account, password)} + value={password} + /> + ; + }); +}; + const StyledCheckBoxGroup = styled.div` display: flex; - align-items: center; - gap: 1rem; + flex-direction: column; + gap: 1.5rem; margin-top: 1.5rem; padding-inline: 2rem; @@ -133,6 +185,25 @@ const StyledCheckBoxGroup = styled.div` font-size: 1rem; font-weight: 500; } + + section > div > label { + text-transform: capitalize; + } +`; + +const BrowserAccountsDiv = styled.div` + width: 100%; + display: flex; + gap: 1rem; + margin-top: 1.5rem; + padding-inline: 2rem; + + div:nth-child(2) { + width: 100%; + label { + font-size: var(--font-size-label); + } + } `; export default React.memo(ExportAll); diff --git a/packages/page-accounts/src/modals/ImportAll.tsx b/packages/page-accounts/src/modals/ImportAll.tsx index 8ec3d0b36c..a29b4ae15a 100644 --- a/packages/page-accounts/src/modals/ImportAll.tsx +++ b/packages/page-accounts/src/modals/ImportAll.tsx @@ -2,13 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import type { Dispatch, SetStateAction } from 'react'; -import type { KeyringPair$Meta } from '@polkadot/keyring/types'; +import type { KeyringPair, KeyringPair$Json } from '@polkadot/keyring/types'; import type { ActionStatus } from '@polkadot/react-components/Status/types'; import type { ModalProps } from '../types.js'; import React, { useCallback, useState } from 'react'; -import { Button, InputFile, MarkError, Modal } from '@polkadot/react-components'; +import { AddressRow, Button, InputFile, MarkError, Modal, Password, styled } from '@polkadot/react-components'; import keyring from '@polkadot/ui-keyring'; import { nextTick, u8aToString } from '@polkadot/util'; @@ -20,15 +20,20 @@ interface Props extends ModalProps { onStatusChange: (status: ActionStatus) => void; } -interface FileAccount { - address: string; - meta: KeyringPair$Meta; +interface TPassword { + account: string, isPassTouched: boolean, password: string } +type FileAccount = (KeyringPair$Json | KeyringPair); + type File = FileAccount[] const acceptedFormats = ['application/json']; +function isInBrowserAccount (pair: FileAccount): boolean { + return !pair.meta.isInjected && !pair.meta.isHardware && !pair.meta.isMultisig && !!(pair as KeyringPair$Json).encoded && !!(pair as KeyringPair$Json).encoding; +} + // Check if file is valid function isValidFile (fileContent: string, setError: Dispatch>): boolean { setError(null); @@ -64,6 +69,7 @@ function ImportAll ({ className, onClose, onStatusChange }: Props): React.ReactE const [isBusy, setIsBusy] = useState(false); const [error, setError] = useState(null); const [file, setFile] = useState(null); + const [allBrowserAccountPassword, setAllBrowserAccountPassword] = useState([]); const _onChangeFile = useCallback( (file: Uint8Array) => { @@ -80,9 +86,16 @@ function ImportAll ({ className, onClose, onStatusChange }: Props): React.ReactE const status: Partial = { action: 'import' }; try { - file?.map((pair) => - keyring.addExternal(pair.address, pair.meta) - ); + file?.forEach((pair) => { + if (isInBrowserAccount(pair)) { + const keyringPair = keyring.createFromJson(pair as KeyringPair$Json); + const password = allBrowserAccountPassword.find((e) => e.account === pair.address)?.password || ''; + + keyring.addPair(keyringPair, password); + } else { + keyring.addExternal(pair.address, pair.meta); + } + }); status.status = 'success'; status.message = t('all accounts imported'); @@ -99,7 +112,7 @@ function ImportAll ({ className, onClose, onStatusChange }: Props): React.ReactE onClose(); } }); - }, [file, onClose, onStatusChange, t]); + }, [allBrowserAccountPassword, file, onClose, onStatusChange, t]); return ( + {file && file.some(isInBrowserAccount) && + + + + } {error && ( @@ -139,4 +161,52 @@ function ImportAll ({ className, onClose, onStatusChange }: Props): React.ReactE ); } +const BrowserAccounts = ({ allPassword, pairs, setAllPassword }: { pairs: FileAccount[], allPassword: TPassword[], setAllPassword: React.Dispatch> }) => { + const { t } = useTranslation(); + + const _onChangePass = useCallback( + (account: string, password: string): void => { + setAllPassword((prev) => { + return [ + ...prev.filter((e) => e.account !== account), + { account, isPassTouched: true, password } + ]; + }); + }, + [setAllPassword] + ); + + return pairs.map((pair) => { + const { address: account } = pair; + const { isPassTouched, password } = allPassword.find((a) => a.account === account) || { isPassTouched: false, password: '' }; + const isPassValid = keyring.isPassValid(password); + + return isInBrowserAccount(pair) && + + _onChangePass(account, password)} + value={password} + /> + ; + }); +}; + +const BrowserAccountsDiv = styled.div` + width: 100%; + display: flex; + gap: 1rem; + margin-top: 1.5rem; + padding-inline: 2rem; + + div:nth-child(2) {width: 100%;} +`; + export default React.memo(ImportAll);