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;