Files
bitvid/js/nwcSettings.js
T

369 lines
9.3 KiB
JavaScript

const DB_NAME = "bitvidSettings";
const DB_VERSION = 1;
const STORE_NAME = "kv";
const KEY_PREFIX = "nwcSettings:";
const LOCALSTORAGE_PREFIX = "bitvid:nwcSettings:";
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
const MIN_DEFAULT_ZAP = 0;
const MAX_DEFAULT_ZAP = 100000000;
const DEFAULT_SETTINGS = Object.freeze({
nwcUri: "",
defaultZap: null,
lastChecked: null,
version: "",
});
function createClone(value) {
return { ...value };
}
function isIndexedDbAvailable() {
try {
return typeof indexedDB !== "undefined";
} catch (error) {
return false;
}
}
function openSettingsDb() {
return new Promise((resolve, reject) => {
if (!isIndexedDbAvailable()) {
resolve(null);
return;
}
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error || new Error("Failed to open settings DB"));
};
});
}
function normalizePubkey(pubkey) {
if (typeof pubkey !== "string") {
return null;
}
const trimmed = pubkey.trim();
if (!trimmed) {
return null;
}
if (HEX64_REGEX.test(trimmed)) {
return trimmed.toLowerCase();
}
if (trimmed.startsWith("npub1") && typeof window !== "undefined") {
try {
const decoded = window?.NostrTools?.nip19?.decode?.(trimmed);
if (decoded?.type === "npub" && typeof decoded.data === "string") {
const hex = decoded.data.trim();
if (HEX64_REGEX.test(hex)) {
return hex.toLowerCase();
}
}
} catch (error) {
// Ignore decode errors and fall through to return null.
}
}
return null;
}
function resolveStorageKeys(pubkey) {
const normalized = normalizePubkey(pubkey);
if (!normalized) {
throw new Error("Invalid pubkey for NWC settings.");
}
return {
normalized,
dbKey: `${KEY_PREFIX}${normalized}`,
fallbackKey: `${LOCALSTORAGE_PREFIX}${normalized}`,
};
}
function sanitizeUri(value) {
return typeof value === "string" ? value.trim() : "";
}
function sanitizeDefaultZap(value) {
if (value === null || value === undefined || value === "") {
return null;
}
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return null;
}
const rounded = Math.round(numeric);
if (!Number.isFinite(rounded)) {
return null;
}
return Math.min(MAX_DEFAULT_ZAP, Math.max(MIN_DEFAULT_ZAP, rounded));
}
function sanitizeTimestamp(value) {
if (!Number.isFinite(value)) {
return null;
}
const rounded = Math.round(value);
if (!Number.isFinite(rounded)) {
return null;
}
return Math.max(0, rounded);
}
function sanitizeVersion(value) {
if (typeof value !== "string") {
return "";
}
return value.trim();
}
function normalizeSettings(raw) {
const base = createClone(DEFAULT_SETTINGS);
if (!raw || typeof raw !== "object") {
return base;
}
if (Object.prototype.hasOwnProperty.call(raw, "nwcUri")) {
base.nwcUri = sanitizeUri(raw.nwcUri);
}
if (Object.prototype.hasOwnProperty.call(raw, "defaultZap")) {
base.defaultZap = sanitizeDefaultZap(raw.defaultZap);
}
if (Object.prototype.hasOwnProperty.call(raw, "lastChecked")) {
base.lastChecked = sanitizeTimestamp(raw.lastChecked);
}
if (Object.prototype.hasOwnProperty.call(raw, "version")) {
base.version = sanitizeVersion(raw.version);
}
return base;
}
function mergeSettings(existing, partial) {
const source = partial && typeof partial === "object" ? partial : {};
return normalizeSettings({ ...existing, ...source });
}
function emitToast(message, { variant = "error", error } = {}) {
const text = typeof message === "string" ? message.trim() : "";
if (!text) {
return;
}
if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") {
try {
window.dispatchEvent(
new CustomEvent("bitvid:toast", {
detail: {
message: text,
variant,
source: "nwc-settings",
error,
},
})
);
} catch (eventError) {
console.warn("[nwcSettings] Failed to dispatch toast event", eventError);
}
}
}
function notifyFailure(message, error) {
const text = message || "NWC settings storage error.";
console.warn(`[nwcSettings] ${text}`, error);
emitToast(text, { variant: "error", error });
}
function notifyWarning(message, error) {
const text = message || "NWC settings storage warning.";
console.warn(`[nwcSettings] ${text}`, error);
emitToast(text, { variant: "warning", error });
}
async function readFromIndexedDb(dbKey) {
try {
const db = await openSettingsDb();
if (!db) {
return null;
}
return await new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readonly");
const store = tx.objectStore(STORE_NAME);
const request = store.get(dbKey);
request.onsuccess = () => {
resolve(request.result || null);
};
request.onerror = () => {
reject(request.error || new Error("Failed to load NWC settings."));
};
});
} catch (error) {
notifyWarning("Unable to read wallet settings from IndexedDB.", error);
return null;
}
}
function readFromLocalStorage(fallbackKey) {
try {
if (typeof localStorage === "undefined") {
return null;
}
const raw = localStorage.getItem(fallbackKey);
if (!raw) {
return null;
}
return JSON.parse(raw);
} catch (error) {
notifyFailure(
"Unable to read wallet settings from local storage fallback.",
error
);
return null;
}
}
async function writeToIndexedDb(dbKey, value) {
try {
const db = await openSettingsDb();
if (!db) {
return false;
}
await new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readwrite");
const store = tx.objectStore(STORE_NAME);
const request = store.put(value, dbKey);
request.onsuccess = () => resolve();
request.onerror = () => {
reject(request.error || new Error("Failed to persist NWC settings."));
};
});
return true;
} catch (error) {
notifyWarning("Unable to save wallet settings to IndexedDB.", error);
return false;
}
}
function writeToLocalStorage(fallbackKey, value) {
try {
if (typeof localStorage === "undefined") {
return false;
}
localStorage.setItem(fallbackKey, JSON.stringify(value));
return true;
} catch (error) {
notifyWarning("Unable to save wallet settings to local storage fallback.", error);
return false;
}
}
async function deleteFromIndexedDb(dbKey) {
try {
const db = await openSettingsDb();
if (!db) {
return false;
}
await new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readwrite");
const store = tx.objectStore(STORE_NAME);
const request = store.delete(dbKey);
request.onsuccess = () => resolve();
request.onerror = () => {
reject(request.error || new Error("Failed to clear NWC settings."));
};
});
return true;
} catch (error) {
notifyWarning("Unable to remove wallet settings from IndexedDB.", error);
return false;
}
}
function deleteFromLocalStorage(fallbackKey) {
try {
if (typeof localStorage === "undefined") {
return false;
}
localStorage.removeItem(fallbackKey);
return true;
} catch (error) {
notifyWarning("Unable to remove wallet settings from local storage fallback.", error);
return false;
}
}
export function createDefaultNwcSettings() {
return createClone(DEFAULT_SETTINGS);
}
export async function loadNwcSettings(pubkey) {
const { dbKey, fallbackKey } = resolveStorageKeys(pubkey);
const stored = await readFromIndexedDb(dbKey);
if (stored) {
return normalizeSettings(stored);
}
const fallback = readFromLocalStorage(fallbackKey);
if (fallback) {
return normalizeSettings(fallback);
}
return normalizeSettings(null);
}
export async function saveNwcSettings(pubkey, partial = {}) {
const { normalized, dbKey, fallbackKey } = resolveStorageKeys(pubkey);
const existing = await readFromIndexedDb(dbKey);
const fallbackExisting = existing || readFromLocalStorage(fallbackKey);
const merged = mergeSettings(fallbackExisting || DEFAULT_SETTINGS, partial);
const idbPersisted = await writeToIndexedDb(dbKey, merged);
const localPersisted = writeToLocalStorage(fallbackKey, merged);
if (!idbPersisted && !localPersisted) {
notifyFailure(
"Failed to persist wallet settings. Recent changes were not saved.",
new Error(`Unable to save settings for ${normalized}`)
);
throw new Error("Unable to persist NWC settings.");
}
return merged;
}
export async function clearNwcSettings(pubkey) {
const { normalized, dbKey, fallbackKey } = resolveStorageKeys(pubkey);
const idbCleared = await deleteFromIndexedDb(dbKey);
const localCleared = deleteFromLocalStorage(fallbackKey);
if (!idbCleared && !localCleared) {
notifyFailure(
"Failed to clear wallet settings from browser storage.",
new Error(`Unable to clear settings for ${normalized}`)
);
throw new Error("Unable to clear NWC settings.");
}
return true;
}