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 <rotimi-best@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-10 04:32:11 +00:00
co-authored by Rotimi Best
parent f262a092ed
commit a6d06f356f
7 changed files with 185 additions and 34 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ export async function sendTemplateEmail<TSchema extends z.ZodType>(
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,
+2
View File
@@ -29,6 +29,8 @@ export interface SendConfig<TSchema extends z.ZodType = z.ZodType> {
fields: z.infer<TSchema>;
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;
}
+8 -5
View File
@@ -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({
`
: `
<p><strong>Hey ${userName} 👋</strong></p>
<p>Welcome to ClassroomIO! In order to get your account ready for usage, we need to verify your email. </p>
<p>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 <strong>Verify</strong> button below
</p>
<p>Welcome to ${orgName}! In order to get your account ready for usage, we need to verify your email.</p>
<p>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 <strong>Verify</strong> button below.</p>
<div>
<a class="button" href="${fields.link}">Verify</a>
</div>
`;
return getDefaultTemplate(content);
return getDefaultTemplate(content, fields.branding);
}
});
+1
View File
@@ -95,6 +95,7 @@ export async function sendEmail<TEmailId extends EmailId>(
fields: validatedFields,
from: config.from ?? template.from,
replyTo: config.replyTo ?? template.replyTo,
subject: config.subject,
ics: config.ics
});
} catch (error) {