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

+66 -2
View File
@@ -15,8 +15,13 @@ import { isSyncEnabled } from "../js/services/settingsSyncFlags.js";
const PUBKEY = "c".repeat(64);
function makeSyncStub({ available = true, enabled = false, remote = false } = {}) {
return {
function makeSyncStub({
available = true,
enabled = false,
remote = false,
local = undefined,
} = {}) {
const stub = {
pulls: 0,
isAvailable: () => available,
isEnabled: () => enabled,
@@ -26,8 +31,16 @@ function makeSyncStub({ available = true, enabled = false, remote = false } = {}
return { found: true, imported: true };
},
};
// Only real services expose hasLocal; omitting it (local === undefined) keeps
// the legacy "assume present" behavior the older tests rely on.
if (local !== undefined) {
stub.hasLocal = async () => local;
}
return stub;
}
const OFFERED_KEY = "bitvid:settings-sync:offered:v1";
test("offers and restores items that have a remote copy and aren't already enabled", async () => {
localStorage.clear();
const storageSync = makeSyncStub({ remote: true });
@@ -51,6 +64,57 @@ test("offers and restores items that have a remote copy and aren't already enabl
assert.deepEqual(restoredCalls, [["storage"]]);
});
test("re-offers a MISSING item even after it was already offered once (restore-like-storage)", async () => {
localStorage.clear();
// Simulate a prior offer for wallet on this device (the old blocker).
localStorage.setItem(OFFERED_KEY, JSON.stringify({ [PUBKEY]: { wallet: true } }));
const walletSync = makeSyncStub({ remote: true, local: false });
const prompt = createSettingsRestorePrompt({
storageSync: makeSyncStub({ remote: false, local: true }),
walletSync,
confirm: () => true,
});
const result = await prompt.maybeOffer(PUBKEY);
assert.equal(result.accepted, true);
assert.deepEqual(result.restored, ["wallet"]);
assert.equal(walletSync.pulls, 1, "missing wallet is re-surfaced despite the prior offer");
});
test("does NOT re-offer an item that is present locally once prompted", async () => {
localStorage.clear();
localStorage.setItem(OFFERED_KEY, JSON.stringify({ [PUBKEY]: { wallet: true } }));
const walletSync = makeSyncStub({ remote: true, local: true });
const prompt = createSettingsRestorePrompt({
storageSync: makeSyncStub({ remote: false, local: true }),
walletSync,
confirm: () => true,
});
const result = await prompt.maybeOffer(PUBKEY);
assert.equal(result.offered, false, "present + already offered → no nag");
assert.equal(walletSync.pulls, 0);
});
test("dismissing a missing item stops it re-offering every login", async () => {
localStorage.clear();
const walletSync = makeSyncStub({ remote: true, local: false });
const prompt = createSettingsRestorePrompt({
storageSync: makeSyncStub({ remote: false, local: true }),
walletSync,
confirm: () => false, // decline
});
const first = await prompt.maybeOffer(PUBKEY);
assert.equal(first.offered, true);
assert.equal(first.accepted, false);
// Next login: still missing locally, but the decline is remembered.
const second = await prompt.maybeOffer(PUBKEY);
assert.equal(second.offered, false, "declined → stays quiet even though still missing");
assert.equal(walletSync.pulls, 0);
});
test("declining the prompt restores nothing", async () => {
localStorage.clear();
const storageSync = makeSyncStub({ remote: true });