From 419bb0261081e9bd46bd32dd2bdac0b0bb4ec198 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:54:10 -0400 Subject: [PATCH] Refactor NWC wallet settings into service --- js/app.js | 262 ++++++------------------ js/services/nwcSettingsService.js | 324 ++++++++++++++++++++++++++++++ js/ui/profileModalController.js | 184 +++++++++++------ js/ui/zapController.js | 34 +++- tests/video-modal-zap.test.mjs | 2 +- 5 files changed, 526 insertions(+), 280 deletions(-) create mode 100644 js/services/nwcSettingsService.js diff --git a/js/app.js b/js/app.js index b63b5f86..3575fbaf 100644 --- a/js/app.js +++ b/js/app.js @@ -33,6 +33,7 @@ import watchHistoryService from "./watchHistoryService.js"; import r2Service from "./services/r2Service.js"; import PlaybackService from "./services/playbackService.js"; import AuthService from "./services/authService.js"; +import NwcSettingsService from "./services/nwcSettingsService.js"; import nostrService from "./services/nostrService.js"; import { initQuickR2Upload } from "./r2-quick.js"; import { createWatchHistoryRenderer } from "./historyView.js"; @@ -46,12 +47,6 @@ import { formatViewCount, ingestLocalViewEvent, } from "./viewCounter.js"; -import { - loadNwcSettings, - saveNwcSettings, - clearNwcSettings, - createDefaultNwcSettings, -} from "./nwcSettings.js"; import { splitAndZap as splitAndZapDefault } from "./payments/zapSplit.js"; import { formatAbsoluteTimestamp as formatAbsoluteTimestampUtil, @@ -111,7 +106,6 @@ const FALLBACK_THUMBNAIL_SRC = "assets/jpg/video-thumbnail-fallback.jpg"; const MAX_DISCUSSION_COUNT_VIDEOS = 24; const VIDEO_EVENT_KIND = 30078; const HEX64_REGEX = /^[0-9a-f]{64}$/i; -const NWC_URI_SCHEME = "nostr+walletconnect://"; /** * Simple IntersectionObserver-based lazy loader for images (or videos). * @@ -200,6 +194,31 @@ class Application { this.splitAndZap = (services.payments && services.payments.splitAndZap) || splitAndZapDefault; + + this.nwcSettingsService = + services.nwcSettingsService || + new NwcSettingsService({ + normalizeHexPubkey: (value) => this.normalizeHexPubkey(value), + getActivePubkey: () => this.pubkey, + payments: this.payments, + logger: { + warn: (...args) => console.warn(...args), + }, + notifyError: (message) => this.showError(message), + maxWalletDefaultZap: MAX_WALLET_DEFAULT_ZAP, + }); + if ( + this.nwcSettingsService && + typeof this.nwcSettingsService.setActivePubkeyGetter === "function" + ) { + this.nwcSettingsService.setActivePubkeyGetter(() => this.pubkey); + } + if ( + this.nwcSettingsService && + typeof this.nwcSettingsService.setPayments === "function" + ) { + this.nwcSettingsService.setPayments(this.payments); + } if ( this.feedEngine && typeof this.feedEngine.run !== "function" && @@ -394,13 +413,7 @@ class Application { nostrClient, accessControl, getCurrentUserNpub: () => this.getCurrentUserNpub(), - getActiveNwcSettings: () => this.getActiveNwcSettings(), - updateActiveNwcSettings: (partial) => - this.updateActiveNwcSettings(partial), - hydrateNwcSettingsForPubkey: (pubkey) => - this.hydrateNwcSettingsForPubkey(pubkey), - createDefaultNwcSettings: () => createDefaultNwcSettings(), - ensureWallet: (options) => this.ensureWallet(options), + nwcSettings: this.nwcSettingsService, loadVideos: (forceFetch, context) => this.loadVideos(forceFetch, context), onVideosShouldRefresh: (context) => @@ -514,9 +527,8 @@ class Application { this.zapController = new ZapController({ videoModal: this.videoModal, getCurrentVideo: () => this.currentVideo, - getActiveNwcSettings: () => this.getActiveNwcSettings(), + nwcSettings: this.nwcSettingsService, isUserLoggedIn: () => this.isUserLoggedIn(), - hasActiveWalletConnection: () => this.hasActiveWalletConnection(), splitAndZap: (...args) => this.splitAndZap(...args), payments: this.payments, callbacks: { @@ -630,7 +642,6 @@ class Application { this.moreMenuGlobalHandlerBound = false; this.boundMoreMenuDocumentClick = null; this.boundMoreMenuDocumentKeydown = null; - this.nwcSettings = new Map(); this.boundNwcSettingsToastHandler = null; Object.defineProperty(this, "savedProfiles", { configurable: false, @@ -2650,35 +2661,18 @@ class Application { return context; } - async handleProfileWalletPersist({ - nwcUri, - defaultZap, - lastChecked, - } = {}) { - const partial = {}; - if (nwcUri !== undefined) { - partial.nwcUri = nwcUri; - } - if (defaultZap !== undefined) { - partial.defaultZap = defaultZap; - } - if (lastChecked !== undefined) { - partial.lastChecked = lastChecked; - } - - if (!Object.keys(partial).length) { - return this.getActiveNwcSettings(); - } - - return this.updateActiveNwcSettings(partial); + async handleProfileWalletPersist(options = {}) { + return this.nwcSettingsService.handleProfileWalletPersist(options); } async handleProfileWalletTest({ nwcUri, defaultZap } = {}) { - return this.ensureWallet({ nwcUri, defaultZap }); + return this.nwcSettingsService.ensureWallet({ nwcUri, defaultZap }); } async handleProfileWalletDisconnect() { - return this.updateActiveNwcSettings(createDefaultNwcSettings()); + return this.nwcSettingsService.updateActiveNwcSettings( + this.nwcSettingsService.createDefaultNwcSettings(), + ); } handleProfileAdminNotifyError({ error } = {}) { @@ -2725,69 +2719,19 @@ class Application { } async hydrateNwcSettingsForPubkey(pubkey) { - const normalized = this.normalizeHexPubkey(pubkey); - if (!normalized) { - return createDefaultNwcSettings(); - } - - try { - const settings = await loadNwcSettings(normalized); - const record = - settings && typeof settings === "object" - ? { ...settings } - : createDefaultNwcSettings(); - this.nwcSettings.set(normalized, record); - return { ...record }; - } catch (error) { - console.warn( - `[nwcSettings] Failed to load settings for ${normalized}:`, - error - ); - const fallback = createDefaultNwcSettings(); - this.nwcSettings.set(normalized, fallback); - return { ...fallback }; - } + return this.nwcSettingsService.hydrateNwcSettingsForPubkey(pubkey); } getActiveNwcSettings() { - const normalized = this.normalizeHexPubkey(this.pubkey); - if (!normalized) { - return createDefaultNwcSettings(); - } - const cached = this.nwcSettings.get(normalized); - return cached ? { ...cached } : createDefaultNwcSettings(); + return this.nwcSettingsService.getActiveNwcSettings(); } hasActiveWalletConnection() { - const settings = this.getActiveNwcSettings(); - const candidate = - typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : ""; - return candidate.length > 0; + return this.nwcSettingsService.hasActiveWalletConnection(); } - 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 }; + validateWalletUri(uri, options) { + return this.nwcSettingsService.validateWalletUri(uri, options); } isUserLoggedIn() { @@ -2795,96 +2739,15 @@ class Application { } async updateActiveNwcSettings(partial = {}) { - const normalized = this.normalizeHexPubkey(this.pubkey); - if (!normalized) { - console.warn( - "[nwcSettings] Cannot update settings without an active pubkey." - ); - return createDefaultNwcSettings(); - } - - try { - const updated = await saveNwcSettings(normalized, partial); - const record = - updated && typeof updated === "object" - ? { ...updated } - : createDefaultNwcSettings(); - this.nwcSettings.set(normalized, record); - return { ...record }; - } catch (error) { - console.warn( - `[nwcSettings] Failed to save settings for ${normalized}:`, - error - ); - return this.getActiveNwcSettings(); - } + return this.nwcSettingsService.updateActiveNwcSettings(partial); } async ensureWallet({ nwcUri, defaultZap } = {}) { - const activeSettings = this.getActiveNwcSettings(); - const candidateUri = - typeof nwcUri === "string" && nwcUri.trim() - ? nwcUri.trim() - : activeSettings.nwcUri || ""; - const { valid, sanitized, message } = this.validateWalletUri(candidateUri, { - requireValue: true, - }); - if (!valid) { - throw new Error(message || "Invalid wallet URI provided."); - } - - const merged = { - ...activeSettings, - nwcUri: sanitized, - }; - - if (typeof defaultZap === "number" && Number.isFinite(defaultZap)) { - const rounded = Math.max(0, Math.round(defaultZap)); - merged.defaultZap = Math.min(MAX_WALLET_DEFAULT_ZAP, rounded); - } else if (defaultZap === null) { - merged.defaultZap = null; - } - - if (this.payments && typeof this.payments.ensureWallet === "function") { - return this.payments.ensureWallet({ settings: merged }); - } - - console.warn( - "[wallet] Falling back to stub ensureWallet implementation. Returning settings without performing a connection test." - ); - return merged; + return this.nwcSettingsService.ensureWallet({ nwcUri, defaultZap }); } - async clearStoredNwcSettings(pubkey, { silent = false } = {}) { - const normalized = this.normalizeHexPubkey(pubkey); - if (!normalized) { - return false; - } - - try { - await clearNwcSettings(normalized); - } catch (error) { - console.warn( - `[nwcSettings] Failed to clear settings for ${normalized}:`, - error - ); - if (!silent) { - this.showError("Failed to clear wallet settings for this account."); - } - try { - await saveNwcSettings(normalized, createDefaultNwcSettings()); - } catch (persistError) { - console.warn( - `[nwcSettings] Failed to overwrite settings for ${normalized}:`, - persistError - ); - } - this.nwcSettings.delete(normalized); - return false; - } - - this.nwcSettings.delete(normalized); - return true; + async clearStoredNwcSettings(pubkey, options = {}) { + return this.nwcSettingsService.clearStoredNwcSettings(pubkey, options); } async handleAuthLogin(detail = {}) { @@ -2896,26 +2759,15 @@ class Application { } } - const normalizedActive = this.normalizeHexPubkey( - detail?.pubkey || this.pubkey - ); - const normalizedPrevious = this.normalizeHexPubkey(detail?.previousPubkey); - if (detail?.identityChanged) { this.resetViewLoggingState(); - this.nwcSettings.clear(); } - if ( - normalizedPrevious && - (!normalizedActive || normalizedPrevious !== normalizedActive) - ) { - await this.clearStoredNwcSettings(normalizedPrevious, { silent: true }); - } - - if (normalizedActive) { - await this.hydrateNwcSettingsForPubkey(normalizedActive); - } + await this.nwcSettingsService.onLogin({ + pubkey: detail?.pubkey || this.pubkey, + previousPubkey: detail?.previousPubkey, + identityChanged: Boolean(detail?.identityChanged), + }); if (this.profileController) { try { @@ -2979,13 +2831,10 @@ class Application { async handleAuthLogout(detail = {}) { this.resetViewLoggingState(); - const normalizedPrevious = this.normalizeHexPubkey( - detail?.previousPubkey || detail?.pubkey || this.pubkey - ); - if (normalizedPrevious) { - await this.clearStoredNwcSettings(normalizedPrevious, { silent: false }); - } - this.nwcSettings.clear(); + await this.nwcSettingsService.onLogout({ + pubkey: detail?.pubkey || this.pubkey, + previousPubkey: detail?.previousPubkey, + }); if (this.profileController) { try { @@ -7244,8 +7093,11 @@ class Application { this.boundNwcSettingsToastHandler = null; } - if (this.nwcSettings instanceof Map) { - this.nwcSettings.clear(); + if ( + this.nwcSettingsService && + typeof this.nwcSettingsService.clearCache === "function" + ) { + this.nwcSettingsService.clearCache(); } if (typeof this.watchHistoryPreferenceUnsubscribe === "function") { diff --git a/js/services/nwcSettingsService.js b/js/services/nwcSettingsService.js new file mode 100644 index 00000000..27f665c5 --- /dev/null +++ b/js/services/nwcSettingsService.js @@ -0,0 +1,324 @@ +import { + loadNwcSettings, + saveNwcSettings, + clearNwcSettings, + createDefaultNwcSettings, +} from "../nwcSettings.js"; +import { MAX_WALLET_DEFAULT_ZAP } from "../config.js"; + +const NWC_URI_SCHEME = "nostr+walletconnect://"; + +function isFunction(value) { + return typeof value === "function"; +} + +export default class NwcSettingsService { + constructor({ + normalizeHexPubkey, + getActivePubkey, + payments = null, + logger = console, + notifyError, + maxWalletDefaultZap = MAX_WALLET_DEFAULT_ZAP, + loadSettings = loadNwcSettings, + saveSettings = saveNwcSettings, + clearSettings = clearNwcSettings, + createDefaultSettings = createDefaultNwcSettings, + } = {}) { + this.normalizeHexPubkey = isFunction(normalizeHexPubkey) + ? normalizeHexPubkey + : (value) => (typeof value === "string" ? value.trim() : null); + this.getActivePubkey = isFunction(getActivePubkey) + ? getActivePubkey + : () => null; + this.payments = payments || null; + this.logger = logger || console; + this.notifyError = isFunction(notifyError) ? notifyError : null; + this.maxWalletDefaultZap = + Number.isFinite(maxWalletDefaultZap) && maxWalletDefaultZap > 0 + ? maxWalletDefaultZap + : MAX_WALLET_DEFAULT_ZAP; + this.loadSettings = isFunction(loadSettings) + ? loadSettings + : loadNwcSettings; + this.saveSettings = isFunction(saveSettings) + ? saveSettings + : saveNwcSettings; + this.clearSettings = isFunction(clearSettings) + ? clearSettings + : clearNwcSettings; + this.createDefaultSettings = isFunction(createDefaultSettings) + ? createDefaultSettings + : () => createDefaultNwcSettings(); + this.cache = new Map(); + } + + cloneSettings(settings) { + if (!settings || typeof settings !== "object") { + return this.createDefaultNwcSettings(); + } + return { ...settings }; + } + + setPayments(payments) { + this.payments = payments || null; + } + + setActivePubkeyGetter(fn) { + if (isFunction(fn)) { + this.getActivePubkey = fn; + } + } + + clearCache() { + this.cache.clear(); + } + + getNormalizedPubkey(pubkey) { + try { + return this.normalizeHexPubkey(pubkey); + } catch (error) { + if (this.logger && isFunction(this.logger.warn)) { + this.logger.warn("[nwcSettings] Failed to normalize pubkey:", error); + } + return null; + } + } + + async hydrateNwcSettingsForPubkey(pubkey) { + const normalized = this.getNormalizedPubkey(pubkey); + if (!normalized) { + return this.createDefaultNwcSettings(); + } + + try { + const settings = await this.loadSettings(normalized); + const record = + settings && typeof settings === "object" + ? { ...settings } + : this.createDefaultNwcSettings(); + this.cache.set(normalized, record); + return this.cloneSettings(record); + } catch (error) { + if (this.logger && isFunction(this.logger.warn)) { + this.logger.warn( + `[nwcSettings] Failed to load settings for ${normalized}:`, + error, + ); + } + const fallback = this.createDefaultNwcSettings(); + this.cache.set(normalized, fallback); + return this.cloneSettings(fallback); + } + } + + getActiveNwcSettings() { + const normalized = this.getNormalizedPubkey(this.getActivePubkey()); + if (!normalized) { + return this.createDefaultNwcSettings(); + } + const cached = this.cache.get(normalized); + return cached ? this.cloneSettings(cached) : this.createDefaultNwcSettings(); + } + + createDefaultNwcSettings() { + return this.createDefaultSettings(); + } + + hasActiveWalletConnection() { + const settings = this.getActiveNwcSettings(); + const candidate = + typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : ""; + return candidate.length > 0; + } + + 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 updateActiveNwcSettings(partial = {}) { + const normalized = this.getNormalizedPubkey(this.getActivePubkey()); + if (!normalized) { + if (this.logger && isFunction(this.logger.warn)) { + this.logger.warn( + "[nwcSettings] Cannot update settings without an active pubkey.", + ); + } + return this.createDefaultNwcSettings(); + } + + try { + const updated = await this.saveSettings(normalized, partial); + const record = + updated && typeof updated === "object" + ? { ...updated } + : this.createDefaultNwcSettings(); + this.cache.set(normalized, record); + return this.cloneSettings(record); + } catch (error) { + if (this.logger && isFunction(this.logger.warn)) { + this.logger.warn( + `[nwcSettings] Failed to save settings for ${normalized}:`, + error, + ); + } + return this.getActiveNwcSettings(); + } + } + + async handleProfileWalletPersist({ + nwcUri, + defaultZap, + lastChecked, + } = {}) { + const partial = {}; + if (nwcUri !== undefined) { + partial.nwcUri = nwcUri; + } + if (defaultZap !== undefined) { + partial.defaultZap = defaultZap; + } + if (lastChecked !== undefined) { + partial.lastChecked = lastChecked; + } + + if (!Object.keys(partial).length) { + return this.getActiveNwcSettings(); + } + + return this.updateActiveNwcSettings(partial); + } + + async ensureWallet({ nwcUri, defaultZap } = {}) { + const activeSettings = this.getActiveNwcSettings(); + const candidateUri = + typeof nwcUri === "string" && nwcUri.trim() + ? nwcUri.trim() + : activeSettings.nwcUri || ""; + const { valid, sanitized, message } = this.validateWalletUri(candidateUri, { + requireValue: true, + }); + if (!valid) { + throw new Error(message || "Invalid wallet URI provided."); + } + + const merged = { + ...activeSettings, + nwcUri: sanitized, + }; + + if (typeof defaultZap === "number" && Number.isFinite(defaultZap)) { + const rounded = Math.max(0, Math.round(defaultZap)); + merged.defaultZap = Math.min(this.maxWalletDefaultZap, rounded); + } else if (defaultZap === null) { + merged.defaultZap = null; + } + + if (this.payments && isFunction(this.payments.ensureWallet)) { + return this.payments.ensureWallet({ settings: merged }); + } + + if (this.logger && isFunction(this.logger.warn)) { + this.logger.warn( + "[wallet] Falling back to stub ensureWallet implementation. Returning settings without performing a connection test.", + ); + } + return merged; + } + + async clearStoredNwcSettings(pubkey, { silent = false } = {}) { + const normalized = this.getNormalizedPubkey(pubkey); + if (!normalized) { + return false; + } + + try { + await this.clearSettings(normalized); + } catch (error) { + if (this.logger && isFunction(this.logger.warn)) { + this.logger.warn( + `[nwcSettings] Failed to clear settings for ${normalized}:`, + error, + ); + } + if (!silent && this.notifyError) { + this.notifyError("Failed to clear wallet settings for this account."); + } + try { + await this.saveSettings(normalized, this.createDefaultNwcSettings()); + } catch (persistError) { + if (this.logger && isFunction(this.logger.warn)) { + this.logger.warn( + `[nwcSettings] Failed to overwrite settings for ${normalized}:`, + persistError, + ); + } + } + this.cache.delete(normalized); + return false; + } + + this.cache.delete(normalized); + return true; + } + + async onLogin({ pubkey, previousPubkey, identityChanged } = {}) { + if (identityChanged) { + this.onIdentityChanged(); + } + + const normalizedActive = this.getNormalizedPubkey( + pubkey !== undefined ? pubkey : this.getActivePubkey(), + ); + const normalizedPrevious = this.getNormalizedPubkey(previousPubkey); + + if ( + normalizedPrevious && + (!normalizedActive || normalizedPrevious !== normalizedActive) + ) { + await this.clearStoredNwcSettings(normalizedPrevious, { silent: true }); + } + + if (normalizedActive) { + await this.hydrateNwcSettingsForPubkey(normalizedActive); + } + + return this.getActiveNwcSettings(); + } + + async onLogout({ pubkey, previousPubkey } = {}) { + const normalizedPrevious = this.getNormalizedPubkey( + previousPubkey !== undefined ? previousPubkey : pubkey, + ); + if (normalizedPrevious) { + await this.clearStoredNwcSettings(normalizedPrevious, { silent: false }); + } + this.clearCache(); + return this.createDefaultNwcSettings(); + } + + onIdentityChanged() { + this.clearCache(); + } +} diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 541fb23d..3a77edda 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -144,52 +144,115 @@ const SERVICE_CONTRACT = [ fallback: () => () => null, }, { - key: "getActiveNwcSettings", - type: "function", + key: "nwcSettings", + type: "object", description: - "Reads the active Nostr Wallet Connect settings (uri/default zap) for the logged in profile.", - fallback: ({ internalState }) => () => { - const current = ensureInternalWalletSettings(internalState); - return { ...current }; - }, - }, - { - key: "updateActiveNwcSettings", - type: "function", - description: "Persists updates to the active wallet settings and returns the new state.", - fallback: ({ internalState }) => (partial = {}) => { - const current = ensureInternalWalletSettings(internalState); - const next = { - ...current, - ...(partial && typeof partial === "object" ? partial : {}), + "Service that manages Nostr Wallet Connect settings and interactions for the active profile.", + fallback: ({ internalState }) => { + const ensure = () => ensureInternalWalletSettings(internalState); + const service = {}; + + service.createDefaultNwcSettings = () => createInternalDefaultNwcSettings(); + + service.getActiveNwcSettings = () => { + const current = ensure(); + return { ...current }; }; - internalState.walletSettings = next; - return { ...next }; + + service.updateActiveNwcSettings = (partial = {}) => { + const current = ensure(); + const next = { + ...current, + ...(partial && typeof partial === "object" ? partial : {}), + }; + internalState.walletSettings = next; + return { ...next }; + }; + + service.hydrateNwcSettingsForPubkey = async () => { + return service.getActiveNwcSettings(); + }; + + service.handleProfileWalletPersist = async (options = {}) => { + const { nwcUri, defaultZap, lastChecked } = + options && typeof options === "object" ? options : {}; + const partial = {}; + if (nwcUri !== undefined) { + partial.nwcUri = nwcUri; + } + if (defaultZap !== undefined) { + partial.defaultZap = defaultZap; + } + if (lastChecked !== undefined) { + partial.lastChecked = lastChecked; + } + if (!Object.keys(partial).length) { + return service.getActiveNwcSettings(); + } + return service.updateActiveNwcSettings(partial); + }; + + service.hasActiveWalletConnection = () => { + const settings = ensure(); + const candidate = + typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : ""; + return candidate.length > 0; + }; + + service.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 }; + }; + + service.ensureWallet = async ({ nwcUri } = {}) => { + const settings = service.getActiveNwcSettings(); + const candidate = + typeof nwcUri === "string" && nwcUri.trim() + ? nwcUri.trim() + : settings.nwcUri || ""; + const { valid, sanitized, message } = service.validateWalletUri( + candidate, + { requireValue: true }, + ); + if (!valid) { + throw new Error(message || "Invalid wallet URI provided."); + } + return { ...settings, nwcUri: sanitized }; + }; + + service.clearStoredNwcSettings = async () => true; + + service.onLogin = async () => service.getActiveNwcSettings(); + + service.onLogout = async () => service.createDefaultNwcSettings(); + + service.onIdentityChanged = () => { + internalState.walletSettings = createInternalDefaultNwcSettings(); + }; + + return service; }, }, - { - key: "hydrateNwcSettingsForPubkey", - type: "function", - description: - "Ensures wallet settings are loaded for a given pubkey before the pane renders.", - fallback: ({ internalState }) => async () => { - const current = ensureInternalWalletSettings(internalState); - return { ...current }; - }, - }, - { - key: "createDefaultNwcSettings", - type: "function", - description: "Creates a baseline wallet settings object when none exists.", - fallback: () => () => createInternalDefaultNwcSettings(), - }, - { - key: "ensureWallet", - type: "function", - description: - "Guarantees a wallet connection exists before issuing wallet related operations (connect/test).", - fallback: () => async () => ({ ok: false, reason: "not-implemented" }), - }, { key: "loadVideos", type: "function", @@ -2113,9 +2176,9 @@ export class ProfileModalController { return; } - let settings = this.services.getActiveNwcSettings(); + let settings = this.services.nwcSettings.getActiveNwcSettings(); if (!settings || typeof settings !== "object") { - settings = this.services.createDefaultNwcSettings(); + settings = this.services.nwcSettings.createDefaultNwcSettings(); } setInputValue(this.walletUriInput, settings.nwcUri || ""); setInputValue( @@ -2367,9 +2430,9 @@ export class ProfileModalController { context.success = true; context.reason = "tested"; - let currentSettings = this.services.getActiveNwcSettings(); + let currentSettings = this.services.nwcSettings.getActiveNwcSettings(); if (!currentSettings || typeof currentSettings !== "object") { - currentSettings = this.services.createDefaultNwcSettings(); + currentSettings = this.services.nwcSettings.createDefaultNwcSettings(); } if (currentSettings.nwcUri === sanitized) { await this.persistWalletSettings({ @@ -3412,22 +3475,11 @@ export class ProfileModalController { } } - const partial = {}; - if (nwcUri !== undefined) { - partial.nwcUri = nwcUri; - } - if (defaultZap !== undefined) { - partial.defaultZap = defaultZap; - } - if (lastChecked !== undefined) { - partial.lastChecked = lastChecked; - } - - if (!Object.keys(partial).length) { - return this.services.getActiveNwcSettings(); - } - - return this.services.updateActiveNwcSettings(partial); + return this.services.nwcSettings.handleProfileWalletPersist({ + nwcUri, + defaultZap, + lastChecked, + }); } async testWalletConnection({ nwcUri, defaultZap, activePubkey } = {}) { @@ -3444,7 +3496,7 @@ export class ProfileModalController { } } - return this.services.ensureWallet({ nwcUri, defaultZap }); + return this.services.nwcSettings.ensureWallet({ nwcUri, defaultZap }); } async disconnectWallet({ activePubkey } = {}) { @@ -3456,8 +3508,8 @@ export class ProfileModalController { } } - return this.services.updateActiveNwcSettings( - this.services.createDefaultNwcSettings(), + return this.services.nwcSettings.updateActiveNwcSettings( + this.services.nwcSettings.createDefaultNwcSettings(), ); } @@ -3935,7 +3987,7 @@ export class ProfileModalController { } async hydrateActiveWalletSettings(pubkey) { - const hydrate = this.services.hydrateNwcSettingsForPubkey; + const hydrate = this.services.nwcSettings?.hydrateNwcSettingsForPubkey; if (typeof hydrate !== "function") { return null; } diff --git a/js/ui/zapController.js b/js/ui/zapController.js index 32a2dabc..a65155c1 100644 --- a/js/ui/zapController.js +++ b/js/ui/zapController.js @@ -29,6 +29,7 @@ export default class ZapController { constructor({ videoModal, getCurrentVideo, + nwcSettings, getActiveNwcSettings, isUserLoggedIn, hasActiveWalletConnection, @@ -40,16 +41,33 @@ export default class ZapController { this.videoModal = videoModal || null; this.getCurrentVideo = typeof getCurrentVideo === "function" ? getCurrentVideo : () => null; - this.getActiveNwcSettings = - typeof getActiveNwcSettings === "function" - ? getActiveNwcSettings - : () => ({}); + this.nwcSettings = + nwcSettings && + typeof nwcSettings === "object" && + typeof nwcSettings.getActiveNwcSettings === "function" + ? nwcSettings + : null; + if (this.nwcSettings) { + this.getActiveNwcSettings = () => + this.nwcSettings.getActiveNwcSettings(); + } else if (typeof getActiveNwcSettings === "function") { + this.getActiveNwcSettings = getActiveNwcSettings; + } else { + this.getActiveNwcSettings = () => ({}); + } this.isUserLoggedIn = typeof isUserLoggedIn === "function" ? isUserLoggedIn : () => false; - this.hasActiveWalletConnection = - typeof hasActiveWalletConnection === "function" - ? hasActiveWalletConnection - : () => false; + if ( + this.nwcSettings && + typeof this.nwcSettings.hasActiveWalletConnection === "function" + ) { + this.hasActiveWalletConnection = () => + this.nwcSettings.hasActiveWalletConnection(); + } else if (typeof hasActiveWalletConnection === "function") { + this.hasActiveWalletConnection = hasActiveWalletConnection; + } else { + this.hasActiveWalletConnection = () => false; + } this.splitAndZap = typeof splitAndZap === "function" ? splitAndZap : () => Promise.resolve(); this.payments = payments || {}; diff --git a/tests/video-modal-zap.test.mjs b/tests/video-modal-zap.test.mjs index 500c3cc8..bb6c20f1 100644 --- a/tests/video-modal-zap.test.mjs +++ b/tests/video-modal-zap.test.mjs @@ -349,7 +349,7 @@ await (async () => { const pubkeyHex = "b".repeat(64); app.pubkey = pubkeyHex; const normalized = app.normalizeHexPubkey(pubkeyHex); - app.nwcSettings.set(normalized, { + app.nwcSettingsService.cache.set(normalized, { nwcUri: "nostr+walletconnect://example", defaultZap: null, lastChecked: null,