Hold the settings-restore prompt until the onboarding tour is done

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-08 06:45:17 -04:00
1 parent 0e2f81d222
commit c562d9ee65
2 files changed
+75 -8

No files matched your search

+59 -5
View File
@@ -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(
+16 -3
View File
@@ -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;