mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Completes todo #15. On login, if an encrypted copy of the user's storage and/or wallet settings exists on their Nostr account — and this device hasn't already opted in — offer to restore it. - js/nostr/encryptedSync.js: exists(dTag) — cheap list-only existence peek (NO decrypt, so it doesn't spend the signer budget); decryption only on accept. - storage/walletSyncService: hasRemote() wrappers over exists(). - js/services/settingsRestorePrompt.js: one-time per-pubkey offer. Only prompts for items with a remote copy not already enabled locally; if nothing remote exists it does NOT consume the one-time offer (a note published later can still prompt next login). On accept, pulls + applies. - app.js handleLoginModalSuccess: fires maybeOfferSettingsRestore(pubkey) after login, deferred ~2s so relays connect; best-effort, never blocks login; toasts on restore. tests/settings-restore-prompt.test.mjs: offer+restore-only-remote-items, decline, prompt-once, skip-already-enabled, don't-consume-when-empty. Mutation-verified. Co-Authored-By: Claude Opus 4.8 <[email protected]>
128 lines
3.8 KiB
JavaScript
128 lines
3.8 KiB
JavaScript
// One-time "offer to pull on login" for encrypted settings sync (todo #15).
|
|
// On login, if an encrypted copy of the user's storage and/or wallet settings
|
|
// exists on their Nostr account — and this device hasn't already opted in — offer
|
|
// to restore it. Runs AFTER login (never blocks it) and respects the signer
|
|
// budget: the existence check is list-only (no decrypt); decryption happens only
|
|
// if the user accepts.
|
|
|
|
import { storageSyncService as defaultStorageSync } from "./storageSyncService.js";
|
|
|
|
const OFFERED_KEY = "bitvid:settings-sync:offered:v1";
|
|
|
|
function readOffered() {
|
|
try {
|
|
if (typeof localStorage === "undefined") {
|
|
return {};
|
|
}
|
|
const raw = localStorage.getItem(OFFERED_KEY);
|
|
const parsed = raw ? JSON.parse(raw) : {};
|
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function markOffered(pubkey) {
|
|
try {
|
|
if (typeof localStorage === "undefined") {
|
|
return;
|
|
}
|
|
const map = readOffered();
|
|
map[pubkey] = true;
|
|
localStorage.setItem(OFFERED_KEY, JSON.stringify(map));
|
|
} catch (error) {
|
|
// Best-effort.
|
|
}
|
|
}
|
|
|
|
function hasOffered(pubkey) {
|
|
return readOffered()[pubkey] === true;
|
|
}
|
|
|
|
const ITEM_LABELS = {
|
|
storage: "storage settings",
|
|
wallet: "wallet connection",
|
|
};
|
|
|
|
export function createSettingsRestorePrompt({
|
|
storageSync = defaultStorageSync,
|
|
walletSync = null,
|
|
confirm = (message) =>
|
|
typeof window !== "undefined" && typeof window.confirm === "function"
|
|
? window.confirm(message)
|
|
: false,
|
|
logger = null,
|
|
} = {}) {
|
|
// Items not already enabled on THIS device that have a remote copy to restore.
|
|
async function collectCandidates(pubkey) {
|
|
const candidates = [];
|
|
const services = [
|
|
["storage", storageSync],
|
|
["wallet", walletSync],
|
|
];
|
|
for (const [kind, service] of services) {
|
|
if (!service?.isAvailable?.() || service.isEnabled(pubkey)) {
|
|
continue;
|
|
}
|
|
try {
|
|
if (await service.hasRemote()) {
|
|
candidates.push(kind);
|
|
}
|
|
} catch (error) {
|
|
logger?.warn?.(`[settingsRestore] ${kind} hasRemote check failed`, error);
|
|
}
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
async function maybeOffer(pubkey, { onRestored } = {}) {
|
|
const key = typeof pubkey === "string" ? pubkey.trim() : "";
|
|
if (!key) {
|
|
return { offered: false, reason: "no-pubkey" };
|
|
}
|
|
if (hasOffered(key)) {
|
|
return { offered: false, reason: "already-offered" };
|
|
}
|
|
|
|
const candidates = await collectCandidates(key);
|
|
if (!candidates.length) {
|
|
// Don't mark offered — a note may be published later from another device;
|
|
// the existence check is cheap (list-only) so re-checking next login is fine.
|
|
return { offered: false, reason: "no-remote" };
|
|
}
|
|
|
|
// We are about to prompt — mark so we never nag again on this device.
|
|
markOffered(key);
|
|
|
|
const list = candidates.map((kind) => ITEM_LABELS[kind]).join(" and ");
|
|
const accepted = confirm(
|
|
`Found an encrypted copy of your ${list} on your Nostr account. ` +
|
|
`Restore ${candidates.length > 1 ? "them" : "it"} to this device?`
|
|
);
|
|
if (!accepted) {
|
|
return { offered: true, accepted: false };
|
|
}
|
|
|
|
const restored = [];
|
|
const serviceByKind = { storage: storageSync, wallet: walletSync };
|
|
for (const kind of candidates) {
|
|
try {
|
|
const result = await serviceByKind[kind].pull(key);
|
|
if (result?.imported) {
|
|
restored.push(kind);
|
|
}
|
|
} catch (error) {
|
|
logger?.warn?.(`[settingsRestore] ${kind} pull failed`, error);
|
|
}
|
|
}
|
|
if (restored.length && typeof onRestored === "function") {
|
|
onRestored(restored);
|
|
}
|
|
return { offered: true, accepted: true, restored };
|
|
}
|
|
|
|
return { maybeOffer };
|
|
}
|
|
|
|
export default createSettingsRestorePrompt;
|