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:
parent
a0be6f49ca
commit
d27dde4754
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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>';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ function Accounts ({ className }: Props): React.ReactElement {
|
||||
<Header
|
||||
onFilter={_onFilter}
|
||||
showAdd
|
||||
showConnectedAccounts
|
||||
showSearch
|
||||
showSettings
|
||||
text={t<string>('Accounts')}
|
||||
|
||||
@ -21,6 +21,7 @@ const darkTheme = {
|
||||
buttonBackgroundDangerHover: '#D93B3B',
|
||||
buttonBackgroundHover: '#ED9329',
|
||||
buttonTextColor: '#FFFFFF',
|
||||
connectedDotColor: 'seagreen',
|
||||
errorBorderColor: '#7E3530',
|
||||
errorColor: '#E42F2F',
|
||||
fontFamily: 'Nunito, sans-serif',
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user