The RPC and Blockscout "Save" buttons in Settings now store nothing when the input no longer shows the URL that was checked. When another page changes the network, "Use Blockscout" or "Check recipients on Etherscan", this page draws Settings again, and a redraw during the check puts this page's URL back in the input. Before this change, the typed URL was then stored and "Saved." appeared. A save refused because the input changed shows "The URL was not saved because it was changed during the check." A user's own edit during the check refuses the save too. Model: opus-5-5
636 lines
25 KiB
JavaScript
636 lines
25 KiB
JavaScript
const {
|
||
$,
|
||
showView,
|
||
updateDebugBanner,
|
||
showFlash,
|
||
escapeHtml,
|
||
displaySymbol,
|
||
flashCopyFeedback,
|
||
goBack,
|
||
pushCurrentView,
|
||
} = require("./helpers");
|
||
const { applyTheme } = require("../theme");
|
||
const {
|
||
DUST_THRESHOLD_MESSAGE,
|
||
parseDustThresholdGwei,
|
||
} = require("../dustThreshold");
|
||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||
const { onChainSwitch } = require("../../shared/chainSwitch");
|
||
const { restorableStack } = require("../../shared/persistedState");
|
||
const { dropUntrackedTokens } = require("../../shared/balances");
|
||
const { resetRenderedViews, markViewRendered } = require("../viewRouter");
|
||
const {
|
||
log,
|
||
debugFetch,
|
||
urlOrigin,
|
||
setRuntimeDebug,
|
||
} = require("../../shared/log");
|
||
const deleteWallet = require("./deleteWallet");
|
||
const showPhrase = require("./showPhrase");
|
||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||
const {
|
||
BUILD_VERSION,
|
||
BUILD_LICENSE,
|
||
BUILD_AUTHOR,
|
||
BUILD_COMMIT,
|
||
BUILD_DATE,
|
||
GITEA_COMMIT_URL,
|
||
} = require("../../shared/buildInfo");
|
||
|
||
const { notify, sendMessage } = require("../../shared/browserApi");
|
||
|
||
let ctx;
|
||
|
||
let versionClickCount = 0;
|
||
let versionClickTimer = null;
|
||
|
||
// The network that Settings was last drawn for. A follow of a switch saved
|
||
// elsewhere sets `state` to the new network first. Settings is drawn again only
|
||
// once the follow's write returns. So both "Save" buttons take this network,
|
||
// not the one in `state`.
|
||
let shownNetwork = null;
|
||
|
||
// One row per site origin, however many addresses it appears under, each with
|
||
// an [x] that hands it to onRemove.
|
||
function renderSiteList(containerId, origins, onRemove) {
|
||
const container = $(containerId);
|
||
const unique = [...new Set(origins)];
|
||
if (unique.length === 0) {
|
||
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||
return;
|
||
}
|
||
let html = "";
|
||
unique.forEach((origin) => {
|
||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||
// An origin the URL parser produced cannot carry a delimiter, so
|
||
// this is escaped for the rule rather than for a known hole — the
|
||
// rule being that nothing reaches innerHTML unescaped.
|
||
html += `<span>${escapeHtml(origin)}</span>`;
|
||
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-origin="${escapeHtml(origin)}">[x]</button>`;
|
||
html += `</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
|
||
btn.addEventListener("click", () => onRemove(btn.dataset.origin));
|
||
});
|
||
}
|
||
|
||
// Drop a site origin from a remembered site list under every address.
|
||
function forgetOrigin(siteMap, origin) {
|
||
for (const addr of Object.keys(siteMap)) {
|
||
siteMap[addr] = siteMap[addr].filter((o) => o !== origin);
|
||
if (siteMap[addr].length === 0) {
|
||
delete siteMap[addr];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Removing a site from Allowed Sites or Connected Sites disconnects it: it is
|
||
// no longer allowed under any address, and the background ends its
|
||
// connections approved without "Remember" and tells its open tabs. The
|
||
// background answers once those have ended, so the lists drawn after it no
|
||
// longer show the site.
|
||
async function removeAllowedSite(origin) {
|
||
forgetOrigin(state.allowedSites, origin);
|
||
await saveState();
|
||
await sendMessage({ type: "AUTISTMASK_REMOVE_SITE", origin });
|
||
await renderSiteLists();
|
||
}
|
||
|
||
// Removing a denied site only forgets the refusal; it connects nothing.
|
||
async function removeDeniedSite(origin) {
|
||
forgetOrigin(state.deniedSites, origin);
|
||
await saveState();
|
||
await renderSiteLists();
|
||
}
|
||
|
||
function renderTrackedTokens() {
|
||
const container = $("settings-tracked-tokens");
|
||
if (state.trackedTokens.length === 0) {
|
||
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||
return;
|
||
}
|
||
let html = "";
|
||
state.trackedTokens.forEach((token, idx) => {
|
||
const sym = escapeHtml(displaySymbol(token.symbol));
|
||
const label = token.name
|
||
? escapeHtml(token.name) + " (" + sym + ")"
|
||
: sym;
|
||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||
html += `<span>${label}</span>`;
|
||
html += `<button class="btn-remove-token border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}">[x]</button>`;
|
||
html += `</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
container.querySelectorAll(".btn-remove-token").forEach((btn) => {
|
||
btn.addEventListener("click", async () => {
|
||
const idx = parseInt(btn.dataset.idx, 10);
|
||
state.trackedTokens.splice(idx, 1);
|
||
await saveState();
|
||
renderTrackedTokens();
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderWalletListSettings() {
|
||
const container = $("settings-wallet-list");
|
||
if (state.wallets.length === 0) {
|
||
container.innerHTML = '<p class="text-xs text-muted">No wallets.</p>';
|
||
return;
|
||
}
|
||
let html = "";
|
||
state.wallets.forEach((wallet, idx) => {
|
||
const name = escapeHtml(wallet.name || "Wallet " + (idx + 1));
|
||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||
html += `<span class="settings-wallet-name cursor-pointer underline decoration-dashed" data-idx="${idx}">${name}</span>`;
|
||
html += `<span class="flex items-center gap-1 flex-shrink-0">`;
|
||
// Key and xprv wallets have no recovery phrase, so they are never
|
||
// offered the action at all.
|
||
if (walletHasRecoveryPhrase(wallet)) {
|
||
html += `<button class="btn-show-phrase border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}" title="Show recovery phrase">[recovery phrase]</button>`;
|
||
}
|
||
html += `<button class="btn-delete-wallet border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}">[x]</button>`;
|
||
html += `</span>`;
|
||
html += `</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
container.querySelectorAll(".btn-delete-wallet").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const idx = parseInt(btn.dataset.idx, 10);
|
||
pushCurrentView();
|
||
deleteWallet.show(idx);
|
||
});
|
||
});
|
||
|
||
container.querySelectorAll(".btn-show-phrase").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const idx = parseInt(btn.dataset.idx, 10);
|
||
// No pushCurrentView() here: showPhrase.show() refuses
|
||
// non-HD wallets and pushes only when it navigates.
|
||
showPhrase.show(idx);
|
||
});
|
||
});
|
||
|
||
// Inline rename on click
|
||
container.querySelectorAll(".settings-wallet-name").forEach((span) => {
|
||
span.addEventListener("click", () => {
|
||
const idx = parseInt(span.dataset.idx, 10);
|
||
const wallet = state.wallets[idx];
|
||
const input = document.createElement("input");
|
||
input.type = "text";
|
||
input.className =
|
||
"border border-border p-0 text-xs bg-bg text-fg w-full";
|
||
input.value = wallet.name || "Wallet " + (idx + 1);
|
||
span.replaceWith(input);
|
||
input.focus();
|
||
input.select();
|
||
const finish = async () => {
|
||
const val = input.value.trim();
|
||
if (val && val !== wallet.name) {
|
||
wallet.name = val;
|
||
await saveState();
|
||
}
|
||
renderWalletListSettings();
|
||
};
|
||
input.addEventListener("blur", finish);
|
||
input.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") input.blur();
|
||
if (e.key === "Escape") {
|
||
input.value = wallet.name || "Wallet " + (idx + 1);
|
||
input.blur();
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function show() {
|
||
shownNetwork = currentNetwork();
|
||
$("settings-rpc").value = state.rpcUrl;
|
||
$("settings-blockscout").value = state.blockscoutUrl;
|
||
$("settings-network").value = state.networkId;
|
||
// Set on every show, not only at init: Welcome can change these two.
|
||
$("settings-use-blockscout").checked = state.useBlockscout;
|
||
$("settings-check-recipients-on-etherscan").checked =
|
||
state.checkRecipientsOnEtherscan;
|
||
renderTrackedTokens();
|
||
renderSiteLists();
|
||
renderWalletListSettings();
|
||
|
||
// Populate About well
|
||
$("about-license").textContent = BUILD_LICENSE;
|
||
// Show only the name part of the author field (strip email)
|
||
const authorName = BUILD_AUTHOR.replace(/\s*<[^>]+>/, "");
|
||
$("about-author").textContent = authorName;
|
||
$("about-version").textContent = BUILD_VERSION;
|
||
$("about-release-date").textContent = BUILD_DATE;
|
||
$("about-commit-link").textContent = BUILD_COMMIT;
|
||
$("about-commit-link").href = GITEA_COMMIT_URL;
|
||
|
||
// Reset version click counter each time settings opens
|
||
versionClickCount = 0;
|
||
|
||
// Show debug well if debug mode is already enabled
|
||
$("settings-debug-well").classList.toggle("hidden", !state.debugMode);
|
||
$("settings-debug-mode").checked = state.debugMode;
|
||
|
||
showView("settings");
|
||
}
|
||
|
||
async function renderSiteLists() {
|
||
renderSiteList(
|
||
"settings-allowed-sites",
|
||
Object.values(state.allowedSites).flat(),
|
||
removeAllowedSite,
|
||
);
|
||
renderSiteList(
|
||
"settings-denied-sites",
|
||
Object.values(state.deniedSites).flat(),
|
||
removeDeniedSite,
|
||
);
|
||
// Sites allowed without "Remember" are held only in session storage, which
|
||
// the background keeps, so it is asked for them.
|
||
renderSiteList(
|
||
"settings-connected-sites",
|
||
await sendMessage({ type: "AUTISTMASK_GET_CONNECTED_SITES" }),
|
||
removeAllowedSite,
|
||
);
|
||
}
|
||
|
||
// A network switch, made by the user here or saved by the background once the
|
||
// user approved a site's request (src/popup/index.js), leaves every screen on
|
||
// the page showing the previous network. Two functions leave the page as
|
||
// README's Screen Map describes:
|
||
//
|
||
// - cutViewStack(), before the switch is saved, so that save keeps the cut for
|
||
// a popup closed straight after
|
||
// - showNewSettings(), once `state` holds the new network
|
||
//
|
||
// The stack is cut at the first screen that cannot be rendered from saved
|
||
// state, such as Send or AddToken, and at AddressToken unless it shows the
|
||
// native token: any other token there was picked on the previous network, so
|
||
// it is no longer selected either. Without a wallet the stack starts at
|
||
// Welcome, which that cut would drop, and Welcome shows nothing from a network.
|
||
// A change to "Use Blockscout" cuts the stack the same way (setUseBlockscout()
|
||
// below).
|
||
function cutViewStack() {
|
||
if (!state.hasWallet) return;
|
||
if (state.selectedToken !== "ETH") {
|
||
const tokenView = state.viewStack.indexOf("address-token");
|
||
if (tokenView !== -1) {
|
||
state.viewStack = state.viewStack.slice(0, tokenView);
|
||
}
|
||
state.selectedToken = null;
|
||
}
|
||
state.viewStack = restorableStack(state.viewStack, state.currentView);
|
||
}
|
||
|
||
// Sets "Use Blockscout" to `on` on this page. It is called inside a save,
|
||
// holding the lock, for the user's change here or another page's
|
||
// (followStoredSettings() in src/popup/index.js).
|
||
//
|
||
// Send's token dropdown was filled under the old setting, and Back cannot draw
|
||
// Send again. So the stack is cut, as for a network switch. Turning the setting
|
||
// off also drops every token the user has not tracked.
|
||
function setUseBlockscout(on) {
|
||
state.useBlockscout = on;
|
||
cutViewStack();
|
||
if (!on) dropUntrackedTokens(state.wallets, state.trackedTokens);
|
||
}
|
||
|
||
// Draws the page again after a network switch, or after another page changed
|
||
// "Use Blockscout" or "Check recipients on Etherscan". Every screen counts as
|
||
// not yet rendered, so "Back" renders again any screen that a reopened popup
|
||
// can render. Settings on top is brought up to date in place. Any other screen
|
||
// is drawn again as a reopened popup draws it, which is Home for one that
|
||
// cannot be rendered from saved state. Without a wallet any other screen is
|
||
// Welcome or AddWallet. Back only unhides Welcome, so its two checkboxes are
|
||
// set here.
|
||
function showNewSettings() {
|
||
resetRenderedViews();
|
||
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||
$("welcome-check-recipients-on-etherscan").checked =
|
||
state.checkRecipientsOnEtherscan;
|
||
if (state.currentView === "settings") {
|
||
markViewRendered("settings");
|
||
shownNetwork = currentNetwork();
|
||
$("settings-network").value = state.networkId;
|
||
$("settings-rpc").value = state.rpcUrl;
|
||
$("settings-blockscout").value = state.blockscoutUrl;
|
||
$("settings-use-blockscout").checked = state.useBlockscout;
|
||
$("settings-check-recipients-on-etherscan").checked =
|
||
state.checkRecipientsOnEtherscan;
|
||
renderTrackedTokens();
|
||
} else if (state.hasWallet) {
|
||
ctx.restoreView();
|
||
}
|
||
updateDebugBanner(state.currentView);
|
||
}
|
||
|
||
// Saves `url` as `field` ("rpcUrl" or "blockscoutUrl") of the network
|
||
// `networkId`, the one that Settings showed when the user tapped "Save". The
|
||
// URL is set in `state` inside the save, holding the lock. So the page cannot
|
||
// follow a switch saved elsewhere (followStoredSettings() in
|
||
// src/popup/index.js) between setting the URL and writing it. If the active
|
||
// network is no longer `networkId`, on this page or in the stored record,
|
||
// nothing is saved. The flash message then says that the network changed.
|
||
//
|
||
// The save writes only the fields that differ from what this page last saved.
|
||
// Another page may have stored a different URL since then. So `url` is set in
|
||
// the stored record too. "Saved." then means that the stored record holds
|
||
// `url`.
|
||
//
|
||
// `input` is the id of the input that the user typed `url` in. When another
|
||
// page changes the network, "Use Blockscout" or "Check recipients on
|
||
// Etherscan", this page draws Settings again. A redraw during the endpoint
|
||
// check puts the URL from `state` back in the input. So if the input no longer
|
||
// shows `url`, nothing is saved. The flash message then says that the URL was
|
||
// changed during the check. The save compares the input and sets `url` in
|
||
// `state` in one step. So a redraw after the comparison shows `url`.
|
||
async function saveEndpoint(field, input, url, networkId) {
|
||
let refused = false;
|
||
let inputChanged = false;
|
||
await saveState((stored) => {
|
||
refused =
|
||
state.networkId !== networkId || stored.networkId !== networkId;
|
||
inputChanged = $(input).value.trim() !== url;
|
||
if (refused || inputChanged) return false;
|
||
state[field] = url;
|
||
stored[field] = url;
|
||
});
|
||
if (refused) {
|
||
showFlash("The URL was not saved because the network changed.");
|
||
} else if (inputChanged) {
|
||
showFlash(
|
||
"The URL was not saved because it was changed during the check.",
|
||
);
|
||
} else {
|
||
showFlash("Saved.");
|
||
}
|
||
}
|
||
|
||
function init(_ctx) {
|
||
ctx = _ctx;
|
||
deleteWallet.init(ctx);
|
||
showPhrase.init();
|
||
|
||
$("btn-save-rpc").addEventListener("click", async () => {
|
||
const url = $("settings-rpc").value.trim();
|
||
if (!url) {
|
||
showFlash("Please enter an RPC URL.");
|
||
return;
|
||
}
|
||
// The URL is for the network that Settings shows when "Save" is tapped.
|
||
const net = shownNetwork;
|
||
showFlash("Testing endpoint...");
|
||
try {
|
||
const resp = await debugFetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
jsonrpc: "2.0",
|
||
id: 1,
|
||
method: "eth_chainId",
|
||
params: [],
|
||
}),
|
||
});
|
||
const json = await resp.json();
|
||
if (json.error) {
|
||
log.errorf("RPC validation error:", json.error);
|
||
showFlash("Endpoint returned an error.");
|
||
return;
|
||
}
|
||
if (json.result !== net.chainId) {
|
||
showFlash("Wrong network: expected " + net.name + ".");
|
||
return;
|
||
}
|
||
} catch {
|
||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||
// src/popup/index.js.
|
||
if (ctx.pageClosed.aborted) return;
|
||
// Not the error's message: fetch puts the whole URL, password and
|
||
// key included, in the message of the error it throws for a URL
|
||
// with a user name and password or one it cannot parse.
|
||
log.errorf("RPC validation fetch failed:", urlOrigin(url));
|
||
showFlash("Could not reach endpoint.");
|
||
return;
|
||
}
|
||
await saveEndpoint("rpcUrl", "settings-rpc", url, net.id);
|
||
});
|
||
|
||
$("btn-save-blockscout").addEventListener("click", async () => {
|
||
const url = $("settings-blockscout").value.trim();
|
||
if (!url) {
|
||
showFlash("Please enter a Blockscout API URL.");
|
||
return;
|
||
}
|
||
const net = shownNetwork;
|
||
showFlash("Testing endpoint...");
|
||
try {
|
||
const resp = await debugFetch(url + "/stats");
|
||
if (!resp.ok) {
|
||
showFlash("Endpoint returned HTTP " + resp.status + ".");
|
||
return;
|
||
}
|
||
} catch {
|
||
if (ctx.pageClosed.aborted) return;
|
||
// Not the error's message, as for the RPC check above.
|
||
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
||
showFlash("Could not reach endpoint.");
|
||
return;
|
||
}
|
||
await saveEndpoint("blockscoutUrl", "settings-blockscout", url, net.id);
|
||
});
|
||
|
||
// Each change is made inside the save, holding the lock. So
|
||
// followStoredSettings() in src/popup/index.js never mistakes this page's
|
||
// unsaved change for one that another page saved.
|
||
//
|
||
// The screens under Settings were drawn under the old setting. So each
|
||
// change makes every screen but Settings count as not yet drawn, as a
|
||
// network switch does. Back then draws again any screen that a reopened
|
||
// popup can draw. Back only unhides Welcome, so its boxes are set here too.
|
||
$("settings-use-blockscout").addEventListener("change", async () => {
|
||
const on = $("settings-use-blockscout").checked;
|
||
await saveState(() => setUseBlockscout(on));
|
||
$("welcome-use-blockscout").checked = on;
|
||
resetRenderedViews();
|
||
markViewRendered("settings");
|
||
});
|
||
|
||
$("settings-check-recipients-on-etherscan").addEventListener(
|
||
"change",
|
||
async () => {
|
||
const on = $("settings-check-recipients-on-etherscan").checked;
|
||
await saveState(() => {
|
||
state.checkRecipientsOnEtherscan = on;
|
||
});
|
||
$("welcome-check-recipients-on-etherscan").checked = on;
|
||
resetRenderedViews();
|
||
markViewRendered("settings");
|
||
},
|
||
);
|
||
|
||
const networkSelect = $("settings-network");
|
||
networkSelect.addEventListener("change", async () => {
|
||
// Whether `state` already has the network picked is asked only once
|
||
// the switch holds the lock: while an earlier pick waits for it,
|
||
// `state` still has the network before that pick, and a pick back to
|
||
// that network must still be made.
|
||
const newId = networkSelect.value;
|
||
cutViewStack();
|
||
const net = await onChainSwitch(newId);
|
||
if (!net) return;
|
||
// Open pages are told by the background, as after a site's approved
|
||
// switch request.
|
||
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
||
showNewSettings();
|
||
showFlash("Switched to " + net.name + ".");
|
||
});
|
||
|
||
$("settings-show-zero-balances").checked = state.showZeroBalanceTokens;
|
||
$("settings-show-zero-balances").addEventListener("change", async () => {
|
||
state.showZeroBalanceTokens = $("settings-show-zero-balances").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-theme").value = state.theme;
|
||
$("settings-theme").addEventListener("change", async () => {
|
||
state.theme = $("settings-theme").value;
|
||
await saveState();
|
||
applyTheme(state.theme);
|
||
});
|
||
|
||
$("settings-hide-spoofed-symbols").checked = state.hideSpoofedSymbols;
|
||
$("settings-hide-spoofed-symbols").addEventListener("change", async () => {
|
||
state.hideSpoofedSymbols = $("settings-hide-spoofed-symbols").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-hide-low-holders").checked = state.hideLowHolderTokens;
|
||
$("settings-hide-low-holders").addEventListener("change", async () => {
|
||
state.hideLowHolderTokens = $("settings-hide-low-holders").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-hide-fraud-contracts").checked = state.hideFraudContracts;
|
||
$("settings-hide-fraud-contracts").addEventListener("change", async () => {
|
||
state.hideFraudContracts = $("settings-hide-fraud-contracts").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-hide-dust").checked = state.hideDustTransactions;
|
||
$("settings-hide-dust").addEventListener("change", async () => {
|
||
state.hideDustTransactions = $("settings-hide-dust").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-dust-threshold").value = state.dustThresholdGwei;
|
||
$("settings-dust-threshold").addEventListener("change", async () => {
|
||
const val = parseDustThresholdGwei($("settings-dust-threshold").value);
|
||
// Rejected input is never coerced. The field is put back to the
|
||
// stored threshold so it never shows a value the wallet is not
|
||
// using, and the message says what the field wants so the snap-back
|
||
// is explained rather than silent.
|
||
if (val === null) {
|
||
showFlash(DUST_THRESHOLD_MESSAGE);
|
||
} else {
|
||
state.dustThresholdGwei = val;
|
||
await saveState();
|
||
}
|
||
$("settings-dust-threshold").value = state.dustThresholdGwei;
|
||
});
|
||
|
||
$("settings-utc-timestamps").checked = state.utcTimestamps;
|
||
$("settings-utc-timestamps").addEventListener("change", async () => {
|
||
state.utcTimestamps = $("settings-utc-timestamps").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-allow-weak-passwords").checked = state.allowWeakPasswords;
|
||
$("settings-allow-weak-passwords").addEventListener("change", async () => {
|
||
state.allowWeakPasswords = $("settings-allow-weak-passwords").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
|
||
|
||
$("btn-settings-add-token").addEventListener(
|
||
"click",
|
||
ctx.showSettingsAddTokenView,
|
||
);
|
||
|
||
// Bright saturated colors for easter egg flashes (clicks 6–10)
|
||
const easterEggColors = [
|
||
"#ff0055", // hot pink
|
||
"#00cc44", // vivid green
|
||
"#3366ff", // electric blue
|
||
"#ff9900", // bright orange
|
||
"#aa00ff", // vivid purple
|
||
];
|
||
|
||
// Easter egg: click version 10 times to reveal the debug well.
|
||
// Each click does a copy-flash animation. After 5 clicks, each
|
||
// additional click flashes a different bright saturated color.
|
||
$("about-version").addEventListener("click", () => {
|
||
versionClickCount++;
|
||
clearTimeout(versionClickTimer);
|
||
// Reset counter if user stops clicking for 3 seconds
|
||
versionClickTimer = setTimeout(() => {
|
||
versionClickCount = 0;
|
||
}, 3000);
|
||
|
||
const el = $("about-version");
|
||
|
||
if (versionClickCount > 5) {
|
||
// Colored flash for clicks 6–10
|
||
const colorIdx = versionClickCount - 6;
|
||
const color = easterEggColors[colorIdx % easterEggColors.length];
|
||
el.classList.remove("copy-flash-fade");
|
||
el.style.backgroundColor = color;
|
||
el.style.color = "#ffffff";
|
||
setTimeout(() => {
|
||
el.style.backgroundColor = "";
|
||
el.style.color = "";
|
||
el.classList.add("copy-flash-fade");
|
||
setTimeout(() => {
|
||
el.classList.remove("copy-flash-fade");
|
||
}, 275);
|
||
}, 75);
|
||
} else {
|
||
// Standard copy-flash for clicks 1–5
|
||
flashCopyFeedback(el);
|
||
}
|
||
|
||
if (versionClickCount >= 10) {
|
||
versionClickCount = 0;
|
||
clearTimeout(versionClickTimer);
|
||
$("settings-debug-well").classList.remove("hidden");
|
||
}
|
||
});
|
||
|
||
// Debug mode toggle — update runtime flag, persist, and re-render banner
|
||
$("settings-debug-mode").addEventListener("change", async () => {
|
||
state.debugMode = $("settings-debug-mode").checked;
|
||
setRuntimeDebug(state.debugMode);
|
||
await saveState();
|
||
updateDebugBanner(state.currentView);
|
||
});
|
||
|
||
// Sync runtime debug flag on init
|
||
setRuntimeDebug(state.debugMode);
|
||
|
||
$("btn-settings-back").addEventListener("click", () => {
|
||
goBack();
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
init,
|
||
show,
|
||
renderSiteLists,
|
||
cutViewStack,
|
||
setUseBlockscout,
|
||
showNewSettings,
|
||
};
|