fix(dashboard): keep unverified users on enroll page after join
After email login, auto-enroll was redirecting to /lms or the admin /courses/:id/lessons route. Unverified users cannot use the LMS yet (VerifyEmailModal on app routes), which felt like a permission error. Enroll without verification, but stay on the enroll page with a verify prompt instead of redirecting until email is confirmed. Also fix custom domain org lookup to use hostname without backend port. Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
be0085f090
commit
f262a092ed
@@ -54,7 +54,7 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
|
|||||||
if (isURLCustomDomain(url)) {
|
if (isURLCustomDomain(url)) {
|
||||||
console.log('it is custom domain');
|
console.log('it is custom domain');
|
||||||
const apiKeyHeaders = getApiKeyHeaders();
|
const apiKeyHeaders = getApiKeyHeaders();
|
||||||
const orgs = await getOrgsByCustomDomain(url.host, true, apiKeyHeaders);
|
const orgs = await getOrgsByCustomDomain(url.hostname, true, apiKeyHeaders);
|
||||||
|
|
||||||
if (!orgs || orgs.length === 0) {
|
if (!orgs || orgs.length === 0) {
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Du skal betale for dette kursus eller blive inviteret for at få adgang.",
|
"requires_payment_or_invite": "Du skal betale for dette kursus eller blive inviteret for at få adgang.",
|
||||||
"back_to_course": "Tilbage til kurset",
|
"back_to_course": "Tilbage til kurset",
|
||||||
"join_course": "Deltag i kursus"
|
"join_course": "Deltag i kursus",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "Gratis",
|
"free": "Gratis",
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Sie müssen für diesen Kurs bezahlen oder eingeladen werden, um Zugang zu erhalten.",
|
"requires_payment_or_invite": "Sie müssen für diesen Kurs bezahlen oder eingeladen werden, um Zugang zu erhalten.",
|
||||||
"back_to_course": "Zurück zum Kurs",
|
"back_to_course": "Zurück zum Kurs",
|
||||||
"join_course": "Nehmen Sie am Kurs teil"
|
"join_course": "Nehmen Sie am Kurs teil",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "Kostenlos",
|
"free": "Kostenlos",
|
||||||
|
|||||||
@@ -3371,7 +3371,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "You need to pay for this course or be invited to get access.",
|
"requires_payment_or_invite": "You need to pay for this course or be invited to get access.",
|
||||||
"back_to_course": "Back to course",
|
"back_to_course": "Back to course",
|
||||||
"join_course": "Join Course"
|
"join_course": "Join Course",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"course_content": "Course content",
|
"course_content": "Course content",
|
||||||
"modules": "modules",
|
"modules": "modules",
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Debes pagar por este curso o ser invitado a obtener acceso.",
|
"requires_payment_or_invite": "Debes pagar por este curso o ser invitado a obtener acceso.",
|
||||||
"back_to_course": "volver al curso",
|
"back_to_course": "volver al curso",
|
||||||
"join_course": "Unirse al curso"
|
"join_course": "Unirse al curso",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "Gratis",
|
"free": "Gratis",
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Vous devez payer pour ce cours ou être invité pour y accéder.",
|
"requires_payment_or_invite": "Vous devez payer pour ce cours ou être invité pour y accéder.",
|
||||||
"back_to_course": "Retour au cours",
|
"back_to_course": "Retour au cours",
|
||||||
"join_course": "Rejoindre le cours"
|
"join_course": "Rejoindre le cours",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "Gratuit",
|
"free": "Gratuit",
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "आपको इस पाठ्यक्रम के लिए भुगतान करना होगा या प्रवेश पाने के लिए आमंत्रित किया जाना होगा।",
|
"requires_payment_or_invite": "आपको इस पाठ्यक्रम के लिए भुगतान करना होगा या प्रवेश पाने के लिए आमंत्रित किया जाना होगा।",
|
||||||
"back_to_course": "पाठ्यक्रम पर वापस जाएँ",
|
"back_to_course": "पाठ्यक्रम पर वापस जाएँ",
|
||||||
"join_course": "पाठ्यक्रम में शामिल हों"
|
"join_course": "पाठ्यक्रम में शामिल हों",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "निःशुल्क",
|
"free": "निःशुल्क",
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Aby uzyskać dostęp, musisz zapłacić za ten kurs lub otrzymać zaproszenie.",
|
"requires_payment_or_invite": "Aby uzyskać dostęp, musisz zapłacić za ten kurs lub otrzymać zaproszenie.",
|
||||||
"back_to_course": "Powrót na kurs",
|
"back_to_course": "Powrót na kurs",
|
||||||
"join_course": "Dołącz do kursu"
|
"join_course": "Dołącz do kursu",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "Bezpłatny",
|
"free": "Bezpłatny",
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Você precisa pagar por este curso ou ser convidado para obter acesso.",
|
"requires_payment_or_invite": "Você precisa pagar por este curso ou ser convidado para obter acesso.",
|
||||||
"back_to_course": "De volta ao curso",
|
"back_to_course": "De volta ao curso",
|
||||||
"join_course": "Participe do curso"
|
"join_course": "Participe do curso",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "Grátis",
|
"free": "Grátis",
|
||||||
|
|||||||
@@ -892,7 +892,11 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Вам нужно заплатить за этот курс или получить приглашение для получения доступа.",
|
"requires_payment_or_invite": "Вам нужно заплатить за этот курс или получить приглашение для получения доступа.",
|
||||||
"back_to_course": "Вернуться к курсу",
|
"back_to_course": "Вернуться к курсу",
|
||||||
"join_course": "Присоединиться к курсу"
|
"join_course": "Присоединиться к курсу",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area.",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "Бесплатно",
|
"free": "Бесплатно",
|
||||||
|
|||||||
@@ -892,7 +892,9 @@
|
|||||||
"enroll_page": {
|
"enroll_page": {
|
||||||
"requires_payment_or_invite": "Bạn cần phải trả tiền cho khóa học này hoặc được mời để có quyền truy cập.",
|
"requires_payment_or_invite": "Bạn cần phải trả tiền cho khóa học này hoặc được mời để có quyền truy cập.",
|
||||||
"back_to_course": "Quay lại khóa học",
|
"back_to_course": "Quay lại khóa học",
|
||||||
"join_course": "Tham gia khóa học"
|
"join_course": "Tham gia khóa học",
|
||||||
|
"enrolled_success": "You're enrolled in this course.",
|
||||||
|
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
|
||||||
},
|
},
|
||||||
"pricing_section": {
|
"pricing_section": {
|
||||||
"free": "miễn phí",
|
"free": "miễn phí",
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import { PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES } from '$lib/utils/constants/routes';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const load = async ({ parent }) => {
|
function canResolveOrgFromCoursePath(pathname: string): boolean {
|
||||||
|
return PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES.some((pattern) => new RegExp(pattern).test(pathname));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const load = async ({ parent, url }) => {
|
||||||
const { org, isOrgSite } = await parent();
|
const { org, isOrgSite } = await parent();
|
||||||
|
|
||||||
// An org-site subdomain that doesn't resolve to a real org must 404 rather
|
// An org-site subdomain that doesn't resolve to a real org must 404 rather
|
||||||
// than fall back to whatever org a requested course happens to belong to.
|
// than fall back to whatever org a requested course happens to belong to.
|
||||||
if (isOrgSite && !org) {
|
// Course landing/enroll pages can still resolve org context from the course
|
||||||
|
// slug when the host lookup fails (e.g. custom domain with a backend port).
|
||||||
|
if (isOrgSite && !org && !canResolveOrgFromCoursePath(url.pathname)) {
|
||||||
throw error(404, 'Organization not found');
|
throw error(404, 'Organization not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,12 @@ export const load = async ({ params, url }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const courseResult = await safeServerApi<GetCourseBySlugSuccess>(() =>
|
const courseResult = await safeServerApi<GetCourseBySlugSuccess>(() =>
|
||||||
classroomio.course.slug[':slug'].$get({
|
classroomio.course.slug[':slug'].$get(
|
||||||
param: { slug }
|
{
|
||||||
})
|
param: { slug }
|
||||||
|
},
|
||||||
|
getApiKeyHeaders()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!courseResult.ok || !courseResult.body.data) {
|
if (!courseResult.ok || !courseResult.body.data) {
|
||||||
|
|||||||
@@ -12,17 +12,46 @@
|
|||||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { authClient } from '$lib/utils/services/auth/client';
|
import { authClient } from '$lib/utils/services/auth/client';
|
||||||
|
import { appInitApi } from '$features/app/init.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let enrollmentInFlight = $state(false);
|
let enrollmentInFlight = $state(false);
|
||||||
let enrollmentAttemptKey = $state('');
|
let enrollmentAttemptKey = $state('');
|
||||||
|
let enrolledPendingVerification = $state(false);
|
||||||
|
|
||||||
const session = authClient.useSession();
|
const session = authClient.useSession();
|
||||||
const sessionReady = $derived(!$session.isPending && !$session.isRefetching);
|
const sessionReady = $derived(!$session.isPending && !$session.isRefetching);
|
||||||
const sessionUser = $derived($session.data?.user ?? null);
|
const sessionUser = $derived($session.data?.user ?? null);
|
||||||
const isLoggedIn = $derived(sessionReady && Boolean(sessionUser));
|
const isLoggedIn = $derived(sessionReady && Boolean(sessionUser));
|
||||||
|
const isEmailVerified = $derived(Boolean($profile.isEmailVerified || sessionUser?.emailVerified));
|
||||||
|
|
||||||
|
function resolvePostEnrollRedirect(): string {
|
||||||
|
// Org-site enrollments are always students — the API's `/courses/:id/lessons`
|
||||||
|
// target is the admin course editor and 404s or blocks students on custom domains.
|
||||||
|
return '/lms';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendVerificationEmail() {
|
||||||
|
const email = $profile.email || sessionUser?.email;
|
||||||
|
if (!email) {
|
||||||
|
snackbar.error('verify_email_modal.snackbar.error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const callbackURL = new URL(page.url.href);
|
||||||
|
callbackURL.searchParams.set('trigger', 'app');
|
||||||
|
|
||||||
|
await authClient.sendVerificationEmail({
|
||||||
|
email,
|
||||||
|
callbackURL: callbackURL.toString()
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
snackbar.error('verify_email_modal.snackbar.error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const inviteStatus = $derived(data.invite?.status ?? 'INVALID');
|
const inviteStatus = $derived(data.invite?.status ?? 'INVALID');
|
||||||
const hasActiveInvite = $derived(Boolean(data.invite) && inviteStatus === 'ACTIVE');
|
const hasActiveInvite = $derived(Boolean(data.invite) && inviteStatus === 'ACTIVE');
|
||||||
@@ -74,6 +103,7 @@
|
|||||||
const result = await courseApi.enroll(data.course.id, body);
|
const result = await courseApi.enroll(data.course.id, body);
|
||||||
|
|
||||||
if (!result?.data) {
|
if (!result?.data) {
|
||||||
|
snackbar.error('snackbar.invite.failed_join');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +117,14 @@
|
|||||||
already_joined: result.data.alreadyJoined
|
already_joined: result.data.alreadyJoined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isEmailVerified) {
|
||||||
|
enrolledPendingVerification = true;
|
||||||
|
void sendVerificationEmail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
navigatingAway = true;
|
navigatingAway = true;
|
||||||
window.location.href = result.data.redirectTo || '/lms';
|
window.location.href = resolvePostEnrollRedirect();
|
||||||
} finally {
|
} finally {
|
||||||
enrollmentInFlight = false;
|
enrollmentInFlight = false;
|
||||||
if (!navigatingAway) {
|
if (!navigatingAway) {
|
||||||
@@ -133,6 +169,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!appInitApi.isInitializedAndReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const attemptKey = `${sessionUser?.id ?? ''}:${data.course?.id ?? ''}`;
|
const attemptKey = `${sessionUser?.id ?? ''}:${data.course?.id ?? ''}`;
|
||||||
if (enrollmentAttemptKey === attemptKey) {
|
if (enrollmentAttemptKey === attemptKey) {
|
||||||
return;
|
return;
|
||||||
@@ -167,14 +207,26 @@
|
|||||||
<AuthUI
|
<AuthUI
|
||||||
isLogin={false}
|
isLogin={false}
|
||||||
{handleSubmit}
|
{handleSubmit}
|
||||||
isLoading={loading || !sessionReady || (isLoggedIn && canJoinCourse)}
|
isLoading={loading || !sessionReady || (isLoggedIn && canJoinCourse && enrollmentInFlight)}
|
||||||
showOnlyContent={true}
|
showOnlyContent={true}
|
||||||
showLogo={true}
|
showLogo={true}
|
||||||
>
|
>
|
||||||
<div class="mt-0 w-full">
|
<div class="mt-0 w-full">
|
||||||
<h3 class="mt-0 mb-4 text-center text-lg font-medium dark:text-white">{data.course?.title}</h3>
|
<h3 class="mt-0 mb-4 text-center text-lg font-medium dark:text-white">{data.course?.title}</h3>
|
||||||
<p class="text-center text-sm font-light dark:text-white">{data.course?.description}</p>
|
<p class="text-center text-sm font-light dark:text-white">{data.course?.description}</p>
|
||||||
{#if data.requiresPaymentOrInvite}
|
{#if enrolledPendingVerification}
|
||||||
|
<p class="ui:text-primary mt-3 text-center text-sm font-medium">
|
||||||
|
{$t('course.navItem.landing_page.enroll_page.enrolled_success')}
|
||||||
|
</p>
|
||||||
|
<p class="ui:text-muted-foreground mt-2 text-center text-sm">
|
||||||
|
{$t('course.navItem.landing_page.enroll_page.enrolled_verify_email')}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<Button type="button" variant="outline" onclick={sendVerificationEmail}>
|
||||||
|
{$t('verify_email_modal.resend')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else if data.requiresPaymentOrInvite}
|
||||||
<p class="ui:text-muted-foreground mt-3 text-center text-sm">
|
<p class="ui:text-muted-foreground mt-3 text-center text-sm">
|
||||||
{$t('course.navItem.landing_page.enroll_page.requires_payment_or_invite')}
|
{$t('course.navItem.landing_page.enroll_page.requires_payment_or_invite')}
|
||||||
</p>
|
</p>
|
||||||
@@ -189,9 +241,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !isLoggedIn}
|
{#if !enrolledPendingVerification && (!isLoggedIn || (isLoggedIn && canJoinCourse && !enrollmentInFlight))}
|
||||||
<div class="my-4 flex w-full items-center justify-center">
|
<div class="my-4 flex w-full items-center justify-center">
|
||||||
<Button type="submit" disabled={!canJoinCourse || loading || !sessionReady} {loading}>
|
<Button type="submit" disabled={!canJoinCourse || loading || !sessionReady || enrollmentInFlight} {loading}>
|
||||||
{$t('course.navItem.landing_page.enroll_page.join_course')}
|
{$t('course.navItem.landing_page.enroll_page.join_course')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user