mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
370 lines
9.3 KiB
JavaScript
370 lines
9.3 KiB
JavaScript
import { userLogger } from "./utils/logger.js";
|
|
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) {
|
|
userLogger.warn("[nwcSettings] Failed to dispatch toast event", eventError);
|
|
}
|
|
}
|
|
}
|
|
|
|
function notifyFailure(message, error) {
|
|
const text = message || "NWC settings storage error.";
|
|
userLogger.warn(`[nwcSettings] ${text}`, error);
|
|
emitToast(text, { variant: "error", error });
|
|
}
|
|
|
|
function notifyWarning(message, error) {
|
|
const text = message || "NWC settings storage warning.";
|
|
userLogger.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;
|
|
}
|