Files
bitvid/js/ui/profileModal/ProfileWalletController.js
T
thePR0M3TH3ANandClaude Opus 4.8 dec5cf8fe1 feat(sync): warn before overwriting a newer remote copy (conflict guard)
Saves push silently as before, but if the relay copy is NEWER than the one this
device last pushed (another device changed it), the user is now asked to confirm
before overwriting — preventing a silent multi-device clobber. No nag on normal
saves.

- js/services/settingsSyncFlags.js: track per-pubkey+kind last-pushed created_at
  (getSyncPushedAt/setSyncPushedAt).
- js/services/syncConflict.js: pushWithConflictCheck — peek remote created_at
  (list-only, no decrypt), compare to last push, call confirmOverwrite only on a
  real conflict, then push strictly-newer than remote so the replace wins.
- storage/walletSyncService push()/enable() route through it and accept a
  confirmOverwrite callback.
- ProfileStorage/WalletController supply confirmOverwrite (window.confirm) and
  surface a "kept the newer copy" status when the user declines.

tests/sync-conflict.test.mjs: no-remote→silent, newer→prompt(+decline blocks
push), confirm→strictly-newer push, older→no prompt. Mutation-verified
(ignoring the decline fails). Service-test twins gained exists().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 21:21:54 -04:00

963 lines
30 KiB
JavaScript

import { devLogger } from "../../utils/logger.js";
import { NWC_URI_SCHEME } from "../profileModalContract.js";
import { createWalletSyncService } from "../../services/walletSyncService.js";
const noop = () => {};
const SECRET_PLACEHOLDER = "*****";
export class ProfileWalletController {
constructor(mainController) {
this.mainController = mainController;
this.walletUriInput = null;
this.walletDefaultZapInput = null;
this.walletSaveButton = null;
this.walletTestButton = null;
this.walletDisconnectButton = null;
this.walletStatusText = null;
this.walletPane = null;
this.walletSyncSection = null;
this.walletSyncToggle = null;
this.walletSyncRestoreBtn = null;
this.walletSyncStatus = null;
this._walletSyncService = null;
}
cacheDomReferences() {
this.walletUriInput = document.getElementById("profileWalletUri") || null;
this.walletDefaultZapInput = document.getElementById("profileWalletDefaultZap") || null;
this.walletSaveButton = document.getElementById("profileWalletSave") || null;
this.walletTestButton = document.getElementById("profileWalletTest") || null;
this.walletDisconnectButton = document.getElementById("profileWalletDisconnect") || null;
this.walletStatusText = document.getElementById("profileWalletStatus") || null;
this.walletPane = document.getElementById("profilePaneWallet") || null;
this.walletSyncSection = document.getElementById("profileWalletSync") || null;
this.walletSyncToggle = document.getElementById("walletSyncToggle") || null;
this.walletSyncRestoreBtn = document.getElementById("walletSyncRestoreBtn") || null;
this.walletSyncStatus = document.getElementById("walletSyncStatus") || null;
// Backwards compatibility alias if needed by tests/external code
this.mainController.profileWalletStatusText = this.walletStatusText;
}
registerEventListeners() {
if (this.walletUriInput instanceof HTMLElement) {
this.walletUriInput.addEventListener("focus", () => {
this.revealSecretInputValue(this.walletUriInput);
});
this.walletUriInput.addEventListener("blur", () => {
this.handleSecretInputBlur(this.walletUriInput);
this.applyWalletControlState();
});
this.walletUriInput.addEventListener("input", () => {
this.handleSecretInputChange(this.walletUriInput);
this.applyWalletControlState();
});
}
if (this.walletDefaultZapInput instanceof HTMLElement) {
this.walletDefaultZapInput.addEventListener("input", () => {
this.applyWalletControlState();
});
}
if (this.walletSaveButton instanceof HTMLElement) {
this.walletSaveButton.addEventListener("click", () => {
void this.handleWalletSave();
});
}
if (this.walletTestButton instanceof HTMLElement) {
this.walletTestButton.addEventListener("click", () => {
void this.handleWalletTest();
});
}
if (this.walletDisconnectButton instanceof HTMLElement) {
this.walletDisconnectButton.addEventListener("click", () => {
void this.handleWalletDisconnect();
});
}
if (this.walletSyncToggle instanceof HTMLElement) {
this.walletSyncToggle.addEventListener("change", () => {
void this.handleToggleSync();
});
}
if (this.walletSyncRestoreBtn instanceof HTMLElement) {
this.walletSyncRestoreBtn.addEventListener("click", () => {
void this.handleRestoreSync();
});
}
}
getWalletSyncService() {
if (!this._walletSyncService) {
this._walletSyncService = createWalletSyncService({
nwcSettings: this.mainController.services.nwcSettings,
});
}
return this._walletSyncService;
}
confirmSyncOverwrite() {
if (typeof window === "undefined" || typeof window.confirm !== "function") {
return true;
}
return window.confirm(
"A newer copy of your wallet connection is on your account (changed on " +
"another device). Overwrite it with this one?"
);
}
renderSyncSection(pubkey) {
if (!(this.walletSyncSection instanceof HTMLElement)) {
return;
}
const sync = this.getWalletSyncService();
if (!pubkey || !sync.isAvailable()) {
this.walletSyncSection.classList.add("hidden");
return;
}
this.walletSyncSection.classList.remove("hidden");
if (this.walletSyncToggle instanceof HTMLInputElement) {
this.walletSyncToggle.checked = sync.isEnabled(pubkey);
}
this.setSyncStatus("");
}
setSyncStatus(message, tone = "info") {
if (!(this.walletSyncStatus instanceof HTMLElement)) {
return;
}
this.walletSyncStatus.textContent = message || "";
const toneClass =
tone === "success"
? "text-status-success"
: tone === "error"
? "text-status-danger"
: "text-muted";
this.walletSyncStatus.className = `text-xs ${toneClass}`;
}
async handleToggleSync() {
const pubkey = this.mainController.normalizeHexPubkey(
this.mainController.getActivePubkey()
);
if (!pubkey) {
return;
}
const sync = this.getWalletSyncService();
const enabled =
this.walletSyncToggle instanceof HTMLInputElement
? this.walletSyncToggle.checked
: false;
try {
if (enabled) {
// Spending capability — require explicit confirmation before publishing.
const confirmed =
typeof window !== "undefined" && typeof window.confirm === "function"
? window.confirm(
"This publishes an ENCRYPTED copy of your wallet connection to public relays.\n\n" +
"A wallet connect URI can SPEND from your wallet. Anyone who controls your " +
"Nostr key could decrypt this and spend from this wallet.\n\nContinue?"
)
: true;
if (!confirmed) {
if (this.walletSyncToggle instanceof HTMLInputElement) {
this.walletSyncToggle.checked = false;
}
return;
}
this.setSyncStatus("Encrypting and publishing…");
const result = await sync.enable(pubkey, {
confirmOverwrite: () => this.confirmSyncOverwrite(),
});
if (result?.ok) {
this.setSyncStatus(
`Synced to ${result.accepted}/${result.total} relays.`,
"success"
);
this.mainController.showSuccess("Wallet connection synced (encrypted).");
} else if (result?.conflict) {
// User declined to overwrite a newer copy from another device. Keep
// sync on (flag set) and the newer remote intact.
this.setSyncStatus(
"Kept the newer copy on your account. Use Restore to pull it, or save again to overwrite.",
);
} else {
if (this.walletSyncToggle instanceof HTMLInputElement) {
this.walletSyncToggle.checked = false;
}
await sync.disable(pubkey).catch(() => {});
this.setSyncStatus(
result?.error === "nothing-to-sync"
? "Connect a wallet first, then enable sync."
: "Could not publish the encrypted copy. Try again.",
"error"
);
}
} else {
this.setSyncStatus("Removing the synced copy…");
await sync.disable(pubkey);
this.setSyncStatus("Sync turned off; the synced copy was cleared.");
this.mainController.showSuccess("Wallet sync turned off.");
}
} catch (error) {
devLogger.error("[ProfileModal] Wallet sync toggle failed:", error);
this.setSyncStatus("Sync failed. Please try again.", "error");
}
}
async handleRestoreSync() {
const pubkey = this.mainController.normalizeHexPubkey(
this.mainController.getActivePubkey()
);
if (!pubkey) {
return;
}
const sync = this.getWalletSyncService();
try {
this.setSyncStatus("Fetching and decrypting…");
const result = await sync.pull(pubkey);
if (result?.found && result.imported) {
this.setSyncStatus("Restored from your Nostr account.", "success");
this.mainController.showSuccess("Wallet connection restored.");
this.refreshWalletPaneState();
} else if (result?.found && !result.imported) {
this.setSyncStatus("Found a copy but could not import it.", "error");
} else if (result?.cleared) {
this.setSyncStatus("No synced wallet found (it was cleared).");
} else {
this.setSyncStatus("No synced wallet found on your account.");
}
} catch (error) {
devLogger.error("[ProfileModal] Wallet sync restore failed:", error);
this.setSyncStatus("Restore failed. Please try again.", "error");
}
}
applyWalletControlState() {
const hasActive = Boolean(
this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey())
);
const busy = this.isWalletBusy();
const uriValue = this.getSecretInputValue(this.walletUriInput);
const hasUri = uriValue.length > 0;
const applyDisabledState = (element, disabled) => {
if (!(element instanceof HTMLElement)) {
return;
}
if ("disabled" in element) {
element.disabled = disabled;
}
if (disabled) {
element.setAttribute("aria-disabled", "true");
} else {
element.removeAttribute("aria-disabled");
}
};
applyDisabledState(this.walletUriInput, busy || !hasActive);
applyDisabledState(this.walletDefaultZapInput, busy || !hasActive);
applyDisabledState(this.walletSaveButton, busy || !hasActive);
const testDisabled = busy || !hasActive || !hasUri;
applyDisabledState(this.walletTestButton, testDisabled);
const disconnectDisabled = busy || !hasActive || !hasUri;
applyDisabledState(this.walletDisconnectButton, disconnectDisabled);
if (this.walletDisconnectButton instanceof HTMLElement) {
this.walletDisconnectButton.classList.toggle("hidden", !hasUri);
if (!hasUri) {
this.walletDisconnectButton.setAttribute("aria-hidden", "true");
} else {
this.walletDisconnectButton.removeAttribute("aria-hidden");
}
}
}
updateWalletStatus(message, variant = "info") {
if (!(this.walletStatusText instanceof HTMLElement)) {
return;
}
const element = this.walletStatusText;
const variants = {
success: "text-status-success",
error: "text-status-danger",
info: "text-status-info",
neutral: "text-status-neutral",
};
element.classList.remove(
"text-status-info",
"text-status-success",
"text-status-danger",
"text-status-neutral",
);
const variantClass = variants[variant] || variants.neutral;
element.classList.add(variantClass);
element.textContent = message || "";
}
refreshWalletPaneState() {
const hasActive = Boolean(
this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey())
);
if (!hasActive) {
this.setSecretInputValue(this.walletUriInput, "");
if (this.walletDefaultZapInput && "value" in this.walletDefaultZapInput) {
try {
this.walletDefaultZapInput.value = "";
} catch (error) {
if (this.walletDefaultZapInput instanceof HTMLElement) {
this.walletDefaultZapInput.setAttribute("data-value", "");
}
}
}
this.updateWalletStatus("Sign in to connect a wallet.", "info");
this.renderSyncSection("");
this.applyWalletControlState();
return;
}
let settings = this.mainController.services.nwcSettings.getActiveNwcSettings();
if (!settings || typeof settings !== "object") {
settings = this.mainController.services.nwcSettings.createDefaultNwcSettings();
}
this.setSecretInputValue(this.walletUriInput, settings.nwcUri || "");
if (this.walletDefaultZapInput && "value" in this.walletDefaultZapInput) {
const defaultZapValue =
settings.defaultZap === null || settings.defaultZap === undefined
? ""
: String(settings.defaultZap);
try {
this.walletDefaultZapInput.value = defaultZapValue;
} catch (error) {
if (this.walletDefaultZapInput instanceof HTMLElement) {
this.walletDefaultZapInput.setAttribute("data-value", defaultZapValue);
}
}
}
if (settings.nwcUri) {
this.updateWalletStatus(
"Wallet connected via Nostr Wallet Connect.",
"success",
);
} else {
this.updateWalletStatus("No wallet connected yet.", "info");
}
this.renderSyncSection(
this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey())
);
this.applyWalletControlState();
}
isSecretInputElement(element) {
if (!element || typeof element !== "object") {
return false;
}
if (
typeof HTMLInputElement !== "undefined" &&
element instanceof HTMLInputElement
) {
return true;
}
return typeof element.value === "string";
}
sanitizeSecretValue(value) {
return typeof value === "string" ? value.trim() : "";
}
getSecretInputValue(element) {
if (!this.isSecretInputElement(element)) {
return "";
}
const placeholder =
typeof element.dataset?.secretPlaceholder === "string"
? element.dataset.secretPlaceholder
: SECRET_PLACEHOLDER;
const stored = this.sanitizeSecretValue(
typeof element.dataset?.secretValue === "string"
? element.dataset.secretValue
: ""
);
const raw = this.sanitizeSecretValue(element.value);
const isMasked = element.dataset?.secretMasked === "true";
if (isMasked && placeholder && raw === placeholder) {
return stored;
}
if (!raw && isMasked) {
return stored;
}
return raw;
}
setSecretInputValue(element, value) {
if (!this.isSecretInputElement(element)) {
return;
}
const sanitized = this.sanitizeSecretValue(value);
if (!sanitized) {
if (element.dataset) {
delete element.dataset.secretValue;
delete element.dataset.secretMasked;
delete element.dataset.secretPlaceholder;
}
element.value = "";
return;
}
const placeholder = SECRET_PLACEHOLDER;
if (element.dataset) {
element.dataset.secretValue = sanitized;
element.dataset.secretPlaceholder = placeholder;
element.dataset.secretMasked = "true";
}
element.value = placeholder;
}
revealSecretInputValue(element) {
if (!this.isSecretInputElement(element)) {
return;
}
const stored = this.sanitizeSecretValue(
typeof element.dataset?.secretValue === "string"
? element.dataset.secretValue
: ""
);
if (!stored) {
if (element.dataset) {
delete element.dataset.secretMasked;
}
return;
}
if (element.dataset) {
element.dataset.secretMasked = "false";
}
element.value = stored;
try {
if (typeof element.setSelectionRange === "function") {
const length = stored.length;
element.setSelectionRange(length, length);
}
} catch (error) {
// Ignore selection errors on unsupported input types.
}
}
handleSecretInputChange(element) {
if (!this.isSecretInputElement(element)) {
return;
}
const value = this.sanitizeSecretValue(element.value);
if (!value) {
if (element.dataset) {
element.dataset.secretMasked = "false";
delete element.dataset.secretValue;
}
return;
}
if (element.dataset) {
element.dataset.secretValue = value;
element.dataset.secretMasked = "false";
if (!element.dataset.secretPlaceholder) {
element.dataset.secretPlaceholder = SECRET_PLACEHOLDER;
}
}
}
handleSecretInputBlur(element) {
if (!this.isSecretInputElement(element)) {
return;
}
const value = this.sanitizeSecretValue(this.getSecretInputValue(element));
if (!value) {
if (element.dataset) {
delete element.dataset.secretValue;
delete element.dataset.secretMasked;
delete element.dataset.secretPlaceholder;
}
element.value = "";
return;
}
if (element.dataset) {
element.dataset.secretValue = value;
element.dataset.secretPlaceholder =
element.dataset.secretPlaceholder || SECRET_PLACEHOLDER;
element.dataset.secretMasked = "true";
}
element.value = element.dataset?.secretPlaceholder || SECRET_PLACEHOLDER;
}
getWalletFormValues() {
const uri = this.getSecretInputValue(this.walletUriInput);
const defaultZapRaw =
typeof this.walletDefaultZapInput?.value === "string"
? this.walletDefaultZapInput.value.trim()
: "";
if (defaultZapRaw) {
const numeric = Number(defaultZapRaw);
if (!Number.isFinite(numeric)) {
return { uri, error: "Default zap amount must be a number." };
}
const rounded = Math.round(numeric);
if (!Number.isFinite(rounded) || rounded < 0) {
return {
uri,
error: "Default zap amount must be a positive whole number.",
};
}
const clamped = Math.min(this.mainController.maxWalletDefaultZap, rounded);
return { uri, defaultZap: clamped };
}
return { uri, defaultZap: null };
}
validateWalletUri(uri, { requireValue = false } = {}) {
const value = typeof uri === "string" ? uri.trim() : "";
if (!value) {
if (requireValue) {
return {
valid: false,
sanitized: "",
message: "Enter a wallet connect URI before continuing.",
};
}
return { valid: true, sanitized: "" };
}
if (!value.toLowerCase().startsWith(NWC_URI_SCHEME)) {
return {
valid: false,
sanitized: value,
message: `Wallet URI must start with ${NWC_URI_SCHEME}.`,
};
}
return { valid: true, sanitized: value };
}
async handleWalletSave() {
const { uri, defaultZap, error } = this.getWalletFormValues();
const context = {
uri,
defaultZap: defaultZap ?? null,
sanitizedUri: null,
success: false,
reason: null,
error: error || null,
status: null,
variant: null,
};
if (this.isWalletBusy()) {
context.reason = "busy";
this.mainController.callbacks.onWalletSave(context, this.mainController);
return context;
}
if (error) {
this.updateWalletStatus(error, "error");
this.mainController.showError(error);
context.reason = "invalid-default-zap";
if (this.walletDefaultZapInput instanceof HTMLElement) {
this.walletDefaultZapInput.focus();
}
this.mainController.callbacks.onWalletSave(context, this.mainController);
return context;
}
const { valid, sanitized, message } = this.validateWalletUri(uri);
context.sanitizedUri = sanitized;
if (!valid) {
this.updateWalletStatus(message, "error");
this.mainController.showError(message);
context.reason = "invalid-uri";
context.error = message;
if (this.walletUriInput instanceof HTMLElement) {
this.walletUriInput.focus();
}
this.mainController.callbacks.onWalletSave(context, this.mainController);
return context;
}
const normalizedActive = this.mainController.normalizeHexPubkey(
this.mainController.getActivePubkey()
);
if (!normalizedActive) {
const loginMessage = "Sign in to save wallet settings.";
this.updateWalletStatus(loginMessage, "error");
this.mainController.showError(loginMessage);
context.reason = "no-active-pubkey";
context.error = loginMessage;
this.mainController.callbacks.onWalletSave(context, this.mainController);
return context;
}
this.setWalletPaneBusy(true);
let finalStatus = null;
let finalVariant = "info";
try {
const persistResult = await this.persistWalletSettings({
nwcUri: sanitized,
defaultZap,
activePubkey: normalizedActive,
});
context.persistResult = persistResult || null;
if (sanitized) {
finalStatus = "Wallet settings saved.";
finalVariant = "success";
this.mainController.showSuccess("Wallet settings saved.");
context.reason = "saved";
// Keep the encrypted synced copy current if the user opted in. Warn
// before overwriting a newer copy changed on another device.
const sync = this.getWalletSyncService();
if (sync.isEnabled(normalizedActive)) {
try {
const syncResult = await sync.push(normalizedActive, {
confirmOverwrite: () => this.confirmSyncOverwrite(),
});
if (syncResult?.conflict) {
this.setSyncStatus(
"Synced copy on your account is newer — not overwritten.",
);
}
} catch (syncError) {
devLogger.warn(
"[ProfileModal] Wallet re-sync after save failed:",
syncError
);
}
}
} else {
finalStatus = "Wallet connection removed.";
finalVariant = "info";
this.mainController.showStatus("Wallet connection removed.");
context.reason = "cleared";
// NOTE: deliberately do NOT clear the remote synced note here. An empty
// save happens routinely (e.g. saving before the wallet has hydrated /
// been restored on a fresh device), and wiping the encrypted note on
// relays would destroy a good copy that another device relies on. The
// remote note is only ever changed via the explicit sync toggle
// (enable/disable) or a successful save (push). Toggling sync off is the
// single, deliberate way to clear the remote.
}
context.success = true;
} catch (error) {
const fallbackMessage = "Failed to save wallet settings.";
const detail =
error && typeof error.message === "string" && error.message.trim()
? error.message.trim()
: fallbackMessage;
finalStatus = detail;
finalVariant = "error";
context.error = detail;
context.reason = error?.code || "service-error";
this.mainController.showError(detail);
} finally {
this.setWalletPaneBusy(false);
this.refreshWalletPaneState();
if (finalStatus) {
this.updateWalletStatus(finalStatus, finalVariant);
}
context.status = finalStatus;
context.variant = finalVariant;
this.mainController.callbacks.onWalletSave(context, this.mainController);
}
return context;
}
async handleWalletTest() {
const { uri, defaultZap, error } = this.getWalletFormValues();
const context = {
uri,
defaultZap: defaultZap ?? null,
sanitizedUri: null,
success: false,
reason: null,
error: error || null,
status: null,
variant: null,
result: null,
};
if (this.isWalletBusy()) {
context.reason = "busy";
this.mainController.callbacks.onWalletTest(context, this.mainController);
return context.result;
}
if (error) {
this.updateWalletStatus(error, "error");
this.mainController.showError(error);
context.reason = "invalid-default-zap";
if (this.walletDefaultZapInput instanceof HTMLElement) {
this.walletDefaultZapInput.focus();
}
this.mainController.callbacks.onWalletTest(context, this.mainController);
return context.result;
}
const { valid, sanitized, message } = this.validateWalletUri(uri, {
requireValue: true,
});
context.sanitizedUri = sanitized;
if (!valid) {
this.updateWalletStatus(message, "error");
this.mainController.showError(message);
context.reason = "invalid-uri";
context.error = message;
if (this.walletUriInput instanceof HTMLElement) {
this.walletUriInput.focus();
}
this.mainController.callbacks.onWalletTest(context, this.mainController);
return context.result;
}
const normalizedActive = this.mainController.normalizeHexPubkey(
this.mainController.getActivePubkey()
);
if (!normalizedActive) {
const loginMessage = "Sign in to test your wallet connection.";
this.updateWalletStatus(loginMessage, "error");
this.mainController.showError(loginMessage);
context.reason = "no-active-pubkey";
context.error = loginMessage;
this.mainController.callbacks.onWalletTest(context, this.mainController);
return context.result;
}
this.setWalletPaneBusy(true);
let finalStatus = null;
let finalVariant = "info";
try {
const result = await this.testWalletConnection({
nwcUri: sanitized,
defaultZap,
activePubkey: normalizedActive,
});
finalStatus = "Wallet connection confirmed.";
finalVariant = "success";
this.mainController.showSuccess("Wallet connection confirmed.");
context.result = result;
context.success = true;
context.reason = "tested";
let currentSettings = this.mainController.services.nwcSettings.getActiveNwcSettings();
if (!currentSettings || typeof currentSettings !== "object") {
currentSettings = this.mainController.services.nwcSettings.createDefaultNwcSettings();
}
if (currentSettings.nwcUri === sanitized) {
await this.persistWalletSettings({
lastChecked: Date.now(),
activePubkey: normalizedActive,
});
}
} catch (error) {
const fallbackMessage = "Failed to reach wallet.";
const detail =
error && typeof error.message === "string" && error.message.trim()
? error.message.trim()
: fallbackMessage;
finalStatus = detail;
finalVariant = "error";
context.error = detail;
context.reason = error?.code || "service-error";
this.mainController.showError(detail);
} finally {
this.setWalletPaneBusy(false);
// Do NOT refreshWalletPaneState() here: it re-renders the form from SAVED
// settings and would wipe a URI the user typed but hasn't saved yet (then a
// subsequent Save sees an empty field and "removes" the wallet). Testing
// must preserve the entered value — only refresh the button state.
this.applyWalletControlState();
if (finalStatus) {
this.updateWalletStatus(finalStatus, finalVariant);
}
context.status = finalStatus;
context.variant = finalVariant;
this.mainController.callbacks.onWalletTest(context, this.mainController);
}
return context.result;
}
async handleWalletDisconnect() {
const context = {
success: false,
reason: null,
error: null,
status: null,
variant: null,
};
if (this.isWalletBusy()) {
context.reason = "busy";
this.mainController.callbacks.onWalletDisconnect(
context,
this.mainController
);
return context;
}
const normalizedActive = this.mainController.normalizeHexPubkey(
this.mainController.getActivePubkey()
);
if (!normalizedActive) {
const loginMessage = "Sign in to disconnect your wallet.";
this.updateWalletStatus(loginMessage, "error");
this.mainController.showError(loginMessage);
context.reason = "no-active-pubkey";
context.error = loginMessage;
this.mainController.callbacks.onWalletDisconnect(
context,
this.mainController
);
return context;
}
this.setWalletPaneBusy(true);
let finalStatus = null;
let finalVariant = "info";
try {
const disconnectResult = await this.disconnectWallet({
activePubkey: normalizedActive,
});
context.result = disconnectResult || null;
finalStatus = "Wallet disconnected.";
this.mainController.showStatus("Wallet disconnected.");
context.success = true;
context.reason = "disconnected";
} catch (error) {
const fallbackMessage = "Failed to disconnect wallet.";
const detail =
error && typeof error.message === "string" && error.message.trim()
? error.message.trim()
: fallbackMessage;
finalStatus = detail;
finalVariant = "error";
context.error = detail;
context.reason = error?.code || "service-error";
this.mainController.showError(detail);
} finally {
this.setWalletPaneBusy(false);
this.refreshWalletPaneState();
if (finalStatus) {
this.updateWalletStatus(finalStatus, finalVariant);
}
context.status = finalStatus;
context.variant = finalVariant;
this.mainController.callbacks.onWalletDisconnect(
context,
this.mainController
);
}
return context;
}
isWalletBusy() {
return Boolean(this.mainController.state.getWalletBusy());
}
setWalletPaneBusy(isBusy) {
const result = this.mainController.state.setWalletBusy(Boolean(isBusy));
if (this.walletPane instanceof HTMLElement) {
this.walletPane.setAttribute(
"aria-busy",
this.isWalletBusy() ? "true" : "false"
);
}
this.applyWalletControlState();
return result;
}
async persistWalletSettings({
nwcUri,
defaultZap,
lastChecked,
activePubkey,
} = {}) {
const callback = this.mainController.callbacks.onWalletPersist;
if (callback && callback !== noop) {
const result = await callback({
controller: this.mainController,
nwcUri,
defaultZap,
lastChecked,
activePubkey,
});
if (result !== undefined) {
return result;
}
}
return this.mainController.services.nwcSettings.handleProfileWalletPersist({
nwcUri,
defaultZap,
lastChecked,
});
}
async testWalletConnection({ nwcUri, defaultZap, activePubkey } = {}) {
const callback = this.mainController.callbacks.onWalletTestRequest;
if (callback && callback !== noop) {
const result = await callback({
controller: this.mainController,
nwcUri,
defaultZap,
activePubkey,
});
if (result !== undefined) {
return result;
}
}
return this.mainController.services.nwcSettings.ensureWallet({
nwcUri,
defaultZap,
});
}
async disconnectWallet({ activePubkey } = {}) {
const callback = this.mainController.callbacks.onWalletDisconnectRequest;
if (callback && callback !== noop) {
const result = await callback({
controller: this.mainController,
activePubkey,
});
if (result !== undefined) {
return result;
}
}
return this.mainController.services.nwcSettings.updateActiveNwcSettings(
this.mainController.services.nwcSettings.createDefaultNwcSettings()
);
}
}