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]>
Two fixes from live testing:
- "NWC doesn't sync unless I click Restore": devices with sync ENABLED only
ever pushed-on-save — edits from another device sat unseen until the
manual Restore click. encryptedSyncItem.autoPullIfNewer pulls the remote
note at login when its created_at is newer than this device's last-synced
marker (set on push AND now after a successful pull, so the same note is
never re-pulled and the next conflict check stays honest). Wired into the
login restore flow ahead of the one-time offer; disabled items are never
touched.
- "Could not get user information" on a known-good creator address: that
reason string comes from the LNURL SERVER, and the failing fetch was the
PLATFORM-FEE address, not the creator's. prepareLightningContext now
wraps both metadata fetches so the toast names which address failed
(e.g. "Platform fee Lightning address (…): Could not get user
information") instead of blaming the creator.
Co-Authored-By: Claude Fable 5 <[email protected]>
Accepting the one-time login restore offer used to pull the encrypted copy once
but never set the per-device sync flag — so users "restored once and stopped
syncing", and the settings toggle showed OFF despite having just restored. That
is exactly the "not syncing / has to keep confirming" behavior we're removing.
Accepting now means "keep this device in sync": after a successful pull we call
setSyncEnabled(pubkey, kind, true), so future saves auto-push (conflict-checked)
with no further prompts. Declining still enables nothing. No immediate push is
needed — we just pulled, so local == remote.
This completes the seamless model: enable once (or accept the offer once) →
saves auto-push on conflict-only confirm, restores pull with a small toast.
Tests: settings-restore-prompt.test.mjs +2 — accept enables the restored item
(and only that item); decline enables nothing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Every browser alert()/confirm() now uses the app's UI instead of native dialogs
(no prompt() existed). Two new pieces:
- js/ui/confirmDialog.js — showConfirm(message, opts) -> Promise<boolean>, a styled
`bv-modal modal-always-on-top` dialog (Confirm/Cancel/backdrop/Escape; danger variant
focuses Cancel). Importable anywhere, so future confirmations use it too. It stacks
above other modals and is typable thanks to the recent stacked-modal focus fix.
- js/ui/appNotify.js — notifyError/notifyStatus/notifySuccess routing to the app's
NotificationController, for modules without a local showError; re-exports showConfirm.
Migrated: UploadModal (10 alert -> this.showError, 1 confirm), subscriptionHistory
(3 alert -> notify*, 1 confirm), MyVideosController (2), ProfileStorage/Wallet/Relay
controllers, profileModalController (2), settingsRestorePrompt (default confirm now
showConfirm), nostr/client deleteAllVersions. confirmSyncOverwrite helpers made async
(syncConflict already awaits confirmOverwrite).
Tests: tests/confirm-dialog.test.mjs (6 — confirm/cancel/backdrop/Escape resolution,
DOM cleanup, danger focus, no-DOM fallback). Build + lint clean; affected suites green.
(Note: build/lint don't parse JS — caught a multi-line-import split + a wrong import
path via module-import checks.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A static client can't fetch LNURL hosts that omit CORS headers, so zaps to
those creators fail. Add a browser-side reachability probe
(checkLightningAddressZappable) and surface it on both sides: the creator
is warned at wallet-connect time if their own address isn't browser-
zappable, and the zapper sees a heads-up when opening a non-reachable
creator's zap dialog.
Also fix encrypted settings restore: the "already offered" flag was
per-pubkey, so once storage was offered the wallet note was never offered
later. Track it per-item so the wallet is offered independently.
Zappability check extracted to walletZappabilityCheck.js for the size cap.
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]>