From f262a092ed6fb31c00d06f54d55c53a92118f44e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 04:18:40 +0000 Subject: [PATCH 1/4] 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 --- .../src/lib/features/app/layout-setup.ts | 2 +- .../src/lib/utils/translations/da.json | 4 +- .../src/lib/utils/translations/de.json | 4 +- .../src/lib/utils/translations/en.json | 4 +- .../src/lib/utils/translations/es.json | 4 +- .../src/lib/utils/translations/fr.json | 4 +- .../src/lib/utils/translations/hi.json | 4 +- .../src/lib/utils/translations/pl.json | 4 +- .../src/lib/utils/translations/pt.json | 4 +- .../src/lib/utils/translations/ru.json | 6 +- .../src/lib/utils/translations/vi.json | 4 +- .../src/routes/(org-site)/+layout.server.ts | 11 +++- .../course/[slug]/enroll/+page.server.ts | 9 ++- .../course/[slug]/enroll/+page.svelte | 62 +++++++++++++++++-- 14 files changed, 105 insertions(+), 21 deletions(-) diff --git a/apps/dashboard/src/lib/features/app/layout-setup.ts b/apps/dashboard/src/lib/features/app/layout-setup.ts index 1dc582a4..015ae09a 100644 --- a/apps/dashboard/src/lib/features/app/layout-setup.ts +++ b/apps/dashboard/src/lib/features/app/layout-setup.ts @@ -54,7 +54,7 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise { +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'); } diff --git a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts index 635f1b3a..c1a11a1f 100644 --- a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts +++ b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts @@ -56,9 +56,12 @@ export const load = async ({ params, url }) => { } const courseResult = await safeServerApi(() => - classroomio.course.slug[':slug'].$get({ - param: { slug } - }) + classroomio.course.slug[':slug'].$get( + { + param: { slug } + }, + getApiKeyHeaders() + ) ); if (!courseResult.ok || !courseResult.body.data) { diff --git a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte index 478f6912..8be53027 100644 --- a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte +++ b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte @@ -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 @@

{data.course?.title}

{data.course?.description}

- {#if data.requiresPaymentOrInvite} + {#if enrolledPendingVerification} +

+ {$t('course.navItem.landing_page.enroll_page.enrolled_success')} +

+

+ {$t('course.navItem.landing_page.enroll_page.enrolled_verify_email')} +

+
+ +
+ {:else if data.requiresPaymentOrInvite}

{$t('course.navItem.landing_page.enroll_page.requires_payment_or_invite')}

@@ -189,9 +241,9 @@ {/if}
- {#if !isLoggedIn} + {#if !enrolledPendingVerification && (!isLoggedIn || (isLoggedIn && canJoinCourse && !enrollmentInFlight))}
-
From a6d06f356f65763fc33d6b45e3cda1090d77bba0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 04:22:16 +0000 Subject: [PATCH 2/4] feat(email): make verify-email org-aware like other transactional mail Resolve the tenant org from the verification link callback URL (custom domain, tenant subdomain, or ?org=) and apply org logo, theme color, from name, and subject. Falls back to ClassroomIO branding on the main app. Co-authored-by: Rotimi Best --- packages/db/src/auth/email-verification.ts | 70 ++++++---- .../db/src/auth/resolve-verification-org.ts | 123 ++++++++++++++++++ packages/email/src/core/base.ts | 2 +- packages/email/src/core/types.ts | 2 + packages/email/src/emails/verify-email.ts | 13 +- packages/email/src/send.ts | 1 + .../storybook/src/templates/email/fixtures.ts | 8 +- 7 files changed, 185 insertions(+), 34 deletions(-) create mode 100644 packages/db/src/auth/resolve-verification-org.ts diff --git a/packages/db/src/auth/email-verification.ts b/packages/db/src/auth/email-verification.ts index 79ffeca6..9e2fb595 100644 --- a/packages/db/src/auth/email-verification.ts +++ b/packages/db/src/auth/email-verification.ts @@ -1,5 +1,7 @@ import { type BetterAuthOptions } from 'better-auth'; -import { sendEmail } from '@cio/email'; +import { buildEmailBranding, buildEmailFromName, sendEmail } from '@cio/email'; + +import { resolveVerificationOrg } from './resolve-verification-org'; type EmailVerificationOptions = Parameters< NonNullable['sendVerificationEmail']> @@ -9,17 +11,38 @@ type ChangeEmailConfirmationOptions = Parameters< NonNullable['changeEmail']>['sendChangeEmailConfirmation']> >[0]; -/** - * Email verification flow. - * - * 1. user says I want to change my email from email@old.com to email@new.com - * 2. we send an email to email@old.com asking user to verify if they really want to change their email to email@new.com `sendChangeEmailConfirmation()`. [better-auth]. - * 3. user clicks the link in the email - * 4. we update the email in the database. [better-auth]. - * 5. we send a confirmation email to email@new.com `sendVerificationEmail()`. [dashboard]. - * 6. user clicks the link in the email - * 7. we mark email@new.com as verified. [better-auth]. - */ +function stripTriggerParam(url: string): string { + return url.replace('trigger=app', '').replace('trigger%3Dapp', ''); +} + +async function sendOrgAwareVerifyEmail(options: { + to: string; + verificationUrl: string; + userId: string; + userName?: string | null; + newEmail?: string; +}) { + const org = await resolveVerificationOrg({ + verificationUrl: options.verificationUrl, + userId: options.userId + }); + + const orgName = org?.name?.trim() || 'ClassroomIO'; + const branding = buildEmailBranding(org ? { name: org.name, avatarUrl: org.avatarUrl, theme: org.theme } : undefined); + + await sendEmail('verifyEmail', { + to: options.to, + from: org?.name ? buildEmailFromName(`${org.name} (via ClassroomIO.com)`) : undefined, + subject: org?.name ? `Confirm your email for ${org.name}` : undefined, + fields: { + link: stripTriggerParam(options.verificationUrl), + newEmail: options.newEmail, + userName: options.userName || undefined, + orgName, + branding + } + }); +} /** * Sends a verification email to the user when they sign up or change their email address. @@ -35,15 +58,11 @@ export const sendVerificationEmail = async (options: EmailVerificationOptions) = return; } - // Remove trigger=app or trigger%3Dapp from the URL - const link = url.replace('trigger=app', '').replace('trigger%3Dapp', ''); - - await sendEmail('verifyEmail', { + await sendOrgAwareVerifyEmail({ to: user.email, - fields: { - link, - userName: user.name || undefined - } + verificationUrl: url, + userId: user.id, + userName: user.name }); }; @@ -56,12 +75,11 @@ export const sendChangeEmailConfirmation = async (options: ChangeEmailConfirmati const { user, newEmail, url } = options; console.log('\nsendChangeEmailConfirmation', options); - await sendEmail('verifyEmail', { + await sendOrgAwareVerifyEmail({ to: user.email, - fields: { - link: url, - newEmail, - userName: user.name || undefined - } + verificationUrl: url, + userId: user.id, + userName: user.name, + newEmail }); }; diff --git a/packages/db/src/auth/resolve-verification-org.ts b/packages/db/src/auth/resolve-verification-org.ts new file mode 100644 index 00000000..25aa5b27 --- /dev/null +++ b/packages/db/src/auth/resolve-verification-org.ts @@ -0,0 +1,123 @@ +import { TENANT_ROOT_DOMAIN, BRAND_ROOT_DOMAIN } from '@cio/utils/constants/domains'; + +import type { TOrganization } from '../types'; +import { + getFirstOrganization, + getOrganizationByCustomDomain, + getOrganizationByProfileId, + getOrganizationBySiteName +} from '../queries/organization/organization'; + +function extractCallbackUrl(verificationUrl: string): URL | null { + try { + const parsed = new URL(verificationUrl); + const callback = parsed.searchParams.get('callbackURL'); + if (!callback) { + return null; + } + + return new URL(callback); + } catch { + return null; + } +} + +function isTenantSubdomainHost(hostname: string): boolean { + const host = hostname.replace(/^www\./, '').toLowerCase(); + const suffix = `.${TENANT_ROOT_DOMAIN}`; + + return host.endsWith(suffix) && host.length > suffix.length; +} + +function getTenantSiteName(hostname: string): string | null { + const host = hostname.replace(/^www\./, '').toLowerCase(); + const suffix = `.${TENANT_ROOT_DOMAIN}`; + if (!host.endsWith(suffix)) { + return null; + } + + const siteName = host.slice(0, -suffix.length); + if (!siteName || siteName.includes('.')) { + return null; + } + + return siteName; +} + +function isCustomDomainHost(hostname: string): boolean { + const host = hostname.replace(/^www\./, '').toLowerCase(); + if (host.includes('localhost')) { + return false; + } + + if (host === BRAND_ROOT_DOMAIN || host === `app.${BRAND_ROOT_DOMAIN}`) { + return false; + } + + if (isTenantSubdomainHost(host)) { + return false; + } + + return !host.endsWith(`.${BRAND_ROOT_DOMAIN}`); +} + +async function resolveOrgFromCallbackUrl(callbackUrl: URL): Promise { + const orgSiteName = callbackUrl.searchParams.get('org')?.trim(); + if (orgSiteName) { + const org = await getOrganizationBySiteName(orgSiteName); + if (org) { + return org; + } + } + + const hostname = callbackUrl.hostname.toLowerCase(); + + if (isCustomDomainHost(hostname)) { + const org = await getOrganizationByCustomDomain(hostname); + if (org) { + return org; + } + } + + const siteName = getTenantSiteName(hostname); + if (siteName) { + return getOrganizationBySiteName(siteName); + } + + return null; +} + +/** + * Resolves the tenant org that should brand a verification email from the + * Better Auth verification URL's `callbackURL` (where the user started the + * flow), with fallbacks for self-hosted and existing memberships. + */ +export async function resolveVerificationOrg(options: { + verificationUrl: string; + userId?: string; +}): Promise { + const callbackUrl = extractCallbackUrl(options.verificationUrl); + if (callbackUrl) { + const org = await resolveOrgFromCallbackUrl(callbackUrl); + if (org) { + return org; + } + } + + if (process.env.PUBLIC_IS_SELFHOSTED === 'true') { + const org = await getFirstOrganization(); + if (org) { + return org; + } + } + + if (options.userId) { + const memberships = await getOrganizationByProfileId(options.userId); + const org = memberships[0]; + if (org) { + return org; + } + } + + return null; +} diff --git a/packages/email/src/core/base.ts b/packages/email/src/core/base.ts index 99247790..f0e24e2e 100644 --- a/packages/email/src/core/base.ts +++ b/packages/email/src/core/base.ts @@ -16,7 +16,7 @@ export async function sendTemplateEmail( return deliverEmail([ { to: config.to, - subject: config.template.subject, + subject: config.subject ?? config.template.subject, content, from: config.from ?? config.template.from, replyTo: config.replyTo ?? config.template.replyTo, diff --git a/packages/email/src/core/types.ts b/packages/email/src/core/types.ts index ecf906c2..e4442ecc 100644 --- a/packages/email/src/core/types.ts +++ b/packages/email/src/core/types.ts @@ -29,6 +29,8 @@ export interface SendConfig { fields: z.infer; from?: string; replyTo?: string; + /** Override the template's default subject (e.g. org-scoped transactional mail). */ + subject?: string; /** Optional iCalendar (.ics) body delivered as a text/calendar part. */ ics?: string; } diff --git a/packages/email/src/emails/verify-email.ts b/packages/email/src/emails/verify-email.ts index e8696068..29a06e81 100644 --- a/packages/email/src/emails/verify-email.ts +++ b/packages/email/src/emails/verify-email.ts @@ -2,6 +2,7 @@ import * as z from 'zod'; import { defineEmail } from '../send'; import { getDefaultTemplate } from '../templates'; +import { ZEmailBranding } from '../core/branding'; export const verifyEmailEmail = defineEmail({ id: 'verifyEmail', @@ -9,11 +10,14 @@ export const verifyEmailEmail = defineEmail({ schema: z.object({ link: z.url(), newEmail: z.email().optional(), - userName: z.string().optional() + userName: z.string().optional(), + orgName: z.string().min(1), + branding: ZEmailBranding }), render: (fields) => { const isEmailChange = !!fields.newEmail; const userName = fields.userName || 'there'; + const orgName = fields.orgName; const content = isEmailChange ? ` @@ -27,14 +31,13 @@ export const verifyEmailEmail = defineEmail({ ` : `

Hey ${userName} 👋

-

Welcome to ClassroomIO! In order to get your account ready for usage, we need to verify your email.

-

We do this to make sure we don't get fake user emails in our signup. To get the best out of our product, we'll need you to verify your email by clicking the Verify button below -

+

Welcome to ${orgName}! In order to get your account ready for usage, we need to verify your email.

+

We do this to make sure we don't get fake user emails in our signup. To get the best out of our product, we'll need you to verify your email by clicking the Verify button below.

`; - return getDefaultTemplate(content); + return getDefaultTemplate(content, fields.branding); } }); diff --git a/packages/email/src/send.ts b/packages/email/src/send.ts index 714b4263..144a67ea 100644 --- a/packages/email/src/send.ts +++ b/packages/email/src/send.ts @@ -95,6 +95,7 @@ export async function sendEmail( fields: validatedFields, from: config.from ?? template.from, replyTo: config.replyTo ?? template.replyTo, + subject: config.subject, ics: config.ics }); } catch (error) { diff --git a/packages/storybook/src/templates/email/fixtures.ts b/packages/storybook/src/templates/email/fixtures.ts index c26023aa..de23f487 100644 --- a/packages/storybook/src/templates/email/fixtures.ts +++ b/packages/storybook/src/templates/email/fixtures.ts @@ -4,7 +4,7 @@ import { getDefaultTemplate } from '@cio/email/templates'; const VERIFY_EMAIL_CONTENT = `

Hey there 👋

-

Welcome to ClassroomIO! In order to get your account ready for usage, we need to verify your email.

+

Welcome to Acme Coding School! In order to get your account ready for usage, we need to verify your email.

We do this to make sure we don't get fake user emails in our signup. To get the best out of our product, we'll need you to verify your email by clicking the Verify button below.

`; @@ -59,7 +59,11 @@ const STUDENT_JOINED_CONTENT = ` // ─── Exported email HTML strings ────────────────────────────────────────── -export const verifyEmailHtml = getDefaultTemplate(VERIFY_EMAIL_CONTENT); +export const verifyEmailHtml = getDefaultTemplate(VERIFY_EMAIL_CONTENT, { + orgName: 'Acme Coding School', + logoUrl: 'https://placehold.co/120x40/png?text=Acme', + themeColor: '#1d4ed8' +}); export const forgotPasswordHtml = getDefaultTemplate(FORGOT_PASSWORD_CONTENT); export const welcomeHtml = getDefaultTemplate(WELCOME_CONTENT); export const courseWelcomeHtml = getDefaultTemplate(COURSE_WELCOME_CONTENT); From 5de1f51e76f35dc3ff8534b1ccc9284744c44338 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 04:51:30 +0000 Subject: [PATCH 3/4] fix: address PR review comments on enroll and verify email - Revert org-site layout 404 bypass; custom domain org lookup uses hostname - Remove unnecessary comment in enroll redirect helper - Restore email verification flow documentation - Use generic welcome copy when no org branding is resolved Co-authored-by: Rotimi Best --- .../src/routes/(org-site)/+layout.server.ts | 11 ++--------- .../course/[slug]/enroll/+page.svelte | 2 -- packages/db/src/auth/email-verification.ts | 18 ++++++++++++++++++ packages/email/src/emails/verify-email.ts | 7 +++++-- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/src/routes/(org-site)/+layout.server.ts b/apps/dashboard/src/routes/(org-site)/+layout.server.ts index d634b1ae..d1079dcf 100644 --- a/apps/dashboard/src/routes/(org-site)/+layout.server.ts +++ b/apps/dashboard/src/routes/(org-site)/+layout.server.ts @@ -1,18 +1,11 @@ -import { PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES } from '$lib/utils/constants/routes'; import { error } from '@sveltejs/kit'; -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 }) => { +export const load = async ({ parent }) => { 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. - // 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)) { + if (isOrgSite && !org) { throw error(404, 'Organization not found'); } diff --git a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte index 8be53027..29b83edf 100644 --- a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte +++ b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte @@ -28,8 +28,6 @@ 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'; } diff --git a/packages/db/src/auth/email-verification.ts b/packages/db/src/auth/email-verification.ts index 9e2fb595..309ba51c 100644 --- a/packages/db/src/auth/email-verification.ts +++ b/packages/db/src/auth/email-verification.ts @@ -11,10 +11,28 @@ type ChangeEmailConfirmationOptions = Parameters< NonNullable['changeEmail']>['sendChangeEmailConfirmation']> >[0]; +/** + * Email verification flow. + * + * 1. user says I want to change my email from email@old.com to email@new.com + * 2. we send an email to email@old.com asking user to verify if they really want to change their email to email@new.com `sendChangeEmailConfirmation()`. [better-auth]. + * 3. user clicks the link in the email + * 4. we update the email in the database. [better-auth]. + * 5. we send a confirmation email to email@new.com `sendVerificationEmail()`. [dashboard]. + * 6. user clicks the link in the email + * 7. we mark email@new.com as verified. [better-auth]. + */ + function stripTriggerParam(url: string): string { return url.replace('trigger=app', '').replace('trigger%3Dapp', ''); } +/** + * Brands verification mail with the org from `resolveVerificationOrg` (callback URL + * host/?org=, self-hosted first org, then the user's first membership). When no + * org applies — e.g. dashboard signup before onboarding creates an org — copy + * and masthead fall back to ClassroomIO defaults. + */ async function sendOrgAwareVerifyEmail(options: { to: string; verificationUrl: string; diff --git a/packages/email/src/emails/verify-email.ts b/packages/email/src/emails/verify-email.ts index 29a06e81..fa358756 100644 --- a/packages/email/src/emails/verify-email.ts +++ b/packages/email/src/emails/verify-email.ts @@ -17,7 +17,10 @@ export const verifyEmailEmail = defineEmail({ render: (fields) => { const isEmailChange = !!fields.newEmail; const userName = fields.userName || 'there'; - const orgName = fields.orgName; + const hasOrgBranding = Boolean(fields.branding?.orgName); + const welcomeLine = hasOrgBranding + ? `Welcome to ${fields.orgName}! In order to get your account ready for usage, we need to verify your email.` + : 'Welcome! In order to get your account ready for usage, we need to verify your email.'; const content = isEmailChange ? ` @@ -31,7 +34,7 @@ export const verifyEmailEmail = defineEmail({ ` : `

Hey ${userName} 👋

-

Welcome to ${orgName}! In order to get your account ready for usage, we need to verify your email.

+

${welcomeLine}

We do this to make sure we don't get fake user emails in our signup. To get the best out of our product, we'll need you to verify your email by clicking the Verify button below.

Verify From 9cedaf815390aa59a43aa86cba31b836eb562be9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 05:51:36 +0000 Subject: [PATCH 4/4] fix(dashboard): address PR feedback on enroll load and copy Remove unnecessary getApiKeyHeaders from public course-by-slug fetch. Shorten enrolled_verify_email copy, fix ru.json duplicate keys, and translate enroll verification strings in non-English locales. Co-authored-by: Rotimi Best --- apps/dashboard/src/lib/utils/translations/da.json | 4 ++-- apps/dashboard/src/lib/utils/translations/de.json | 4 ++-- apps/dashboard/src/lib/utils/translations/en.json | 2 +- apps/dashboard/src/lib/utils/translations/es.json | 4 ++-- apps/dashboard/src/lib/utils/translations/fr.json | 4 ++-- apps/dashboard/src/lib/utils/translations/hi.json | 4 ++-- apps/dashboard/src/lib/utils/translations/pl.json | 4 ++-- apps/dashboard/src/lib/utils/translations/pt.json | 4 ++-- apps/dashboard/src/lib/utils/translations/ru.json | 6 ++---- apps/dashboard/src/lib/utils/translations/vi.json | 4 ++-- .../(org-site)/course/[slug]/enroll/+page.server.ts | 9 +++------ 11 files changed, 22 insertions(+), 27 deletions(-) diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index 8106aa1d..dca5794c 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -893,8 +893,8 @@ "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", - "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": "Du er tilmeldt dette kursus.", + "enrolled_verify_email": "Du er tilmeldt! Bekræft din e-mail for at få adgang til dette kursus." }, "pricing_section": { "free": "Gratis", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 500b863e..8ece8bd7 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -893,8 +893,8 @@ "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", - "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": "Sie sind für diesen Kurs eingeschrieben.", + "enrolled_verify_email": "Sie sind eingeschrieben! Bestätigen Sie Ihre E-Mail, um auf diesen Kurs zuzugreifen." }, "pricing_section": { "free": "Kostenlos", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index 54c61441..97bcf398 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -3373,7 +3373,7 @@ "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_verify_email": "You're enrolled! Verify your email to access this course." }, "course_content": "Course content", "modules": "modules", diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 8da7c7f5..93254f28 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -893,8 +893,8 @@ "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", - "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": "Estás inscrito en este curso.", + "enrolled_verify_email": "¡Estás inscrito! Verifica tu correo electrónico para acceder a este curso." }, "pricing_section": { "free": "Gratis", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 4b443884..9723cfd5 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -893,8 +893,8 @@ "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", - "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": "Vous êtes inscrit à ce cours.", + "enrolled_verify_email": "Vous êtes inscrit ! Vérifiez votre e-mail pour accéder à ce cours." }, "pricing_section": { "free": "Gratuit", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 951ad311..b0382f36 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -893,8 +893,8 @@ "requires_payment_or_invite": "आपको इस पाठ्यक्रम के लिए भुगतान करना होगा या प्रवेश पाने के लिए आमंत्रित किया जाना होगा।", "back_to_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": "आप इस पाठ्यक्रम में नामांकित हैं।", + "enrolled_verify_email": "आप नामांकित हैं! इस पाठ्यक्रम तक पहुँचने के लिए अपना ईमेल सत्यापित करें।" }, "pricing_section": { "free": "निःशुल्क", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index 52108c7a..af13b44b 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -893,8 +893,8 @@ "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", - "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": "Zapisano Cię na ten kurs.", + "enrolled_verify_email": "Jesteś zapisany! Zweryfikuj swój e-mail, aby uzyskać dostęp do tego kursu." }, "pricing_section": { "free": "Bezpłatny", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index b0fe2a03..661512b5 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -893,8 +893,8 @@ "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", - "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": "Você está inscrito neste curso.", + "enrolled_verify_email": "Você está inscrito! Verifique seu e-mail para acessar este curso." }, "pricing_section": { "free": "Grátis", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 01afd4bb..c791f0bb 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -893,10 +893,8 @@ "requires_payment_or_invite": "Вам нужно заплатить за этот курс или получить приглашение для получения доступа.", "back_to_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." + "enrolled_success": "Вы записаны на этот курс.", + "enrolled_verify_email": "Вы записаны! Подтвердите email, чтобы получить доступ к курсу." }, "pricing_section": { "free": "Бесплатно", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 2d28e592..a1d290f3 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -893,8 +893,8 @@ "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", - "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": "Bạn đã ghi danh khóa học này.", + "enrolled_verify_email": "Bạn đã ghi danh! Xác minh email để truy cập khóa học này." }, "pricing_section": { "free": "miễn phí", diff --git a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts index c1a11a1f..635f1b3a 100644 --- a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts +++ b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts @@ -56,12 +56,9 @@ export const load = async ({ params, url }) => { } const courseResult = await safeServerApi(() => - classroomio.course.slug[':slug'].$get( - { - param: { slug } - }, - getApiKeyHeaders() - ) + classroomio.course.slug[':slug'].$get({ + param: { slug } + }) ); if (!courseResult.ok || !courseResult.body.data) {