@cms0/transactional 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ # Plunk API Key
2
+ # Get your API key from https://app.useplunk.com/settings/api-keys
3
+ PLUNK_API_KEY=sk_your_api_key_here
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # @cms0/transactional
2
+
3
+ Transactional email package using [Plunk](https://useplunk.com) and [React Email](https://react.email).
4
+
5
+ ## Installation
6
+
7
+ This package is part of the workspace. Install dependencies from the root:
8
+
9
+ ```bash
10
+ pnpm install
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ Set your Plunk API key as an environment variable:
16
+
17
+ ```bash
18
+ PLUNK_API_KEY=sk_your_api_key_here
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Import and use the email sending functions:
24
+
25
+ ```typescript
26
+ import {
27
+ sendTeamInvite,
28
+ sendResetPassword,
29
+ } from '@cms0/transactional';
30
+
31
+ // Send a team invitation
32
+ await sendTeamInvite('user@example.com', {
33
+ teamName: 'Engineering Team',
34
+ inviterName: 'John Doe',
35
+ inviteUrl: 'https://app.example.com/accept-invite?token=xyz',
36
+ recipientEmail: 'user@example.com',
37
+ });
38
+
39
+ // Send a password reset email
40
+ await sendResetPassword('user@example.com', {
41
+ resetUrl: 'https://app.example.com/reset-password?token=xyz',
42
+ userName: 'John', // optional
43
+ });
44
+ ```
45
+
46
+ ### Custom Options
47
+
48
+ All send functions accept an optional third parameter for custom options:
49
+
50
+ ```typescript
51
+ await sendTeamInvite(
52
+ 'user@example.com',
53
+ {
54
+ teamName: 'Engineering Team',
55
+ inviterName: 'John Doe',
56
+ inviteUrl: 'https://app.example.com/accept-invite?token=xyz',
57
+ recipientEmail: 'user@example.com',
58
+ },
59
+ {
60
+ from: { name: 'Support Team', email: 'support@example.com' },
61
+ replyTo: 'support@example.com',
62
+ headers: {
63
+ 'X-Custom-Header': 'value',
64
+ },
65
+ }
66
+ );
67
+ ```
68
+
69
+ ## Development
70
+
71
+ ### Preview Emails
72
+
73
+ Start the React Email development server to preview all email templates:
74
+
75
+ ```bash
76
+ cd packages/transactional
77
+ pnpm dev
78
+ ```
79
+
80
+ Then open [http://localhost:3000](http://localhost:3000) to preview your emails.
81
+
82
+ ### Build
83
+
84
+ Compile TypeScript to JavaScript:
85
+
86
+ ```bash
87
+ pnpm build
88
+ ```
89
+
90
+ ## Email Templates
91
+
92
+ This package includes the following email templates:
93
+
94
+ 1. **team-invite** - Invite users to join a team
95
+ 2. **reset-password** - Password reset instructions
96
+
97
+ All templates are built using React Email components and are located in the `emails/` directory.
98
+
99
+ ## API Reference
100
+
101
+ ### `sendTeamInvite(to, props, options?)`
102
+
103
+ Send a team invitation email.
104
+
105
+ **Parameters:**
106
+ - `to` (string): Recipient email address
107
+ - `props` (TeamInviteProps):
108
+ - `teamName` (string): Name of the team
109
+ - `inviterName` (string): Name of the person sending the invite
110
+ - `inviteUrl` (string): URL to accept the invitation
111
+ - `recipientEmail` (string): Email address of the recipient
112
+ - `options` (SendEmailOptions, optional): Custom email options
113
+
114
+ ### `sendResetPassword(to, props, options?)`
115
+
116
+ Send a password reset email.
117
+
118
+ **Parameters:**
119
+ - `to` (string): Recipient email address
120
+ - `props` (ResetPasswordProps):
121
+ - `resetUrl` (string): URL to reset password
122
+ - `userName` (string, optional): Name of the user
123
+ - `options` (SendEmailOptions, optional): Custom email options
124
+
125
+ ## Advanced Usage
126
+
127
+ ### Custom Plunk Client
128
+
129
+ You can create and set a custom Plunk client:
130
+
131
+ ```typescript
132
+ import { PlunkClient, setDefaultClient } from '@cms0/transactional';
133
+
134
+ const customClient = new PlunkClient('sk_custom_api_key');
135
+ setDefaultClient(customClient);
136
+ ```
@@ -0,0 +1,107 @@
1
+ import * as React from "react";
2
+ import {
3
+ Html,
4
+ Head,
5
+ Body,
6
+ Container,
7
+ Section,
8
+ Text,
9
+ Button,
10
+ Hr,
11
+ } from "@react-email/components";
12
+
13
+ interface ResetPasswordEmailProps {
14
+ resetUrl: string;
15
+ userName?: string;
16
+ }
17
+
18
+ export default function ResetPasswordEmail({
19
+ resetUrl = "https://example.com/reset-password",
20
+ userName = "there",
21
+ }: ResetPasswordEmailProps) {
22
+ return (
23
+ <Html lang="en">
24
+ <Head />
25
+ <Body style={main}>
26
+ <Container style={container}>
27
+ <Section style={content}>
28
+ <Text style={heading}>Reset your password</Text>
29
+ <Text style={paragraph}>Hi {userName},</Text>
30
+ <Text style={paragraph}>
31
+ We received a request to reset your password. Click the button below to create a new
32
+ password:
33
+ </Text>
34
+ <Button href={resetUrl} style={button}>
35
+ Reset Password
36
+ </Button>
37
+ <Hr style={hr} />
38
+ <Text style={paragraph}>
39
+ This link will expire in 1 hour for security reasons.
40
+ </Text>
41
+ <Text style={footer}>
42
+ If you didn't request a password reset, you can safely ignore this email. Your
43
+ password will not be changed.
44
+ </Text>
45
+ </Section>
46
+ </Container>
47
+ </Body>
48
+ </Html>
49
+ );
50
+ }
51
+
52
+ const main = {
53
+ backgroundColor: "#f6f9fc",
54
+ fontFamily:
55
+ '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
56
+ };
57
+
58
+ const container = {
59
+ backgroundColor: "#ffffff",
60
+ margin: "0 auto",
61
+ padding: "20px 0 48px",
62
+ marginBottom: "64px",
63
+ };
64
+
65
+ const content = {
66
+ padding: "0 48px",
67
+ };
68
+
69
+ const heading = {
70
+ fontSize: "32px",
71
+ lineHeight: "1.3",
72
+ fontWeight: "700",
73
+ color: "#484848",
74
+ padding: "17px 0 0",
75
+ };
76
+
77
+ const paragraph = {
78
+ margin: "0 0 15px",
79
+ fontSize: "15px",
80
+ lineHeight: "1.4",
81
+ color: "#3c4149",
82
+ };
83
+
84
+ const button = {
85
+ backgroundColor: "#000000",
86
+ borderRadius: "5px",
87
+ color: "#fff",
88
+ fontSize: "16px",
89
+ fontWeight: "bold",
90
+ textDecoration: "none",
91
+ textAlign: "center" as const,
92
+ display: "block",
93
+ width: "100%",
94
+ padding: "12px",
95
+ margin: "20px 0",
96
+ };
97
+
98
+ const hr = {
99
+ borderColor: "#dfe1e4",
100
+ margin: "42px 0 26px",
101
+ };
102
+
103
+ const footer = {
104
+ color: "#8898aa",
105
+ fontSize: "12px",
106
+ lineHeight: "16px",
107
+ };
@@ -0,0 +1,109 @@
1
+ import * as React from "react";
2
+ import {
3
+ Html,
4
+ Head,
5
+ Body,
6
+ Container,
7
+ Section,
8
+ Text,
9
+ Button,
10
+ Hr,
11
+ } from "@react-email/components";
12
+
13
+ interface TeamInviteEmailProps {
14
+ teamName: string;
15
+ inviterName: string;
16
+ inviteUrl: string;
17
+ recipientEmail: string;
18
+ }
19
+
20
+ export default function TeamInviteEmail({
21
+ teamName = "Your Team",
22
+ inviterName = "A team member",
23
+ inviteUrl = "https://example.com/accept-invite",
24
+ recipientEmail = "user@example.com",
25
+ }: TeamInviteEmailProps) {
26
+ return (
27
+ <Html lang="en">
28
+ <Head />
29
+ <Body style={main}>
30
+ <Container style={container}>
31
+ <Section style={content}>
32
+ <Text style={heading}>You've been invited to join a team</Text>
33
+ <Text style={paragraph}>
34
+ {inviterName} has invited you to join <strong>{teamName}</strong>.
35
+ </Text>
36
+ <Text style={paragraph}>
37
+ Click the button below to accept the invitation and join the team:
38
+ </Text>
39
+ <Button href={inviteUrl} style={button}>
40
+ Accept Invitation
41
+ </Button>
42
+ <Hr style={hr} />
43
+ <Text style={footer}>
44
+ This invitation was sent to {recipientEmail}. If you weren't expecting this
45
+ invitation, you can safely ignore this email.
46
+ </Text>
47
+ </Section>
48
+ </Container>
49
+ </Body>
50
+ </Html>
51
+ );
52
+ }
53
+
54
+ const main = {
55
+ backgroundColor: "#f6f9fc",
56
+ fontFamily:
57
+ '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
58
+ };
59
+
60
+ const container = {
61
+ backgroundColor: "#ffffff",
62
+ margin: "0 auto",
63
+ padding: "20px 0 48px",
64
+ marginBottom: "64px",
65
+ };
66
+
67
+ const content = {
68
+ padding: "0 48px",
69
+ };
70
+
71
+ const heading = {
72
+ fontSize: "32px",
73
+ lineHeight: "1.3",
74
+ fontWeight: "700",
75
+ color: "#484848",
76
+ padding: "17px 0 0",
77
+ };
78
+
79
+ const paragraph = {
80
+ margin: "0 0 15px",
81
+ fontSize: "15px",
82
+ lineHeight: "1.4",
83
+ color: "#3c4149",
84
+ };
85
+
86
+ const button = {
87
+ backgroundColor: "#000000",
88
+ borderRadius: "5px",
89
+ color: "#fff",
90
+ fontSize: "16px",
91
+ fontWeight: "bold",
92
+ textDecoration: "none",
93
+ textAlign: "center" as const,
94
+ display: "block",
95
+ width: "100%",
96
+ padding: "12px",
97
+ margin: "20px 0",
98
+ };
99
+
100
+ const hr = {
101
+ borderColor: "#dfe1e4",
102
+ margin: "42px 0 26px",
103
+ };
104
+
105
+ const footer = {
106
+ color: "#8898aa",
107
+ fontSize: "12px",
108
+ lineHeight: "16px",
109
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@cms0/transactional",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "main": "dist/index.js",
9
+ "types": "./src/index.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.ts",
13
+ "import": "./src/index.ts",
14
+ "require": "./dist/index.js",
15
+ "default": "./src/index.ts"
16
+ },
17
+ "./*": {
18
+ "types": "./src/*.ts",
19
+ "import": "./src/*.ts",
20
+ "require": "./dist/*.js"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "@react-email/components": "0.0.31",
25
+ "react": "19.0.0",
26
+ "react-dom": "19.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.0.0",
30
+ "@types/react": "^19.0.0",
31
+ "@types/react-dom": "^19.0.0",
32
+ "typescript": "^5.0.0",
33
+ "react-email": "3.0.3",
34
+ "@cms0/typescript-config": "0.0.1"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "dev": "email dev"
39
+ }
40
+ }
package/src/client.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { PlunkSendEmailRequest, PlunkSendEmailResponse } from "./types";
2
+
3
+ const PLUNK_API_BASE_URL = "https://next-api.useplunk.com";
4
+
5
+ export class PlunkClient {
6
+ private apiKey: string;
7
+
8
+ constructor(apiKey?: string) {
9
+ this.apiKey = apiKey || process.env.PLUNK_API_KEY || "";
10
+
11
+ if (!this.apiKey) {
12
+ throw new Error(
13
+ "Plunk API key is required. Set PLUNK_API_KEY environment variable or pass it to the constructor."
14
+ );
15
+ }
16
+ }
17
+
18
+ async sendEmail(request: PlunkSendEmailRequest): Promise<PlunkSendEmailResponse> {
19
+ try {
20
+ const response = await fetch(`${PLUNK_API_BASE_URL}/v1/send`, {
21
+ method: "POST",
22
+ headers: {
23
+ "Authorization": `Bearer ${this.apiKey}`,
24
+ "Content-Type": "application/json",
25
+ },
26
+ body: JSON.stringify(request),
27
+ });
28
+
29
+ const data = await response.json() as PlunkSendEmailResponse;
30
+
31
+ if (!response.ok) {
32
+ throw new Error(
33
+ data.error?.message || `Failed to send email: ${response.statusText}`
34
+ );
35
+ }
36
+
37
+ return data;
38
+ } catch (error) {
39
+ if (error instanceof Error) {
40
+ throw error;
41
+ }
42
+ throw new Error("An unknown error occurred while sending email");
43
+ }
44
+ }
45
+ }
46
+
47
+ // Export a default instance
48
+ let defaultClient: PlunkClient | null = null;
49
+
50
+ export function getDefaultClient(): PlunkClient {
51
+ if (!defaultClient) {
52
+ defaultClient = new PlunkClient();
53
+ }
54
+ return defaultClient;
55
+ }
56
+
57
+ export function setDefaultClient(client: PlunkClient): void {
58
+ defaultClient = client;
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { getDefaultClient, PlunkClient, setDefaultClient } from "./client";
2
+ import {
3
+ renderTeamInvite,
4
+ renderResetPassword,
5
+ } from "./templates";
6
+ import type {
7
+ TeamInviteProps,
8
+ ResetPasswordProps,
9
+ SendEmailOptions,
10
+ PlunkSendEmailResponse,
11
+ } from "./types";
12
+
13
+ /**
14
+ * Send a team invitation email
15
+ */
16
+ export async function sendTeamInvite(
17
+ to: string,
18
+ props: TeamInviteProps,
19
+ options?: SendEmailOptions
20
+ ): Promise<PlunkSendEmailResponse> {
21
+ const client = getDefaultClient();
22
+ const html = await renderTeamInvite(props);
23
+
24
+ return await client.sendEmail({
25
+ to,
26
+ subject: `Join ${props.teamName}`,
27
+ body: html,
28
+ from: options?.from,
29
+ reply: options?.replyTo,
30
+ headers: options?.headers,
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Send a password reset email
36
+ */
37
+ export async function sendResetPassword(
38
+ to: string,
39
+ props: ResetPasswordProps,
40
+ options?: SendEmailOptions
41
+ ): Promise<PlunkSendEmailResponse> {
42
+ const client = getDefaultClient();
43
+ const html = await renderResetPassword(props);
44
+
45
+ return await client.sendEmail({
46
+ to,
47
+ subject: "Reset your password",
48
+ body: html,
49
+ from: options?.from,
50
+ reply: options?.replyTo,
51
+ headers: options?.headers,
52
+ });
53
+ }
54
+
55
+ // Export types and utilities
56
+ export type {
57
+ TeamInviteProps,
58
+ ResetPasswordProps,
59
+ SendEmailOptions,
60
+ PlunkSendEmailResponse,
61
+ };
62
+
63
+ export { PlunkClient, setDefaultClient };
@@ -0,0 +1,15 @@
1
+ import { render } from "@react-email/components";
2
+ import TeamInviteEmail from "../emails/team-invite";
3
+ import ResetPasswordEmail from "../emails/reset-password";
4
+ import type {
5
+ TeamInviteProps,
6
+ ResetPasswordProps,
7
+ } from "./types";
8
+
9
+ export async function renderTeamInvite(props: TeamInviteProps): Promise<string> {
10
+ return await render(<TeamInviteEmail {...props} />);
11
+ }
12
+
13
+ export async function renderResetPassword(props: ResetPasswordProps): Promise<string> {
14
+ return await render(<ResetPasswordEmail {...props} />);
15
+ }
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ // Plunk API types
2
+ export interface PlunkSendEmailRequest {
3
+ to: string | { name: string; email: string } | Array<string | { name: string; email: string }>;
4
+ subject: string;
5
+ body: string;
6
+ from?: string | { name: string; email: string };
7
+ name?: string;
8
+ subscribed?: boolean;
9
+ data?: Record<string, unknown>;
10
+ headers?: Record<string, string>;
11
+ reply?: string;
12
+ attachments?: Array<{
13
+ filename: string;
14
+ content: string; // base64
15
+ contentType: string;
16
+ }>;
17
+ }
18
+
19
+ export interface PlunkSendEmailResponse {
20
+ success: boolean;
21
+ data?: {
22
+ emails: Array<{
23
+ contact: string;
24
+ email: string;
25
+ }>;
26
+ timestamp: string;
27
+ };
28
+ error?: {
29
+ code: string;
30
+ error: string;
31
+ message: string;
32
+ timestamp: string;
33
+ };
34
+ }
35
+
36
+ // Email template props
37
+ export interface TeamInviteProps {
38
+ teamName: string;
39
+ inviterName: string;
40
+ inviteUrl: string;
41
+ recipientEmail: string;
42
+ }
43
+
44
+ export interface ResetPasswordProps {
45
+ resetUrl: string;
46
+ userName?: string;
47
+ }
48
+
49
+ // Send email options
50
+ export interface SendEmailOptions {
51
+ from?: string | { name: string; email: string };
52
+ replyTo?: string;
53
+ headers?: Record<string, string>;
54
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@cms0/typescript-config/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "jsx": "react-jsx"
6
+ },
7
+ "include": [
8
+ "src",
9
+ "emails"
10
+ ]
11
+ }