Skip to content

Commit e70e44f

Browse files
chore: Refactor offscreen creation logic (#25302)
## **Description** Refactor initialization logic to defer creation of the offscreen document until the `MetaMaskController` is initialized. This adds a `offscreenPromise` to the controller that can be awaited for functionality that requires the offscreen document to be created. Additionally this PR adds a message that the offscreen document will send once initial execution of the offscreen page has finished. This is awaited in the `offscreenPromise`. We await `offscreenPromise` before unlocking the keyrings as some keyrings rely on the offscreen document to process requests, e.g. hardware wallets. There may be room for more improvements here though, that I have not tackled in this PR. As the hardware wallet logic doesn't seem to wait for iframes to fully load, so there is a chance of some missed messages. I have tested that hardware wallet support, at least for Ledger, is still working following the changes in this PR. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25302?quickstart=1)
1 parent 68d35f0 commit e70e44f

File tree

6 files changed

+64
-24
lines changed

6 files changed

+64
-24
lines changed

app/scripts/app-init.js

-24
Original file line numberDiff line numberDiff line change
@@ -185,27 +185,3 @@ const registerInPageContentScript = async () => {
185185
};
186186

187187
registerInPageContentScript();
188-
189-
/**
190-
* Creates an offscreen document that can be used to load additional scripts
191-
* and iframes that can communicate with the extension through the chrome
192-
* runtime API. Only one offscreen document may exist, so any iframes required
193-
* by extension can be embedded in the offscreen.html file. See the offscreen
194-
* folder for more details.
195-
*/
196-
async function createOffscreen() {
197-
if (!chrome.offscreen || (await chrome.offscreen.hasDocument())) {
198-
return;
199-
}
200-
201-
await chrome.offscreen.createDocument({
202-
url: './offscreen.html',
203-
reasons: ['IFRAME_SCRIPTING'],
204-
justification:
205-
'Used for Hardware Wallet and Snaps scripts to communicate with the extension.',
206-
});
207-
208-
console.debug('Offscreen iframe loaded');
209-
}
210-
211-
createOffscreen();

app/scripts/background.js

+7
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
shouldEmitDappViewedEvent,
6868
} from './lib/util';
6969
import { generateSkipOnboardingState } from './skip-onboarding';
70+
import { createOffscreen } from './offscreen';
7071

7172
/* eslint-enable import/first */
7273

@@ -261,6 +262,8 @@ function saveTimestamp() {
261262
*/
262263
async function initialize() {
263264
try {
265+
const offscreenPromise = isManifestV3 ? createOffscreen() : null;
266+
264267
const initData = await loadStateFromPersistence();
265268

266269
const initState = initData.data;
@@ -293,6 +296,7 @@ async function initialize() {
293296
{},
294297
isFirstMetaMaskControllerSetup,
295298
initData.meta,
299+
offscreenPromise,
296300
);
297301
if (!isManifestV3) {
298302
await loadPhishingWarningPage();
@@ -507,13 +511,15 @@ function emitDappViewedMetricEvent(
507511
* @param {object} overrides - object with callbacks that are allowed to override the setup controller logic
508512
* @param isFirstMetaMaskControllerSetup
509513
* @param {object} stateMetadata - Metadata about the initial state and migrations, including the most recent migration version
514+
* @param {Promise<void>} offscreenPromise - A promise that resolves when the offscreen document has finished initialization.
510515
*/
511516
export function setupController(
512517
initState,
513518
initLangCode,
514519
overrides,
515520
isFirstMetaMaskControllerSetup,
516521
stateMetadata,
522+
offscreenPromise,
517523
) {
518524
//
519525
// MetaMask Controller
@@ -542,6 +548,7 @@ export function setupController(
542548
isFirstMetaMaskControllerSetup,
543549
currentMigrationVersion: stateMetadata.version,
544550
featureFlags: {},
551+
offscreenPromise,
545552
});
546553

547554
setupEnsIpfsResolver({

app/scripts/metamask-controller.js

+6
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ export default class MetamaskController extends EventEmitter {
374374
// the only thing that uses controller connections are open metamask UI instances
375375
this.activeControllerConnections = 0;
376376

377+
this.offscreenPromise = opts.offscreenPromise ?? Promise.resolve();
378+
377379
this.getRequestAccountTabIds = opts.getRequestAccountTabIds;
378380
this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds;
379381

@@ -4069,6 +4071,10 @@ export default class MetamaskController extends EventEmitter {
40694071
*/
40704072
async submitPassword(password) {
40714073
const { completedOnboarding } = this.onboardingController.store.getState();
4074+
4075+
// Before attempting to unlock the keyrings, we need the offscreen to have loaded.
4076+
await this.offscreenPromise;
4077+
40724078
await this.keyringController.submitPassword(password);
40734079

40744080
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)

app/scripts/offscreen.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { OffscreenCommunicationTarget } from '../../shared/constants/offscreen-communication';
2+
3+
/**
4+
* Creates an offscreen document that can be used to load additional scripts
5+
* and iframes that can communicate with the extension through the chrome
6+
* runtime API. Only one offscreen document may exist, so any iframes required
7+
* by extension can be embedded in the offscreen.html file. See the offscreen
8+
* folder for more details.
9+
*/
10+
export async function createOffscreen() {
11+
const { chrome } = globalThis;
12+
if (!chrome.offscreen || (await chrome.offscreen.hasDocument())) {
13+
return;
14+
}
15+
16+
const loadPromise = new Promise((resolve) => {
17+
const messageListener = (msg) => {
18+
if (
19+
msg.target === OffscreenCommunicationTarget.extensionMain &&
20+
msg.isBooted
21+
) {
22+
chrome.runtime.onMessage.removeListener(messageListener);
23+
resolve();
24+
}
25+
};
26+
chrome.runtime.onMessage.addListener(messageListener);
27+
});
28+
29+
await chrome.offscreen.createDocument({
30+
url: './offscreen.html',
31+
reasons: ['IFRAME_SCRIPTING'],
32+
justification:
33+
'Used for Hardware Wallet and Snaps scripts to communicate with the extension.',
34+
});
35+
36+
// In case we are in a bad state where the offscreen document is not loading, timeout and let execution continue.
37+
const timeoutPromise = new Promise((resolve) => {
38+
setTimeout(resolve, 5000);
39+
});
40+
41+
await Promise.race([loadPromise, timeoutPromise]);
42+
43+
console.debug('Offscreen iframe loaded');
44+
}

offscreen/scripts/offscreen.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BrowserRuntimePostMessageStream } from '@metamask/post-message-stream';
22
import { ProxySnapExecutor } from '@metamask/snaps-execution-environments';
3+
import { OffscreenCommunicationTarget } from '../../shared/constants/offscreen-communication';
34
import initLedger from './ledger';
45
import initTrezor from './trezor';
56
import initLattice from './lattice';
@@ -20,3 +21,8 @@ const parentStream = new BrowserRuntimePostMessageStream({
2021
});
2122

2223
ProxySnapExecutor.initialize(parentStream, './snaps/index.html');
24+
25+
chrome.runtime.sendMessage({
26+
target: OffscreenCommunicationTarget.extensionMain,
27+
isBooted: true,
28+
});

shared/constants/offscreen-communication.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export enum OffscreenCommunicationTarget {
77
ledgerOffscreen = 'ledger-offscreen',
88
latticeOffscreen = 'lattice-offscreen',
99
extension = 'extension-offscreen',
10+
extensionMain = 'extension',
1011
}
1112

1213
/**

0 commit comments

Comments
 (0)