1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02:00
metamask-extension/app/scripts/ui.js
Olaf Tomalka 95c37e1ba3
feat: add yaml feature management (#18125)
* feat: add yaml feature management

Add yaml feature file per build type.
Also add method to parse yaml and set
enabled features env to true. The build
process will then replace any process.env[feature]
that exists on the config by its value

* chore: add example for desktop

* Added initial draft of build features

* [TMP] Sync between computers

* Is able to succesfully build stable extension with snaps feature

* Removing var context from builds.yml

* Add asssets to builds.yml

* Minor bug fixes and removing debug logs

* [WIP] Test changes

* Removed TODOs

* Fix regession bug

Also
* remove debug logs
* merge Variables.set and Variables.setMany with an overload

* Fix build, lint and a bunch of issues

* Update LavaMoat policies

* Re-add desktop build type

* Fix some tests

* Fix desktop build

* Define some env variables used by MV3

* Fix lint

* Fix remove-fenced-code tests

* Fix README typo

* Move new code

* Fix missing asset copy

* Move Jest env setup

* Fix path for test after rebase

* Fix code fences

* Fix fencing and LavaMoat policies

* Fix MMI code-fencing after rebase

* Fix MMI code fencing after merge

* Fix more MMI code fencing

---------

Co-authored-by: cryptotavares <joao.tavares@consensys.net>
Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>
Co-authored-by: Brad Decker <bhdecker84@gmail.com>
2023-04-25 16:32:51 +02:00

437 lines
14 KiB
JavaScript

// polyfills
import '@formatjs/intl-relativetimeformat/polyfill';
// dev only, "react-devtools" import is skipped in prod builds
import 'react-devtools';
import PortStream from 'extension-port-stream';
import browser from 'webextension-polyfill';
import Eth from 'ethjs';
import EthQuery from 'eth-query';
import StreamProvider from 'web3-stream-provider';
import log from 'loglevel';
import launchMetaMaskUi, { updateBackgroundConnection } from '../../ui';
import {
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_FIREFOX,
} from '../../shared/constants/app';
import { isManifestV3 } from '../../shared/modules/mv3.utils';
import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils';
import { SUPPORT_LINK } from '../../shared/lib/ui-utils';
import {
getErrorHtml,
///: BEGIN:ONLY_INCLUDE_IN(desktop)
registerDesktopErrorActions,
///: END:ONLY_INCLUDE_IN
} from '../../shared/lib/error-utils';
import ExtensionPlatform from './platforms/extension';
import { setupMultiplex } from './lib/stream-utils';
import { getEnvironmentType, getPlatform } from './lib/util';
import metaRPCClientFactory from './lib/metaRPCClientFactory';
const container = document.getElementById('app-content');
const ONE_SECOND_IN_MILLISECONDS = 1_000;
// Service Worker Keep Alive Message Constants
const WORKER_KEEP_ALIVE_INTERVAL = ONE_SECOND_IN_MILLISECONDS;
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
const ACK_KEEP_ALIVE_WAIT_TIME = 60_000; // 1 minute
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
// Timeout for initializing phishing warning page.
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
const PHISHING_WARNING_SW_STORAGE_KEY = 'phishing-warning-sw-registered';
let lastMessageReceivedTimestamp = Date.now();
let extensionPort;
let ackTimeoutToDisplayError;
/*
* As long as UI is open it will keep sending messages to service worker
* In service worker as this message is received
* if service worker is inactive it is reactivated and script re-loaded
* Time has been kept to 1000ms but can be reduced for even faster re-activation of service worker
*/
if (isManifestV3) {
// Checking for SW aliveness (or stuckness) flow
// 1. Check if we have an extensionPort, if yes
// 2a. Send a keep alive message to the background via extensionPort
// 2b. Add a listener to it (if not already added)
// 3a. Set a timeout to check if we have received an ACK from background
// 3b. If we have not received an ACK within ACK_KEEP_ALIVE_WAIT_TIME,
// we know the background is stuck or dead
// 4. If we recieve an ACK_KEEP_ALIVE_MESSAGE from the service worker, we know it is alive
const ackKeepAliveListener = (message) => {
if (message.name === ACK_KEEP_ALIVE_MESSAGE) {
lastMessageReceivedTimestamp = Date.now();
clearTimeout(ackTimeoutToDisplayError);
}
};
const keepAliveInterval = setInterval(() => {
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
if (extensionPort !== null && extensionPort !== undefined) {
extensionPort.postMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
if (extensionPort.onMessage.hasListener(ackKeepAliveListener) === false) {
extensionPort.onMessage.addListener(ackKeepAliveListener);
}
}
ackTimeoutToDisplayError = setTimeout(() => {
if (
Date.now() - lastMessageReceivedTimestamp >
ACK_KEEP_ALIVE_WAIT_TIME
) {
clearInterval(keepAliveInterval);
displayCriticalError(
'somethingIsWrong',
new Error("Something's gone wrong. Try reloading the page."),
);
}
}, ACK_KEEP_ALIVE_WAIT_TIME);
}, WORKER_KEEP_ALIVE_INTERVAL);
}
start().catch(log.error);
async function start() {
// create platform global
global.platform = new ExtensionPlatform();
// identify window type (popup, notification)
const windowType = getEnvironmentType();
let isUIInitialised = false;
// setup stream to background
extensionPort = browser.runtime.connect({ name: windowType });
let connectionStream = new PortStream(extensionPort);
const activeTab = await queryCurrentActiveTab(windowType);
let loadPhishingWarningPage;
if (isManifestV3) {
/*
* In case of MV3 the issue of blank screen was very frequent, it is caused by UI initialising before background is ready to send state.
* Code below ensures that UI is rendered only after "CONNECTION_READY" or "startUISync"
* messages are received thus the background is ready, and ensures that streams and
* phishing warning page load only after the "startUISync" message is received.
* In case the UI is already rendered, only update the streams.
*/
const messageListener = async (message) => {
if (message?.data?.method === 'startUISync') {
if (isUIInitialised) {
// Currently when service worker is revived we create new streams
// in later version we might try to improve it by reviving same streams.
updateUiStreams();
} else {
initializeUiWithTab(activeTab);
}
await loadPhishingWarningPage();
}
};
/**
* An error thrown if the phishing warning page takes too long to load.
*/
class PhishingWarningPageTimeoutError extends Error {
constructor() {
super('Timeout failed');
}
}
/**
* Load the phishing warning page temporarily to ensure the service
* worker has been registered, so that the warning page works offline.
*/
loadPhishingWarningPage = async function () {
// Check session storage for whether we've already initialized the phishing warning
// service worker in this browser session and do not attempt to re-initialize if so.
const phishingSWMemoryFetch = await browser.storage.session.get(
PHISHING_WARNING_SW_STORAGE_KEY,
);
if (phishingSWMemoryFetch[PHISHING_WARNING_SW_STORAGE_KEY]) {
return;
}
const currentPlatform = getPlatform();
let iframe;
try {
const extensionStartupPhishingPageUrl = new URL(
process.env.PHISHING_WARNING_PAGE_URL,
);
// The `extensionStartup` hash signals to the phishing warning page that it should not bother
// setting up streams for user interaction. Otherwise this page load would cause a console
// error.
extensionStartupPhishingPageUrl.hash = '#extensionStartup';
iframe = window.document.createElement('iframe');
iframe.setAttribute('src', extensionStartupPhishingPageUrl.href);
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
// Create "deferred Promise" to allow passing resolve/reject to event handlers
let deferredResolve;
let deferredReject;
const loadComplete = new Promise((resolve, reject) => {
deferredResolve = resolve;
deferredReject = reject;
});
// The load event is emitted once loading has completed, even if the loading failed.
// If loading failed we can't do anything about it, so we don't need to check.
iframe.addEventListener('load', deferredResolve);
// This step initiates the page loading.
window.document.body.appendChild(iframe);
// This timeout ensures that this iframe gets cleaned up in a reasonable
// timeframe, and ensures that the "initialization complete" message
// doesn't get delayed too long.
setTimeout(
() => deferredReject(new PhishingWarningPageTimeoutError()),
PHISHING_WARNING_PAGE_TIMEOUT,
);
await loadComplete;
// store a flag in sessions storage that we've already loaded the service worker
// and don't need to try again
if (currentPlatform === PLATFORM_FIREFOX) {
// Firefox does not yet support the storage.session API introduced in MV3
// Tracked here: https://bugzilla.mozilla.org/show_bug.cgi?id=1687778
console.error(
'Firefox does not support required MV3 APIs: Phishing warning page iframe and service worker will reload each page refresh',
);
} else {
browser.storage.session.set({
[PHISHING_WARNING_SW_STORAGE_KEY]: true,
});
}
} catch (error) {
if (error instanceof PhishingWarningPageTimeoutError) {
console.warn(
'Phishing warning page timeout; page not guaranteed to work offline.',
);
} else {
console.error('Failed to initialize phishing warning page', error);
}
} finally {
if (iframe) {
iframe.remove();
}
}
};
// resetExtensionStreamAndListeners takes care to remove listeners from closed streams
// it also creates new streams and attaches event listeners to them
const resetExtensionStreamAndListeners = () => {
extensionPort.onMessage.removeListener(messageListener);
extensionPort.onDisconnect.removeListener(
resetExtensionStreamAndListeners,
);
// message below will try to activate service worker
// in MV3 is likely that reason of stream closing is service worker going in-active
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
extensionPort = browser.runtime.connect({ name: windowType });
connectionStream = new PortStream(extensionPort);
extensionPort.onMessage.addListener(messageListener);
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
};
extensionPort.onMessage.addListener(messageListener);
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
} else {
initializeUiWithTab(activeTab);
}
function initializeUiWithTab(tab) {
initializeUi(
tab,
connectionStream,
(
err,
store,
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
) => {
if (err) {
// if there's an error, store will be = metamaskState
displayCriticalError(
'troubleStarting',
err,
store,
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
);
return;
}
isUIInitialised = true;
const state = store.getState();
const { metamask: { completedOnboarding } = {} } = state;
if (
!completedOnboarding &&
windowType !== ENVIRONMENT_TYPE_FULLSCREEN
) {
global.platform.openExtensionInBrowser();
}
},
);
}
// Function to update new backgroundConnection in the UI
function updateUiStreams() {
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
if (err) {
displayCriticalError(
'troubleStarting',
err,
///: BEGIN:ONLY_INCLUDE_IN(desktop)
undefined,
backgroundConnection,
///: END:ONLY_INCLUDE_IN
);
return;
}
updateBackgroundConnection(backgroundConnection);
});
}
}
async function queryCurrentActiveTab(windowType) {
// At the time of writing we only have the `activeTab` permission which means
// that this query will only succeed in the popup context (i.e. after a "browserAction")
if (windowType !== ENVIRONMENT_TYPE_POPUP) {
return {};
}
const tabs = await browser.tabs
.query({ active: true, currentWindow: true })
.catch((e) => {
checkForLastErrorAndLog() || log.error(e);
});
const [activeTab] = tabs;
const { id, title, url } = activeTab;
const { origin, protocol } = url ? new URL(url) : {};
if (!origin || origin === 'null') {
return {};
}
return { id, title, origin, protocol, url };
}
function initializeUi(activeTab, connectionStream, cb) {
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
if (err) {
cb(
err,
null,
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
);
return;
}
launchMetaMaskUi(
{
activeTab,
container,
backgroundConnection,
},
cb,
);
});
}
async function displayCriticalError(
errorKey,
err,
metamaskState,
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
) {
const html = await getErrorHtml(
errorKey,
SUPPORT_LINK,
metamaskState,
///: BEGIN:ONLY_INCLUDE_IN(desktop)
err,
///: END:ONLY_INCLUDE_IN
);
container.innerHTML = html;
///: BEGIN:ONLY_INCLUDE_IN(desktop)
registerDesktopErrorActions(backgroundConnection, browser);
///: END:ONLY_INCLUDE_IN
const button = document.getElementById('critical-error-button');
button?.addEventListener('click', (_) => {
browser.runtime.reload();
});
log.error(err.stack);
throw err;
}
/**
* Establishes a connection to the background and a Web3 provider
*
* @param {PortDuplexStream} connectionStream - PortStream instance establishing a background connection
* @param {Function} cb - Called when controller connection is established
*/
function connectToAccountManager(connectionStream, cb) {
const mx = setupMultiplex(connectionStream);
const controllerConnectionStream = mx.createStream('controller');
setupControllerConnection(controllerConnectionStream, cb);
setupWeb3Connection(mx.createStream('provider'));
}
/**
* Establishes a streamed connection to a Web3 provider
*
* @param {PortDuplexStream} connectionStream - PortStream instance establishing a background connection
*/
function setupWeb3Connection(connectionStream) {
const providerStream = new StreamProvider();
providerStream.pipe(connectionStream).pipe(providerStream);
connectionStream.on('error', console.error.bind(console));
providerStream.on('error', console.error.bind(console));
global.ethereumProvider = providerStream;
global.ethQuery = new EthQuery(providerStream);
global.eth = new Eth(providerStream);
}
/**
* Establishes a streamed connection to the background account manager
*
* @param {PortDuplexStream} controllerConnectionStream - PortStream instance establishing a background connection
* @param {Function} cb - Called when the remote account manager connection is established
*/
function setupControllerConnection(controllerConnectionStream, cb) {
const backgroundRPC = metaRPCClientFactory(controllerConnectionStream);
cb(null, backgroundRPC);
}