Add link to accounts connected to the current tab in the header (#1138)

* link to connected account in the header

* better naming consistency
This commit is contained in:
Thibaut Sardan 2022-09-06 13:36:55 +01:00 committed by GitHub
parent a0be6f49ca
commit d27dde4754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 171 additions and 21 deletions

View File

@ -6,7 +6,7 @@ import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/
import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestActiveTabsUrlUpdate, RequestAuthorizeApprove, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults';
import { TypeRegistry } from '@polkadot/types';
@ -518,9 +518,17 @@ export default class Extension {
return this.#state.deleteAuthRequest(requestId);
}
private updateCurrentTabs ({ urls }: RequestActiveTabsUrlUpdate) {
this.#state.udateCurrentTabsUrl(urls);
}
private getConnectedTabsUrl () {
return this.#state.getConnectedTabsUrl();
}
// Weird thought, the eslint override is not needed in Tabs
// eslint-disable-next-line @typescript-eslint/require-await
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise<ResponseType<TMessageType>> {
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port?: chrome.runtime.Port): Promise<ResponseType<TMessageType>> {
switch (type) {
case 'pri(authorize.approve)':
return this.authorizeApprove(request as RequestAuthorizeApprove);
@ -535,7 +543,7 @@ export default class Extension {
return this.deleteAuthRequest(request as string);
case 'pri(authorize.requests)':
return this.authorizeSubscribe(id, port);
return port && this.authorizeSubscribe(id, port);
case 'pri(authorize.update)':
return this.authorizeUpdate(request as RequestUpdateAuthorizedAccounts);
@ -568,7 +576,7 @@ export default class Extension {
return this.accountsShow(request as RequestAccountShow);
case 'pri(accounts.subscribe)':
return this.accountsSubscribe(id, port);
return port && this.accountsSubscribe(id, port);
case 'pri(accounts.tie)':
return this.accountsTie(request as RequestAccountTie);
@ -589,7 +597,13 @@ export default class Extension {
return this.metadataReject(request as RequestMetadataReject);
case 'pri(metadata.requests)':
return this.metadataSubscribe(id, port);
return port && this.metadataSubscribe(id, port);
case 'pri(activeTabsUrl.update)':
return this.updateCurrentTabs(request as RequestActiveTabsUrlUpdate);
case 'pri(connectedTabsUrl.get)':
return this.getConnectedTabsUrl();
case 'pri(derivation.create)':
return this.derivationCreate(request as RequestDeriveCreate);
@ -628,7 +642,7 @@ export default class Extension {
return this.signingIsLocked(request as RequestSigningIsLocked);
case 'pri(signing.requests)':
return this.signingSubscribe(id, port);
return port && this.signingSubscribe(id, port);
case 'pri(window.open)':
return this.windowOpen(request as AllowedPath);

View File

@ -153,6 +153,8 @@ export default class State {
#windows: number[] = [];
#connectedTabsUrl: string[] = [];
public readonly authSubject: BehaviorSubject<AuthorizeRequest[]> = new BehaviorSubject<AuthorizeRequest[]>([]);
public readonly metaSubject: BehaviorSubject<MetadataRequest[]> = new BehaviorSubject<MetadataRequest[]>([]);
@ -267,6 +269,31 @@ export default class State {
};
};
public udateCurrentTabsUrl (urls: string[]) {
const connectedTabs = urls.map((url) => {
let strippedUrl = '';
// the assert in stripUrl may throw for new tabs with "chrome://newtab/"
try {
strippedUrl = this.stripUrl(url);
} catch (e) {
console.error(e);
}
// return the stripped url only if this website is known
return !!strippedUrl && this.authUrls[strippedUrl]
? strippedUrl
: undefined;
})
.filter((value) => !!value) as string[];
this.#connectedTabsUrl = connectedTabs;
}
public getConnectedTabsUrl () {
return this.#connectedTabsUrl;
}
public deleteAuthRequest (requestId: string) {
delete this.#authRequests[requestId];
this.updateIconAuth(true);

View File

@ -210,7 +210,7 @@ export default class Tabs {
return false;
}
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]> {
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port?: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]> {
if (type === 'pub(phishing.redirectIfDenied)') {
return this.redirectIfPhishing(url);
}
@ -227,7 +227,7 @@ export default class Tabs {
return this.accountsListAuthorized(url, request as RequestAccountList);
case 'pub(accounts.subscribe)':
return this.accountsSubscribeAuthorized(url, id, port);
return port && this.accountsSubscribeAuthorized(url, id, port);
case 'pub(accounts.unsubscribe)':
return this.accountsUnsubscribe(url, request as RequestAccountUnsubscribe);
@ -248,19 +248,19 @@ export default class Tabs {
return this.rpcListProviders();
case 'pub(rpc.send)':
return this.rpcSend(request as RequestRpcSend, port);
return port && this.rpcSend(request as RequestRpcSend, port);
case 'pub(rpc.startProvider)':
return this.rpcStartProvider(request as string, port);
return port && this.rpcStartProvider(request as string, port);
case 'pub(rpc.subscribe)':
return this.rpcSubscribe(request as RequestRpcSubscribe, id, port);
return port && this.rpcSubscribe(request as RequestRpcSubscribe, id, port);
case 'pub(rpc.subscribeConnected)':
return this.rpcSubscribeConnected(request as null, id, port);
return port && this.rpcSubscribeConnected(request as null, id, port);
case 'pub(rpc.unsubscribe)':
return this.rpcUnsubscribe(request as RequestRpcUnsubscribe, port);
return port && this.rpcUnsubscribe(request as RequestRpcUnsubscribe, port);
default:
throw new Error(`Unable to handle message of type ${type}`);

View File

@ -14,9 +14,11 @@ const state = new State();
const extension = new Extension(state);
const tabs = new Tabs(state);
export default function handler<TMessageType extends MessageTypes> ({ id, message, request }: TransportRequestMessage<TMessageType>, port: chrome.runtime.Port, extensionPortName = PORT_EXTENSION): void {
const isExtension = port.name === extensionPortName;
const sender = port.sender as chrome.runtime.MessageSender;
export default function handler<TMessageType extends MessageTypes> ({ id, message, request }: TransportRequestMessage<TMessageType>, port?: chrome.runtime.Port, extensionPortName = PORT_EXTENSION): void {
const isExtension = !port || port?.name === extensionPortName;
const sender = port?.sender as chrome.runtime.MessageSender;
const from = isExtension
? 'extension'
: (sender.tab && sender.tab.url) || sender.url || '<unknown>';

View File

@ -75,6 +75,8 @@ export interface SigningRequest {
url: string;
}
export type ConnectedTabsUrlResponse = string[]
// [MessageType]: [RequestType, ResponseType, SubscriptionMessageType?]
export interface RequestSignatures {
// private/internal requests, i.e. from a popup
@ -96,7 +98,9 @@ export interface RequestSignatures {
'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
'pri(authorize.remove)': [string, ResponseAuthorizeList];
'pri(authorize.delete.request)': [string, void];
'pri(authorize.update)': [RequestUpdateAuthorizedAccounts, void]
'pri(authorize.update)': [RequestUpdateAuthorizedAccounts, void];
'pri(activeTabsUrl.update)': [RequestActiveTabsUrlUpdate, void];
'pri(connectedTabsUrl.get)': [null, ConnectedTabsUrlResponse];
'pri(derivation.create)': [RequestDeriveCreate, boolean];
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
'pri(json.restore)': [RequestJsonRestore, void];
@ -262,6 +266,10 @@ export interface RequestAccountList {
export type RequestAccountSubscribe = null;
export interface RequestActiveTabsUrlUpdate {
urls: string[];
}
export interface RequestAccountUnsubscribe {
id: string;
}

View File

@ -50,6 +50,7 @@ function Accounts ({ className }: Props): React.ReactElement {
<Header
onFilter={_onFilter}
showAdd
showConnectedAccounts
showSearch
showSettings
text={t<string>('Accounts')}

View File

@ -21,6 +21,7 @@ const darkTheme = {
buttonBackgroundDangerHover: '#D93B3B',
buttonBackgroundHover: '#ED9329',
buttonTextColor: '#FFFFFF',
connectedDotColor: 'seagreen',
errorBorderColor: '#7E3530',
errorColor: '#E42F2F',
fontFamily: 'Nunito, sans-serif',

View File

@ -1,7 +1,7 @@
// Copyright 2019-2022 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, SeedLengths, SigningRequest, SubscriptionMessageTypes } from '@polkadot/extension-base/background/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, ConnectedTabsUrlResponse, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, SeedLengths, SigningRequest, SubscriptionMessageTypes } from '@polkadot/extension-base/background/types';
import type { Message } from '@polkadot/extension-base/types';
import type { Chain } from '@polkadot/extension-chains/types';
import type { KeyringPair$Json } from '@polkadot/keyring/types';
@ -172,6 +172,10 @@ export async function getMetadata (genesisHash?: string | null, isPartial = fals
return null;
}
export async function getConnectedTabsUrl (): Promise<ConnectedTabsUrlResponse> {
return sendMessage('pri(connectedTabsUrl.get)', null);
}
export async function rejectMetaRequest (id: string): Promise<boolean> {
return sendMessage('pri(metadata.reject)', { id });
}

View File

@ -5,7 +5,7 @@ import type { ThemeProps } from '../types';
import { faArrowLeft, faCog, faPlusCircle, faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import logo from '../assets/pjs.svg';
@ -13,6 +13,7 @@ import InputFilter from '../components/InputFilter';
import Link from '../components/Link';
import useOutsideClick from '../hooks/useOutsideClick';
import useTranslation from '../hooks/useTranslation';
import { getConnectedTabsUrl } from '../messaging';
import MenuAdd from './MenuAdd';
import MenuSettings from './MenuSettings';
@ -22,22 +23,36 @@ interface Props extends ThemeProps {
onFilter?: (filter: string) => void;
showAdd?: boolean;
showBackArrow?: boolean;
showConnectedAccounts?: boolean;
showSearch?: boolean;
showSettings?: boolean;
smallMargin?: boolean;
text?: React.ReactNode;
}
function Header ({ children, className = '', onFilter, showAdd, showBackArrow, showSearch, showSettings, smallMargin = false, text }: Props): React.ReactElement<Props> {
function Header ({ children, className = '', onFilter, showAdd, showBackArrow, showConnectedAccounts, showSearch, showSettings, smallMargin = false, text }: Props): React.ReactElement<Props> {
const [isAddOpen, setShowAdd] = useState(false);
const [isSettingsOpen, setShowSettings] = useState(false);
const [isSearchOpen, setShowSearch] = useState(false);
const [filter, setFilter] = useState('');
const [connectedTabsUrl, setConnectedTabsUrl] = useState<string[]>([]);
const { t } = useTranslation();
const addIconRef = useRef(null);
const addMenuRef = useRef(null);
const setIconRef = useRef(null);
const setMenuRef = useRef(null);
const isConnected = useMemo(() => connectedTabsUrl.length >= 1
, [connectedTabsUrl]);
useEffect(() => {
if (!showConnectedAccounts) {
return;
}
getConnectedTabsUrl()
.then((tabsUrl) => setConnectedTabsUrl(tabsUrl))
.catch(console.error);
}, [showConnectedAccounts]);
useOutsideClick([addIconRef, addMenuRef], (): void => {
isAddOpen && setShowAdd(!isAddOpen);
@ -103,6 +118,16 @@ function Header ({ children, className = '', onFilter, showAdd, showBackArrow, s
</div>
{showSearch && (
<div className={`searchBarWrapper ${isSearchOpen ? 'selected' : ''}`}>
{showConnectedAccounts && !!isConnected && !isSearchOpen && (
<div className='connectedAccountsWrapper'>
<Link
className='connectedAccounts'
to={connectedTabsUrl.length === 1 ? `/url/manage/${connectedTabsUrl[0]}` : '/auth-list'}
>
<span className='greenDot'>•</span>Connected
</Link>
</div>
)}
{isSearchOpen && (
<InputFilter
className='inputFilter'
@ -207,11 +232,31 @@ export default React.memo(styled(Header)(({ theme }: Props) => `
align-self: center;
}
.connectedAccountsWrapper {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.connectedAccounts {
border: 1px solid ${theme.inputBorderColor};
border-radius: 4px;
padding: 0 0.5rem;
.greenDot {
margin-right: 0.3rem;
font-size: 1.5rem;
color: ${theme.connectedDotColor};
padding-bottom: 0.2rem;
}
}
.searchBarWrapper {
flex: 1;
display: flex;
justify-content: end;
align-items: center;;
align-items: center;
.searchIcon {
margin-right: 8px;

View File

@ -28,6 +28,54 @@ chrome.runtime.onConnect.addListener((port): void => {
port.onDisconnect.addListener(() => console.log(`Disconnected from ${port.name}`));
});
function getActiveTabs () {
// queriing the current active tab in the current window should only ever return 1 tab
// although an array is specified here
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
// get the urls of the active tabs. In the case of new tab the url may be empty or undefined
// we filter these out
const urls: string[] = tabs
.map(({ url }) => url)
.filter((url) => !!url) as string[];
const request: TransportRequestMessage<'pri(activeTabsUrl.update)'> = {
id: 'background',
message: 'pri(activeTabsUrl.update)',
origin: 'background',
request: { urls }
};
handlers(request);
});
}
// listen to tab updates this is fired on url change
chrome.tabs.onUpdated.addListener((_, changeInfo) => {
// we are only interested in url change
if (!changeInfo.url) {
return;
}
getActiveTabs();
});
// the list of active tab changes when switching window
// in a mutli window setup
chrome.windows.onFocusChanged.addListener(() =>
getActiveTabs()
);
// when clicking on an existing tab or opening a new tab this will be fired
// before the url is entered by users
chrome.tabs.onActivated.addListener(() => {
getActiveTabs();
});
// when deleting a tab this will be fired
chrome.tabs.onRemoved.addListener(() => {
getActiveTabs();
});
// initial setup
cryptoWaitReady()
.then((): void => {