From fddafca7fea261fc3151ee5966c30ad5071e2726 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Thu, 22 Aug 2024 17:57:04 +0400 Subject: [PATCH 01/12] Create popup ready message --- apps/extension/src/hooks/popup-ready.ts | 23 +++++++++ apps/extension/src/message/popup.ts | 7 ++- apps/extension/src/popup.ts | 49 ++++++++++++------- .../src/routes/popup/popup-layout.tsx | 15 ++++-- 4 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 apps/extension/src/hooks/popup-ready.ts diff --git a/apps/extension/src/hooks/popup-ready.ts b/apps/extension/src/hooks/popup-ready.ts new file mode 100644 index 00000000..3c470e26 --- /dev/null +++ b/apps/extension/src/hooks/popup-ready.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { useSearchParams, URLSearchParams, SetURLSearchParams } from 'react-router-dom'; +import { PopupReadyMessage } from '../message/popup'; + +type IsReady = boolean | undefined; + +interface PopupURLSearchParams extends URLSearchParams { + popupId?: string; +} + +// signals that react is ready (mounted) to service worker +export const usePopupReady = (isReady: IsReady = undefined) => { + const [searchParams] = useSearchParams() as [PopupURLSearchParams, SetURLSearchParams]; + + useEffect(() => { + if (searchParams.popupId && (isReady === undefined || isReady)) { + void chrome.runtime.sendMessage({ + popupReady: true, + popupId: searchParams.popupId, + } as PopupReadyMessage); + } + }, [searchParams.popupId, isReady]); +}; diff --git a/apps/extension/src/message/popup.ts b/apps/extension/src/message/popup.ts index 29290e34..6f18adf3 100644 --- a/apps/extension/src/message/popup.ts +++ b/apps/extension/src/message/popup.ts @@ -13,13 +13,18 @@ export enum PopupType { OriginApproval = 'OriginApproval', } +export interface PopupReadyMessage { + popupReady: boolean; + popupId: string; +} + export type PopupMessage = TxApproval | OriginApproval; export type PopupRequest = InternalRequest; export type PopupResponse = InternalResponse; export type OriginApproval = InternalMessage< PopupType.OriginApproval, - { origin: string; favIconUrl?: string; title?: string; lastRequest?: number }, + { id: string; origin: string; favIconUrl?: string; title?: string; lastRequest?: number }, null | OriginRecord >; diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index ed9bec7a..1b7af71e 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -1,5 +1,5 @@ import { sessionExtStorage } from './storage/session'; -import { PopupMessage, PopupRequest, PopupType } from './message/popup'; +import { PopupMessage, PopupRequest, PopupType, PopupReadyMessage } from './message/popup'; import { PopupPath } from './routes/popup/paths'; import type { InternalRequest, InternalResponse } from '@penumbra-zone/types/internal-msg/shared'; import { Code, ConnectError } from '@connectrpc/connect'; @@ -18,23 +18,36 @@ const isChromeResponderDroppedError = ( export const popup = async ( req: PopupRequest, ): Promise => { - await spawnPopup(req.type); - // We have to wait for React to bootup, navigate to the page, and render the components - await new Promise(resolve => setTimeout(resolve, 800)); - const response = await chrome.runtime - .sendMessage, InternalResponse>(req) - .catch((e: unknown) => { - if (isChromeResponderDroppedError(e)) { - return null; - } else { - throw e; + console.log('TCL: req', req); + const popupId = crypto.randomUUID(); + await spawnPopup(req.type, popupId); + + return new Promise((resolve, reject) => { + chrome.runtime.onMessage.addListener(async function handleReactReady( + res: PopupReadyMessage, + ): void { + console.log('TCL: res', res); + if (res.popupReady && res.popupId === popupId) { + console.log('TCL: res.popupReady', res.popupReady); + chrome.runtime.onMessage.removeListener(handleReactReady); + + const response = await chrome.runtime + .sendMessage, InternalResponse>(req) + .catch((e: unknown) => { + if (isChromeResponderDroppedError(e)) { + return null; + } else { + throw e; + } + }); + if (response && 'error' in response) { + reject(errorFromJson(response.error, undefined, ConnectError.from(response))); + } else { + resolve(response && response.data); + } } }); - if (response && 'error' in response) { - throw errorFromJson(response.error, undefined, ConnectError.from(response)); - } else { - return response && response.data; - } + }); }; const spawnDetachedPopup = async (path: string) => { @@ -73,8 +86,8 @@ const throwIfNeedsLogin = async () => { } }; -const spawnPopup = async (pop: PopupType) => { - const popUrl = new URL(chrome.runtime.getURL('popup.html')); +const spawnPopup = async (pop: PopupType, popupId: string) => { + const popUrl = new URL(chrome.runtime.getURL(`popup.html?popupId=${popupId}`)); await throwIfNeedsLogin(); diff --git a/apps/extension/src/routes/popup/popup-layout.tsx b/apps/extension/src/routes/popup/popup-layout.tsx index 11989b0f..fc306285 100644 --- a/apps/extension/src/routes/popup/popup-layout.tsx +++ b/apps/extension/src/routes/popup/popup-layout.tsx @@ -1,4 +1,5 @@ import { Outlet } from 'react-router-dom'; +import { usePopupReady } from '../../hooks/popup-ready'; /** * @todo: Fix the issue where the detached popup isn't sized correctly. This @@ -11,8 +12,12 @@ import { Outlet } from 'react-router-dom'; * routes here in `PopupLayout`, and using a different root class name for each, * then removing the hard-coded width from `globals.css`. */ -export const PopupLayout = () => ( -
- -
-); +export const PopupLayout = () => { + usePopupReady(); + + return ( +
+ +
+ ); +} From 831ea52ac2c872dbc73f97ae0e3554436aac1579 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Thu, 22 Aug 2024 17:58:08 +0400 Subject: [PATCH 02/12] Add scripts/private to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9df713cf..33169bb8 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ packages/*/package apps/extension/chromium-profile +scripts/private From 5564da12d24140f722806f5128919756ad5538cb Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Fri, 23 Aug 2024 16:21:41 +0400 Subject: [PATCH 03/12] Abstract popupReady --- apps/extension/src/hooks/popup-ready.ts | 27 ++++++----- apps/extension/src/message/popup.ts | 15 ++++-- apps/extension/src/popup.ts | 64 ++++++++++++++----------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/apps/extension/src/hooks/popup-ready.ts b/apps/extension/src/hooks/popup-ready.ts index 3c470e26..6fac64c4 100644 --- a/apps/extension/src/hooks/popup-ready.ts +++ b/apps/extension/src/hooks/popup-ready.ts @@ -1,23 +1,24 @@ -import { useEffect } from 'react'; -import { useSearchParams, URLSearchParams, SetURLSearchParams } from 'react-router-dom'; -import { PopupReadyMessage } from '../message/popup'; +import { useEffect, useRef } from 'react'; +import { PopupType, PopupReadyResponse } from '../message/popup'; type IsReady = boolean | undefined; -interface PopupURLSearchParams extends URLSearchParams { - popupId?: string; -} - // signals that react is ready (mounted) to service worker export const usePopupReady = (isReady: IsReady = undefined) => { - const [searchParams] = useSearchParams() as [PopupURLSearchParams, SetURLSearchParams]; + const sentMessagedRef = useRef(new Set()); + const searchParams = new URLSearchParams(window.location.search); + const popupId = searchParams.get('popupId'); useEffect(() => { - if (searchParams.popupId && (isReady === undefined || isReady)) { + if (popupId && (isReady === undefined || isReady) && !sentMessagedRef.current.has(popupId)) { + sentMessagedRef.current.add(popupId); + void chrome.runtime.sendMessage({ - popupReady: true, - popupId: searchParams.popupId, - } as PopupReadyMessage); + type: PopupType.Ready, + data: { + popupId, + }, + } as PopupReadyResponse); } - }, [searchParams.popupId, isReady]); + }, [popupId, isReady]); }; diff --git a/apps/extension/src/message/popup.ts b/apps/extension/src/message/popup.ts index 6f18adf3..f857a001 100644 --- a/apps/extension/src/message/popup.ts +++ b/apps/extension/src/message/popup.ts @@ -11,16 +11,13 @@ import { OriginRecord } from '../storage/types'; export enum PopupType { TxApproval = 'TxApproval', OriginApproval = 'OriginApproval', -} - -export interface PopupReadyMessage { - popupReady: boolean; - popupId: string; + Ready = 'PopupReady', } export type PopupMessage = TxApproval | OriginApproval; export type PopupRequest = InternalRequest; export type PopupResponse = InternalResponse; +export type PopupReadyResponse = InternalResponse; export type OriginApproval = InternalMessage< PopupType.OriginApproval, @@ -39,6 +36,14 @@ export type TxApproval = InternalMessage< } >; +export type Ready = InternalMessage< + PopupType.Ready, + null, + { + popupId: string; + } +>; + export const isPopupRequest = (req: unknown): req is PopupRequest => req != null && typeof req === 'object' && diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index 1b7af71e..d4a3ae36 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -1,5 +1,5 @@ import { sessionExtStorage } from './storage/session'; -import { PopupMessage, PopupRequest, PopupType, PopupReadyMessage } from './message/popup'; +import { PopupMessage, PopupRequest, PopupType, PopupReadyResponse } from './message/popup'; import { PopupPath } from './routes/popup/paths'; import type { InternalRequest, InternalResponse } from '@penumbra-zone/types/internal-msg/shared'; import { Code, ConnectError } from '@connectrpc/connect'; @@ -18,36 +18,25 @@ const isChromeResponderDroppedError = ( export const popup = async ( req: PopupRequest, ): Promise => { - console.log('TCL: req', req); const popupId = crypto.randomUUID(); await spawnPopup(req.type, popupId); + await popupReady(popupId); - return new Promise((resolve, reject) => { - chrome.runtime.onMessage.addListener(async function handleReactReady( - res: PopupReadyMessage, - ): void { - console.log('TCL: res', res); - if (res.popupReady && res.popupId === popupId) { - console.log('TCL: res.popupReady', res.popupReady); - chrome.runtime.onMessage.removeListener(handleReactReady); - - const response = await chrome.runtime - .sendMessage, InternalResponse>(req) - .catch((e: unknown) => { - if (isChromeResponderDroppedError(e)) { - return null; - } else { - throw e; - } - }); - if (response && 'error' in response) { - reject(errorFromJson(response.error, undefined, ConnectError.from(response))); - } else { - resolve(response && response.data); - } + const response = await chrome.runtime + .sendMessage, InternalResponse>(req) + .catch((e: unknown) => { + if (isChromeResponderDroppedError(e)) { + return null; + } else { + throw e; } }); - }); + + if (response && 'error' in response) { + throw errorFromJson(response.error, undefined, ConnectError.from(response)); + } else { + return response && response.data; + } }; const spawnDetachedPopup = async (path: string) => { @@ -87,7 +76,9 @@ const throwIfNeedsLogin = async () => { }; const spawnPopup = async (pop: PopupType, popupId: string) => { - const popUrl = new URL(chrome.runtime.getURL(`popup.html?popupId=${popupId}`)); + const popUrl = new URL( + chrome.runtime.getURL(`popup.html?popupId=${encodeURIComponent(popupId)}`), + ); await throwIfNeedsLogin(); @@ -102,3 +93,22 @@ const spawnPopup = async (pop: PopupType, popupId: string) => { throw Error('Unknown popup type'); } }; + +const POPUP_READY_TIMEOUT = 60 * 1000; + +const popupReady = async (popupId: string): Promise => { + return new Promise((resolve, reject): void => { + setTimeout(() => { + reject(new Error('Popup ready timed out')); + }, POPUP_READY_TIMEOUT); + + const handlePopupReady = (res: PopupReadyResponse): void => { + if (res.type === PopupType.Ready && res.data.popupId === popupId) { + chrome.runtime.onMessage.removeListener(handlePopupReady); + resolve(); + } + }; + + chrome.runtime.onMessage.addListener(handlePopupReady); + }); +}; From 8e03a5d9dee40909512d905b2df6676da217274e Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Fri, 23 Aug 2024 16:46:23 +0400 Subject: [PATCH 04/12] Refactor popup ready --- apps/extension/src/message/popup.ts | 2 +- apps/extension/src/popup.ts | 5 ++--- apps/extension/src/routes/popup/popup-layout.tsx | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/extension/src/message/popup.ts b/apps/extension/src/message/popup.ts index f857a001..a55e34c7 100644 --- a/apps/extension/src/message/popup.ts +++ b/apps/extension/src/message/popup.ts @@ -21,7 +21,7 @@ export type PopupReadyResponse = InternalResponse; export type OriginApproval = InternalMessage< PopupType.OriginApproval, - { id: string; origin: string; favIconUrl?: string; title?: string; lastRequest?: number }, + { origin: string; favIconUrl?: string; title?: string; lastRequest?: number }, null | OriginRecord >; diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index d4a3ae36..bbbc887c 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -1,5 +1,6 @@ import { sessionExtStorage } from './storage/session'; import { PopupMessage, PopupRequest, PopupType, PopupReadyResponse } from './message/popup'; +// import { PopupMessage, PopupRequest, PopupType } from './message/popup'; import { PopupPath } from './routes/popup/paths'; import type { InternalRequest, InternalResponse } from '@penumbra-zone/types/internal-msg/shared'; import { Code, ConnectError } from '@connectrpc/connect'; @@ -76,9 +77,7 @@ const throwIfNeedsLogin = async () => { }; const spawnPopup = async (pop: PopupType, popupId: string) => { - const popUrl = new URL( - chrome.runtime.getURL(`popup.html?popupId=${encodeURIComponent(popupId)}`), - ); + const popUrl = new URL(chrome.runtime.getURL(`popup.html?popupId=${popupId}`)); await throwIfNeedsLogin(); diff --git a/apps/extension/src/routes/popup/popup-layout.tsx b/apps/extension/src/routes/popup/popup-layout.tsx index fc306285..1d609adb 100644 --- a/apps/extension/src/routes/popup/popup-layout.tsx +++ b/apps/extension/src/routes/popup/popup-layout.tsx @@ -20,4 +20,4 @@ export const PopupLayout = () => { ); -} +}; From 3f788eb3d71e47b2b06b223bae1c44637dadf89e Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Fri, 23 Aug 2024 15:18:23 +0200 Subject: [PATCH 05/12] [pairing] update types --- apps/extension/src/entry/popup-root.tsx | 1 + apps/extension/src/hooks/popup-ready.ts | 4 ++-- apps/extension/src/message/popup.ts | 10 ++++++---- apps/extension/src/popup.ts | 17 +++++++++++++---- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/extension/src/entry/popup-root.tsx b/apps/extension/src/entry/popup-root.tsx index 4f42f0d1..5aab9d1a 100644 --- a/apps/extension/src/entry/popup-root.tsx +++ b/apps/extension/src/entry/popup-root.tsx @@ -21,6 +21,7 @@ chrome.runtime.onMessage.addListener( if (isTxApprovalRequest(req)) { void txApprovalSelector(useStore.getState()).acceptRequest(req, responder); } else if (isOriginApprovalRequest(req)) { + req.request.origin; originApprovalSelector(useStore.getState()).acceptRequest(req, responder); } else { throw new Error('Unknown popup request'); diff --git a/apps/extension/src/hooks/popup-ready.ts b/apps/extension/src/hooks/popup-ready.ts index 6fac64c4..f385f0c9 100644 --- a/apps/extension/src/hooks/popup-ready.ts +++ b/apps/extension/src/hooks/popup-ready.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { PopupType, PopupReadyResponse } from '../message/popup'; +import { PopupResponse, PopupType, Ready } from '../message/popup'; type IsReady = boolean | undefined; @@ -18,7 +18,7 @@ export const usePopupReady = (isReady: IsReady = undefined) => { data: { popupId, }, - } as PopupReadyResponse); + } as PopupResponse); } }, [popupId, isReady]); }; diff --git a/apps/extension/src/message/popup.ts b/apps/extension/src/message/popup.ts index a55e34c7..973ad583 100644 --- a/apps/extension/src/message/popup.ts +++ b/apps/extension/src/message/popup.ts @@ -14,10 +14,9 @@ export enum PopupType { Ready = 'PopupReady', } -export type PopupMessage = TxApproval | OriginApproval; +export type PopupMessage = TxApproval | OriginApproval | Ready; export type PopupRequest = InternalRequest; export type PopupResponse = InternalResponse; -export type PopupReadyResponse = InternalResponse; export type OriginApproval = InternalMessage< PopupType.OriginApproval, @@ -53,7 +52,10 @@ export const isPopupRequest = (req: unknown): req is PopupRequest => req.type in PopupType; export const isOriginApprovalRequest = (req: unknown): req is InternalRequest => - isPopupRequest(req) && req.type === PopupType.OriginApproval && 'origin' in req.request; + isPopupRequest(req) && req.type === PopupType.OriginApproval; export const isTxApprovalRequest = (req: unknown): req is InternalRequest => - isPopupRequest(req) && req.type === PopupType.TxApproval && 'authorizeRequest' in req.request; + isPopupRequest(req) && req.type === PopupType.TxApproval; + +export const isPopupReadyResponse = (req: unknown): req is InternalResponse => + isPopupRequest(req) && req.type === PopupType.Ready; diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index bbbc887c..8f540d90 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -1,6 +1,11 @@ import { sessionExtStorage } from './storage/session'; -import { PopupMessage, PopupRequest, PopupType, PopupReadyResponse } from './message/popup'; -// import { PopupMessage, PopupRequest, PopupType } from './message/popup'; +import { + isPopupReadyResponse, + PopupMessage, + PopupRequest, + PopupResponse, + PopupType, +} from './message/popup'; import { PopupPath } from './routes/popup/paths'; import type { InternalRequest, InternalResponse } from '@penumbra-zone/types/internal-msg/shared'; import { Code, ConnectError } from '@connectrpc/connect'; @@ -101,8 +106,12 @@ const popupReady = async (popupId: string): Promise => { reject(new Error('Popup ready timed out')); }, POPUP_READY_TIMEOUT); - const handlePopupReady = (res: PopupReadyResponse): void => { - if (res.type === PopupType.Ready && res.data.popupId === popupId) { + const handlePopupReady = (res: PopupResponse): void => { + if (!isPopupReadyResponse(res)) { + return; + } + + if ('data' in res && res.data.popupId === popupId) { chrome.runtime.onMessage.removeListener(handlePopupReady); resolve(); } From ccd44ae9b4a6050e31954d5cf1c167d7b970bc61 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Fri, 23 Aug 2024 18:25:24 +0400 Subject: [PATCH 06/12] Refactor isPopupReadyResponse --- apps/extension/src/entry/popup-root.tsx | 1 - apps/extension/src/message/popup.ts | 12 ++++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/extension/src/entry/popup-root.tsx b/apps/extension/src/entry/popup-root.tsx index 5aab9d1a..4f42f0d1 100644 --- a/apps/extension/src/entry/popup-root.tsx +++ b/apps/extension/src/entry/popup-root.tsx @@ -21,7 +21,6 @@ chrome.runtime.onMessage.addListener( if (isTxApprovalRequest(req)) { void txApprovalSelector(useStore.getState()).acceptRequest(req, responder); } else if (isOriginApprovalRequest(req)) { - req.request.origin; originApprovalSelector(useStore.getState()).acceptRequest(req, responder); } else { throw new Error('Unknown popup request'); diff --git a/apps/extension/src/message/popup.ts b/apps/extension/src/message/popup.ts index 973ad583..092cc61f 100644 --- a/apps/extension/src/message/popup.ts +++ b/apps/extension/src/message/popup.ts @@ -51,11 +51,19 @@ export const isPopupRequest = (req: unknown): req is PopupRequest => typeof req.type === 'string' && req.type in PopupType; +export const isPopupResponse = (res: unknown): res is PopupResponse => + res != null && + typeof res === 'object' && + ('data' in res || 'error' in res) && + 'type' in res && + typeof res.type === 'string' && + res.type in PopupType; + export const isOriginApprovalRequest = (req: unknown): req is InternalRequest => isPopupRequest(req) && req.type === PopupType.OriginApproval; export const isTxApprovalRequest = (req: unknown): req is InternalRequest => isPopupRequest(req) && req.type === PopupType.TxApproval; -export const isPopupReadyResponse = (req: unknown): req is InternalResponse => - isPopupRequest(req) && req.type === PopupType.Ready; +export const isPopupReadyResponse = (res: unknown): res is InternalResponse => + isPopupResponse(res) && res.type === PopupType.Ready; From 7e7355b0de1e93318b7606a9365b4f2aa77b8a6c Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Fri, 23 Aug 2024 18:32:40 +0400 Subject: [PATCH 07/12] Fix typo --- apps/extension/src/hooks/popup-ready.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/extension/src/hooks/popup-ready.ts b/apps/extension/src/hooks/popup-ready.ts index f385f0c9..ebcca4c4 100644 --- a/apps/extension/src/hooks/popup-ready.ts +++ b/apps/extension/src/hooks/popup-ready.ts @@ -5,13 +5,13 @@ type IsReady = boolean | undefined; // signals that react is ready (mounted) to service worker export const usePopupReady = (isReady: IsReady = undefined) => { - const sentMessagedRef = useRef(new Set()); + const sentMessagesRef = useRef(new Set()); const searchParams = new URLSearchParams(window.location.search); const popupId = searchParams.get('popupId'); useEffect(() => { - if (popupId && (isReady === undefined || isReady) && !sentMessagedRef.current.has(popupId)) { - sentMessagedRef.current.add(popupId); + if (popupId && (isReady === undefined || isReady) && !sentMessagesRef.current.has(popupId)) { + sentMessagesRef.current.add(popupId); void chrome.runtime.sendMessage({ type: PopupType.Ready, From 6829bf6f9343635a8af37baad501ea4530373e0a Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Tue, 27 Aug 2024 21:31:12 +0400 Subject: [PATCH 08/12] Refactor popup ready msging --- apps/extension/src/hooks/popup-ready.ts | 11 +++----- apps/extension/src/message/popup.ts | 26 +++--------------- apps/extension/src/popup.ts | 36 +++++++++---------------- 3 files changed, 18 insertions(+), 55 deletions(-) diff --git a/apps/extension/src/hooks/popup-ready.ts b/apps/extension/src/hooks/popup-ready.ts index ebcca4c4..3123da63 100644 --- a/apps/extension/src/hooks/popup-ready.ts +++ b/apps/extension/src/hooks/popup-ready.ts @@ -1,24 +1,19 @@ import { useEffect, useRef } from 'react'; -import { PopupResponse, PopupType, Ready } from '../message/popup'; +import { useSearchParams } from 'react-router-dom'; type IsReady = boolean | undefined; // signals that react is ready (mounted) to service worker export const usePopupReady = (isReady: IsReady = undefined) => { const sentMessagesRef = useRef(new Set()); - const searchParams = new URLSearchParams(window.location.search); + const [searchParams] = useSearchParams(); const popupId = searchParams.get('popupId'); useEffect(() => { if (popupId && (isReady === undefined || isReady) && !sentMessagesRef.current.has(popupId)) { sentMessagesRef.current.add(popupId); - void chrome.runtime.sendMessage({ - type: PopupType.Ready, - data: { - popupId, - }, - } as PopupResponse); + void chrome.runtime.sendMessage(popupId); } }, [popupId, isReady]); }; diff --git a/apps/extension/src/message/popup.ts b/apps/extension/src/message/popup.ts index 092cc61f..29290e34 100644 --- a/apps/extension/src/message/popup.ts +++ b/apps/extension/src/message/popup.ts @@ -11,10 +11,9 @@ import { OriginRecord } from '../storage/types'; export enum PopupType { TxApproval = 'TxApproval', OriginApproval = 'OriginApproval', - Ready = 'PopupReady', } -export type PopupMessage = TxApproval | OriginApproval | Ready; +export type PopupMessage = TxApproval | OriginApproval; export type PopupRequest = InternalRequest; export type PopupResponse = InternalResponse; @@ -35,14 +34,6 @@ export type TxApproval = InternalMessage< } >; -export type Ready = InternalMessage< - PopupType.Ready, - null, - { - popupId: string; - } ->; - export const isPopupRequest = (req: unknown): req is PopupRequest => req != null && typeof req === 'object' && @@ -51,19 +42,8 @@ export const isPopupRequest = (req: unknown): req is PopupRequest => typeof req.type === 'string' && req.type in PopupType; -export const isPopupResponse = (res: unknown): res is PopupResponse => - res != null && - typeof res === 'object' && - ('data' in res || 'error' in res) && - 'type' in res && - typeof res.type === 'string' && - res.type in PopupType; - export const isOriginApprovalRequest = (req: unknown): req is InternalRequest => - isPopupRequest(req) && req.type === PopupType.OriginApproval; + isPopupRequest(req) && req.type === PopupType.OriginApproval && 'origin' in req.request; export const isTxApprovalRequest = (req: unknown): req is InternalRequest => - isPopupRequest(req) && req.type === PopupType.TxApproval; - -export const isPopupReadyResponse = (res: unknown): res is InternalResponse => - isPopupResponse(res) && res.type === PopupType.Ready; + isPopupRequest(req) && req.type === PopupType.TxApproval && 'authorizeRequest' in req.request; diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index 8f540d90..c42dd93b 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -1,11 +1,5 @@ import { sessionExtStorage } from './storage/session'; -import { - isPopupReadyResponse, - PopupMessage, - PopupRequest, - PopupResponse, - PopupType, -} from './message/popup'; +import { PopupMessage, PopupRequest, PopupType } from './message/popup'; import { PopupPath } from './routes/popup/paths'; import type { InternalRequest, InternalResponse } from '@penumbra-zone/types/internal-msg/shared'; import { Code, ConnectError } from '@connectrpc/connect'; @@ -82,16 +76,16 @@ const throwIfNeedsLogin = async () => { }; const spawnPopup = async (pop: PopupType, popupId: string) => { - const popUrl = new URL(chrome.runtime.getURL(`popup.html?popupId=${popupId}`)); + const popUrl = new URL(chrome.runtime.getURL('popup.html')); await throwIfNeedsLogin(); switch (pop) { case PopupType.OriginApproval: - popUrl.hash = PopupPath.ORIGIN_APPROVAL; + popUrl.hash = `${PopupPath.ORIGIN_APPROVAL}?popupId=${popupId}`; return spawnDetachedPopup(popUrl.href); case PopupType.TxApproval: - popUrl.hash = PopupPath.TRANSACTION_APPROVAL; + popUrl.hash = `${PopupPath.TRANSACTION_APPROVAL}?popupId=${popupId}`; return spawnDetachedPopup(popUrl.href); default: throw Error('Unknown popup type'); @@ -100,23 +94,17 @@ const spawnPopup = async (pop: PopupType, popupId: string) => { const POPUP_READY_TIMEOUT = 60 * 1000; -const popupReady = async (popupId: string): Promise => { - return new Promise((resolve, reject): void => { - setTimeout(() => { - reject(new Error('Popup ready timed out')); - }, POPUP_READY_TIMEOUT); +const popupReady = (popupId: string): Promise => + new Promise((resolve, reject) => { + AbortSignal.timeout(POPUP_READY_TIMEOUT).onabort = reject; - const handlePopupReady = (res: PopupResponse): void => { - if (!isPopupReadyResponse(res)) { - return; - } - - if ('data' in res && res.data.popupId === popupId) { - chrome.runtime.onMessage.removeListener(handlePopupReady); + const idListen = (msg: unknown, _: chrome.runtime.MessageSender, respond: () => void) => { + if (msg === popupId) { resolve(); + chrome.runtime.onMessage.removeListener(idListen); + respond(); } }; - chrome.runtime.onMessage.addListener(handlePopupReady); + chrome.runtime.onMessage.addListener(idListen); }); -}; From 3cba6f590de2ff49f5af5b049bf766aad9c5aa16 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Tue, 27 Aug 2024 21:47:39 +0400 Subject: [PATCH 09/12] Add comment --- apps/extension/src/popup.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index c42dd93b..388cd95b 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -20,6 +20,9 @@ export const popup = async ( ): Promise => { const popupId = crypto.randomUUID(); await spawnPopup(req.type, popupId); + + // this is necessary given it takes a bit of time for the popup + // to be ready to accept messages from the service worker. await popupReady(popupId); const response = await chrome.runtime From a9d0478e3c5cc72e6eb5136d80c5a49de092deb9 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 28 Aug 2024 13:52:02 +0400 Subject: [PATCH 10/12] Remove usePopupReady arg --- apps/extension/src/hooks/popup-ready.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/extension/src/hooks/popup-ready.ts b/apps/extension/src/hooks/popup-ready.ts index 3123da63..38cc0a88 100644 --- a/apps/extension/src/hooks/popup-ready.ts +++ b/apps/extension/src/hooks/popup-ready.ts @@ -1,19 +1,17 @@ import { useEffect, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; -type IsReady = boolean | undefined; - // signals that react is ready (mounted) to service worker -export const usePopupReady = (isReady: IsReady = undefined) => { +export const usePopupReady = () => { const sentMessagesRef = useRef(new Set()); const [searchParams] = useSearchParams(); const popupId = searchParams.get('popupId'); useEffect(() => { - if (popupId && (isReady === undefined || isReady) && !sentMessagesRef.current.has(popupId)) { + if (popupId && !sentMessagesRef.current.has(popupId)) { sentMessagesRef.current.add(popupId); void chrome.runtime.sendMessage(popupId); } - }, [popupId, isReady]); + }, [popupId]); }; From 861180e2d886c41edaee1bbd0af16a2808e34223 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 28 Aug 2024 13:58:03 +0400 Subject: [PATCH 11/12] Use pathname for throwIfAlreadyOpen --- apps/extension/src/popup.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index 388cd95b..694d74a1 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -42,13 +42,13 @@ export const popup = async ( } }; -const spawnDetachedPopup = async (path: string) => { - await throwIfAlreadyOpen(path); +const spawnDetachedPopup = async (url: URL) => { + await throwIfAlreadyOpen(url.pathname); const { top, left, width } = await chrome.windows.getLastFocused(); await chrome.windows.create({ - url: path, + url: url.href, type: 'popup', width: 400, height: 628, @@ -86,10 +86,10 @@ const spawnPopup = async (pop: PopupType, popupId: string) => { switch (pop) { case PopupType.OriginApproval: popUrl.hash = `${PopupPath.ORIGIN_APPROVAL}?popupId=${popupId}`; - return spawnDetachedPopup(popUrl.href); + return spawnDetachedPopup(popUrl); case PopupType.TxApproval: popUrl.hash = `${PopupPath.TRANSACTION_APPROVAL}?popupId=${popupId}`; - return spawnDetachedPopup(popUrl.href); + return spawnDetachedPopup(popUrl); default: throw Error('Unknown popup type'); } From 20472ac3371532579c0451a046d5c0117784a01d Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 28 Aug 2024 14:10:14 +0400 Subject: [PATCH 12/12] Refactor throwIfAlreadyOpen to use hashPath --- apps/extension/src/popup.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index 694d74a1..f1dcd177 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -43,7 +43,8 @@ export const popup = async ( }; const spawnDetachedPopup = async (url: URL) => { - await throwIfAlreadyOpen(url.pathname); + const [hashPath] = url.hash.split('?'); + await throwIfAlreadyOpen(hashPath!); const { top, left, width } = await chrome.windows.getLastFocused(); @@ -80,10 +81,10 @@ const throwIfNeedsLogin = async () => { const spawnPopup = async (pop: PopupType, popupId: string) => { const popUrl = new URL(chrome.runtime.getURL('popup.html')); - await throwIfNeedsLogin(); switch (pop) { + // set path as hash since we use a hash router within the popup case PopupType.OriginApproval: popUrl.hash = `${PopupPath.ORIGIN_APPROVAL}?popupId=${popupId}`; return spawnDetachedPopup(popUrl);