@cms0/transactional 0.2.19 → 0.2.21
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/README.md +17 -9
- package/dist/emails/reset-password.js +4 -7
- package/dist/emails/team-invite.js +4 -7
- package/dist/src/client.d.ts +7 -8
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +148 -44
- package/dist/src/index.d.ts +7 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +20 -25
- package/dist/src/templates.d.ts +1 -1
- package/dist/src/templates.d.ts.map +1 -1
- package/dist/src/templates.js +8 -15
- package/dist/src/types.d.ts +36 -44
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -2
- package/package.json +27 -21
- package/.env.example +0 -3
- package/CHANGELOG.md +0 -32
- package/emails/reset-password.tsx +0 -107
- package/emails/team-invite.tsx +0 -109
- package/src/client.ts +0 -59
- package/src/index.ts +0 -63
- package/src/templates.tsx +0 -15
- package/src/types.ts +0 -54
- package/tsconfig.json +0 -11
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @cms0/transactional
|
|
2
2
|
|
|
3
|
-
Transactional email package
|
|
3
|
+
Transactional email package with provider-agnostic transports and React Email templates.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
This package is part of the
|
|
7
|
+
This package is part of the package graph. Install dependencies from the root:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pnpm install
|
|
@@ -12,10 +12,14 @@ pnpm install
|
|
|
12
12
|
|
|
13
13
|
## Configuration
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Configure one of the supported transports in the consuming app:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
18
|
+
CMS0_EMAIL_TRANSPORT=log
|
|
19
|
+
# or:
|
|
20
|
+
CMS0_EMAIL_TRANSPORT=smtp
|
|
21
|
+
# or:
|
|
22
|
+
CMS0_EMAIL_TRANSPORT=plunk
|
|
19
23
|
```
|
|
20
24
|
|
|
21
25
|
## Usage
|
|
@@ -124,13 +128,17 @@ Send a password reset email.
|
|
|
124
128
|
|
|
125
129
|
## Advanced Usage
|
|
126
130
|
|
|
127
|
-
### Custom
|
|
131
|
+
### Custom Email Service
|
|
128
132
|
|
|
129
|
-
You can create and set a custom
|
|
133
|
+
You can create and set a custom default email service:
|
|
130
134
|
|
|
131
135
|
```typescript
|
|
132
|
-
import {
|
|
136
|
+
import { createEmailService, setDefaultEmailService } from '@cms0/transactional';
|
|
133
137
|
|
|
134
|
-
const
|
|
135
|
-
|
|
138
|
+
const customService = createEmailService({
|
|
139
|
+
transport: { kind: 'log' },
|
|
140
|
+
defaultFrom: 'no-reply@example.com',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
setDefaultEmailService(customService);
|
|
136
144
|
```
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const components_1 = require("@react-email/components");
|
|
6
|
-
function ResetPasswordEmail({ resetUrl = "https://example.com/reset-password", userName = "there", }) {
|
|
7
|
-
return ((0, jsx_runtime_1.jsxs)(components_1.Html, { lang: "en", children: [(0, jsx_runtime_1.jsx)(components_1.Head, {}), (0, jsx_runtime_1.jsx)(components_1.Body, { style: main, children: (0, jsx_runtime_1.jsx)(components_1.Container, { style: container, children: (0, jsx_runtime_1.jsxs)(components_1.Section, { style: content, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: heading, children: "Reset your password" }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: paragraph, children: ["Hi ", userName, ","] }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "We received a request to reset your password. Click the button below to create a new password:" }), (0, jsx_runtime_1.jsx)(components_1.Button, { href: resetUrl, style: button, children: "Reset Password" }), (0, jsx_runtime_1.jsx)(components_1.Hr, { style: hr }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "This link will expire in 1 hour for security reasons." }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: footer, children: "If you didn't request a password reset, you can safely ignore this email. Your password will not be changed." })] }) }) })] }));
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html, Head, Body, Container, Section, Text, Button, Hr, } from "@react-email/components";
|
|
3
|
+
export default function ResetPasswordEmail({ resetUrl = "https://example.com/reset-password", userName = "there", }) {
|
|
4
|
+
return (_jsxs(Html, { lang: "en", children: [_jsx(Head, {}), _jsx(Body, { style: main, children: _jsx(Container, { style: container, children: _jsxs(Section, { style: content, children: [_jsx(Text, { style: heading, children: "Reset your password" }), _jsxs(Text, { style: paragraph, children: ["Hi ", userName, ","] }), _jsx(Text, { style: paragraph, children: "We received a request to reset your password. Click the button below to create a new password:" }), _jsx(Button, { href: resetUrl, style: button, children: "Reset Password" }), _jsx(Hr, { style: hr }), _jsx(Text, { style: paragraph, children: "This link will expire in 1 hour for security reasons." }), _jsx(Text, { style: footer, children: "If you didn't request a password reset, you can safely ignore this email. Your password will not be changed." })] }) }) })] }));
|
|
8
5
|
}
|
|
9
6
|
const main = {
|
|
10
7
|
backgroundColor: "#f6f9fc",
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const components_1 = require("@react-email/components");
|
|
6
|
-
function TeamInviteEmail({ teamName = "Your Team", inviterName = "A team member", inviteUrl = "https://example.com/accept-invite", recipientEmail = "user@example.com", }) {
|
|
7
|
-
return ((0, jsx_runtime_1.jsxs)(components_1.Html, { lang: "en", children: [(0, jsx_runtime_1.jsx)(components_1.Head, {}), (0, jsx_runtime_1.jsx)(components_1.Body, { style: main, children: (0, jsx_runtime_1.jsx)(components_1.Container, { style: container, children: (0, jsx_runtime_1.jsxs)(components_1.Section, { style: content, children: [(0, jsx_runtime_1.jsx)(components_1.Text, { style: heading, children: "You've been invited to join a team" }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: paragraph, children: [inviterName, " has invited you to join ", (0, jsx_runtime_1.jsx)("strong", { children: teamName }), "."] }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: paragraph, children: "Click the button below to accept the invitation and join the team:" }), (0, jsx_runtime_1.jsx)(components_1.Button, { href: inviteUrl, style: button, children: "Accept Invitation" }), (0, jsx_runtime_1.jsx)(components_1.Hr, { style: hr }), (0, jsx_runtime_1.jsxs)(components_1.Text, { style: footer, children: ["This invitation was sent to ", recipientEmail, ". If you weren't expecting this invitation, you can safely ignore this email."] })] }) }) })] }));
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html, Head, Body, Container, Section, Text, Button, Hr, } from "@react-email/components";
|
|
3
|
+
export default function TeamInviteEmail({ teamName = "Your Team", inviterName = "A team member", inviteUrl = "https://example.com/accept-invite", recipientEmail = "user@example.com", }) {
|
|
4
|
+
return (_jsxs(Html, { lang: "en", children: [_jsx(Head, {}), _jsx(Body, { style: main, children: _jsx(Container, { style: container, children: _jsxs(Section, { style: content, children: [_jsx(Text, { style: heading, children: "You've been invited to join a team" }), _jsxs(Text, { style: paragraph, children: [inviterName, " has invited you to join ", _jsx("strong", { children: teamName }), "."] }), _jsx(Text, { style: paragraph, children: "Click the button below to accept the invitation and join the team:" }), _jsx(Button, { href: inviteUrl, style: button, children: "Accept Invitation" }), _jsx(Hr, { style: hr }), _jsxs(Text, { style: footer, children: ["This invitation was sent to ", recipientEmail, ". If you weren't expecting this invitation, you can safely ignore this email."] })] }) }) })] }));
|
|
8
5
|
}
|
|
9
6
|
const main = {
|
|
10
7
|
backgroundColor: "#f6f9fc",
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export declare
|
|
8
|
-
export declare function setDefaultClient(client: PlunkClient): void;
|
|
1
|
+
import type { EmailService, EmailServiceConfig, EmailTransport } from "./types.js";
|
|
2
|
+
import type { EmailTransportConfig } from "@cms0/shared";
|
|
3
|
+
export declare const createEmailTransport: (config: EmailTransportConfig) => EmailTransport;
|
|
4
|
+
export declare const createEmailService: (config: EmailServiceConfig) => EmailService;
|
|
5
|
+
export declare const getDefaultEmailService: () => EmailService;
|
|
6
|
+
export declare const setDefaultEmailService: (service: EmailService) => void;
|
|
7
|
+
export declare const clearDefaultEmailService: () => void;
|
|
9
8
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/src/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAEV,oBAAoB,EACrB,MAAM,cAAc,CAAC;AA6KtB,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,oBAAoB,KAC3B,cAWF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,kBAAkB,KACzB,YAYF,CAAC;AAIF,eAAO,MAAM,sBAAsB,oBAQlC,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,SAAS,YAAY,SAE3D,CAAC;AAEF,eAAO,MAAM,wBAAwB,YAEpC,CAAC"}
|
package/dist/src/client.js
CHANGED
|
@@ -1,50 +1,154 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
constructor(apiKey) {
|
|
10
|
-
this.apiKey = apiKey || process.env.PLUNK_API_KEY || "";
|
|
11
|
-
if (!this.apiKey) {
|
|
12
|
-
throw new Error("Plunk API key is required. Set PLUNK_API_KEY environment variable or pass it to the constructor.");
|
|
13
|
-
}
|
|
1
|
+
const DEFAULT_PLUNK_API_BASE_URL = "https://next-api.useplunk.com";
|
|
2
|
+
const importNodemailer = new Function("return import('nodemailer')");
|
|
3
|
+
const normalizeAddress = (value) => {
|
|
4
|
+
if (!value) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
if (typeof value === "string") {
|
|
8
|
+
return value;
|
|
14
9
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
return value.name?.trim()
|
|
11
|
+
? `${value.name.trim()} <${value.email}>`
|
|
12
|
+
: value.email;
|
|
13
|
+
};
|
|
14
|
+
const normalizeRecipients = (value) => Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
.map((entry) => normalizeAddress(entry))
|
|
17
|
+
.filter((entry) => Boolean(entry))
|
|
18
|
+
: ensureEmailAddress(value, "Recipient");
|
|
19
|
+
const ensureEmailAddress = (value, label) => {
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw new Error(`${label} email address is required.`);
|
|
22
|
+
}
|
|
23
|
+
return normalizeAddress(value);
|
|
24
|
+
};
|
|
25
|
+
const stringifyRecipients = (value) => {
|
|
26
|
+
const normalized = normalizeRecipients(value);
|
|
27
|
+
return Array.isArray(normalized) ? normalized.join(", ") : normalized;
|
|
28
|
+
};
|
|
29
|
+
const createLogEmailTransport = () => ({
|
|
30
|
+
kind: "log",
|
|
31
|
+
async send(message) {
|
|
32
|
+
console.info("[cms0/transactional:log]", JSON.stringify({
|
|
33
|
+
from: normalizeAddress(message.from),
|
|
34
|
+
provider: "log",
|
|
35
|
+
subject: message.subject,
|
|
36
|
+
to: stringifyRecipients(message.to),
|
|
37
|
+
}, null, 2));
|
|
38
|
+
return {
|
|
39
|
+
accepted: true,
|
|
40
|
+
messageId: null,
|
|
41
|
+
provider: "log",
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const createSmtpEmailTransport = (config) => {
|
|
46
|
+
let transporter = null;
|
|
47
|
+
const getTransporter = async () => {
|
|
48
|
+
if (!transporter) {
|
|
49
|
+
const nodemailer = await importNodemailer();
|
|
50
|
+
const createTransport = nodemailer.default?.createTransport ?? nodemailer.createTransport;
|
|
51
|
+
const nextTransporter = createTransport({
|
|
52
|
+
auth: config.username || config.password
|
|
53
|
+
? {
|
|
54
|
+
pass: config.password ?? undefined,
|
|
55
|
+
user: config.username ?? undefined,
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
58
|
+
host: config.host,
|
|
59
|
+
port: config.port,
|
|
60
|
+
secure: config.secure,
|
|
24
61
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
throw new Error(data.error?.message || `Failed to send email: ${response.statusText}`);
|
|
28
|
-
}
|
|
29
|
-
return data;
|
|
62
|
+
transporter = nextTransporter;
|
|
63
|
+
return nextTransporter;
|
|
30
64
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
return transporter;
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
kind: "smtp",
|
|
69
|
+
async send(message) {
|
|
70
|
+
const activeTransporter = await getTransporter();
|
|
71
|
+
const info = (await activeTransporter.sendMail({
|
|
72
|
+
attachments: message.attachments,
|
|
73
|
+
from: ensureEmailAddress(message.from, "Sender"),
|
|
74
|
+
headers: message.headers,
|
|
75
|
+
html: message.html,
|
|
76
|
+
replyTo: normalizeAddress(message.replyTo),
|
|
77
|
+
subject: message.subject,
|
|
78
|
+
text: message.text,
|
|
79
|
+
to: normalizeRecipients(message.to),
|
|
80
|
+
}));
|
|
81
|
+
return {
|
|
82
|
+
accepted: Boolean(info.accepted?.length ?? 0),
|
|
83
|
+
messageId: info.messageId ?? null,
|
|
84
|
+
provider: "smtp",
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const createPlunkEmailTransport = (config) => ({
|
|
90
|
+
kind: "plunk",
|
|
91
|
+
async send(message) {
|
|
92
|
+
const response = await fetch(`${config.baseUrl ?? DEFAULT_PLUNK_API_BASE_URL}/v1/send`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
body: message.html,
|
|
100
|
+
from: normalizeAddress(message.from),
|
|
101
|
+
headers: message.headers,
|
|
102
|
+
reply: normalizeAddress(message.replyTo),
|
|
103
|
+
subject: message.subject,
|
|
104
|
+
to: message.to,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
const payload = (await response.json().catch(() => null));
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(payload?.error?.message ||
|
|
110
|
+
`Failed to send email via Plunk: ${response.status} ${response.statusText}`);
|
|
36
111
|
}
|
|
112
|
+
return {
|
|
113
|
+
accepted: Boolean(payload?.data?.emails?.length ?? 0),
|
|
114
|
+
messageId: null,
|
|
115
|
+
provider: "plunk",
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
export const createEmailTransport = (config) => {
|
|
120
|
+
switch (config.kind) {
|
|
121
|
+
case "log":
|
|
122
|
+
return createLogEmailTransport();
|
|
123
|
+
case "smtp":
|
|
124
|
+
return createSmtpEmailTransport(config);
|
|
125
|
+
case "plunk":
|
|
126
|
+
return createPlunkEmailTransport(config);
|
|
37
127
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
128
|
+
throw new Error("Unsupported email transport configuration.");
|
|
129
|
+
};
|
|
130
|
+
export const createEmailService = (config) => {
|
|
131
|
+
const transport = createEmailTransport(config.transport);
|
|
132
|
+
return {
|
|
133
|
+
send(message) {
|
|
134
|
+
return transport.send({
|
|
135
|
+
...message,
|
|
136
|
+
from: message.from ?? config.defaultFrom,
|
|
137
|
+
replyTo: message.replyTo ?? config.defaultReplyTo,
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
let defaultEmailService = null;
|
|
143
|
+
export const getDefaultEmailService = () => {
|
|
144
|
+
if (!defaultEmailService) {
|
|
145
|
+
throw new Error("Default email service has not been configured for @cms0/transactional.");
|
|
45
146
|
}
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
147
|
+
return defaultEmailService;
|
|
148
|
+
};
|
|
149
|
+
export const setDefaultEmailService = (service) => {
|
|
150
|
+
defaultEmailService = service;
|
|
151
|
+
};
|
|
152
|
+
export const clearDefaultEmailService = () => {
|
|
153
|
+
defaultEmailService = null;
|
|
154
|
+
};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { TeamInviteProps, ResetPasswordProps, SendEmailOptions
|
|
1
|
+
import { createEmailService, createEmailTransport, setDefaultEmailService, clearDefaultEmailService } from "./client.js";
|
|
2
|
+
import type { EmailService, EmailServiceConfig, EmailSendResult, EmailTransport, TeamInviteProps, ResetPasswordProps, SendEmailOptions } from "./types.js";
|
|
3
|
+
import type { EmailTransportConfig } from "@cms0/shared";
|
|
3
4
|
/**
|
|
4
5
|
* Send a team invitation email
|
|
5
6
|
*/
|
|
6
|
-
export declare function sendTeamInvite(to: string, props: TeamInviteProps, options?: SendEmailOptions): Promise<
|
|
7
|
+
export declare function sendTeamInvite(to: string, props: TeamInviteProps, options?: SendEmailOptions): Promise<EmailSendResult>;
|
|
7
8
|
/**
|
|
8
9
|
* Send a password reset email
|
|
9
10
|
*/
|
|
10
|
-
export declare function sendResetPassword(to: string, props: ResetPasswordProps, options?: SendEmailOptions): Promise<
|
|
11
|
-
export type { TeamInviteProps, ResetPasswordProps, SendEmailOptions,
|
|
12
|
-
export {
|
|
11
|
+
export declare function sendResetPassword(to: string, props: ResetPasswordProps, options?: SendEmailOptions): Promise<EmailSendResult>;
|
|
12
|
+
export type { EmailService, EmailServiceConfig, EmailTransport, EmailTransportConfig, TeamInviteProps, ResetPasswordProps, SendEmailOptions, EmailSendResult, };
|
|
13
|
+
export { clearDefaultEmailService, createEmailService, createEmailTransport, setDefaultEmailService, };
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EAEpB,sBAAsB,EACtB,wBAAwB,EACzB,MAAM,aAAa,CAAC;AAKrB,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAKzD;;GAEG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAY1B;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAY1B;AAGD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,GAChB,CAAC;AAEF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,GACvB,CAAC"}
|
package/dist/src/index.js
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.sendTeamInvite = sendTeamInvite;
|
|
5
|
-
exports.sendResetPassword = sendResetPassword;
|
|
6
|
-
const client_1 = require("./client");
|
|
7
|
-
Object.defineProperty(exports, "PlunkClient", { enumerable: true, get: function () { return client_1.PlunkClient; } });
|
|
8
|
-
Object.defineProperty(exports, "setDefaultClient", { enumerable: true, get: function () { return client_1.setDefaultClient; } });
|
|
9
|
-
const templates_1 = require("./templates");
|
|
1
|
+
import { createEmailService, createEmailTransport, getDefaultEmailService, setDefaultEmailService, clearDefaultEmailService, } from "./client.js";
|
|
2
|
+
import { renderTeamInvite, renderResetPassword, } from "./templates.js";
|
|
3
|
+
const getEmailService = (options) => options?.service ?? getDefaultEmailService();
|
|
10
4
|
/**
|
|
11
5
|
* Send a team invitation email
|
|
12
6
|
*/
|
|
13
|
-
async function sendTeamInvite(to, props, options) {
|
|
14
|
-
const
|
|
15
|
-
const html = await
|
|
16
|
-
return
|
|
17
|
-
to,
|
|
18
|
-
subject: `Join ${props.teamName}`,
|
|
19
|
-
body: html,
|
|
7
|
+
export async function sendTeamInvite(to, props, options) {
|
|
8
|
+
const service = getEmailService(options);
|
|
9
|
+
const html = await renderTeamInvite(props);
|
|
10
|
+
return service.send({
|
|
20
11
|
from: options?.from,
|
|
21
|
-
reply: options?.replyTo,
|
|
22
12
|
headers: options?.headers,
|
|
13
|
+
html,
|
|
14
|
+
replyTo: options?.replyTo,
|
|
15
|
+
subject: `Join ${props.teamName}`,
|
|
16
|
+
to,
|
|
23
17
|
});
|
|
24
18
|
}
|
|
25
19
|
/**
|
|
26
20
|
* Send a password reset email
|
|
27
21
|
*/
|
|
28
|
-
async function sendResetPassword(to, props, options) {
|
|
29
|
-
const
|
|
30
|
-
const html = await
|
|
31
|
-
return
|
|
32
|
-
to,
|
|
33
|
-
subject: "Reset your password",
|
|
34
|
-
body: html,
|
|
22
|
+
export async function sendResetPassword(to, props, options) {
|
|
23
|
+
const service = getEmailService(options);
|
|
24
|
+
const html = await renderResetPassword(props);
|
|
25
|
+
return service.send({
|
|
35
26
|
from: options?.from,
|
|
36
|
-
reply: options?.replyTo,
|
|
37
27
|
headers: options?.headers,
|
|
28
|
+
html,
|
|
29
|
+
replyTo: options?.replyTo,
|
|
30
|
+
subject: "Reset your password",
|
|
31
|
+
to,
|
|
38
32
|
});
|
|
39
33
|
}
|
|
34
|
+
export { clearDefaultEmailService, createEmailService, createEmailTransport, setDefaultEmailService, };
|
package/dist/src/templates.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TeamInviteProps, ResetPasswordProps } from "./types";
|
|
1
|
+
import type { TeamInviteProps, ResetPasswordProps } from "./types.js";
|
|
2
2
|
export declare function renderTeamInvite(props: TeamInviteProps): Promise<string>;
|
|
3
3
|
export declare function renderResetPassword(props: ResetPasswordProps): Promise<string>;
|
|
4
4
|
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/templates.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/templates.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|
package/dist/src/templates.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
exports.renderResetPassword = renderResetPassword;
|
|
8
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
9
|
-
const components_1 = require("@react-email/components");
|
|
10
|
-
const team_invite_1 = __importDefault(require("../emails/team-invite"));
|
|
11
|
-
const reset_password_1 = __importDefault(require("../emails/reset-password"));
|
|
12
|
-
async function renderTeamInvite(props) {
|
|
13
|
-
return await (0, components_1.render)((0, jsx_runtime_1.jsx)(team_invite_1.default, { ...props }));
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "@react-email/components";
|
|
3
|
+
import TeamInviteEmail from "../emails/team-invite.js";
|
|
4
|
+
import ResetPasswordEmail from "../emails/reset-password.js";
|
|
5
|
+
export async function renderTeamInvite(props) {
|
|
6
|
+
return await render(_jsx(TeamInviteEmail, { ...props }));
|
|
14
7
|
}
|
|
15
|
-
async function renderResetPassword(props) {
|
|
16
|
-
return await
|
|
8
|
+
export async function renderResetPassword(props) {
|
|
9
|
+
return await render(_jsx(ResetPasswordEmail, { ...props }));
|
|
17
10
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,43 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from?:
|
|
12
|
-
name: string;
|
|
13
|
-
email: string;
|
|
14
|
-
};
|
|
15
|
-
name?: string;
|
|
16
|
-
subscribed?: boolean;
|
|
17
|
-
data?: Record<string, unknown>;
|
|
1
|
+
import type { EmailAddress, EmailTransportConfig } from "@cms0/shared";
|
|
2
|
+
export type EmailAddressLike = string | EmailAddress;
|
|
3
|
+
export type EmailRecipient = EmailAddressLike | Array<EmailAddressLike>;
|
|
4
|
+
export interface EmailAttachment {
|
|
5
|
+
content: string;
|
|
6
|
+
contentType: string;
|
|
7
|
+
filename: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EmailMessage {
|
|
10
|
+
attachments?: EmailAttachment[];
|
|
11
|
+
from?: EmailAddressLike;
|
|
18
12
|
headers?: Record<string, string>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
13
|
+
html: string;
|
|
14
|
+
replyTo?: EmailAddressLike;
|
|
15
|
+
subject: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
to: EmailRecipient;
|
|
18
|
+
}
|
|
19
|
+
export interface EmailSendResult {
|
|
20
|
+
accepted: boolean;
|
|
21
|
+
messageId: string | null;
|
|
22
|
+
provider: EmailTransportConfig["kind"];
|
|
23
|
+
}
|
|
24
|
+
export interface EmailTransport {
|
|
25
|
+
readonly kind: EmailTransportConfig["kind"];
|
|
26
|
+
send(message: EmailMessage): Promise<EmailSendResult>;
|
|
27
|
+
}
|
|
28
|
+
export interface EmailService {
|
|
29
|
+
send(message: EmailMessage): Promise<EmailSendResult>;
|
|
30
|
+
}
|
|
31
|
+
export interface EmailServiceConfig {
|
|
32
|
+
defaultFrom?: EmailAddressLike;
|
|
33
|
+
defaultReplyTo?: EmailAddressLike;
|
|
34
|
+
transport: EmailTransportConfig;
|
|
41
35
|
}
|
|
42
36
|
export interface TeamInviteProps {
|
|
43
37
|
teamName: string;
|
|
@@ -50,11 +44,9 @@ export interface ResetPasswordProps {
|
|
|
50
44
|
userName?: string;
|
|
51
45
|
}
|
|
52
46
|
export interface SendEmailOptions {
|
|
53
|
-
from?:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
replyTo?: string;
|
|
47
|
+
from?: EmailAddressLike;
|
|
48
|
+
replyTo?: EmailAddressLike;
|
|
49
|
+
service?: EmailService;
|
|
58
50
|
headers?: Record<string, string>;
|
|
59
51
|
}
|
|
60
52
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/src/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEvE,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,YAAY,CAAC;AAErD,MAAM,MAAM,cAAc,GACtB,gBAAgB,GAChB,KAAK,CAAC,gBAAgB,CAAC,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,cAAc,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,SAAS,EAAE,oBAAoB,CAAC;CACjC;AAGD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC"}
|
package/dist/src/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,44 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cms0/transactional",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"private": false,
|
|
3
|
+
"version": "0.2.21",
|
|
5
4
|
"publishConfig": {
|
|
6
5
|
"access": "restricted"
|
|
7
6
|
},
|
|
8
|
-
"
|
|
9
|
-
"
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/src/index.js",
|
|
9
|
+
"types": "./dist/src/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
-
"types": "./src/index.ts",
|
|
13
|
-
"import": "./src/index.
|
|
12
|
+
"types": "./dist/src/index.d.ts",
|
|
13
|
+
"import": "./dist/src/index.js",
|
|
14
14
|
"require": "./dist/src/index.js",
|
|
15
|
-
"default": "./src/index.
|
|
15
|
+
"default": "./dist/src/index.js"
|
|
16
16
|
},
|
|
17
17
|
"./*": {
|
|
18
|
-
"types": "./src/*.ts",
|
|
19
|
-
"import": "./src/*.
|
|
18
|
+
"types": "./dist/src/*.d.ts",
|
|
19
|
+
"import": "./dist/src/*.js",
|
|
20
20
|
"require": "./dist/src/*.js"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
23
26
|
"dependencies": {
|
|
24
|
-
"@react-email/components": "0.
|
|
27
|
+
"@react-email/components": "^0.5.7",
|
|
28
|
+
"nodemailer": "^7.0.10",
|
|
29
|
+
"@cms0/shared": "0.2.21"
|
|
25
30
|
},
|
|
26
31
|
"peerDependencies": {
|
|
27
|
-
"react": "^19.
|
|
28
|
-
"react-dom": "^19.
|
|
32
|
+
"react": "^19.2.0",
|
|
33
|
+
"react-dom": "^19.2.0"
|
|
29
34
|
},
|
|
30
35
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^
|
|
32
|
-
"@types/
|
|
33
|
-
"@types/react
|
|
34
|
-
"react": "
|
|
35
|
-
"
|
|
36
|
-
"typescript": "^5.0.0",
|
|
37
|
-
"react-email": "3.0.3",
|
|
36
|
+
"@types/node": "^24.10.0",
|
|
37
|
+
"@types/nodemailer": "^7.0.3",
|
|
38
|
+
"@types/react": "^19.2.4",
|
|
39
|
+
"react": "19.2.0",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
38
41
|
"@cms0/typescript-config": "0.0.1"
|
|
39
42
|
},
|
|
40
43
|
"scripts": {
|
|
41
|
-
"build": "tsc",
|
|
42
|
-
"
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
46
|
+
"lint": "tsc --noEmit -p tsconfig.json",
|
|
47
|
+
"test:unit": "pnpm -C ../.. exec vitest run --config vitest.config.ts --project transactional-unit",
|
|
48
|
+
"test": "pnpm test:unit"
|
|
43
49
|
}
|
|
44
50
|
}
|
package/.env.example
DELETED
package/CHANGELOG.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# @cms0/transactional
|
|
2
|
-
|
|
3
|
-
## 0.2.19
|
|
4
|
-
|
|
5
|
-
## 0.2.18
|
|
6
|
-
|
|
7
|
-
## 0.2.17
|
|
8
|
-
|
|
9
|
-
## 0.2.16
|
|
10
|
-
|
|
11
|
-
## 0.2.15
|
|
12
|
-
|
|
13
|
-
## 0.2.14
|
|
14
|
-
|
|
15
|
-
### Patch Changes
|
|
16
|
-
|
|
17
|
-
- e76b090: Republish the public runtime stack with a single compatibility boundary so
|
|
18
|
-
Canvas, shared helpers, admin, and SDK packages cannot drift into broken
|
|
19
|
-
published combinations.
|
|
20
|
-
|
|
21
|
-
## 0.0.3
|
|
22
|
-
|
|
23
|
-
### Patch Changes
|
|
24
|
-
|
|
25
|
-
- d131093: Harden npm package publication by rebuilding tarballs during `prepack` and verifying packed artifacts in the main-branch release workflow. This fixes missing build output in published Canvas packages and corrects the transactional package entrypoint paths used by consumers.
|
|
26
|
-
|
|
27
|
-
## 0.0.2
|
|
28
|
-
|
|
29
|
-
### Patch Changes
|
|
30
|
-
|
|
31
|
-
- bef5ec7: Ensure the admin server binary is executable in Docker images.
|
|
32
|
-
- 2ee81b6: Fix rich text editor transaction sync issues and stabilize localized field test behavior.
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
};
|
package/emails/team-invite.tsx
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
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/src/client.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
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 };
|
package/src/templates.tsx
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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
|
-
}
|