Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Sidepanel #596

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 61 additions & 10 deletions src/background/NotificationWindow/NotificationWindow.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { createNanoEvents } from 'nanoevents';
import { nanoid } from 'nanoid';
import type { ErrorResponse } from '@walletconnect/jsonrpc-utils';
import browser from 'webextension-polyfill';
import type { RpcError, RpcResult } from 'src/shared/custom-rpc';
import { UserRejected } from 'src/shared/errors/errors';
import { PersistentStore } from 'src/modules/persistent-store';
import { produce } from 'immer';
import { isSidepanelOpen } from 'src/shared/sidepanel/sidepanel-messaging.background';
import { getSidepanelUrl } from 'src/shared/getPopupUrl';
import { emitter as globalEmitter } from '../events';
import type { WindowProps } from './createBrowserWindow';
import { createBrowserWindow } from './createBrowserWindow';

Expand Down Expand Up @@ -38,6 +42,7 @@ type PendingState = Record<string, { windowId: number; id: string }>;

export type NotificationWindowProps<T> = Omit<WindowProps, 'type'> & {
type?: WindowProps['type'];
// tabId?: number;
requestId: string;
onDismiss: (error?: ErrorResponse) => void;
onResolve: (data: T) => void;
Expand Down Expand Up @@ -107,6 +112,12 @@ export class NotificationWindow extends PersistentStore<PendingState> {
browser.windows.remove(windowId);
this.idsMap.delete(id);
this.requestIds.delete(id);
} else if (windowId === 0) {
// For sidepanel, we maybe do not need to close it.
// Currently we only show Dapp Request in sidepanel if it already was open,
// so it doesn't need to be closed after. But if this pattern changes,
// this is the place to close it
// console.log('maybe close sidepanel');
}
}

Expand Down Expand Up @@ -160,6 +171,19 @@ export class NotificationWindow extends PersistentStore<PendingState> {
this.closeWindow(windowId);
}
});
globalEmitter.on('uiClosed', ({ url }) => {
if (!url) {
return;
}
const id = new URLSearchParams(new URL(url).hash).get('windowId');
if (id) {
this.events.emit('settle', {
status: 'rejected',
id,
error: new UserRejected('Sidepanel Closed'),
});
}
});
}

private trackOpenedWindows() {
Expand Down Expand Up @@ -212,6 +236,7 @@ export class NotificationWindow extends PersistentStore<PendingState> {
route,
type = 'dialog',
search,
// tabId,
onDismiss,
onResolve,
width,
Expand Down Expand Up @@ -243,17 +268,43 @@ export class NotificationWindow extends PersistentStore<PendingState> {
return pendingWindows[requestId].id;
}

const { id, windowId } = await createBrowserWindow({
width,
height,
route,
type,
search,
const currentWindow = await browser.windows.getCurrent();
const sidepanelIsOpen = await isSidepanelOpen({
windowId: currentWindow.id ?? null,
});
this.events.emit('open', { requestId, windowId, id });
this.requestIds.set(id, requestId);
this.idsMap.set(id, windowId);
return id;
if (sidepanelIsOpen) {
const sidepanelPath = getSidepanelUrl();
const searchParams = new URLSearchParams(search);
const id = nanoid();
sidepanelPath.searchParams.append('windowId', id);
searchParams.append('windowId', id);
sidepanelPath.hash = `${route}?${searchParams}`;
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
chrome.sidePanel.setOptions({
path: sidepanelPath.toString(),
tabId: tabs[0]?.id,
});
const windowId = 0; // special number for sidepanel?
this.events.emit('open', { requestId, windowId, id });
this.idsMap.set(id, windowId);
this.requestIds.set(id, requestId);
return id;
} else {
const { id, windowId } = await createBrowserWindow({
width,
height,
route,
type,
search,
});
this.events.emit('open', { requestId, windowId, id });
this.requestIds.set(id, requestId);
this.idsMap.set(id, windowId);
return id;
}
}

/** @deprecated */
Expand Down
2 changes: 1 addition & 1 deletion src/background/NotificationWindow/createBrowserWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function createBrowserWindow({
}: WindowProps) {
const id = nanoid();
const params = new URLSearchParams(search);
params.append('windowId', String(id));
params.append('windowId', id);

const {
top: currentWindowTop = 0,
Expand Down
19 changes: 17 additions & 2 deletions src/background/Wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ export class Wallet {
return privateKey === value;
}

async uiGetCurrentWallet({ context }: WalletMethodParams) {
getCurrentWalletSync({ context }: WalletMethodParams) {
this.verifyInternalOrigin(context);
if (!this.id) {
return null;
Expand All @@ -484,6 +484,11 @@ export class Wallet {
return null;
}

async uiGetCurrentWallet({ context }: WalletMethodParams) {
this.verifyInternalOrigin(context);
return this.getCurrentWalletSync({ context });
}

async uiGetWalletByAddress({
context,
params: { address, groupId },
Expand Down Expand Up @@ -1720,7 +1725,17 @@ class PublicController {
]
>) {
const currentAddress = this.wallet.ensureCurrentAddress();
const currentWallet = await this.wallet.uiGetCurrentWallet({
// NOTE: I switched to synchronous method in an attempt to
// synchronously open sidepanel in response to a dapp request
// because browser only allows to open sidepanel synchronously after
// a user action. But currently I abandoned the idea of opening sidepanel
// for dapp requests. Instead, we use sidepanel if it is already opened
// So this sync method is not necessary.
// NOTE:
// There is another possible workaround to opening sidepanel but keeping these methods
// asyncronous. We can synchronously open sidepanel with some loading UI,
// and then later update it with the desired view by calling `.setOptions()` API.
const currentWallet = this.wallet.getCurrentWalletSync({
context: INTERNAL_SYMBOL_CONTEXT,
});
// TODO: should we check transaction.from instead of currentAddress?
Expand Down
1 change: 1 addition & 0 deletions src/background/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ export const emitter = createNanoEvents<{
) => void;
holdToSignPreferenceChange: (active: boolean) => void;
eip6963SupportDetected: (data: { origin: string }) => void;
uiClosed: (data: { url: string | null }) => void;
}>();
4 changes: 4 additions & 0 deletions src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SessionCacheService } from 'src/background/resource/sessionCacheService
import { openOnboarding } from 'src/shared/openOnboarding';
import { userLifecycleStore } from 'src/shared/analytics/shared/UserLifecycle';
import { UrlContextParam } from 'src/shared/types/UrlContext';
import { initializeSidepanelCommands } from 'src/shared/sidepanel/sidepanel-commands.background';
import { initialize } from './initialize';
import { PortRegistry } from './messaging/PortRegistry';
import { createWalletMessageHandler } from './messaging/port-message-handlers/createWalletMessageHandler';
Expand All @@ -23,6 +24,8 @@ import { TransactionService } from './transactions/TransactionService';

Object.assign(globalThis, { ethers });

initializeSidepanelCommands();

globalThis.addEventListener('install', (_event) => {
/** Seems to be recommended when clients always expect a service worker */
// @ts-ignore sw service-worker environment
Expand Down Expand Up @@ -184,6 +187,7 @@ initialize().then((values) => {
) {
// Means extension UI is closed
account.expirePasswordSession();
emitter.emit('uiClosed', { url: port.sender?.url || null });
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export class EthereumEventsBroadcaster implements Listener {
private getClientPorts() {
const ports = this.getActivePorts();
return ports.filter(
(port) => port.name === `${browser.runtime.id}/ethereum`
(port) =>
port.name === `${browser.runtime.id}/ethereum` ||
// "/wallet" is our own UI and we need to notify changes to it
// when it's opened as a sidepanel
port.name === `${browser.runtime.id}/wallet`
);
}

Expand Down
15 changes: 14 additions & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"48": "images/logo-icon-48.png",
"128": "images/logo-icon-128.png"
},
"author": "https://zerion.io/",
"action": {
"default_icon": {
"16": "images/logo-icon-16.png",
Expand All @@ -21,7 +22,18 @@
"default_title": "Zerion",
"default_popup": "ui/popup.html"
},
"author": "https://zerion.io/",
"side_panel": {
"default_path": "ui/sidepanel.html"
},
"commands": {
"sidepanel_open_custom": {
"suggested_key": { "default": "Ctrl+Shift+S" },
"description": "Toggle Sidepanel View"
},
"_execute_action": {
"suggested_key": { "default": "Ctrl+Shift+E" }
}
},
"background": {
"service_worker": "background/index.ts",
"type": "module"
Expand All @@ -44,6 +56,7 @@
"activeTab",
"alarms",
"scripting",
"sidePanel",
"storage",
"unlimitedStorage"
],
Expand Down
11 changes: 9 additions & 2 deletions src/shared/UrlContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ import type {
} from './types/UrlContext';
import { UrlContextParam } from './types/UrlContext';

function getWindowType(params: URLSearchParams): WindowType {
if (window.location.pathname.startsWith('/sidepanel')) {
return 'sidepanel';
}
return (params.get(UrlContextParam.windowType) as WindowType) || 'popup';
}

function getUrlContext(): UrlContext {
const params = new URL(window.location.href).searchParams;
return {
appMode: (params.get(UrlContextParam.appMode) as AppMode) || 'wallet',
windowType:
(params.get(UrlContextParam.windowType) as WindowType) || 'popup',
windowType: getWindowType(params),
// (params.get(UrlContextParam.windowType) as WindowType) || 'popup',
windowLayout:
(params.get(UrlContextParam.windowLayout) as WindowLayout) || 'column',
};
Expand Down
11 changes: 11 additions & 0 deletions src/shared/getPopupUrl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import browser from 'webextension-polyfill';

// TODO: rename to getPopupPath
export function getPopupUrl() {
/**
* Normally, we'd get the path to popup.html like this:
Expand All @@ -14,3 +15,13 @@ export function getPopupUrl() {
}
return new URL(browser.runtime.getURL(popupUrl));
}

// TODO: rename to getSidepanelPath
export function getSidepanelUrl() {
// @ts-ignore extension manifest types
const sidepanelUrl = browser.runtime.getManifest().side_panel?.default_path;
if (!sidepanelUrl) {
throw new Error('sidepanelUrl not found');
}
return new URL(browser.runtime.getURL(sidepanelUrl));
}
25 changes: 25 additions & 0 deletions src/shared/sidepanel/BrowserState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import browser from 'webextension-polyfill';

class BrowserState {
currentWindowId?: number;
initialWindowId?: number;

constructor() {
this.getInitialWindow();
this.addListeners();
}

async getInitialWindow() {
const currentWindow = await browser.windows.getCurrent();
this.currentWindowId = currentWindow.id;
this.initialWindowId = currentWindow.id;
}

addListeners() {
browser.windows.onFocusChanged.addListener((windowId) => {
this.currentWindowId = windowId;
});
}
}

export const browserState = new BrowserState();
Loading
Loading