ghost-keychain/packages/extension-ui/src/Popup/Signing/Request.tsx
Thibaut Sardan 34e16c2a04
Refactor password error in request pop up and remember checkbox (#503)
* error and remember checkbox refactoring

* make it build

* remove margin

Co-authored-by: Github Actions <action@github.com>
2020-10-20 16:34:48 +02:00

259 lines
6.7 KiB
TypeScript

// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ExtrinsicPayload } from '@polkadot/types/interfaces';
import { AccountJson, RequestSign } from '@polkadot/extension-base/background/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import React, { useCallback, useEffect, useContext, useState } from 'react';
import { PASSWORD_EXPIRY_MIN } from '@polkadot/extension-base/defaults';
import { TypeRegistry } from '@polkadot/types';
import { ActionBar, ActionContext, Address, Button, ButtonArea, Checkbox, Link, VerticalSpace } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { approveSignPassword, approveSignSignature, cancelSignRequest, isSignLocked } from '../../messaging';
import Bytes from './Bytes';
import Extrinsic from './Extrinsic';
import Qr from './Qr';
import Unlock from './Unlock';
import styled from 'styled-components';
interface Props {
account: AccountJson;
buttonText: string;
isFirst?: boolean;
request: RequestSign;
signId: string;
url: string;
}
interface Data {
hexBytes: string | null;
payload: ExtrinsicPayload | null;
}
// keep it global, we can and will re-use this across requests
const registry = new TypeRegistry();
function isRawPayload (payload: SignerPayloadJSON | SignerPayloadRaw): payload is SignerPayloadRaw {
return !!(payload as SignerPayloadRaw).data;
}
export default function Request ({ account: { isExternal }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null);
const [isBusy, setIsBusy] = useState(false);
const [isLocked, setIsLocked] = useState<boolean | null>(null);
const [savePass, setSavePass] = useState(false);
const [password, setPassword] = useState('');
useEffect(() => {
setIsLocked(null);
let timeout: number;
!isExternal && isSignLocked(signId)
.then(({ isLocked, remainingTime }) => {
setIsLocked(isLocked);
timeout = setTimeout(() => {
setIsLocked(true);
}, remainingTime);
// if the account was unlocked check the remember me
// automatically to prolong the unlock period
!isLocked && setSavePass(true);
})
.catch((error: Error) => console.error(error));
return () => { !!timeout && clearTimeout(timeout); };
}, [isExternal, signId]);
useEffect((): void => {
const payload = request.payload;
if (isRawPayload(payload)) {
setData({
hexBytes: payload.data,
payload: null
});
} else {
registry.setSignedExtensions(payload.signedExtensions);
setData({
hexBytes: null,
payload: registry.createType('ExtrinsicPayload', payload, { version: payload.version })
});
}
}, [request]);
const _onCancel = useCallback(
(): Promise<void> => cancelSignRequest(signId)
.then(() => onAction())
.catch((error: Error) => console.error(error)),
[onAction, signId]
);
const _onSign = useCallback(
(): Promise<void> => {
setIsBusy(true);
return approveSignPassword(signId, savePass, password)
.then((): void => {
setIsBusy(false);
onAction();
})
.catch((error: Error): void => {
setIsBusy(false);
setError(error.message);
console.error(error);
});
},
[onAction, password, savePass, signId]
);
const _onSignature = useCallback(
({ signature }: { signature: string }): Promise<void> =>
approveSignSignature(signId, signature)
.then(() => onAction())
.catch((error: Error): void => {
setError(error.message);
console.error(error);
}),
[onAction, signId]
);
const RememberPasswordCheckbox = () => (
<Checkbox
checked={savePass}
label={ isLocked
? t<string>(
'Remember my password for the next {{expiration}} minutes',
{ replace: { expiration: PASSWORD_EXPIRY_MIN } }
)
: t<string>(
'Extend the period without password by {{expiration}} minutes',
{ replace: { expiration: PASSWORD_EXPIRY_MIN } }
)
}
onChange={setSavePass}
/>
);
const SignButton = () => (
<>
{ isLocked && (
<Unlock
error={error}
isBusy={isBusy}
onSign={_onSign}
password={password}
setError={setError}
setPassword={setPassword}
/>
)}
<RememberPasswordCheckbox />
<Button
isBusy={isBusy}
isDisabled={(!!isLocked && !password) || !!error}
onClick={_onSign}
>
{buttonText}
</Button>
</>
);
if (payload !== null) {
const json = request.payload as SignerPayloadJSON;
return (
<>
<div>
<Address
address={json.address}
genesisHash={json.genesisHash}
isExternal={isExternal}
/>
</div>
{isExternal
? (
<Qr
onSignature={_onSignature}
payload={payload}
request={json}
/>
) : (
<Extrinsic
isDecoded={true}
payload={payload}
request={json}
url={url}
/>
)
}
<SignArea>
{isFirst && !isExternal && (
<SignButton/>
)}
<CancelButton>
<Link
isDanger
onClick={_onCancel}
>
{t<string>('Cancel')}
</Link>
</CancelButton>
</SignArea>
</>
);
} else if (hexBytes !== null) {
const raw = request.payload as SignerPayloadRaw;
return (
<>
<div>
<Address
address={raw.address}
isExternal={isExternal}
/>
</div>
<Bytes
bytes={raw.data}
url={url}
/>
<VerticalSpace />
<SignArea>
{!isExternal && <SignButton/>}
<CancelButton>
<Link
isDanger
onClick={_onCancel}
>
{t<string>('Reject')}
</Link>
</CancelButton>
</SignArea>
</>
);
}
return null;
}
const SignArea = styled(ButtonArea)`
flex-direction: column;
padding: 6px 24px;
`;
const CancelButton = styled(ActionBar)`
margin-top: 4px;
margin-bottom: 4px;
text-decoration: underline;
a {
margin: auto;
}
`;
CancelButton.displayName = 'CancelButton';