Social account recovery (#2187)
* Social account recovery ... start * Dialogs with send buttons * TxButton icons (before I forget) * Recovery start * withSpinner
This commit is contained in:
parent
a5ea8d1bf9
commit
63d22395ca
@ -4,18 +4,23 @@
|
||||
|
||||
import { DeriveAccountInfo } from '@polkadot/api-derive/types';
|
||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||
import { RecoveryConfig } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Label } from 'semantic-ui-react';
|
||||
import styled from 'styled-components';
|
||||
import { AddressInfo, AddressSmall, Button, ChainLock, Forget, Icon, InputTags, LinkPolkascan, Menu, Popup, Input } from '@polkadot/react-components';
|
||||
import { AddressInfo, AddressSmall, Badge, Button, ChainLock, Forget, Icon, IdentityIcon, InputTags, LinkPolkascan, Menu, Popup, Input } from '@polkadot/react-components';
|
||||
import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
|
||||
import { Option } from '@polkadot/types';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import { formatBalance, formatNumber } from '@polkadot/util';
|
||||
|
||||
import Backup from './modals/Backup';
|
||||
import ChangePass from './modals/ChangePass';
|
||||
import Derive from './modals/Derive';
|
||||
import Identity from './modals/Identity';
|
||||
import RecoverAccount from './modals/RecoverAccount';
|
||||
import RecoverSetup from './modals/RecoverSetup';
|
||||
import Transfer from './modals/Transfer';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
@ -31,6 +36,10 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
const { t } = useTranslation();
|
||||
const api = useApi();
|
||||
const info = useCall<DeriveAccountInfo>(api.api.derive.accounts.info as any, [address]);
|
||||
const recoveryInfo = useCall<RecoveryConfig | null>(api.api.query.recovery?.recoverable, [address], {
|
||||
transform: (opt: Option<RecoveryConfig>): RecoveryConfig | null =>
|
||||
opt.unwrapOr(null)
|
||||
});
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [accName, setAccName] = useState('');
|
||||
const [genesisHash, setGenesisHash] = useState<string | null>(null);
|
||||
@ -43,6 +52,8 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
const [isForgetOpen, toggleForget] = useToggle();
|
||||
const [isIdentityOpen, toggleIdentity] = useToggle();
|
||||
const [isPasswordOpen, togglePassword] = useToggle();
|
||||
const [isRecoverAccountOpen, toggleRecoverAccount] = useToggle();
|
||||
const [isRecoverSetupOpen, toggleRecoverSetup] = useToggle();
|
||||
const [isSettingsOpen, toggleSettings] = useToggle();
|
||||
const [isTransferOpen, toggleTransfer] = useToggle();
|
||||
|
||||
@ -158,6 +169,46 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
onClick={_onFavorite}
|
||||
/>
|
||||
</td>
|
||||
<td className='together'>
|
||||
{recoveryInfo && (
|
||||
<Badge
|
||||
hover={
|
||||
<div>
|
||||
<p>{t('This account is recoverable, with the following friends:')}</p>
|
||||
<div>
|
||||
{recoveryInfo.friends.map((friend, index): React.ReactNode => (
|
||||
<IdentityIcon
|
||||
key={index}
|
||||
size={24}
|
||||
value={friend}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{t('threshold')}</td>
|
||||
<td>{formatNumber(recoveryInfo.threshold)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('delay')}</td>
|
||||
<td>{formatNumber(recoveryInfo.delayPeriod)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('deposit')}</td>
|
||||
<td>{formatBalance(recoveryInfo.deposit)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
info={<Icon name='shield' />}
|
||||
isInline
|
||||
isTooltip
|
||||
type='online'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='top'>
|
||||
<AddressSmall
|
||||
overrideName={
|
||||
@ -222,6 +273,20 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
senderId={address}
|
||||
/>
|
||||
)}
|
||||
{isRecoverAccountOpen && (
|
||||
<RecoverAccount
|
||||
address={address}
|
||||
key='recover-account'
|
||||
onClose={toggleRecoverAccount}
|
||||
/>
|
||||
)}
|
||||
{isRecoverSetupOpen && (
|
||||
<RecoverSetup
|
||||
address={address}
|
||||
key='recover-setup'
|
||||
onClose={toggleRecoverSetup}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='top'>
|
||||
{isEditingTags
|
||||
@ -321,6 +386,19 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
>
|
||||
{t('Forget this account')}
|
||||
</Menu.Item>
|
||||
{api.api.tx.recovery?.createRecovery && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
{!recoveryInfo && (
|
||||
<Menu.Item onClick={toggleRecoverSetup}>
|
||||
{t('Make recoverable')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item onClick={toggleRecoverAccount}>
|
||||
{t('Initiate recovery for another')}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
{!api.isDevelopment && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
52
packages/app-accounts/src/modals/RecoverAccount.tsx
Normal file
52
packages/app-accounts/src/modals/RecoverAccount.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function RecoverAccount ({ address, className, onClose }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [recover, setRecover] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Initiate account recovery')}
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
label={t('the account to recover to')}
|
||||
value={address}
|
||||
/>
|
||||
<InputAddress
|
||||
help={t('Select the account you wish to recover into this account.')}
|
||||
label={t('recover this account')}
|
||||
onChange={setRecover}
|
||||
type='allPlus'
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
<TxButton
|
||||
accountId={address}
|
||||
icon='recycle'
|
||||
isDisabled={!recover || recover === address}
|
||||
label={t('Start recovery')}
|
||||
onClick={onClose}
|
||||
params={[recover]}
|
||||
tx='recovery.initiateRecovery'
|
||||
withSpinner={false}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
88
packages/app-accounts/src/modals/RecoverSetup.tsx
Normal file
88
packages/app-accounts/src/modals/RecoverSetup.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AddressMulti, InputAddress, InputNumber, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useAddresses } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const MAX_HELPERS = 16;
|
||||
|
||||
export default function RecoverSetup ({ address, className, onClose }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { allAccounts } = useAccounts();
|
||||
const { allAddresses } = useAddresses();
|
||||
const [availableHelpers, setAvailableHelpers] = useState<string[]>([]);
|
||||
const [delay, setDelay] = useState<BN | undefined>();
|
||||
const [helpers, setHelpers] = useState<string[]>([]);
|
||||
const [threshold, setThreshold] = useState<BN | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
if (allAccounts && allAddresses) {
|
||||
setAvailableHelpers(
|
||||
[...allAccounts, ...allAddresses].filter((a): boolean => a !== address)
|
||||
);
|
||||
}
|
||||
}, [address, allAccounts, allAddresses]);
|
||||
|
||||
const isErrorDelay = !delay;
|
||||
const isErrorHelpers = !helpers.length;
|
||||
const isErrorThreshold = !threshold || !threshold.gtn(0) || threshold.gtn(helpers.length);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Setup account as recoverable')}
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
label={t('the account to make recoverable')}
|
||||
value={address}
|
||||
/>
|
||||
<AddressMulti
|
||||
available={availableHelpers}
|
||||
help={t('The addresses that are able to help in recovery. You can select up to {{maxHelpers}} trusted helpers.', { replace: { maxHelpers: MAX_HELPERS } })}
|
||||
label={t('trusted social recovery helpers')}
|
||||
onChange={setHelpers}
|
||||
maxCount={MAX_HELPERS}
|
||||
value={helpers}
|
||||
/>
|
||||
<InputNumber
|
||||
help={t('The threshold of vouches that is to be reached for the account to be recovered.')}
|
||||
isError={isErrorThreshold}
|
||||
label={t('recoverey threshold')}
|
||||
onChange={setThreshold}
|
||||
/>
|
||||
<InputNumber
|
||||
help={t('The delay between vouching and the availability of the recovered account.')}
|
||||
isError={isErrorDelay}
|
||||
isZeroable
|
||||
label={t('recoverey block delay')}
|
||||
onChange={setDelay}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
<TxButton
|
||||
accountId={address}
|
||||
icon='share alternate'
|
||||
isDisabled={isErrorHelpers || isErrorThreshold || isErrorDelay}
|
||||
label={t('Make recoverable')}
|
||||
onClick={onClose}
|
||||
params={[helpers, threshold, delay]}
|
||||
tx='recovery.createRecovery'
|
||||
withSpinner={false}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -186,7 +186,7 @@ export default styled(Status)`
|
||||
position: fixed;
|
||||
right: 0.25rem;
|
||||
top: 0.25rem;
|
||||
width: 20rem;
|
||||
width: 23rem;
|
||||
z-index: 1001;
|
||||
|
||||
.dismiss {
|
||||
@ -233,7 +233,7 @@ export default styled(Status)`
|
||||
.short {
|
||||
font-size: 2.5rem;
|
||||
opacity: 0.75;
|
||||
padding: 0.5rem;
|
||||
padding: 0.5rem 0 0.5rem 0.5rem;
|
||||
|
||||
i.icon {
|
||||
line-height: 1;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user