walletSyncService and storageSyncService were near-identical copies (the "two
sync systems"). Extract the shared machinery into createEncryptedSyncItem() —
per-pubkey enable flags, conflict-checked push, cheap remote-exists check,
enable/disable/pull — so each service is now a thin config differing only in
d-tag, kind, availability, and buildPayload/applyPayload. Public surface
(isAvailable/isEnabled/hasRemote/push/pull/enable/disable + the *_DTAG export)
preserved; both dedicated suites pass (8).
Next (step 2): shared confirm dialog + consistent notifications + seamless
auto-sync in the two profile controllers (the user-facing UX unification).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Saves push silently as before, but if the relay copy is NEWER than the one this
device last pushed (another device changed it), the user is now asked to confirm
before overwriting — preventing a silent multi-device clobber. No nag on normal
saves.
- js/services/settingsSyncFlags.js: track per-pubkey+kind last-pushed created_at
(getSyncPushedAt/setSyncPushedAt).
- js/services/syncConflict.js: pushWithConflictCheck — peek remote created_at
(list-only, no decrypt), compare to last push, call confirmOverwrite only on a
real conflict, then push strictly-newer than remote so the replace wins.
- storage/walletSyncService push()/enable() route through it and accept a
confirmOverwrite callback.
- ProfileStorage/WalletController supply confirmOverwrite (window.confirm) and
surface a "kept the newer copy" status when the user declines.
tests/sync-conflict.test.mjs: no-remote→silent, newer→prompt(+decline blocks
push), confirm→strictly-newer push, older→no prompt. Mutation-verified
(ignoring the decline fails). Service-test twins gained exists().
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Adds encrypted cross-login sync for the NWC wallet connection (todo #15),
reusing the encrypted-sync core under d-tag "bitvid:nwc".
- js/services/settingsSyncFlags.js: shared per-pubkey opt-in flag store
(storage/wallet); storageSyncService refactored onto it (identical behavior,
tests still green).
- js/services/walletSyncService.js: factory (NwcSettingsService is per-app, not a
singleton). push() syncs the current nwcUri (+defaultZap); pull() applies it via
updateActiveNwcSettings; disable() wipes the note.
- Wallet pane (components/profile-modal.html + ProfileWalletController): opt-in
toggle + Restore button + a strong SPENDING warning, shown only when a signer
can encrypt. Enabling requires an explicit window.confirm (the URI can spend).
Re-pushes on save when enabled; and on wallet DISCONNECT while synced it clears
the note so a removed spending URI doesn't linger (encrypted) on relays.
tests/wallet-sync-service.test.mjs: enable-pushes-uri, pull-restores-via-
updateActiveNwcSettings, disable-clears, no-wallet-doesn't-publish. Mutation-
verified (skip-apply + skip-clear both fail).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wires the encrypted-sync core (0abb82b9) to the live client + active signer and
adds the storage-credential sync service. No UI yet — the Storage pane toggle is
the next increment.
- js/nostr/encryptedSyncFacade.js: default encryptedSync instance bound to
nostrClient (write set via getDeletePublishRelays, read relays, pool) and the
active signer's signEvent. Keeps the core pure/testable.
- js/services/storageService.js: exportAccountRecord / importAccountRecord — get
the on-disk account record and restore it on another device. Import REPLACES
the local record (same-pubkey envelope unlocks with the same signer); cross-
device merge is a follow-up.
- js/services/storageSyncService.js: enable/disable/push/pull under d-tag
"bitvid:storage-connections", with a per-pubkey opt-in flag (off by default).
enable() pushes immediately; disable() wipes the published note.
tests/storage-sync-service.test.mjs: enable-pushes-exported-record, pull-restores
on a fresh device, disable-clears, empty-record-doesn't-publish. Mutation-verified
(skip-import and skip-clear both fail).
Co-Authored-By: Claude Opus 4.8 <[email protected]>