fix(dashboard): open upgrade modal without triggering unsaved-changes prompt (#919)

* fix(dashboard): open the upgrade modal without navigating

Opening the upgrade modal set `?upgrade=true` via `goto`, which SvelteKit treats
as a navigation. On pages guarded by UnsavedChanges (the exercise editor, course
settings, certificates) that triggered the native "You have unsaved changes"
confirm before the modal could appear, and cancelling blocked it entirely.

The modal now lives in app state instead of the URL, so opening and closing it
never navigates. `?upgrade=true` is still honoured for billing emails and the
post-checkout return URL: it is consumed once on load and stripped from the
address bar with shallow routing.

Because checkout is an external redirect that unloads the page, editors can
register a handoff to persist in-progress work first. The exercise editor stashes
its draft, and the checkout return URL now points back at the page the user
upgraded from, so their questions are restored instead of lost.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): close question type select before opening upgrade modal

The select dropdown sits at z-250 while the upgrade dialog is z-200, so
opening the modal while the menu was still mounted left it covering the
dialog. Close the select first, wait for its close animation, then open
the upgrade modal.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): close question-type select before opening upgrade modal

The premium-type button left the select open, and its z-menu-elevated
content sat in front of the upgrade dialog. Close the select first and
wait for the close animation before opening the modal.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-08-18 13:17:06 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 564b0735f0
commit 4cd2f5f295
26 changed files with 298 additions and 71 deletions
@@ -48,6 +48,8 @@
} from '$features/ui';
import { isSelfPacedLikeCourse } from '$features/course/utils/compliance-utils';
import { getOrderedNavigableContent } from '$features/course/utils/content';
import { saveExerciseDraft } from '$features/course/utils/exercise-draft';
import { onUpgradeCheckoutHandoff } from '$lib/utils/store/upgrade-modal';
import {
hydrateExercisePageData
// , refreshExercisePageData
@@ -463,6 +465,19 @@
selectedTab = normalizeExerciseTab(page.url.searchParams.get('tab'));
});
// Upgrading from a premium question type sends the browser to an external checkout. Stash the
// editor so it can be restored on the way back, and stand the leave guard down — with the work
// persisted there is nothing to warn about.
onMount(() =>
onUpgradeCheckoutHandoff(() => {
const courseId = courseApi.course?.id;
if ($isOrgStudent || !courseId || !hasDirtyQuestionnaire()) return;
saveExerciseDraft(courseId, exerciseId, $questionnaire);
hasUnsavedChanges = false;
})
);
$effect(() => {
const nextTab = normalizeExerciseTab(page.url.searchParams.get('tab'));
if (nextTab === untrack(() => selectedTab)) return;