feat: add cancel authorization request functionality and improve UI components (#1491)

This commit is contained in:
Francis O'Brien
2024-12-20 09:03:36 -05:00
committed by GitHub
parent 6f29ec7eb4
commit 52acbc830a
9 changed files with 235 additions and 83 deletions
@@ -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;
}
}
`;
+4
View File
@@ -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": ""
}