From c562d9ee65e7a4311709ddeacf2a5b7e8deb96e9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 8 Jul 2026 06:45:17 -0400 Subject: [PATCH] Hold the settings-restore prompt until the onboarding tour is done MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "restore storage/wallet from Nostr" confirm was popping over the guided tour (both fire on login). Gate the restore dialog behind a "tour gate": - maybeOfferOnboardingTour opens the gate synchronously when the tour will run (shouldOfferOnboarding), before its own start delay — so ordering can't race. The tour offer is now called before the restore offer on login. - launchBitvidTour gains an onEnd(status) hook (fires on finish AND skip); startOnboardingTour closes the gate on end, and also closes it if the tour never starts (logged out, no eligible steps) so restore can't hang. - The restore prompt's confirm now awaits whenOnboardingTourInactive() before showing — the remote existence check still runs in parallel, only the dialog is held back. No tour → gate stays closed → restore behaves as before. Co-Authored-By: Claude Opus 4.8 --- js/app.js | 64 +++++++++++++++++++++++++++++++--- js/ui/onboarding/bitvidTour.js | 19 ++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/js/app.js b/js/app.js index 7a05fc74..18ac9cf6 100644 --- a/js/app.js +++ b/js/app.js @@ -102,6 +102,8 @@ import { import { resolveSignerCapabilities } from "./nostr/signerCapabilities.js"; import { rankHashtagsByFrequency } from "./utils/hashtagSuggestions.js"; import { launchBitvidTour } from "./ui/onboarding/bitvidTour.js"; +import { shouldOfferOnboarding } from "./services/onboardingService.js"; +import { showConfirm } from "./ui/confirmDialog.js"; import { decideSignerEnsure, isSignerCapable, @@ -1759,13 +1761,40 @@ class Application { } if (pubkey) { - // Best-effort, never blocks login: offer to restore encrypted settings - // synced from another device. - this.maybeOfferSettingsRestore(pubkey); + // Order matters: offer the tour FIRST so it opens the "tour gate" + // synchronously when it will run. The settings-restore prompt then waits + // on that gate before showing its dialog, so it can't pop over the tour. this.maybeOfferOnboardingTour(pubkey); + this.maybeOfferSettingsRestore(pubkey); } } + // A gate the settings-restore prompt awaits so its confirmation dialog never + // pops over the onboarding tour. Opened synchronously when the tour will run, + // resolved when the tour finishes/exits (or fails to start). + _openTourGate() { + if (this._tourGateResolve) { + return; + } + this._tourGate = new Promise((resolve) => { + this._tourGateResolve = resolve; + }); + } + + _closeTourGate() { + const resolve = this._tourGateResolve; + this._tourGateResolve = null; + this._tourGate = null; + if (typeof resolve === "function") { + resolve(); + } + } + + // Resolves immediately unless the onboarding tour is (about to be) active. + whenOnboardingTourInactive() { + return this._tourGate || Promise.resolve(); + } + // First login of this pubkey on this device: offer the guided tour // (docs/onboarding-plan.md). Deferred so the feed has rendered and the // settings-restore prompt (2s window; only fires for accounts with synced @@ -1775,14 +1804,26 @@ class Application { if (!key) { return; } + // If the tour will actually run, open the gate NOW (synchronously) so the + // settings-restore prompt waits — even though the tour itself starts on a + // delay. If it won't run, leave the gate closed so restore proceeds normally. + if (!shouldOfferOnboarding(key)) { + return; + } + this._openTourGate(); setTimeout(() => { try { if (!this.isUserLoggedIn()) { + this._closeTourGate(); return; } - this.startOnboardingTour({ pubkey: key, force: false }); + const started = this.startOnboardingTour({ pubkey: key, force: false }); + if (!started) { + this._closeTourGate(); + } } catch (error) { devLogger.warn("[Application] Onboarding tour offer failed:", error); + this._closeTourGate(); } }, 3500); } @@ -1795,7 +1836,7 @@ class Application { if (!key) { return false; } - return launchBitvidTour({ + const started = launchBitvidTour({ pubkey: key, force, openProfilePane: (pane) => { @@ -1805,7 +1846,13 @@ class Application { devLogger.warn("[Application] Tour deep-link failed:", error); } }, + // Release the settings-restore prompt once the tour is done/exited. + onEnd: () => this._closeTourGate(), }); + if (!started) { + this._closeTourGate(); + } + return started; } // One-time offer to restore encrypted storage/wallet settings on login (todo @@ -1826,6 +1873,13 @@ class Application { walletSync: createWalletSyncService({ nwcSettings: this.nwcSettingsService, }), + // Never show the restore dialog over the onboarding tour — wait for + // it to finish/exit first. (The remote existence check still runs in + // parallel; only the confirmation dialog is held back.) + confirm: async (message) => { + await this.whenOnboardingTourInactive(); + return showConfirm(message, { confirmLabel: "Restore" }); + }, }); } Promise.resolve( diff --git a/js/ui/onboarding/bitvidTour.js b/js/ui/onboarding/bitvidTour.js index 395a8a1a..724d8ffd 100644 --- a/js/ui/onboarding/bitvidTour.js +++ b/js/ui/onboarding/bitvidTour.js @@ -74,19 +74,32 @@ export function launchBitvidTour({ openProfilePane, document: doc, force = false, + onEnd, } = {}) { if (!force && !shouldOfferOnboarding(pubkey)) { return false; } + // Fire onEnd on both finish and skip so callers can release anything they + // deferred for the tour's duration (e.g. the settings-restore prompt). + const end = (status) => { + markOnboarding(pubkey, status); + try { + onEnd?.(status); + } catch (error) { + // best-effort + } + }; + const tour = createTour({ steps: buildBitvidTourSteps({ openProfilePane }), document: doc, - onFinish: () => markOnboarding(pubkey, "completed"), - onSkip: () => markOnboarding(pubkey, "skipped"), + onFinish: () => end("completed"), + onSkip: () => end("skipped"), }); - return Boolean(tour?.start()); + const started = Boolean(tour?.start()); + return started; } export default launchBitvidTour;