// AutistMask popup entry point. // Loads state, initializes views, triggers first render. const { state, saveState, onSaveFailure, loadState, } = require("../shared/state"); const { StateUnusableError } = require("../shared/stateSchema"); const { log, setRuntimeDebug } = require("../shared/log"); const { refreshPrices } = require("../shared/prices"); const { refreshBalances } = require("../shared/balances"); const { $, showView, updateDebugBanner, setBackRenderer, showSaveFailureBanner, pushCurrentView, goBack, } = require("./views/helpers"); const { applyTheme } = require("./theme"); // Renders a view the popup lands on without having navigated to it forward: // on restore here, and on Back. Only the views that can be fully re-rendered // from persisted state (RESTORABLE_VIEWS, src/shared/restorableViews.js) go // through it; anything else falls back to the nearest restorable parent. const { renderView, makeBackRenderer } = require("./viewRouter"); const home = require("./views/home"); const welcome = require("./views/welcome"); const addWallet = require("./views/addWallet"); const addressDetail = require("./views/addressDetail"); const addressToken = require("./views/addressToken"); const send = require("./views/send"); const confirmTx = require("./views/confirmTx"); const txStatus = require("./views/txStatus"); const transactionDetail = require("./views/transactionDetail"); const receive = require("./views/receive"); const addToken = require("./views/addToken"); const settings = require("./views/settings"); const settingsAddToken = require("./views/settingsAddToken"); const deleteAddress = require("./views/deleteAddress"); const approval = require("./views/approval"); const stateRecovery = require("./views/stateRecovery"); function renderWalletList() { home.render(ctx); } let refreshInFlight = false; async function doRefreshAndRender() { if (refreshInFlight) return; refreshInFlight = true; try { await Promise.all([ refreshPrices(), refreshBalances( state.wallets, state.rpcUrl, state.blockscoutUrl, state.trackedTokens, state.networkId, ), ]); state.lastBalanceRefresh = Date.now(); await saveState(); renderWalletList(); } catch (e) { // Every call site fires this and walks away — the boot below, the ten // second interval, and eight views through ctx — so it must never // reject: an unhandled rejection is not a report of anything. The save // inside it reports its own failure through onSaveFailure() (see // src/shared/state.js); what is left here is a failed network round // trip, which the next tick retries. log.errorf("popup: background refresh failed:", e); } finally { refreshInFlight = false; } } const ctx = { renderWalletList, doRefreshAndRender, showAddWalletView: () => { pushCurrentView(); addWallet.show(); }, showAddressDetail: () => { pushCurrentView(); addressDetail.show(); }, showAddressToken: () => { pushCurrentView(); addressToken.show(); }, showAddTokenView: () => { pushCurrentView(); addToken.show(); }, showConfirmTx: (txInfo) => { pushCurrentView(); confirmTx.show(txInfo); }, showReceive: () => { pushCurrentView(); receive.show(); }, showTransactionDetail: (tx) => { pushCurrentView(); transactionDetail.show(tx); }, showSettingsView: () => { pushCurrentView(); settings.show(); }, showSettingsAddTokenView: () => { pushCurrentView(); settingsAddToken.show(); }, showDeleteAddress: (walletIdx, addrIdx) => { pushCurrentView(); deleteAddress.show(walletIdx, addrIdx); }, }; // The view modules the router renders through, keyed as it expects them. const viewModules = { main: { show: () => fallbackView() }, addressDetail, addressToken, receive, settings, settingsAddToken, confirmTx, transactionDetail, txStatus, }; function restoreView() { if (!renderView(state.currentView, state, viewModules)) { fallbackView(); } } function fallbackView() { renderWalletList(); showView("main"); } async function init() { // First, before anything can save: showView() saves on every navigation // without awaiting, so a save that fails from here on has somewhere to be // reported rather than being swallowed by the save queue // (https://git.eeqj.de/sneak/AutistMask/issues/362). Registered ahead of // the approval-window branch below too, since that window saves as well. onSaveFailure(showSaveFailureBanner); try { await loadState(); } catch (e) { // A profile this build cannot read is the one failure that must not // fall through to the rest of init(). It used to: the load "succeeded" // on a record nothing had validated, and the first dereference below // threw, leaving a popup with no view, no message and no control on // it, and no way out of the wallet from inside the product // (https://git.eeqj.de/sneak/AutistMask/issues/311). Now the load // refuses, and this is the screen that says so. if (e instanceof StateUnusableError) { stateRecovery.show(e); return; } throw e; } applyTheme(state.theme); // Sync runtime debug flag from persisted state before first render setRuntimeDebug(state.debugMode); // Create the debug/testnet banner if needed (uses runtime debug state) updateDebugBanner(); // Auto-default active address if ( state.activeAddress === null && state.wallets.length > 0 && state.wallets[0].addresses.length > 0 ) { state.activeAddress = state.wallets[0].addresses[0].address; await saveState(); } // Always init approval and txStatus — they may run in the approval popup window approval.init(ctx); txStatus.init(ctx); // Check for approval mode const params = new URLSearchParams(window.location.search); const approvalId = params.get("approval"); if (approvalId) { // Deliberately not awaited, and deliberately not .catch()ed. show() // is async, so a throw past its first await surfaces as an unhandled // rejection rather than an uncaught error — measured as still failing // the run on both harnesses (Playwright `pageerror`, and the Firefox // driver's console-service drain), so nothing is lost by leaving it // on that path. approval.show(approvalId); showView("approve-site"); return; } $("btn-settings").addEventListener("click", () => { if ( !document .getElementById("view-settings") .classList.contains("hidden") ) { goBack(); return; } pushCurrentView(); settings.show(); }); setBackRenderer(makeBackRenderer(state, viewModules)); welcome.init(ctx); addWallet.init(ctx); home.init(ctx); addressDetail.init(ctx); addressToken.init(ctx); send.init(ctx); confirmTx.init(ctx); transactionDetail.init(ctx); receive.init(ctx); addToken.init(ctx); settings.init(ctx); settingsAddToken.init(ctx); deleteAddress.init(ctx); if (!state.hasWallet) { showView("welcome"); } else { renderWalletList(); restoreView(); doRefreshAndRender(); setInterval(doRefreshAndRender, 10000); } } document.addEventListener("DOMContentLoaded", init);