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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
co-authored by
Rotimi Best
parent
564b0735f0
commit
4cd2f5f295
@@ -14,7 +14,7 @@
|
||||
import { ZCourseUpdate } from '@cio/utils/validation/course/course';
|
||||
import { validateWithTranslation } from '$lib/utils/validation';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { openUpgradeModal } from '$lib/utils/functions/org';
|
||||
import { openUpgradeModal } from '$lib/utils/store/upgrade-modal';
|
||||
|
||||
let errors = $state<Record<string, string>>({});
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
import { courseApi } from '$features/course/api';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { hydrateExercisePageData } from '$features/course/utils/exercise-page-utils';
|
||||
import { restoreExerciseDraft } from '$features/course/utils/exercise-draft';
|
||||
import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { ContentType } from '@cio/utils/constants/content';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
@@ -47,6 +49,11 @@
|
||||
}
|
||||
|
||||
hydrateExercisePageData(data.exercise, data.exerciseId);
|
||||
|
||||
// Puts back work stashed before an upgrade checkout redirect.
|
||||
if (restoreExerciseDraft(data.courseId, data.exerciseId)) {
|
||||
snackbar.success('snackbar.exercise.draft_restored');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import GraduationCapIcon from '@lucide/svelte/icons/graduation-cap';
|
||||
import CompassIcon from '@lucide/svelte/icons/compass';
|
||||
import AwardIcon from '@lucide/svelte/icons/award';
|
||||
import { openUpgradeModal } from '$lib/utils/functions/org';
|
||||
import { openUpgradeModal } from '$lib/utils/store/upgrade-modal';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { AI_CHAT_MODEL_STORAGE_KEY } from '$features/ai-assistant/utils/constants';
|
||||
import type { TCourseType } from '@cio/db/types';
|
||||
|
||||
@@ -14,6 +14,17 @@ const SUBSCRIPTION_PRODUCT_IDS = new Set(
|
||||
.filter((id): id is string => Boolean(id))
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends the customer back to the page they upgraded from. Only in-app paths are accepted:
|
||||
* `//host` and `/\host` are protocol-relative URLs that would redirect off-site.
|
||||
*/
|
||||
function getReturnPath(returnTo: string | null, fallbackPath: string) {
|
||||
if (!returnTo || !returnTo.startsWith('/')) return fallbackPath;
|
||||
if (returnTo.startsWith('//') || returnTo.startsWith('/\\')) return fallbackPath;
|
||||
|
||||
return returnTo;
|
||||
}
|
||||
|
||||
export const GET = async ({ url, locals, cookies }: RequestEvent) => {
|
||||
const { user } = locals;
|
||||
|
||||
@@ -60,11 +71,17 @@ export const GET = async ({ url, locals, cookies }: RequestEvent) => {
|
||||
server: dev ? 'sandbox' : 'production'
|
||||
});
|
||||
|
||||
const returnPath = getReturnPath(url.searchParams.get('returnTo'), `/org/${org.siteName}`);
|
||||
const successUrl = new URL(returnPath, url.origin);
|
||||
|
||||
successUrl.searchParams.set('upgrade', 'true');
|
||||
successUrl.searchParams.set('confirmation', 'true');
|
||||
|
||||
const checkout = await polar.checkouts.create({
|
||||
products: [productId],
|
||||
customerEmail: accountData.profile?.email ?? user.email,
|
||||
customerName: accountData.profile?.fullname || undefined,
|
||||
successUrl: `${url.origin}/org/${org.siteName}?upgrade=true&confirmation=true`,
|
||||
successUrl: successUrl.toString(),
|
||||
metadata: {
|
||||
orgId: org.id,
|
||||
orgSlug: org.siteName,
|
||||
|
||||
Reference in New Issue
Block a user