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)) {
|
||||
console.log('it is custom domain');
|
||||
const apiKeyHeaders = getApiKeyHeaders();
|
||||
const orgs = await getOrgsByCustomDomain(url.host, true, apiKeyHeaders);
|
||||
const orgs = await getOrgsByCustomDomain(url.hostname, true, apiKeyHeaders);
|
||||
|
||||
if (!orgs || orgs.length === 0) {
|
||||
return response;
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "Du skal betale for dette kursus eller blive inviteret for at få adgang.",
|
||||
"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": {
|
||||
"free": "Gratis",
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"enroll_page": {
|
||||
"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",
|
||||
"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": {
|
||||
"free": "Kostenlos",
|
||||
|
||||
@@ -3371,7 +3371,9 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "You need to pay for this course or be invited to get access.",
|
||||
"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",
|
||||
"modules": "modules",
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "Debes pagar por este curso o ser invitado a obtener acceso.",
|
||||
"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": {
|
||||
"free": "Gratis",
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "Vous devez payer pour ce cours ou être invité pour y accéder.",
|
||||
"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": {
|
||||
"free": "Gratuit",
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "आपको इस पाठ्यक्रम के लिए भुगतान करना होगा या प्रवेश पाने के लिए आमंत्रित किया जाना होगा।",
|
||||
"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": {
|
||||
"free": "निःशुल्क",
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "Aby uzyskać dostęp, musisz zapłacić za ten kurs lub otrzymać zaproszenie.",
|
||||
"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": {
|
||||
"free": "Bezpłatny",
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "Você precisa pagar por este curso ou ser convidado para obter acesso.",
|
||||
"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": {
|
||||
"free": "Grátis",
|
||||
|
||||
@@ -892,7 +892,11 @@
|
||||
"enroll_page": {
|
||||
"requires_payment_or_invite": "Вам нужно заплатить за этот курс или получить приглашение для получения доступа.",
|
||||
"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": {
|
||||
"free": "Бесплатно",
|
||||
|
||||
@@ -892,7 +892,9 @@
|
||||
"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.",
|
||||
"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": {
|
||||
"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';
|
||||
|
||||
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();
|
||||
|
||||
// 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.
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,12 @@ export const load = async ({ params, url }) => {
|
||||
}
|
||||
|
||||
const courseResult = await safeServerApi<GetCourseBySlugSuccess>(() =>
|
||||
classroomio.course.slug[':slug'].$get({
|
||||
classroomio.course.slug[':slug'].$get(
|
||||
{
|
||||
param: { slug }
|
||||
})
|
||||
},
|
||||
getApiKeyHeaders()
|
||||
)
|
||||
);
|
||||
|
||||
if (!courseResult.ok || !courseResult.body.data) {
|
||||
|
||||
@@ -12,17 +12,46 @@
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { resolve } from '$app/paths';
|
||||
import { authClient } from '$lib/utils/services/auth/client';
|
||||
import { appInitApi } from '$features/app/init.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let enrollmentInFlight = $state(false);
|
||||
let enrollmentAttemptKey = $state('');
|
||||
let enrolledPendingVerification = $state(false);
|
||||
|
||||
const session = authClient.useSession();
|
||||
const sessionReady = $derived(!$session.isPending && !$session.isRefetching);
|
||||
const sessionUser = $derived($session.data?.user ?? null);
|
||||
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 hasActiveInvite = $derived(Boolean(data.invite) && inviteStatus === 'ACTIVE');
|
||||
@@ -74,6 +103,7 @@
|
||||
const result = await courseApi.enroll(data.course.id, body);
|
||||
|
||||
if (!result?.data) {
|
||||
snackbar.error('snackbar.invite.failed_join');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,8 +117,14 @@
|
||||
already_joined: result.data.alreadyJoined
|
||||
});
|
||||
|
||||
if (!isEmailVerified) {
|
||||
enrolledPendingVerification = true;
|
||||
void sendVerificationEmail();
|
||||
return;
|
||||
}
|
||||
|
||||
navigatingAway = true;
|
||||
window.location.href = result.data.redirectTo || '/lms';
|
||||
window.location.href = resolvePostEnrollRedirect();
|
||||
} finally {
|
||||
enrollmentInFlight = false;
|
||||
if (!navigatingAway) {
|
||||
@@ -133,6 +169,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appInitApi.isInitializedAndReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attemptKey = `${sessionUser?.id ?? ''}:${data.course?.id ?? ''}`;
|
||||
if (enrollmentAttemptKey === attemptKey) {
|
||||
return;
|
||||
@@ -167,14 +207,26 @@
|
||||
<AuthUI
|
||||
isLogin={false}
|
||||
{handleSubmit}
|
||||
isLoading={loading || !sessionReady || (isLoggedIn && canJoinCourse)}
|
||||
isLoading={loading || !sessionReady || (isLoggedIn && canJoinCourse && enrollmentInFlight)}
|
||||
showOnlyContent={true}
|
||||
showLogo={true}
|
||||
>
|
||||
<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>
|
||||
<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">
|
||||
{$t('course.navItem.landing_page.enroll_page.requires_payment_or_invite')}
|
||||
</p>
|
||||
@@ -189,9 +241,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !isLoggedIn}
|
||||
{#if !enrolledPendingVerification && (!isLoggedIn || (isLoggedIn && canJoinCourse && !enrollmentInFlight))}
|
||||
<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')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user