feat: add cancel authorization request functionality and improve UI components (#1491)
This commit is contained in:
@@ -524,6 +524,8 @@ export default class Extension {
|
||||
return { list: remAuth };
|
||||
}
|
||||
|
||||
// Reject the authorization request and add the URL to the authorized list with no keys.
|
||||
// The site will not prompt for re-authorization on future visits.
|
||||
private rejectAuthRequest (id: string): void {
|
||||
const queued = this.#state.getAuthRequest(id);
|
||||
|
||||
@@ -534,6 +536,18 @@ export default class Extension {
|
||||
reject(new Error('Rejected'));
|
||||
}
|
||||
|
||||
// Cancel the authorization request and do not add the URL to the authorized list.
|
||||
// The site will prompt for authorization on future visits.
|
||||
private cancelAuthRequest (id: string): void {
|
||||
const queued = this.#state.getAuthRequest(id);
|
||||
|
||||
assert(queued, 'Unable to find request');
|
||||
|
||||
const { reject } = queued;
|
||||
|
||||
reject(new Error('Cancelled'));
|
||||
}
|
||||
|
||||
private updateCurrentTabs ({ urls }: RequestActiveTabsUrlUpdate) {
|
||||
this.#state.updateCurrentTabsUrl(urls);
|
||||
}
|
||||
@@ -558,6 +572,9 @@ export default class Extension {
|
||||
case 'pri(authorize.reject)':
|
||||
return this.rejectAuthRequest(request as string);
|
||||
|
||||
case 'pri(authorize.cancel)':
|
||||
return this.cancelAuthRequest(request as string);
|
||||
|
||||
case 'pri(authorize.requests)':
|
||||
return port && this.authorizeSubscribe(id, port);
|
||||
|
||||
|
||||
@@ -273,8 +273,14 @@ export default class State {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
reject: async (error: Error): Promise<void> => {
|
||||
await complete();
|
||||
reject(error);
|
||||
if (error.message === 'Cancelled') {
|
||||
delete this.#authRequests[id];
|
||||
this.updateIconAuth(true);
|
||||
reject(new Error('Connection request was cancelled by the user.'));
|
||||
} else {
|
||||
await complete();
|
||||
reject(new Error('Connection request was rejected by the user.'));
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
resolve: async ({ authorizedAccounts, result }: AuthResponse): Promise<void> => {
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface RequestSignatures {
|
||||
'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
|
||||
'pri(authorize.remove)': [string, ResponseAuthorizeList];
|
||||
'pri(authorize.reject)': [string, void];
|
||||
'pri(authorize.cancel)': [string, void];
|
||||
'pri(authorize.update)': [RequestUpdateAuthorizedAccounts, void];
|
||||
'pri(activeTabsUrl.update)': [RequestActiveTabsUrlUpdate, void];
|
||||
'pri(connectedTabsUrl.get)': [null, ConnectedTabsUrlResponse];
|
||||
|
||||
@@ -42,41 +42,54 @@ function AccountManagement ({ className }: Props): React.ReactElement<Props> {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${className}`}>
|
||||
<Header
|
||||
showBackArrow
|
||||
smallMargin={true}
|
||||
text={t('Accounts connected to {{url}}', { replace: { url } })}
|
||||
/>
|
||||
<div className={className}>
|
||||
<div className='content'>
|
||||
<AccountSelection
|
||||
className='accountSelection'
|
||||
origin={origin}
|
||||
origin={url}
|
||||
showHidden={true}
|
||||
url={url}
|
||||
withWarning={false}
|
||||
/>
|
||||
<Button
|
||||
className='acceptButton'
|
||||
onClick={_onApprove}
|
||||
>
|
||||
{t('Connect {{total}} account(s)', { replace: {
|
||||
total: selectedAccounts.length
|
||||
} })}
|
||||
</Button>
|
||||
<div className='footer'>
|
||||
<Button
|
||||
className='acceptButton'
|
||||
onClick={_onApprove}
|
||||
>
|
||||
{t('Connect {{total}} account(s)', { replace: {
|
||||
total: selectedAccounts.length
|
||||
} })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(AccountManagement)<Props>`
|
||||
.accountSelection{
|
||||
.accountList{
|
||||
height: 390px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 550px;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: var(--background);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.acceptButton {
|
||||
width: 90%;
|
||||
margin: 0.5rem auto 0;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import type { RequestAuthorizeTab } from '@polkadot/extension-base/background/types';
|
||||
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { AccountContext, ActionBar, ActionContext, Button, Link } from '../../components/index.js';
|
||||
import { AccountContext, ActionContext, Button } from '../../components/index.js';
|
||||
import { useTranslation } from '../../hooks/index.js';
|
||||
import { approveAuthRequest, rejectAuthRequest } from '../../messaging.js';
|
||||
import { approveAuthRequest, cancelAuthRequest, rejectAuthRequest } from '../../messaging.js';
|
||||
import { AccountSelection } from '../../partials/index.js';
|
||||
import { styled } from '../../styled.js';
|
||||
import NoAccount from './NoAccount.js';
|
||||
@@ -15,15 +15,15 @@ import NoAccount from './NoAccount.js';
|
||||
interface Props {
|
||||
authId: string;
|
||||
className?: string;
|
||||
isFirst: boolean;
|
||||
request: RequestAuthorizeTab;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function Request ({ authId, className, isFirst, request: { origin }, url }: Props): React.ReactElement<Props> {
|
||||
function Request ({ authId, className, request: { origin }, url }: Props): React.ReactElement<Props> {
|
||||
const { accounts, selectedAccounts = [], setSelectedAccounts } = useContext(AccountContext);
|
||||
const { t } = useTranslation();
|
||||
const onAction = useContext(ActionContext);
|
||||
const [dontAskAgain, setDontAskAgain] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultAccountSelection = accounts
|
||||
@@ -42,13 +42,22 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
|
||||
[authId, onAction, selectedAccounts]
|
||||
);
|
||||
|
||||
const _onClose = useCallback(
|
||||
const _onReject = useCallback(
|
||||
(): void => {
|
||||
rejectAuthRequest(authId)
|
||||
const rejectFunction = dontAskAgain ? rejectAuthRequest : cancelAuthRequest;
|
||||
|
||||
rejectFunction(authId)
|
||||
.then(() => onAction())
|
||||
.catch((error: Error) => console.error(error));
|
||||
},
|
||||
[authId, onAction]
|
||||
[authId, onAction, dontAskAgain]
|
||||
);
|
||||
|
||||
const _onToggleDontAskAgain = useCallback(
|
||||
(): void => {
|
||||
setDontAskAgain((prev) => !prev);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (!accounts.length) {
|
||||
@@ -61,42 +70,69 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
|
||||
origin={origin}
|
||||
url={url}
|
||||
/>
|
||||
{isFirst && (
|
||||
<Button
|
||||
className='acceptButton'
|
||||
onClick={_onApprove}
|
||||
>
|
||||
{t('Connect {{total}} account(s)', { replace: {
|
||||
total: selectedAccounts.length
|
||||
} })}
|
||||
</Button>
|
||||
)}
|
||||
<ActionBar className='rejectionButton'>
|
||||
<Link
|
||||
className='closeLink'
|
||||
isDanger
|
||||
onClick={_onClose}
|
||||
>
|
||||
{t('Reject')}
|
||||
</Link>
|
||||
</ActionBar>
|
||||
<div className='footer'>
|
||||
<div className='buttonContainer'>
|
||||
<Button
|
||||
className='acceptButton'
|
||||
onClick={_onApprove}
|
||||
>
|
||||
{t('Connect {{total}} account(s)', { replace: {
|
||||
total: selectedAccounts.length
|
||||
} })}
|
||||
</Button>
|
||||
<Button
|
||||
className='rejectButton'
|
||||
isDanger
|
||||
onClick={_onReject}
|
||||
>
|
||||
{t('Reject')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='dontAskAgainContainer'>
|
||||
<input
|
||||
checked={dontAskAgain}
|
||||
onChange={_onToggleDontAskAgain}
|
||||
type='checkbox'
|
||||
/>
|
||||
<label>{t("Don't ask again")}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(Request)<Props>`
|
||||
.acceptButton {
|
||||
width: 90%;
|
||||
margin: .5rem auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.footer {
|
||||
padding: 1rem 1rem 0rem 1rem;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.rejectionButton {
|
||||
margin: 0 0 15px 0;
|
||||
text-decoration: underline;
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.closeLink {
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
.acceptButton, .rejectButton {
|
||||
width: 48%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.dontAskAgainContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2019-2024 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { AuthorizeReqContext } from '../../components/index.js';
|
||||
import { useTranslation } from '../../hooks/index.js';
|
||||
@@ -16,35 +16,76 @@ interface Props {
|
||||
function Authorize ({ className = '' }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const requests = useContext(AuthorizeReqContext);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const currentIndexRef = useRef(currentIndex);
|
||||
|
||||
useEffect(() => {
|
||||
if (requests.length <= currentIndexRef.current) {
|
||||
setCurrentIndex((prevIndex) => Math.max(prevIndex - 1, 0));
|
||||
}
|
||||
}, [requests.length]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
setCurrentIndex((prevIndex) => {
|
||||
const newIndex = Math.min(prevIndex + 1, requests.length - 1);
|
||||
|
||||
currentIndexRef.current = newIndex;
|
||||
|
||||
return newIndex;
|
||||
});
|
||||
}, [requests.length]);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
setCurrentIndex((prevIndex) => {
|
||||
const newIndex = Math.max(prevIndex - 1, 0);
|
||||
|
||||
currentIndexRef.current = newIndex;
|
||||
|
||||
return newIndex;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${className} ${requests.length === 1 ? 'lastRequest' : ''}`}>
|
||||
<Header
|
||||
smallMargin={true}
|
||||
text={t('Account connection request')}
|
||||
<div className={`${className}`}>
|
||||
<Header
|
||||
smallMargin={true}
|
||||
text={t('Account connection request')}
|
||||
/>
|
||||
{requests.length > 1 && (
|
||||
<div className='pagination'>
|
||||
<button
|
||||
className={currentIndex === 0 ? 'hidden' : ''}
|
||||
onClick={handlePrevious}
|
||||
>
|
||||
{t('Previous')}
|
||||
</button>
|
||||
<span>{`${currentIndex + 1} / ${requests.length}`}</span>
|
||||
<button
|
||||
className={currentIndex === requests.length - 1 ? 'hidden' : ''}
|
||||
onClick={handleNext}
|
||||
>
|
||||
{t('Next')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{requests.length > 0 && requests[currentIndex] && (
|
||||
<Request
|
||||
authId={requests[currentIndex].id}
|
||||
className='request'
|
||||
key={requests[currentIndex].id}
|
||||
request={requests[currentIndex].request}
|
||||
url={requests[currentIndex].url}
|
||||
/>
|
||||
{requests.map(({ id, request, url }, index): React.ReactNode => (
|
||||
<Request
|
||||
authId={id}
|
||||
className='request'
|
||||
isFirst={index === 0}
|
||||
key={id}
|
||||
request={request}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(Authorize)<Props>`
|
||||
overflow-y: auto;
|
||||
|
||||
&.lastRequest {
|
||||
overflow: hidden;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&& {
|
||||
padding: 0;
|
||||
@@ -52,5 +93,32 @@ export default styled(Authorize)<Props>`
|
||||
|
||||
.request {
|
||||
padding: 0 24px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: var(--background);
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--textColor);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -224,6 +224,10 @@ export async function rejectAuthRequest (requestId: string): Promise<void> {
|
||||
return sendMessage('pri(authorize.reject)', requestId);
|
||||
}
|
||||
|
||||
export async function cancelAuthRequest (requestId: string): Promise<void> {
|
||||
return sendMessage('pri(authorize.cancel)', requestId);
|
||||
}
|
||||
|
||||
export async function subscribeMetadataRequests (cb: (accounts: MetadataRequest[]) => void): Promise<boolean> {
|
||||
return sendMessage('pri(metadata.requests)', null, cb);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
withWarning?: boolean;
|
||||
}
|
||||
|
||||
function AccounSelection ({ className, origin, showHidden = false, url, withWarning = true }: Props): React.ReactElement<Props> {
|
||||
function AccountSelection ({ className, origin, showHidden = false, url, withWarning = true }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { accounts, hierarchy, selectedAccounts = [], setSelectedAccounts } = useContext(AccountContext);
|
||||
const [isIndeterminate, setIsIndeterminate] = useState(false);
|
||||
@@ -90,10 +90,15 @@ function AccounSelection ({ className, origin, showHidden = false, url, withWarn
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(AccounSelection)<Props>`
|
||||
export default styled(AccountSelection)<Props>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.accountList {
|
||||
overflow-y: auto;
|
||||
height: 270px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.tab-name,
|
||||
|
||||
@@ -179,5 +179,7 @@
|
||||
"This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata. It will also allow the use of Ledger's Generic Polkadot App.": "",
|
||||
"No metadata found for this chain. You must upload the metadata to the extension in order to use Ledger.": "",
|
||||
"This network is not available, please report an issue to update the known chains": "",
|
||||
"Ledger App": ""
|
||||
"Ledger App": "",
|
||||
"Don't ask again": "",
|
||||
"Previous": ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user