@fufulog/brevomorphic-cms-sdk 1.0.0
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/INTEGRATION.md +101 -0
- package/README.md +336 -0
- package/dist/brevoClient.d.ts +53 -0
- package/dist/brevoClient.js +141 -0
- package/dist/controller.d.ts +46 -0
- package/dist/controller.js +173 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/repository.d.ts +34 -0
- package/dist/repository.js +224 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.js +185 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +1 -0
- package/env.example +25 -0
- package/package.json +35 -0
- package/prd.md +135 -0
- package/react/BrevoWysiwyg.css +233 -0
- package/react/BrevoWysiwyg.tsx +388 -0
- package/src/brevoClient.ts +171 -0
- package/src/controller.ts +186 -0
- package/src/index.ts +5 -0
- package/src/repository.ts +229 -0
- package/src/service.ts +221 -0
- package/src/types.ts +77 -0
- package/svelte/BrevoWysiwyg.svelte +572 -0
- package/tests/sdk.test.ts +239 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import './BrevoWysiwyg.css';
|
|
3
|
+
|
|
4
|
+
export interface BrevoVariable {
|
|
5
|
+
label: string;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BrevoWysiwygProps {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (html: string) => void;
|
|
12
|
+
variables?: BrevoVariable[];
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* BrevoWysiwyg — WYSIWYG Email Template Editor (React)
|
|
19
|
+
*
|
|
20
|
+
* Drop-in React component for the brevoCMS SDK.
|
|
21
|
+
* Provides rich-text editing with Source toggle, formatting toolbar,
|
|
22
|
+
* and Brevo variable injection. Does NOT escape Twig brackets.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* <BrevoWysiwyg
|
|
26
|
+
* value={htmlContent}
|
|
27
|
+
* onChange={setHtmlContent}
|
|
28
|
+
* variables={[
|
|
29
|
+
* { label: 'Customer Name', key: 'params.name' },
|
|
30
|
+
* { label: 'Ticket Code', key: 'params.ticket_code' }
|
|
31
|
+
* ]}
|
|
32
|
+
* />
|
|
33
|
+
*/
|
|
34
|
+
export default function BrevoWysiwyg({
|
|
35
|
+
value,
|
|
36
|
+
onChange,
|
|
37
|
+
variables = [],
|
|
38
|
+
placeholder = 'Start designing your email template...',
|
|
39
|
+
disabled = false,
|
|
40
|
+
}: BrevoWysiwygProps) {
|
|
41
|
+
const editorRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const sourceRef = useRef<HTMLTextAreaElement>(null);
|
|
43
|
+
|
|
44
|
+
const [isSourceMode, setIsSourceMode] = useState(false);
|
|
45
|
+
const [sourceCode, setSourceCode] = useState('');
|
|
46
|
+
const [showVariableMenu, setShowVariableMenu] = useState(false);
|
|
47
|
+
const [showHeadingMenu, setShowHeadingMenu] = useState(false);
|
|
48
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
49
|
+
const [activeFormats, setActiveFormats] = useState({
|
|
50
|
+
bold: false,
|
|
51
|
+
italic: false,
|
|
52
|
+
underline: false,
|
|
53
|
+
strikeThrough: false,
|
|
54
|
+
insertOrderedList: false,
|
|
55
|
+
insertUnorderedList: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Sync initial value into editor
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (editorRef.current && !isSourceMode) {
|
|
61
|
+
if (editorRef.current.innerHTML !== value) {
|
|
62
|
+
editorRef.current.innerHTML = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
66
|
+
|
|
67
|
+
// Close menus on outside click
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const handler = (e: MouseEvent) => {
|
|
70
|
+
if (!(e.target as HTMLElement).closest('.brevo-dropdown')) {
|
|
71
|
+
setShowVariableMenu(false);
|
|
72
|
+
setShowHeadingMenu(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
window.addEventListener('click', handler);
|
|
76
|
+
return () => window.removeEventListener('click', handler);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
/** Extract raw HTML from editor — restoring any accidentally encoded Twig brackets */
|
|
80
|
+
const extractHtml = useCallback((): string => {
|
|
81
|
+
if (!editorRef.current) return '';
|
|
82
|
+
return editorRef.current.innerHTML
|
|
83
|
+
.replace(/{/g, '{')
|
|
84
|
+
.replace(/}/g, '}')
|
|
85
|
+
.replace(/{/g, '{')
|
|
86
|
+
.replace(/}/g, '}')
|
|
87
|
+
.replace(/%/g, '%')
|
|
88
|
+
.replace(/%/g, '%');
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const syncFromEditor = useCallback(() => {
|
|
92
|
+
if (!editorRef.current || isSourceMode) return;
|
|
93
|
+
const cleaned = extractHtml();
|
|
94
|
+
onChange(cleaned);
|
|
95
|
+
}, [isSourceMode, onChange, extractHtml]);
|
|
96
|
+
|
|
97
|
+
const updateActiveFormats = useCallback(() => {
|
|
98
|
+
setActiveFormats({
|
|
99
|
+
bold: document.queryCommandState('bold'),
|
|
100
|
+
italic: document.queryCommandState('italic'),
|
|
101
|
+
underline: document.queryCommandState('underline'),
|
|
102
|
+
strikeThrough: document.queryCommandState('strikeThrough'),
|
|
103
|
+
insertOrderedList: document.queryCommandState('insertOrderedList'),
|
|
104
|
+
insertUnorderedList: document.queryCommandState('insertUnorderedList'),
|
|
105
|
+
});
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const execCmd = useCallback(
|
|
109
|
+
(command: string, val: string | null = null) => {
|
|
110
|
+
if (isSourceMode || disabled) return;
|
|
111
|
+
editorRef.current?.focus();
|
|
112
|
+
document.execCommand(command, false, val ?? undefined);
|
|
113
|
+
syncFromEditor();
|
|
114
|
+
updateActiveFormats();
|
|
115
|
+
},
|
|
116
|
+
[isSourceMode, disabled, syncFromEditor, updateActiveFormats]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const toggleSourceMode = useCallback(() => {
|
|
120
|
+
if (isSourceMode) {
|
|
121
|
+
// Source → Visual
|
|
122
|
+
onChange(sourceCode);
|
|
123
|
+
setIsSourceMode(false);
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
if (editorRef.current) editorRef.current.innerHTML = sourceCode;
|
|
126
|
+
}, 0);
|
|
127
|
+
} else {
|
|
128
|
+
// Visual → Source
|
|
129
|
+
const html = extractHtml();
|
|
130
|
+
setSourceCode(html);
|
|
131
|
+
onChange(html);
|
|
132
|
+
setIsSourceMode(true);
|
|
133
|
+
}
|
|
134
|
+
setShowVariableMenu(false);
|
|
135
|
+
setShowHeadingMenu(false);
|
|
136
|
+
}, [isSourceMode, sourceCode, onChange, extractHtml]);
|
|
137
|
+
|
|
138
|
+
const insertLink = useCallback(() => {
|
|
139
|
+
if (isSourceMode || disabled) return;
|
|
140
|
+
const url = prompt('Enter URL:');
|
|
141
|
+
if (url) execCmd('createLink', url);
|
|
142
|
+
}, [isSourceMode, disabled, execCmd]);
|
|
143
|
+
|
|
144
|
+
const insertVariable = useCallback(
|
|
145
|
+
(variable: BrevoVariable) => {
|
|
146
|
+
if (disabled) return;
|
|
147
|
+
const twigExpr = `{{ ${variable.key} }}`;
|
|
148
|
+
|
|
149
|
+
if (isSourceMode && sourceRef.current) {
|
|
150
|
+
const el = sourceRef.current;
|
|
151
|
+
const start = el.selectionStart;
|
|
152
|
+
const end = el.selectionEnd;
|
|
153
|
+
const updated = sourceCode.substring(0, start) + twigExpr + sourceCode.substring(end);
|
|
154
|
+
setSourceCode(updated);
|
|
155
|
+
onChange(updated);
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
el.selectionStart = el.selectionEnd = start + twigExpr.length;
|
|
158
|
+
el.focus();
|
|
159
|
+
}, 0);
|
|
160
|
+
} else if (editorRef.current) {
|
|
161
|
+
editorRef.current.focus();
|
|
162
|
+
document.execCommand('insertHTML', false, twigExpr);
|
|
163
|
+
syncFromEditor();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setShowVariableMenu(false);
|
|
167
|
+
},
|
|
168
|
+
[disabled, isSourceMode, sourceCode, onChange, syncFromEditor]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const handleSourceChange = useCallback(
|
|
172
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
173
|
+
const val = e.target.value;
|
|
174
|
+
setSourceCode(val);
|
|
175
|
+
onChange(val);
|
|
176
|
+
},
|
|
177
|
+
[onChange]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const wrapperClasses = [
|
|
181
|
+
'brevo-wysiwyg',
|
|
182
|
+
disabled && 'brevo-wysiwyg--disabled',
|
|
183
|
+
isFocused && 'brevo-wysiwyg--focused',
|
|
184
|
+
]
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join(' ');
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className={wrapperClasses}>
|
|
190
|
+
{/* ── Toolbar ── */}
|
|
191
|
+
<div className="brevo-toolbar">
|
|
192
|
+
<div className="brevo-toolbar__group">
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
className={`brevo-btn ${activeFormats.bold ? 'brevo-btn--active' : ''}`}
|
|
196
|
+
title="Bold"
|
|
197
|
+
onClick={() => execCmd('bold')}
|
|
198
|
+
disabled={disabled}
|
|
199
|
+
>
|
|
200
|
+
<strong>B</strong>
|
|
201
|
+
</button>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
className={`brevo-btn ${activeFormats.italic ? 'brevo-btn--active' : ''}`}
|
|
205
|
+
title="Italic"
|
|
206
|
+
onClick={() => execCmd('italic')}
|
|
207
|
+
disabled={disabled}
|
|
208
|
+
>
|
|
209
|
+
<em>I</em>
|
|
210
|
+
</button>
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
className={`brevo-btn ${activeFormats.underline ? 'brevo-btn--active' : ''}`}
|
|
214
|
+
title="Underline"
|
|
215
|
+
onClick={() => execCmd('underline')}
|
|
216
|
+
disabled={disabled}
|
|
217
|
+
>
|
|
218
|
+
<u>U</u>
|
|
219
|
+
</button>
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
className={`brevo-btn ${activeFormats.strikeThrough ? 'brevo-btn--active' : ''}`}
|
|
223
|
+
title="Strikethrough"
|
|
224
|
+
onClick={() => execCmd('strikeThrough')}
|
|
225
|
+
disabled={disabled}
|
|
226
|
+
>
|
|
227
|
+
<s>S</s>
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div className="brevo-toolbar__divider" />
|
|
232
|
+
|
|
233
|
+
{/* Heading dropdown */}
|
|
234
|
+
<div className="brevo-dropdown">
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
className="brevo-btn"
|
|
238
|
+
title="Headings"
|
|
239
|
+
onClick={(e) => {
|
|
240
|
+
e.stopPropagation();
|
|
241
|
+
setShowHeadingMenu((v) => !v);
|
|
242
|
+
setShowVariableMenu(false);
|
|
243
|
+
}}
|
|
244
|
+
disabled={disabled}
|
|
245
|
+
>
|
|
246
|
+
H ▾
|
|
247
|
+
</button>
|
|
248
|
+
{showHeadingMenu && (
|
|
249
|
+
<div className="brevo-dropdown__menu">
|
|
250
|
+
<button type="button" onClick={() => { execCmd('formatBlock', 'h1'); setShowHeadingMenu(false); }}>
|
|
251
|
+
Heading 1
|
|
252
|
+
</button>
|
|
253
|
+
<button type="button" onClick={() => { execCmd('formatBlock', 'h2'); setShowHeadingMenu(false); }}>
|
|
254
|
+
Heading 2
|
|
255
|
+
</button>
|
|
256
|
+
<button type="button" onClick={() => { execCmd('formatBlock', 'h3'); setShowHeadingMenu(false); }}>
|
|
257
|
+
Heading 3
|
|
258
|
+
</button>
|
|
259
|
+
<button type="button" onClick={() => { execCmd('formatBlock', 'p'); setShowHeadingMenu(false); }}>
|
|
260
|
+
Paragraph
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div className="brevo-toolbar__divider" />
|
|
267
|
+
|
|
268
|
+
<div className="brevo-toolbar__group">
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
className={`brevo-btn ${activeFormats.insertUnorderedList ? 'brevo-btn--active' : ''}`}
|
|
272
|
+
title="Bullet List"
|
|
273
|
+
onClick={() => execCmd('insertUnorderedList')}
|
|
274
|
+
disabled={disabled}
|
|
275
|
+
>
|
|
276
|
+
• List
|
|
277
|
+
</button>
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
className={`brevo-btn ${activeFormats.insertOrderedList ? 'brevo-btn--active' : ''}`}
|
|
281
|
+
title="Numbered List"
|
|
282
|
+
onClick={() => execCmd('insertOrderedList')}
|
|
283
|
+
disabled={disabled}
|
|
284
|
+
>
|
|
285
|
+
1. List
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div className="brevo-toolbar__divider" />
|
|
290
|
+
|
|
291
|
+
<div className="brevo-toolbar__group">
|
|
292
|
+
<button type="button" className="brevo-btn" title="Align Left" onClick={() => execCmd('justifyLeft')} disabled={disabled}>
|
|
293
|
+
⫷
|
|
294
|
+
</button>
|
|
295
|
+
<button type="button" className="brevo-btn" title="Align Center" onClick={() => execCmd('justifyCenter')} disabled={disabled}>
|
|
296
|
+
⫿
|
|
297
|
+
</button>
|
|
298
|
+
<button type="button" className="brevo-btn" title="Align Right" onClick={() => execCmd('justifyRight')} disabled={disabled}>
|
|
299
|
+
⫸
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div className="brevo-toolbar__divider" />
|
|
304
|
+
|
|
305
|
+
<button type="button" className="brevo-btn" title="Insert Link" onClick={insertLink} disabled={disabled}>
|
|
306
|
+
🔗
|
|
307
|
+
</button>
|
|
308
|
+
|
|
309
|
+
{/* Variable injector */}
|
|
310
|
+
{variables.length > 0 && (
|
|
311
|
+
<>
|
|
312
|
+
<div className="brevo-toolbar__divider" />
|
|
313
|
+
<div className="brevo-dropdown">
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
className="brevo-btn brevo-btn--accent"
|
|
317
|
+
title="Insert Variable"
|
|
318
|
+
onClick={(e) => {
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
setShowVariableMenu((v) => !v);
|
|
321
|
+
setShowHeadingMenu(false);
|
|
322
|
+
}}
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
>
|
|
325
|
+
{'{{ }}'} Insert Variable ▾
|
|
326
|
+
</button>
|
|
327
|
+
{showVariableMenu && (
|
|
328
|
+
<div className="brevo-dropdown__menu brevo-dropdown__menu--variables">
|
|
329
|
+
{variables.map((v) => (
|
|
330
|
+
<button type="button" key={v.key} onClick={() => insertVariable(v)}>
|
|
331
|
+
<span className="brevo-var-label">{v.label}</span>
|
|
332
|
+
<code className="brevo-var-key">{`{{ ${v.key} }}`}</code>
|
|
333
|
+
</button>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{/* Source toggle */}
|
|
342
|
+
<div className="brevo-toolbar__spacer" />
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
className={`brevo-btn brevo-btn--source ${isSourceMode ? 'brevo-btn--active' : ''}`}
|
|
346
|
+
title="Toggle Source View"
|
|
347
|
+
onClick={toggleSourceMode}
|
|
348
|
+
disabled={disabled}
|
|
349
|
+
>
|
|
350
|
+
{isSourceMode ? '✎ Visual' : '</> Source'}
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{/* ── Editor Area ── */}
|
|
355
|
+
<div className="brevo-editor-wrapper">
|
|
356
|
+
{isSourceMode ? (
|
|
357
|
+
<textarea
|
|
358
|
+
ref={sourceRef}
|
|
359
|
+
className="brevo-source"
|
|
360
|
+
value={sourceCode}
|
|
361
|
+
onChange={handleSourceChange}
|
|
362
|
+
onFocus={() => setIsFocused(true)}
|
|
363
|
+
onBlur={() => setIsFocused(false)}
|
|
364
|
+
placeholder={placeholder}
|
|
365
|
+
disabled={disabled}
|
|
366
|
+
spellCheck={false}
|
|
367
|
+
/>
|
|
368
|
+
) : (
|
|
369
|
+
<div
|
|
370
|
+
ref={editorRef}
|
|
371
|
+
className="brevo-editor"
|
|
372
|
+
contentEditable={!disabled}
|
|
373
|
+
role="textbox"
|
|
374
|
+
aria-multiline={true}
|
|
375
|
+
aria-label="Email template editor"
|
|
376
|
+
data-placeholder={placeholder}
|
|
377
|
+
onInput={() => { syncFromEditor(); updateActiveFormats(); }}
|
|
378
|
+
onKeyUp={updateActiveFormats}
|
|
379
|
+
onMouseUp={updateActiveFormats}
|
|
380
|
+
onFocus={() => setIsFocused(true)}
|
|
381
|
+
onBlur={() => setIsFocused(false)}
|
|
382
|
+
suppressContentEditableWarning
|
|
383
|
+
/>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import { BrevoTemplate, BrevoSender } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class BrevoClient {
|
|
5
|
+
private client: AxiosInstance;
|
|
6
|
+
|
|
7
|
+
constructor(apiKey: string) {
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
throw new Error('Brevo API key is required');
|
|
10
|
+
}
|
|
11
|
+
this.client = axios.create({
|
|
12
|
+
baseURL: 'https://api.brevo.com/v3',
|
|
13
|
+
headers: {
|
|
14
|
+
'api-key': apiKey,
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'Accept': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch all templates from Brevo.
|
|
23
|
+
* By default, limits templates but we can fetch them all.
|
|
24
|
+
*/
|
|
25
|
+
async getTemplates(limit = 100, offset = 0): Promise<BrevoTemplate[]> {
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.client.get<{ count: number; templates?: any[] }>(
|
|
28
|
+
`/smtp/templates?limit=${limit}&offset=${offset}`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const templates = response.data.templates || [];
|
|
32
|
+
return templates.map((t: any) => ({
|
|
33
|
+
id: t.id,
|
|
34
|
+
name: t.name,
|
|
35
|
+
subject: t.subject,
|
|
36
|
+
isActive: t.isActive,
|
|
37
|
+
htmlContent: t.htmlContent,
|
|
38
|
+
sender: t.sender ? { id: t.sender.id, name: t.sender.name, email: t.sender.email } : undefined,
|
|
39
|
+
}));
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
const msg = error.response?.data?.message || error.message;
|
|
42
|
+
throw new Error(`Failed to fetch templates from Brevo: ${msg}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch a single template by its Brevo ID
|
|
48
|
+
*/
|
|
49
|
+
async getTemplate(id: number): Promise<BrevoTemplate> {
|
|
50
|
+
try {
|
|
51
|
+
const response = await this.client.get<any>(`/smtp/templates/${id}`);
|
|
52
|
+
const t = response.data;
|
|
53
|
+
return {
|
|
54
|
+
id: t.id,
|
|
55
|
+
name: t.name,
|
|
56
|
+
subject: t.subject,
|
|
57
|
+
isActive: t.isActive,
|
|
58
|
+
htmlContent: t.htmlContent,
|
|
59
|
+
sender: t.sender ? { id: t.sender.id, name: t.sender.name, email: t.sender.email } : undefined,
|
|
60
|
+
};
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
const msg = error.response?.data?.message || error.message;
|
|
63
|
+
throw new Error(`Failed to fetch template #${id} from Brevo: ${msg}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a new SMTP template on Brevo
|
|
69
|
+
*/
|
|
70
|
+
async createTemplate(params: {
|
|
71
|
+
templateName: string;
|
|
72
|
+
subject: string;
|
|
73
|
+
sender: { name?: string; email: string };
|
|
74
|
+
htmlContent: string;
|
|
75
|
+
isActive?: boolean;
|
|
76
|
+
}): Promise<number> {
|
|
77
|
+
try {
|
|
78
|
+
const response = await this.client.post<{ id: number }>(
|
|
79
|
+
'/smtp/templates',
|
|
80
|
+
{
|
|
81
|
+
templateName: params.templateName,
|
|
82
|
+
subject: params.subject,
|
|
83
|
+
sender: params.sender,
|
|
84
|
+
htmlContent: params.htmlContent,
|
|
85
|
+
isActive: params.isActive ?? false,
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
return response.data.id;
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
const msg = error.response?.data?.message || error.message;
|
|
91
|
+
throw new Error(`Failed to create template on Brevo: ${msg}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Update an SMTP template on Brevo
|
|
97
|
+
*/
|
|
98
|
+
async updateTemplate(
|
|
99
|
+
id: number,
|
|
100
|
+
params: {
|
|
101
|
+
templateName: string;
|
|
102
|
+
subject: string;
|
|
103
|
+
sender: { name?: string; email: string; id?: number };
|
|
104
|
+
htmlContent: string;
|
|
105
|
+
isActive: boolean;
|
|
106
|
+
}
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
// Brevo PUT updates require sending sender as object { name, email } (or sender ID)
|
|
110
|
+
const payload: any = {
|
|
111
|
+
templateName: params.templateName,
|
|
112
|
+
subject: params.subject,
|
|
113
|
+
htmlContent: params.htmlContent,
|
|
114
|
+
isActive: params.isActive,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (params.sender) {
|
|
118
|
+
payload.sender = {
|
|
119
|
+
email: params.sender.email,
|
|
120
|
+
};
|
|
121
|
+
if (params.sender.name) {
|
|
122
|
+
payload.sender.name = params.sender.name;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await this.client.put(`/smtp/templates/${id}`, payload);
|
|
127
|
+
} catch (error: any) {
|
|
128
|
+
const msg = error.response?.data?.message || error.message;
|
|
129
|
+
throw new Error(`Failed to update template #${id} on Brevo: ${msg}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get all active and verified sender profiles from Brevo
|
|
135
|
+
*/
|
|
136
|
+
async getSenders(): Promise<BrevoSender[]> {
|
|
137
|
+
try {
|
|
138
|
+
const response = await this.client.get<{ senders?: any[] }>('/senders');
|
|
139
|
+
const senders = response.data.senders || [];
|
|
140
|
+
return senders.map((s: any) => ({
|
|
141
|
+
id: s.id,
|
|
142
|
+
name: s.name,
|
|
143
|
+
email: s.email,
|
|
144
|
+
active: s.active,
|
|
145
|
+
}));
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
const msg = error.response?.data?.message || error.message;
|
|
148
|
+
throw new Error(`Failed to fetch senders from Brevo: ${msg}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Send a transactional template email via Brevo SMTP API
|
|
154
|
+
*/
|
|
155
|
+
async sendEmail(params: {
|
|
156
|
+
templateId: number;
|
|
157
|
+
to: string;
|
|
158
|
+
variables?: Record<string, any>;
|
|
159
|
+
}): Promise<void> {
|
|
160
|
+
try {
|
|
161
|
+
await this.client.post('/smtp/email', {
|
|
162
|
+
templateId: params.templateId,
|
|
163
|
+
to: [{ email: params.to }],
|
|
164
|
+
params: params.variables || {},
|
|
165
|
+
});
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
const msg = error.response?.data?.message || error.message;
|
|
168
|
+
throw new Error(`Failed to send event email via Brevo: ${msg}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|