From 6c3a982daff21d78eeed3fb5217d30949c2c19cf Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Sun, 19 Jul 2026 12:02:11 +0100 Subject: [PATCH] feat(course): auto-enrol button for paid course join requests (#769) * feat(course): add auto-enrol flow for paid course join requests - Extend teacher buy-request email with manual access steps and an Auto Enrol this Student button - Link teachers to the course People page with grantAccess query param - Add grant-access modal that sends the same org audience invite used by the Invite new students flow Co-authored-by: Rotimi Best * fix(i18n): translate grant access modal strings Replace English placeholders copied into non-en locale files with proper translations for each supported language. Co-authored-by: Rotimi Best * fix(email): shorten auto-enrol button label per review Co-authored-by: Rotimi Best * fix(email): use American spelling Enroll in auto-enroll CTA Co-authored-by: Rotimi Best * fix(i18n): localize Danish grant-access send invite label Co-authored-by: Rotimi Best --------- Co-authored-by: Rotimi Best --- .../src/services/course/payment-request.ts | 12 +++ .../people/grant-access-modal.svelte | 91 +++++++++++++++++++ .../lib/features/course/pages/people.svelte | 2 + .../src/lib/utils/translations/da.json | 6 ++ .../src/lib/utils/translations/de.json | 6 ++ .../src/lib/utils/translations/en.json | 6 ++ .../src/lib/utils/translations/es.json | 6 ++ .../src/lib/utils/translations/fr.json | 6 ++ .../src/lib/utils/translations/hi.json | 6 ++ .../src/lib/utils/translations/pl.json | 6 ++ .../src/lib/utils/translations/pt.json | 6 ++ .../src/lib/utils/translations/ru.json | 6 ++ .../src/lib/utils/translations/vi.json | 6 ++ .../src/emails/teacher-student-buy-request.ts | 15 +++ 14 files changed, 180 insertions(+) create mode 100644 apps/dashboard/src/lib/features/course/components/people/grant-access-modal.svelte diff --git a/apps/api/src/services/course/payment-request.ts b/apps/api/src/services/course/payment-request.ts index 828a642d..ffe1ac45 100644 --- a/apps/api/src/services/course/payment-request.ts +++ b/apps/api/src/services/course/payment-request.ts @@ -5,6 +5,7 @@ import { getCourseWithOrgData } from '@cio/db/queries/course'; import { buildEmailFromName, buildEmailBranding } from '@cio/email'; import { enqueueTransactionalEmail } from '@api/services/jobs'; import { trackServerEvent, SERVER_EVENTS } from '@cio/analytics'; +import { getDashboardBaseUrl } from '@cio/core/config/dashboard-url'; export interface PaymentRequestData { courseId: string; @@ -47,6 +48,15 @@ export async function createPaymentRequest(data: PaymentRequestData) { const teacherEmail = teacherResult[0].email; + const dashboardOrg = { + siteName: course.orgSiteName, + customDomain: course.orgCustomDomain, + isCustomDomainVerified: course.orgIsCustomDomainVerified + }; + const dashboardBaseUrl = getDashboardBaseUrl(dashboardOrg); + const courseUrl = `${dashboardBaseUrl}/courses/${data.courseId}`; + const autoEnrollUrl = `${dashboardBaseUrl}/courses/${data.courseId}/people?grantAccess=${encodeURIComponent(data.studentEmail)}`; + try { await enqueueTransactionalEmail('teacherStudentBuyRequest', { to: teacherEmail, @@ -54,6 +64,8 @@ export async function createPaymentRequest(data: PaymentRequestData) { courseName, studentEmail: data.studentEmail, studentFullname: data.studentFullname, + courseUrl, + autoEnrollUrl, branding }, from: buildEmailFromName('ClassroomIO'), diff --git a/apps/dashboard/src/lib/features/course/components/people/grant-access-modal.svelte b/apps/dashboard/src/lib/features/course/components/people/grant-access-modal.svelte new file mode 100644 index 00000000..6fa366e2 --- /dev/null +++ b/apps/dashboard/src/lib/features/course/components/people/grant-access-modal.svelte @@ -0,0 +1,91 @@ + + + { + if (!open) closeModal(); + }} +> + + + {$t('course.navItem.people.grant_access_modal.title')} + + {$t('course.navItem.people.grant_access_modal.description', { email: studentEmail })} + + + + {#if $isStudentLimitReached} + + {$t('course.navItem.people.invite_modal.student_limit_reached')} + + {/if} + +
+ + +
+
+
diff --git a/apps/dashboard/src/lib/features/course/pages/people.svelte b/apps/dashboard/src/lib/features/course/pages/people.svelte index 72dea963..01df058e 100644 --- a/apps/dashboard/src/lib/features/course/pages/people.svelte +++ b/apps/dashboard/src/lib/features/course/pages/people.svelte @@ -11,6 +11,7 @@ import * as Avatar from '@cio/ui/base/avatar'; import { ComingSoon, RoleBasedSecurity, UpgradeBanner } from '$features/ui'; import InvitationModal from '$features/course/components/people/invitation-modal.svelte'; + import GrantAccessModal from '$features/course/components/people/grant-access-modal.svelte'; import DeleteConfirmation from '$features/course/components/people/delete-confirmation.svelte'; import { isStudentLimitReached } from '$lib/utils/store/org'; @@ -101,6 +102,7 @@ + {#if $isStudentLimitReached} {$t('course.navItem.people.invite_modal.student_limit_reached')} diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index a527097c..2a7e489e 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Giv studerende adgang", + "description": "Giv {email} adgang til dette kursus. Vi sender en invitationsmail, så vedkommende kan tilslutte sig din organisation og tilmelde sig kurset.", + "cancel": "Annuller", + "send_invite": "Send invitationen" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index efeee7ef..7fa00a98 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Schülerzugang gewähren", + "description": "Gewähren Sie {email} Zugang zu diesem Kurs. Wir senden eine Einladungs-E-Mail, damit die Person Ihrer Organisation beitreten und in diesem Kurs eingeschrieben werden kann.", + "cancel": "Abbrechen", + "send_invite": "Einladung senden" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index e2db9225..c7011c22 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -3230,6 +3230,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Give student access", + "description": "Give {email} access to this course. We'll send them an invite email to join your organization and enroll in this course.", + "cancel": "Cancel", + "send_invite": "Send invite" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 5b566801..612b2737 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Dar acceso al estudiante", + "description": "Da acceso a {email} a este curso. Le enviaremos un correo de invitación para unirse a tu organización e inscribirse en este curso.", + "cancel": "Cancelar", + "send_invite": "Enviar invitación" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index abbafcd4..105484e1 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Accorder l'accès à l'étudiant", + "description": "Accordez à {email} l'accès à ce cours. Nous lui enverrons un e-mail d'invitation pour rejoindre votre organisation et s'inscrire à ce cours.", + "cancel": "Annuler", + "send_invite": "Envoyer l'invitation" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 7e4be06f..db086fbd 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "छात्र को एक्सेस दें", + "description": "{email} को इस कोर्स का एक्सेस दें। हम उन्हें आपके संगठन में शामिल होने और इस कोर्स में नामांकन के लिए आमंत्रण ईमेल भेजेंगे।", + "cancel": "रद्द करें", + "send_invite": "निमंत्रण भेजें" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index e718ecf9..e43fa495 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Udostępnij dostęp studentowi", + "description": "Przyznaj {email} dostęp do tego kursu. Wyślemy zaproszenie e-mailem, aby dołączył do Twojej organizacji i zapisał się na ten kurs.", + "cancel": "Anuluj", + "send_invite": "Wyślij zaproszenie" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 01036a1b..78acff4c 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Conceder acesso ao aluno", + "description": "Conceda acesso a {email} a este curso. Enviaremos um e-mail de convite para que a pessoa entre na sua organização e se inscreva neste curso.", + "cancel": "Cancelar", + "send_invite": "Enviar convite" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index a6a7f56d..91df12ad 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Предоставить доступ студенту", + "description": "Предоставьте {email} доступ к этому курсу. Мы отправим приглашение по электронной почте, чтобы присоединиться к вашей организации и записаться на курс.", + "cancel": "Отмена", + "send_invite": "Отправить приглашение" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 3cedeee2..2c2a0e34 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -1198,6 +1198,12 @@ "no_matching_existing_students": "No organization students match your search.", "notify_existing_students": "Send course access email", "grant_access": "Grant Access", + "grant_access_modal": { + "title": "Cấp quyền truy cập cho học viên", + "description": "Cấp cho {email} quyền truy cập khóa học này. Chúng tôi sẽ gửi email mời để tham gia tổ chức của bạn và đăng ký khóa học.", + "cancel": "Hủy", + "send_invite": "Gửi lời mời" + }, "invite_new_students_title": "Invite new students", "invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.", "invite_new_students": "Invite Students", diff --git a/packages/email/src/emails/teacher-student-buy-request.ts b/packages/email/src/emails/teacher-student-buy-request.ts index a82202b5..fb5c192d 100644 --- a/packages/email/src/emails/teacher-student-buy-request.ts +++ b/packages/email/src/emails/teacher-student-buy-request.ts @@ -11,6 +11,8 @@ export const teacherStudentBuyRequestEmail = defineEmail({ courseName: z.string().min(1), studentEmail: z.string().email(), studentFullname: z.string().min(1), + courseUrl: z.url(), + autoEnrollUrl: z.url(), branding: ZEmailBranding }), render: (fields) => { @@ -22,6 +24,19 @@ export const teacherStudentBuyRequestEmail = defineEmail({ Name: ${fields.studentFullname}
Email: ${fields.studentEmail}

+

How to give this student access

+
    +
  1. Open the course: ${fields.courseUrl}
  2. +
  3. Click People on the sidebar
  4. +
  5. Click Add
  6. +
  7. Under Invite new students, enter ${fields.studentEmail}
  8. +
  9. Click Invite Students
  10. +
+

Or automatically enroll them

+

Use the button below to open the course and send this student an invite in one step.

+ `; return getDefaultTemplate(content, fields.branding);