Files
classroomio/packages/email
Rotimi Best c7854efae3 feat: redo settings and all org pages. (#601)
* feat: new components, deploy storybook

* refactor: simplify component field definitions and enhance button loading state

- Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook.
- Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props.
- Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback.
- Minor CSS adjustments for better layout and styling consistency.

* chore: add field, card, password components

* feat: improve page component & update AuthUI path

* chore: update bits-ui to version 2.14.4 and all org related UI's

- Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4.
- Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory.
- Removed unused Course components and related files to clean up the codebase.
- Enhanced various components to improve consistency and maintainability.

* chore: move all setting buttons to the top and update auth ui styles

* chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner

- Removed all instances of svelte-loading-spinners from the project.
- Updated components to use the new Spinner from @cio/ui/base/spinner.
- Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners.
- Added new CSS animations for rocket launch effects in app.css.
- Enhanced email change flow with updated verification messages in translations.

* chore: remove supabase file upload from dashboard and move to r2

* chore: update organization API and UI components

- Added new API methods for organization plan operations and team management.
- Introduced middleware for API key authentication and organization admin checks.
- Refactored organization-related components to utilize the new API structure.
- Updated various UI components to improve consistency and maintainability.
- Enhanced error handling and validation for organization-related actions.
- Removed deprecated services and streamlined the codebase.
2025-12-02 09:54:38 +01:00
..
2025-11-25 17:18:15 +00:00

@cio/emails

Overview

A unified email system using a single send() function that accepts an email ID string with full runtime validation via Zod schemas. All emails are defined declaratively with automatic registration.

Key Features

  • Minimal imports: Single import { send } from '@cio/email'
  • Type safety: Type-safe email IDs with autocomplete (TypeScript enforces valid IDs)
  • Runtime validation: Zod schemas validate fields at runtime
  • Auto-registration: Emails are automatically registered when defined
  • Discoverability: All emails accessible via string ID
  • Consistency: Unified pattern for all email templates
  • Validation: Automatic Zod validation of fields with helpful error messages

Usage

Sending an Email

import { sendEmail } from '@cio/email';

await send('forgotPassword', {
  to: 'user@example.com',
  fields: {
    name: 'John Doe',
    email: 'user@example.com',
    link: 'https://app.classroomio.com/reset?token=abc123'
  }
});

With Overrides

await send('forgotPassword', {
  to: 'user@example.com',
  fields: {
    name: 'John Doe',
    email: 'user@example.com',
    link: 'https://app.classroomio.com/reset?token=abc123'
  },
  from: '"Support Team" <support@classroomio.com>',
  replyTo: 'help@classroomio.com'
});

Creating Email Templates

Basic Example

// packages/email/src/emails/forgot-password.ts
import { z } from 'zod';
import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';

export const forgotPasswordEmail = defineEmail({
  id: 'forgotPassword',
  subject: 'Password reset notification - ClassroomIO',
  schema: z.object({
    name: z.string().min(1),
    email: z.string().email(),
    link: z.url()
  }),
  render: (fields) => {
    const content = `Hello ${fields.name},
    <p>You are receiving this email because you have requested a password reset.</p>
    <div>
      <a class="button" href="${fields.link}">Reset my password</a>
    </div>`;

    return getDefaultTemplate(content);
  }
});

TypeScript will provide autocomplete for valid email IDs and show errors for invalid ones. The EmailId type is defined in utils/types.ts based on the EMAIL_IDS constant in utils/constants.ts.

Note: When adding new emails, update both:

  1. The EMAIL_IDS array in utils/constants.ts
  2. The EmailSchemas type mapping in emails/index.ts

This ensures full type safety and schema inference.

Error Handling

The send() function automatically validates fields against the email's schema. If validation fails, a helpful error is thrown:

try {
  await send('forgotPassword', {
    to: 'user@example.com',
    fields: {
      name: '', // Invalid: empty string
      link: 'not-a-url' // Invalid: not a URL
    }
  });
} catch (error) {
  // Error: Invalid fields for email "forgotPassword": name: String must contain at least 1 character(s), link: Invalid url
}

If an email ID doesn't exist:

try {
  await send('nonexistentEmail', { ... });
} catch (error) {
  // Error: Email template "nonexistentEmail" not found. Available emails: forgotPassword, welcome, verifyEmail
}

Architecture

Core Components

  • core/types.ts: Type definitions (EmailTemplate, SendConfig, etc.)
  • core/registry.ts: Email registry that stores all templates
  • core/base.ts: Base send function that validates and sends
  • send.ts: defineEmail() and send() functions
  • utils/constants.ts: EMAIL_IDS constant array with all email IDs
  • utils/types.ts: EmailId type derived from EMAIL_IDS for type safety, and EmailSchemaFor conditional type
  • emails/*.ts: Individual email definitions
  • emails/index.ts: Auto-exports all emails for registration and defines EmailSchemas type mapping
  • index.ts: Main export point

Flow

  1. Email definitions use defineEmail() which automatically registers them
  2. All emails are imported via emails/index.ts when the package is loaded
  3. send() looks up the email by ID, validates fields, and sends via existing infrastructure
  4. Existing deliverEmail() function from send.ts is reused for actual delivery

Adding New Emails

When adding a new email template:

  1. Create a new email file in emails/ directory using defineEmail()
  2. Export it from emails/index.ts
  3. Important: Add the email ID to the EMAIL_IDS array in utils/constants.ts
  4. Important: Add the schema mapping to EmailSchemas type in emails/index.ts for type inference

Example:

// 1. Create emails/new-email.ts
export const newEmail = defineEmail({
  id: 'newEmail',
  subject: 'New Email',
  schema: z.object({
    name: z.string(),
    code: z.string()
  }),
  render: (fields) => { ... }
});

// 2. Export from emails/index.ts
export * from './new-email';

// Import the email definition for type mapping
import type { newEmail } from './new-email';

// Add to EmailSchemas type mapping in emails/index.ts
export type EmailSchemas = {
  forgotPassword: typeof forgotPasswordEmail.template.schema;
  welcome: typeof welcomeEmail.template.schema;
  verifyEmail: typeof verifyEmailEmail.template.schema;
  newEmail: typeof newEmail.template.schema;  // ← Add here for type inference
};

// 3. Add to utils/constants.ts
export const EMAIL_IDS = [
  'forgotPassword',
  'welcome',
  'verifyEmail',
  'newEmail'  // ← Add here for type safety
] as const;