Wallet restore popup: re-offer on login when missing locally (like storage)

The wallet already rode the same login "restore from Nostr" offer as storage,
but a once-per-device "offered" flag suppressed it — so on a device without
your wallet you fell back to the manual Sync button. Now:

- encryptedSyncItem: add hasLocal(pubkey) (non-null payload => item present
  locally, e.g. a connected wallet / configured storage).
- settingsRestorePrompt: re-offer an item whenever it's MISSING locally and a
  Nostr backup exists — bypassing the "offered once" flag — so it surfaces
  exactly when you'd want it. Only respect the once-per-device flag when the
  item IS present locally (no nagging). A new per-device "dismissed" flag,
  set when you decline, keeps it quiet after an explicit "no".
- Accepting enables sync, so it auto-pulls silently thereafter (the "both"
  behavior: silent auto-pull when enabled + popup when missing & off).
- Unknown services (no hasLocal) keep the legacy behavior.
- tests: re-offer when missing despite prior offer; no re-offer when present;
  decline stays quiet.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-08 06:56:52 -04:00
1 parent c562d9ee65
commit 4f56a2bd26
3 files changed
+134 -5

No files matched your search

+17
View File
@@ -59,6 +59,22 @@ export function createEncryptedSyncItem({
return Boolean(result?.exists);
}
// Does this device already have the item locally? (A non-null payload means
// there's something to sync — e.g. a connected wallet / configured storage.)
// Used by the login offer to re-surface a restore when the item is MISSING
// here but a Nostr backup exists.
async function hasLocal(pubkey) {
const key = normalizePubkey(pubkey);
if (!key) {
return false;
}
try {
return Boolean(await buildPayload(key));
} catch (error) {
return false;
}
}
async function push(pubkey, { confirmOverwrite } = {}) {
const key = normalizePubkey(pubkey);
if (!key) {
@@ -151,6 +167,7 @@ export function createEncryptedSyncItem({
isAvailable,
isEnabled,
hasRemote,
hasLocal,
push,
pull,
autoPullIfNewer,
+51 -3
View File
@@ -10,13 +10,16 @@ import { showConfirm } from "../ui/confirmDialog.js";
import { setSyncEnabled } from "./settingsSyncFlags.js";
const OFFERED_KEY = "bitvid:settings-sync:offered:v1";
// Set when the user explicitly declines a restore offer, so the "re-offer while
// the item is missing locally" behavior below doesn't nag them on every login.
const DISMISSED_KEY = "bitvid:settings-sync:dismissed:v1";
function readOffered() {
function readMap(storageKey) {
try {
if (typeof localStorage === "undefined") {
return {};
}
const raw = localStorage.getItem(OFFERED_KEY);
const raw = localStorage.getItem(storageKey);
const parsed = raw ? JSON.parse(raw) : {};
return parsed && typeof parsed === "object" ? parsed : {};
} catch (error) {
@@ -24,6 +27,31 @@ function readOffered() {
}
}
function readOffered() {
return readMap(OFFERED_KEY);
}
function isDismissed(pubkey, kind) {
const entry = readMap(DISMISSED_KEY)[pubkey];
return Boolean(entry && typeof entry === "object" && entry[kind]);
}
function markDismissed(pubkey, kind) {
try {
if (typeof localStorage === "undefined") {
return;
}
const map = readMap(DISMISSED_KEY);
const kinds =
map[pubkey] && typeof map[pubkey] === "object" ? { ...map[pubkey] } : {};
kinds[kind] = true;
map[pubkey] = kinds;
localStorage.setItem(DISMISSED_KEY, JSON.stringify(map));
} catch (error) {
// Best-effort.
}
}
// Per-pubkey, PER-ITEM offered state. Tracking only per-pubkey meant the first
// item offered (usually storage) flagged the whole pubkey, so a wallet note
// published later was never offered — "the wallet doesn't restore like storage".
@@ -78,13 +106,29 @@ export function createSettingsRestorePrompt({
["wallet", walletSync],
];
for (const [kind, service] of services) {
// Enabled items auto-pull silently elsewhere; declined items stay quiet.
if (
!service?.isAvailable?.() ||
service.isEnabled(pubkey) ||
hasOffered(pubkey, kind)
isDismissed(pubkey, kind)
) {
continue;
}
// Re-offer whenever the item is MISSING locally (e.g. a fresh device or a
// cleared wallet) so a Nostr backup gets surfaced — "restore like storage".
// Only when we DO have it locally do we respect the once-per-device flag
// (no nagging). Unknown (no hasLocal) → assume present → keep old behavior.
let localCopy = true;
if (typeof service.hasLocal === "function") {
try {
localCopy = Boolean(await service.hasLocal(pubkey));
} catch (error) {
localCopy = true;
}
}
if (localCopy && hasOffered(pubkey, kind)) {
continue;
}
try {
if (await service.hasRemote()) {
candidates.push(kind);
@@ -147,6 +191,10 @@ export function createSettingsRestorePrompt({
`Restore ${candidates.length > 1 ? "them" : "it"} to this device?`
);
if (!accepted) {
// Remember the decline so we don't re-offer a missing item every login.
for (const kind of candidates) {
markDismissed(key, kind);
}
return { offered: true, accepted: false };
}