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 diff --git a/apps/extension/src/hooks/popup-ready.ts b/apps/extension/src/hooks/popup-ready.ts new file mode 100644 index 00000000..38cc0a88 --- /dev/null +++ b/apps/extension/src/hooks/popup-ready.ts @@ -0,0 +1,17 @@ +import { useEffect, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +// signals that react is ready (mounted) to service worker +export const usePopupReady = () => { + const sentMessagesRef = useRef(new Set()); + const [searchParams] = useSearchParams(); + const popupId = searchParams.get('popupId'); + + useEffect(() => { + if (popupId && !sentMessagesRef.current.has(popupId)) { + sentMessagesRef.current.add(popupId); + + void chrome.runtime.sendMessage(popupId); + } + }, [popupId]); +}; diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index ed9bec7a..f1dcd177 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -18,9 +18,13 @@ 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 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 .sendMessage, InternalResponse>(req) .catch((e: unknown) => { @@ -30,6 +34,7 @@ export const popup = async ( throw e; } }); + if (response && 'error' in response) { throw errorFromJson(response.error, undefined, ConnectError.from(response)); } else { @@ -37,13 +42,14 @@ export const popup = async ( } }; -const spawnDetachedPopup = async (path: string) => { - await throwIfAlreadyOpen(path); +const spawnDetachedPopup = async (url: URL) => { + const [hashPath] = url.hash.split('?'); + await throwIfAlreadyOpen(hashPath!); const { top, left, width } = await chrome.windows.getLastFocused(); await chrome.windows.create({ - url: path, + url: url.href, type: 'popup', width: 400, height: 628, @@ -73,19 +79,36 @@ const throwIfNeedsLogin = async () => { } }; -const spawnPopup = async (pop: PopupType) => { +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; - return spawnDetachedPopup(popUrl.href); + popUrl.hash = `${PopupPath.ORIGIN_APPROVAL}?popupId=${popupId}`; + return spawnDetachedPopup(popUrl); case PopupType.TxApproval: - popUrl.hash = PopupPath.TRANSACTION_APPROVAL; - return spawnDetachedPopup(popUrl.href); + popUrl.hash = `${PopupPath.TRANSACTION_APPROVAL}?popupId=${popupId}`; + return spawnDetachedPopup(popUrl); default: throw Error('Unknown popup type'); } }; + +const POPUP_READY_TIMEOUT = 60 * 1000; + +const popupReady = (popupId: string): Promise => + new Promise((resolve, reject) => { + AbortSignal.timeout(POPUP_READY_TIMEOUT).onabort = reject; + + const idListen = (msg: unknown, _: chrome.runtime.MessageSender, respond: () => void) => { + if (msg === popupId) { + resolve(); + chrome.runtime.onMessage.removeListener(idListen); + respond(); + } + }; + + chrome.runtime.onMessage.addListener(idListen); + }); diff --git a/apps/extension/src/routes/popup/popup-layout.tsx b/apps/extension/src/routes/popup/popup-layout.tsx index 11989b0f..1d609adb 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 ( +
+ +
+ ); +};