payload-plugin-newsletter 0.14.3 → 0.15.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/CHANGELOG.md +55 -0
- package/README.md +47 -0
- package/dist/collections.cjs +2124 -0
- package/dist/collections.cjs.map +1 -0
- package/dist/collections.d.cts +8 -0
- package/dist/collections.d.ts +8 -0
- package/dist/collections.js +2128 -0
- package/dist/collections.js.map +1 -0
- package/dist/fields.cjs +498 -115
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.d.cts +30 -4
- package/dist/fields.d.ts +30 -4
- package/dist/fields.js +481 -114
- package/dist/fields.js.map +1 -1
- package/dist/index.cjs +305 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +305 -118
- package/dist/index.js.map +1 -1
- package/dist/types.d.cts +15 -2
- package/dist/types.d.ts +15 -2
- package/package.json +11 -1
|
@@ -0,0 +1,2124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/types/newsletter.ts
|
|
34
|
+
var init_newsletter = __esm({
|
|
35
|
+
"src/types/newsletter.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/types/broadcast.ts
|
|
41
|
+
var BroadcastProviderError;
|
|
42
|
+
var init_broadcast = __esm({
|
|
43
|
+
"src/types/broadcast.ts"() {
|
|
44
|
+
"use strict";
|
|
45
|
+
init_newsletter();
|
|
46
|
+
BroadcastProviderError = class extends Error {
|
|
47
|
+
constructor(message, code, provider, details) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.code = code;
|
|
50
|
+
this.provider = provider;
|
|
51
|
+
this.details = details;
|
|
52
|
+
this.name = "BroadcastProviderError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// src/types/providers.ts
|
|
59
|
+
var BaseBroadcastProvider;
|
|
60
|
+
var init_providers = __esm({
|
|
61
|
+
"src/types/providers.ts"() {
|
|
62
|
+
"use strict";
|
|
63
|
+
init_broadcast();
|
|
64
|
+
init_newsletter();
|
|
65
|
+
BaseBroadcastProvider = class {
|
|
66
|
+
constructor(config) {
|
|
67
|
+
this.config = config;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Schedule a broadcast - default implementation throws not supported
|
|
71
|
+
*/
|
|
72
|
+
async schedule(_id, _scheduledAt) {
|
|
73
|
+
const capabilities = this.getCapabilities();
|
|
74
|
+
if (!capabilities.supportsScheduling) {
|
|
75
|
+
throw new BroadcastProviderError(
|
|
76
|
+
"Scheduling is not supported by this provider",
|
|
77
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
78
|
+
this.name
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
throw new Error("Method not implemented");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Cancel scheduled broadcast - default implementation throws not supported
|
|
85
|
+
*/
|
|
86
|
+
async cancelSchedule(_id) {
|
|
87
|
+
const capabilities = this.getCapabilities();
|
|
88
|
+
if (!capabilities.supportsScheduling) {
|
|
89
|
+
throw new BroadcastProviderError(
|
|
90
|
+
"Scheduling is not supported by this provider",
|
|
91
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
92
|
+
this.name
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
throw new Error("Method not implemented");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get analytics - default implementation returns zeros
|
|
99
|
+
*/
|
|
100
|
+
async getAnalytics(_id) {
|
|
101
|
+
const capabilities = this.getCapabilities();
|
|
102
|
+
if (!capabilities.supportsAnalytics) {
|
|
103
|
+
throw new BroadcastProviderError(
|
|
104
|
+
"Analytics are not supported by this provider",
|
|
105
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
106
|
+
this.name
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
sent: 0,
|
|
111
|
+
delivered: 0,
|
|
112
|
+
opened: 0,
|
|
113
|
+
clicked: 0,
|
|
114
|
+
bounced: 0,
|
|
115
|
+
complained: 0,
|
|
116
|
+
unsubscribed: 0
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Helper method to validate required fields
|
|
121
|
+
*/
|
|
122
|
+
validateRequiredFields(data, fields) {
|
|
123
|
+
const missing = fields.filter((field) => !data[field]);
|
|
124
|
+
if (missing.length > 0) {
|
|
125
|
+
throw new BroadcastProviderError(
|
|
126
|
+
`Missing required fields: ${missing.join(", ")}`,
|
|
127
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
128
|
+
this.name
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Helper method to check if a status transition is allowed
|
|
134
|
+
*/
|
|
135
|
+
canEditInStatus(status) {
|
|
136
|
+
const capabilities = this.getCapabilities();
|
|
137
|
+
return capabilities.editableStatuses.includes(status);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Helper to build pagination response
|
|
141
|
+
*/
|
|
142
|
+
buildListResponse(items, total, options = {}) {
|
|
143
|
+
const limit = options.limit || 20;
|
|
144
|
+
const offset = options.offset || 0;
|
|
145
|
+
return {
|
|
146
|
+
items,
|
|
147
|
+
total,
|
|
148
|
+
limit,
|
|
149
|
+
offset,
|
|
150
|
+
hasMore: offset + items.length < total
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// src/types/index.ts
|
|
158
|
+
var init_types = __esm({
|
|
159
|
+
"src/types/index.ts"() {
|
|
160
|
+
"use strict";
|
|
161
|
+
init_broadcast();
|
|
162
|
+
init_providers();
|
|
163
|
+
init_newsletter();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// src/providers/broadcast/broadcast.ts
|
|
168
|
+
var broadcast_exports = {};
|
|
169
|
+
__export(broadcast_exports, {
|
|
170
|
+
BroadcastApiProvider: () => BroadcastApiProvider
|
|
171
|
+
});
|
|
172
|
+
var BroadcastApiProvider;
|
|
173
|
+
var init_broadcast2 = __esm({
|
|
174
|
+
"src/providers/broadcast/broadcast.ts"() {
|
|
175
|
+
"use strict";
|
|
176
|
+
init_types();
|
|
177
|
+
BroadcastApiProvider = class extends BaseBroadcastProvider {
|
|
178
|
+
constructor(config) {
|
|
179
|
+
super(config);
|
|
180
|
+
this.name = "broadcast";
|
|
181
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
182
|
+
this.token = config.token;
|
|
183
|
+
if (!this.token) {
|
|
184
|
+
throw new BroadcastProviderError(
|
|
185
|
+
"Broadcast API token is required",
|
|
186
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
187
|
+
this.name
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Broadcast Management Methods
|
|
192
|
+
async list(options) {
|
|
193
|
+
try {
|
|
194
|
+
const params = new URLSearchParams();
|
|
195
|
+
if (options?.limit) params.append("limit", options.limit.toString());
|
|
196
|
+
if (options?.offset) params.append("offset", options.offset.toString());
|
|
197
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts?${params}`, {
|
|
198
|
+
method: "GET",
|
|
199
|
+
headers: {
|
|
200
|
+
"Authorization": `Bearer ${this.token}`,
|
|
201
|
+
"Content-Type": "application/json"
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const error = await response.text();
|
|
206
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
207
|
+
}
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
const broadcasts = data.data.map((broadcast) => this.transformBroadcastFromApi(broadcast));
|
|
210
|
+
return this.buildListResponse(broadcasts, data.total, options);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new BroadcastProviderError(
|
|
213
|
+
`Failed to list broadcasts: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
214
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
215
|
+
this.name,
|
|
216
|
+
error
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async get(id) {
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
223
|
+
method: "GET",
|
|
224
|
+
headers: {
|
|
225
|
+
"Authorization": `Bearer ${this.token}`,
|
|
226
|
+
"Content-Type": "application/json"
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
if (response.status === 404) {
|
|
231
|
+
throw new BroadcastProviderError(
|
|
232
|
+
`Broadcast not found: ${id}`,
|
|
233
|
+
"NOT_FOUND" /* NOT_FOUND */,
|
|
234
|
+
this.name
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const error = await response.text();
|
|
238
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
239
|
+
}
|
|
240
|
+
const broadcast = await response.json();
|
|
241
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
244
|
+
throw new BroadcastProviderError(
|
|
245
|
+
`Failed to get broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
246
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
247
|
+
this.name,
|
|
248
|
+
error
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async create(data) {
|
|
253
|
+
try {
|
|
254
|
+
this.validateRequiredFields(data, ["name", "subject", "content"]);
|
|
255
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts`, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: {
|
|
258
|
+
"Authorization": `Bearer ${this.token}`,
|
|
259
|
+
"Content-Type": "application/json"
|
|
260
|
+
},
|
|
261
|
+
body: JSON.stringify({
|
|
262
|
+
broadcast: {
|
|
263
|
+
name: data.name,
|
|
264
|
+
subject: data.subject,
|
|
265
|
+
preheader: data.preheader,
|
|
266
|
+
body: data.content,
|
|
267
|
+
html_body: true,
|
|
268
|
+
track_opens: data.trackOpens ?? true,
|
|
269
|
+
track_clicks: data.trackClicks ?? true,
|
|
270
|
+
reply_to: data.replyTo,
|
|
271
|
+
segment_ids: data.audienceIds
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
const error = await response.text();
|
|
277
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
278
|
+
}
|
|
279
|
+
const result = await response.json();
|
|
280
|
+
return this.get(result.id.toString());
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
283
|
+
throw new BroadcastProviderError(
|
|
284
|
+
`Failed to create broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
285
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
286
|
+
this.name,
|
|
287
|
+
error
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async update(id, data) {
|
|
292
|
+
try {
|
|
293
|
+
const existing = await this.get(id);
|
|
294
|
+
if (!this.canEditInStatus(existing.status)) {
|
|
295
|
+
throw new BroadcastProviderError(
|
|
296
|
+
`Cannot update broadcast in status: ${existing.status}`,
|
|
297
|
+
"INVALID_STATUS" /* INVALID_STATUS */,
|
|
298
|
+
this.name
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
302
|
+
method: "PATCH",
|
|
303
|
+
headers: {
|
|
304
|
+
"Authorization": `Bearer ${this.token}`,
|
|
305
|
+
"Content-Type": "application/json"
|
|
306
|
+
},
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
broadcast: {
|
|
309
|
+
name: data.name,
|
|
310
|
+
subject: data.subject,
|
|
311
|
+
preheader: data.preheader,
|
|
312
|
+
body: data.content,
|
|
313
|
+
track_opens: data.trackOpens,
|
|
314
|
+
track_clicks: data.trackClicks,
|
|
315
|
+
reply_to: data.replyTo,
|
|
316
|
+
segment_ids: data.audienceIds
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
const error = await response.text();
|
|
322
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
323
|
+
}
|
|
324
|
+
const broadcast = await response.json();
|
|
325
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
328
|
+
throw new BroadcastProviderError(
|
|
329
|
+
`Failed to update broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
330
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
331
|
+
this.name,
|
|
332
|
+
error
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async delete(id) {
|
|
337
|
+
try {
|
|
338
|
+
const existing = await this.get(id);
|
|
339
|
+
if (!this.canEditInStatus(existing.status)) {
|
|
340
|
+
throw new BroadcastProviderError(
|
|
341
|
+
`Cannot delete broadcast in status: ${existing.status}`,
|
|
342
|
+
"INVALID_STATUS" /* INVALID_STATUS */,
|
|
343
|
+
this.name
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
347
|
+
method: "DELETE",
|
|
348
|
+
headers: {
|
|
349
|
+
"Authorization": `Bearer ${this.token}`,
|
|
350
|
+
"Content-Type": "application/json"
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
const error = await response.text();
|
|
355
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
359
|
+
throw new BroadcastProviderError(
|
|
360
|
+
`Failed to delete broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
361
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
362
|
+
this.name,
|
|
363
|
+
error
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async send(id, options) {
|
|
368
|
+
try {
|
|
369
|
+
if (options?.testMode && options.testRecipients?.length) {
|
|
370
|
+
throw new BroadcastProviderError(
|
|
371
|
+
"Test send is not yet implemented for Broadcast provider",
|
|
372
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
373
|
+
this.name
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}/send_broadcast`, {
|
|
377
|
+
method: "POST",
|
|
378
|
+
headers: {
|
|
379
|
+
"Authorization": `Bearer ${this.token}`,
|
|
380
|
+
"Content-Type": "application/json"
|
|
381
|
+
},
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
segment_ids: options?.audienceIds
|
|
384
|
+
})
|
|
385
|
+
});
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
const error = await response.text();
|
|
388
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
389
|
+
}
|
|
390
|
+
const result = await response.json();
|
|
391
|
+
return this.get(result.id.toString());
|
|
392
|
+
} catch (error) {
|
|
393
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
394
|
+
throw new BroadcastProviderError(
|
|
395
|
+
`Failed to send broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
396
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
397
|
+
this.name,
|
|
398
|
+
error
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async schedule(id, scheduledAt) {
|
|
403
|
+
try {
|
|
404
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
405
|
+
method: "PATCH",
|
|
406
|
+
headers: {
|
|
407
|
+
"Authorization": `Bearer ${this.token}`,
|
|
408
|
+
"Content-Type": "application/json"
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
broadcast: {
|
|
412
|
+
scheduled_send_at: scheduledAt.toISOString(),
|
|
413
|
+
// TODO: Handle timezone properly
|
|
414
|
+
scheduled_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
});
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
const error = await response.text();
|
|
420
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
421
|
+
}
|
|
422
|
+
const broadcast = await response.json();
|
|
423
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
throw new BroadcastProviderError(
|
|
426
|
+
`Failed to schedule broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
427
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
428
|
+
this.name,
|
|
429
|
+
error
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async cancelSchedule(id) {
|
|
434
|
+
try {
|
|
435
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
436
|
+
method: "PATCH",
|
|
437
|
+
headers: {
|
|
438
|
+
"Authorization": `Bearer ${this.token}`,
|
|
439
|
+
"Content-Type": "application/json"
|
|
440
|
+
},
|
|
441
|
+
body: JSON.stringify({
|
|
442
|
+
broadcast: {
|
|
443
|
+
scheduled_send_at: null,
|
|
444
|
+
scheduled_timezone: null
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
});
|
|
448
|
+
if (!response.ok) {
|
|
449
|
+
const error = await response.text();
|
|
450
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
451
|
+
}
|
|
452
|
+
const broadcast = await response.json();
|
|
453
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
throw new BroadcastProviderError(
|
|
456
|
+
`Failed to cancel scheduled broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
457
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
458
|
+
this.name,
|
|
459
|
+
error
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async getAnalytics(_id) {
|
|
464
|
+
throw new BroadcastProviderError(
|
|
465
|
+
"Analytics API not yet implemented for Broadcast provider",
|
|
466
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
467
|
+
this.name
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
getCapabilities() {
|
|
471
|
+
return {
|
|
472
|
+
supportsScheduling: true,
|
|
473
|
+
supportsSegmentation: true,
|
|
474
|
+
supportsAnalytics: false,
|
|
475
|
+
// Not documented yet
|
|
476
|
+
supportsABTesting: false,
|
|
477
|
+
supportsTemplates: false,
|
|
478
|
+
supportsPersonalization: true,
|
|
479
|
+
supportsMultipleChannels: false,
|
|
480
|
+
supportsChannelSegmentation: false,
|
|
481
|
+
editableStatuses: ["draft" /* DRAFT */, "scheduled" /* SCHEDULED */],
|
|
482
|
+
supportedContentTypes: ["html", "text"]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async validateConfiguration() {
|
|
486
|
+
try {
|
|
487
|
+
await this.list({ limit: 1 });
|
|
488
|
+
return true;
|
|
489
|
+
} catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
transformBroadcastFromApi(broadcast) {
|
|
494
|
+
return {
|
|
495
|
+
id: broadcast.id.toString(),
|
|
496
|
+
name: broadcast.name,
|
|
497
|
+
subject: broadcast.subject,
|
|
498
|
+
preheader: broadcast.preheader,
|
|
499
|
+
content: broadcast.body,
|
|
500
|
+
status: this.mapBroadcastStatus(broadcast.status),
|
|
501
|
+
trackOpens: broadcast.track_opens,
|
|
502
|
+
trackClicks: broadcast.track_clicks,
|
|
503
|
+
replyTo: broadcast.reply_to,
|
|
504
|
+
recipientCount: broadcast.total_recipients,
|
|
505
|
+
sentAt: broadcast.sent_at ? new Date(broadcast.sent_at) : void 0,
|
|
506
|
+
scheduledAt: broadcast.scheduled_send_at ? new Date(broadcast.scheduled_send_at) : void 0,
|
|
507
|
+
createdAt: new Date(broadcast.created_at),
|
|
508
|
+
updatedAt: new Date(broadcast.updated_at),
|
|
509
|
+
providerData: { broadcast },
|
|
510
|
+
providerId: broadcast.id.toString(),
|
|
511
|
+
providerType: "broadcast"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
mapBroadcastStatus(status) {
|
|
515
|
+
const statusMap = {
|
|
516
|
+
"draft": "draft" /* DRAFT */,
|
|
517
|
+
"scheduled": "scheduled" /* SCHEDULED */,
|
|
518
|
+
"queueing": "sending" /* SENDING */,
|
|
519
|
+
"sending": "sending" /* SENDING */,
|
|
520
|
+
"sent": "sent" /* SENT */,
|
|
521
|
+
"failed": "failed" /* FAILED */,
|
|
522
|
+
"partial_failure": "failed" /* FAILED */,
|
|
523
|
+
"paused": "paused" /* PAUSED */,
|
|
524
|
+
"aborted": "canceled" /* CANCELED */
|
|
525
|
+
};
|
|
526
|
+
return statusMap[status] || "draft" /* DRAFT */;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// src/exports/collections.ts
|
|
533
|
+
var collections_exports = {};
|
|
534
|
+
__export(collections_exports, {
|
|
535
|
+
createBroadcastsCollection: () => createBroadcastsCollection,
|
|
536
|
+
createSubscribersCollection: () => createSubscribersCollection
|
|
537
|
+
});
|
|
538
|
+
module.exports = __toCommonJS(collections_exports);
|
|
539
|
+
|
|
540
|
+
// src/collections/Broadcasts.ts
|
|
541
|
+
init_types();
|
|
542
|
+
|
|
543
|
+
// src/fields/emailContent.ts
|
|
544
|
+
var import_richtext_lexical = require("@payloadcms/richtext-lexical");
|
|
545
|
+
|
|
546
|
+
// src/utils/blockValidation.ts
|
|
547
|
+
var EMAIL_INCOMPATIBLE_TYPES = [
|
|
548
|
+
"chart",
|
|
549
|
+
"dataTable",
|
|
550
|
+
"interactive",
|
|
551
|
+
"streamable",
|
|
552
|
+
"video",
|
|
553
|
+
"iframe",
|
|
554
|
+
"form",
|
|
555
|
+
"carousel",
|
|
556
|
+
"tabs",
|
|
557
|
+
"accordion",
|
|
558
|
+
"map"
|
|
559
|
+
];
|
|
560
|
+
var validateEmailBlocks = (blocks) => {
|
|
561
|
+
blocks.forEach((block) => {
|
|
562
|
+
if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
|
|
563
|
+
console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
|
|
564
|
+
}
|
|
565
|
+
const hasComplexFields = block.fields?.some((field) => {
|
|
566
|
+
const complexTypes = ["code", "json", "richText", "blocks", "array"];
|
|
567
|
+
return complexTypes.includes(field.type);
|
|
568
|
+
});
|
|
569
|
+
if (hasComplexFields) {
|
|
570
|
+
console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
var createEmailSafeBlocks = (customBlocks = []) => {
|
|
575
|
+
validateEmailBlocks(customBlocks);
|
|
576
|
+
const baseBlocks = [
|
|
577
|
+
{
|
|
578
|
+
slug: "button",
|
|
579
|
+
fields: [
|
|
580
|
+
{
|
|
581
|
+
name: "text",
|
|
582
|
+
type: "text",
|
|
583
|
+
label: "Button Text",
|
|
584
|
+
required: true
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "url",
|
|
588
|
+
type: "text",
|
|
589
|
+
label: "Button URL",
|
|
590
|
+
required: true,
|
|
591
|
+
admin: {
|
|
592
|
+
description: "Enter the full URL (including https://)"
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: "style",
|
|
597
|
+
type: "select",
|
|
598
|
+
label: "Button Style",
|
|
599
|
+
defaultValue: "primary",
|
|
600
|
+
options: [
|
|
601
|
+
{ label: "Primary", value: "primary" },
|
|
602
|
+
{ label: "Secondary", value: "secondary" },
|
|
603
|
+
{ label: "Outline", value: "outline" }
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
],
|
|
607
|
+
interfaceName: "EmailButton",
|
|
608
|
+
labels: {
|
|
609
|
+
singular: "Button",
|
|
610
|
+
plural: "Buttons"
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
slug: "divider",
|
|
615
|
+
fields: [
|
|
616
|
+
{
|
|
617
|
+
name: "style",
|
|
618
|
+
type: "select",
|
|
619
|
+
label: "Divider Style",
|
|
620
|
+
defaultValue: "solid",
|
|
621
|
+
options: [
|
|
622
|
+
{ label: "Solid", value: "solid" },
|
|
623
|
+
{ label: "Dashed", value: "dashed" },
|
|
624
|
+
{ label: "Dotted", value: "dotted" }
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
interfaceName: "EmailDivider",
|
|
629
|
+
labels: {
|
|
630
|
+
singular: "Divider",
|
|
631
|
+
plural: "Dividers"
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
];
|
|
635
|
+
return [
|
|
636
|
+
...baseBlocks,
|
|
637
|
+
...customBlocks
|
|
638
|
+
];
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// src/fields/emailContent.ts
|
|
642
|
+
var createEmailSafeFeatures = (additionalBlocks) => {
|
|
643
|
+
const baseBlocks = [
|
|
644
|
+
{
|
|
645
|
+
slug: "button",
|
|
646
|
+
fields: [
|
|
647
|
+
{
|
|
648
|
+
name: "text",
|
|
649
|
+
type: "text",
|
|
650
|
+
label: "Button Text",
|
|
651
|
+
required: true
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
name: "url",
|
|
655
|
+
type: "text",
|
|
656
|
+
label: "Button URL",
|
|
657
|
+
required: true,
|
|
658
|
+
admin: {
|
|
659
|
+
description: "Enter the full URL (including https://)"
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "style",
|
|
664
|
+
type: "select",
|
|
665
|
+
label: "Button Style",
|
|
666
|
+
defaultValue: "primary",
|
|
667
|
+
options: [
|
|
668
|
+
{ label: "Primary", value: "primary" },
|
|
669
|
+
{ label: "Secondary", value: "secondary" },
|
|
670
|
+
{ label: "Outline", value: "outline" }
|
|
671
|
+
]
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
interfaceName: "EmailButton",
|
|
675
|
+
labels: {
|
|
676
|
+
singular: "Button",
|
|
677
|
+
plural: "Buttons"
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
slug: "divider",
|
|
682
|
+
fields: [
|
|
683
|
+
{
|
|
684
|
+
name: "style",
|
|
685
|
+
type: "select",
|
|
686
|
+
label: "Divider Style",
|
|
687
|
+
defaultValue: "solid",
|
|
688
|
+
options: [
|
|
689
|
+
{ label: "Solid", value: "solid" },
|
|
690
|
+
{ label: "Dashed", value: "dashed" },
|
|
691
|
+
{ label: "Dotted", value: "dotted" }
|
|
692
|
+
]
|
|
693
|
+
}
|
|
694
|
+
],
|
|
695
|
+
interfaceName: "EmailDivider",
|
|
696
|
+
labels: {
|
|
697
|
+
singular: "Divider",
|
|
698
|
+
plural: "Dividers"
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
];
|
|
702
|
+
const allBlocks = [
|
|
703
|
+
...baseBlocks,
|
|
704
|
+
...additionalBlocks || []
|
|
705
|
+
];
|
|
706
|
+
return [
|
|
707
|
+
// Toolbars
|
|
708
|
+
(0, import_richtext_lexical.FixedToolbarFeature)(),
|
|
709
|
+
// Fixed toolbar at the top
|
|
710
|
+
(0, import_richtext_lexical.InlineToolbarFeature)(),
|
|
711
|
+
// Floating toolbar when text is selected
|
|
712
|
+
// Basic text formatting
|
|
713
|
+
(0, import_richtext_lexical.BoldFeature)(),
|
|
714
|
+
(0, import_richtext_lexical.ItalicFeature)(),
|
|
715
|
+
(0, import_richtext_lexical.UnderlineFeature)(),
|
|
716
|
+
(0, import_richtext_lexical.StrikethroughFeature)(),
|
|
717
|
+
// Links with enhanced configuration
|
|
718
|
+
(0, import_richtext_lexical.LinkFeature)({
|
|
719
|
+
fields: [
|
|
720
|
+
{
|
|
721
|
+
name: "url",
|
|
722
|
+
type: "text",
|
|
723
|
+
required: true,
|
|
724
|
+
admin: {
|
|
725
|
+
description: "Enter the full URL (including https://)"
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: "newTab",
|
|
730
|
+
type: "checkbox",
|
|
731
|
+
label: "Open in new tab",
|
|
732
|
+
defaultValue: false
|
|
733
|
+
}
|
|
734
|
+
]
|
|
735
|
+
}),
|
|
736
|
+
// Lists
|
|
737
|
+
(0, import_richtext_lexical.OrderedListFeature)(),
|
|
738
|
+
(0, import_richtext_lexical.UnorderedListFeature)(),
|
|
739
|
+
// Headings - limited to h1, h2, h3 for email compatibility
|
|
740
|
+
(0, import_richtext_lexical.HeadingFeature)({
|
|
741
|
+
enabledHeadingSizes: ["h1", "h2", "h3"]
|
|
742
|
+
}),
|
|
743
|
+
// Basic paragraph and alignment
|
|
744
|
+
(0, import_richtext_lexical.ParagraphFeature)(),
|
|
745
|
+
(0, import_richtext_lexical.AlignFeature)(),
|
|
746
|
+
// Blockquotes
|
|
747
|
+
(0, import_richtext_lexical.BlockquoteFeature)(),
|
|
748
|
+
// Upload feature for images
|
|
749
|
+
(0, import_richtext_lexical.UploadFeature)({
|
|
750
|
+
collections: {
|
|
751
|
+
media: {
|
|
752
|
+
fields: [
|
|
753
|
+
{
|
|
754
|
+
name: "caption",
|
|
755
|
+
type: "text",
|
|
756
|
+
admin: {
|
|
757
|
+
description: "Optional caption for the image"
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: "altText",
|
|
762
|
+
type: "text",
|
|
763
|
+
label: "Alt Text",
|
|
764
|
+
required: true,
|
|
765
|
+
admin: {
|
|
766
|
+
description: "Alternative text for accessibility and when image cannot be displayed"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
]
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}),
|
|
773
|
+
// Custom blocks for email-specific content
|
|
774
|
+
(0, import_richtext_lexical.BlocksFeature)({
|
|
775
|
+
blocks: allBlocks
|
|
776
|
+
})
|
|
777
|
+
];
|
|
778
|
+
};
|
|
779
|
+
var createEmailLexicalEditor = (customBlocks = []) => {
|
|
780
|
+
const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
|
|
781
|
+
return (0, import_richtext_lexical.lexicalEditor)({
|
|
782
|
+
features: [
|
|
783
|
+
// Toolbars
|
|
784
|
+
(0, import_richtext_lexical.FixedToolbarFeature)(),
|
|
785
|
+
(0, import_richtext_lexical.InlineToolbarFeature)(),
|
|
786
|
+
// Basic text formatting
|
|
787
|
+
(0, import_richtext_lexical.BoldFeature)(),
|
|
788
|
+
(0, import_richtext_lexical.ItalicFeature)(),
|
|
789
|
+
(0, import_richtext_lexical.UnderlineFeature)(),
|
|
790
|
+
(0, import_richtext_lexical.StrikethroughFeature)(),
|
|
791
|
+
// Links with enhanced configuration
|
|
792
|
+
(0, import_richtext_lexical.LinkFeature)({
|
|
793
|
+
fields: [
|
|
794
|
+
{
|
|
795
|
+
name: "url",
|
|
796
|
+
type: "text",
|
|
797
|
+
required: true,
|
|
798
|
+
admin: {
|
|
799
|
+
description: "Enter the full URL (including https://)"
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: "newTab",
|
|
804
|
+
type: "checkbox",
|
|
805
|
+
label: "Open in new tab",
|
|
806
|
+
defaultValue: false
|
|
807
|
+
}
|
|
808
|
+
]
|
|
809
|
+
}),
|
|
810
|
+
// Lists
|
|
811
|
+
(0, import_richtext_lexical.OrderedListFeature)(),
|
|
812
|
+
(0, import_richtext_lexical.UnorderedListFeature)(),
|
|
813
|
+
// Headings - limited to h1, h2, h3 for email compatibility
|
|
814
|
+
(0, import_richtext_lexical.HeadingFeature)({
|
|
815
|
+
enabledHeadingSizes: ["h1", "h2", "h3"]
|
|
816
|
+
}),
|
|
817
|
+
// Basic paragraph and alignment
|
|
818
|
+
(0, import_richtext_lexical.ParagraphFeature)(),
|
|
819
|
+
(0, import_richtext_lexical.AlignFeature)(),
|
|
820
|
+
// Blockquotes
|
|
821
|
+
(0, import_richtext_lexical.BlockquoteFeature)(),
|
|
822
|
+
// Upload feature for images
|
|
823
|
+
(0, import_richtext_lexical.UploadFeature)({
|
|
824
|
+
collections: {
|
|
825
|
+
media: {
|
|
826
|
+
fields: [
|
|
827
|
+
{
|
|
828
|
+
name: "caption",
|
|
829
|
+
type: "text",
|
|
830
|
+
admin: {
|
|
831
|
+
description: "Optional caption for the image"
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
name: "altText",
|
|
836
|
+
type: "text",
|
|
837
|
+
label: "Alt Text",
|
|
838
|
+
required: true,
|
|
839
|
+
admin: {
|
|
840
|
+
description: "Alternative text for accessibility and when image cannot be displayed"
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
]
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}),
|
|
847
|
+
// Email-safe blocks (processed server-side)
|
|
848
|
+
(0, import_richtext_lexical.BlocksFeature)({
|
|
849
|
+
blocks: emailSafeBlocks
|
|
850
|
+
})
|
|
851
|
+
]
|
|
852
|
+
});
|
|
853
|
+
};
|
|
854
|
+
var emailSafeFeatures = createEmailSafeFeatures();
|
|
855
|
+
var createEmailContentField = (overrides) => {
|
|
856
|
+
const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
|
|
857
|
+
return {
|
|
858
|
+
name: "content",
|
|
859
|
+
type: "richText",
|
|
860
|
+
required: true,
|
|
861
|
+
editor,
|
|
862
|
+
admin: {
|
|
863
|
+
description: "Email content with limited formatting for compatibility",
|
|
864
|
+
...overrides?.admin
|
|
865
|
+
},
|
|
866
|
+
...overrides
|
|
867
|
+
};
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// src/fields/broadcastInlinePreview.ts
|
|
871
|
+
var createBroadcastInlinePreviewField = () => {
|
|
872
|
+
return {
|
|
873
|
+
name: "broadcastInlinePreview",
|
|
874
|
+
type: "ui",
|
|
875
|
+
admin: {
|
|
876
|
+
components: {
|
|
877
|
+
Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// src/utils/emailSafeHtml.ts
|
|
884
|
+
var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
|
|
885
|
+
var EMAIL_SAFE_CONFIG = {
|
|
886
|
+
ALLOWED_TAGS: [
|
|
887
|
+
"p",
|
|
888
|
+
"br",
|
|
889
|
+
"strong",
|
|
890
|
+
"b",
|
|
891
|
+
"em",
|
|
892
|
+
"i",
|
|
893
|
+
"u",
|
|
894
|
+
"strike",
|
|
895
|
+
"s",
|
|
896
|
+
"span",
|
|
897
|
+
"a",
|
|
898
|
+
"h1",
|
|
899
|
+
"h2",
|
|
900
|
+
"h3",
|
|
901
|
+
"ul",
|
|
902
|
+
"ol",
|
|
903
|
+
"li",
|
|
904
|
+
"blockquote",
|
|
905
|
+
"hr",
|
|
906
|
+
"img",
|
|
907
|
+
"div",
|
|
908
|
+
"table",
|
|
909
|
+
"tr",
|
|
910
|
+
"td",
|
|
911
|
+
"th",
|
|
912
|
+
"tbody",
|
|
913
|
+
"thead"
|
|
914
|
+
],
|
|
915
|
+
ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
|
|
916
|
+
ALLOWED_STYLES: {
|
|
917
|
+
"*": [
|
|
918
|
+
"color",
|
|
919
|
+
"background-color",
|
|
920
|
+
"font-size",
|
|
921
|
+
"font-weight",
|
|
922
|
+
"font-style",
|
|
923
|
+
"text-decoration",
|
|
924
|
+
"text-align",
|
|
925
|
+
"margin",
|
|
926
|
+
"margin-top",
|
|
927
|
+
"margin-right",
|
|
928
|
+
"margin-bottom",
|
|
929
|
+
"margin-left",
|
|
930
|
+
"padding",
|
|
931
|
+
"padding-top",
|
|
932
|
+
"padding-right",
|
|
933
|
+
"padding-bottom",
|
|
934
|
+
"padding-left",
|
|
935
|
+
"line-height",
|
|
936
|
+
"border-left",
|
|
937
|
+
"border-left-width",
|
|
938
|
+
"border-left-style",
|
|
939
|
+
"border-left-color"
|
|
940
|
+
]
|
|
941
|
+
},
|
|
942
|
+
FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
|
|
943
|
+
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
|
|
944
|
+
};
|
|
945
|
+
async function convertToEmailSafeHtml(editorState, options) {
|
|
946
|
+
const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
|
|
947
|
+
const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
|
|
948
|
+
if (options?.wrapInTemplate) {
|
|
949
|
+
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
|
|
950
|
+
}
|
|
951
|
+
return sanitizedHtml;
|
|
952
|
+
}
|
|
953
|
+
async function lexicalToEmailHtml(editorState, mediaUrl) {
|
|
954
|
+
const { root } = editorState;
|
|
955
|
+
if (!root || !root.children) {
|
|
956
|
+
return "";
|
|
957
|
+
}
|
|
958
|
+
const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
|
|
959
|
+
return html;
|
|
960
|
+
}
|
|
961
|
+
function convertNode(node, mediaUrl) {
|
|
962
|
+
switch (node.type) {
|
|
963
|
+
case "paragraph":
|
|
964
|
+
return convertParagraph(node, mediaUrl);
|
|
965
|
+
case "heading":
|
|
966
|
+
return convertHeading(node, mediaUrl);
|
|
967
|
+
case "list":
|
|
968
|
+
return convertList(node, mediaUrl);
|
|
969
|
+
case "listitem":
|
|
970
|
+
return convertListItem(node, mediaUrl);
|
|
971
|
+
case "blockquote":
|
|
972
|
+
return convertBlockquote(node, mediaUrl);
|
|
973
|
+
case "text":
|
|
974
|
+
return convertText(node);
|
|
975
|
+
case "link":
|
|
976
|
+
return convertLink(node, mediaUrl);
|
|
977
|
+
case "linebreak":
|
|
978
|
+
return "<br>";
|
|
979
|
+
case "upload":
|
|
980
|
+
return convertUpload(node, mediaUrl);
|
|
981
|
+
case "block":
|
|
982
|
+
return convertBlock(node, mediaUrl);
|
|
983
|
+
default:
|
|
984
|
+
if (node.children) {
|
|
985
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
986
|
+
}
|
|
987
|
+
return "";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function convertParagraph(node, mediaUrl) {
|
|
991
|
+
const align = getAlignment(node.format);
|
|
992
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
993
|
+
if (!children.trim()) {
|
|
994
|
+
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
|
|
995
|
+
}
|
|
996
|
+
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
|
|
997
|
+
}
|
|
998
|
+
function convertHeading(node, mediaUrl) {
|
|
999
|
+
const tag = node.tag || "h1";
|
|
1000
|
+
const align = getAlignment(node.format);
|
|
1001
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1002
|
+
const styles2 = {
|
|
1003
|
+
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
|
|
1004
|
+
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
|
|
1005
|
+
h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
|
|
1006
|
+
};
|
|
1007
|
+
const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
|
|
1008
|
+
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
1009
|
+
}
|
|
1010
|
+
function convertList(node, mediaUrl) {
|
|
1011
|
+
const tag = node.listType === "number" ? "ol" : "ul";
|
|
1012
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1013
|
+
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
|
|
1014
|
+
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
1015
|
+
}
|
|
1016
|
+
function convertListItem(node, mediaUrl) {
|
|
1017
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1018
|
+
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
|
|
1019
|
+
}
|
|
1020
|
+
function convertBlockquote(node, mediaUrl) {
|
|
1021
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1022
|
+
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
|
|
1023
|
+
return `<blockquote style="${style}">${children}</blockquote>`;
|
|
1024
|
+
}
|
|
1025
|
+
function convertText(node) {
|
|
1026
|
+
let text = escapeHtml(node.text || "");
|
|
1027
|
+
if (node.format & 1) {
|
|
1028
|
+
text = `<strong>${text}</strong>`;
|
|
1029
|
+
}
|
|
1030
|
+
if (node.format & 2) {
|
|
1031
|
+
text = `<em>${text}</em>`;
|
|
1032
|
+
}
|
|
1033
|
+
if (node.format & 8) {
|
|
1034
|
+
text = `<u>${text}</u>`;
|
|
1035
|
+
}
|
|
1036
|
+
if (node.format & 4) {
|
|
1037
|
+
text = `<strike>${text}</strike>`;
|
|
1038
|
+
}
|
|
1039
|
+
return text;
|
|
1040
|
+
}
|
|
1041
|
+
function convertLink(node, mediaUrl) {
|
|
1042
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1043
|
+
const url = node.fields?.url || "#";
|
|
1044
|
+
const newTab = node.fields?.newTab ?? false;
|
|
1045
|
+
const targetAttr = newTab ? ' target="_blank"' : "";
|
|
1046
|
+
const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
|
|
1047
|
+
return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
|
|
1048
|
+
}
|
|
1049
|
+
function convertUpload(node, mediaUrl) {
|
|
1050
|
+
const upload = node.value;
|
|
1051
|
+
if (!upload) return "";
|
|
1052
|
+
let src = "";
|
|
1053
|
+
if (typeof upload === "string") {
|
|
1054
|
+
src = upload;
|
|
1055
|
+
} else if (upload.url) {
|
|
1056
|
+
src = upload.url;
|
|
1057
|
+
} else if (upload.filename && mediaUrl) {
|
|
1058
|
+
src = `${mediaUrl}/${upload.filename}`;
|
|
1059
|
+
}
|
|
1060
|
+
const alt = node.fields?.altText || upload.alt || "";
|
|
1061
|
+
const caption = node.fields?.caption || "";
|
|
1062
|
+
const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
|
|
1063
|
+
if (caption) {
|
|
1064
|
+
return `
|
|
1065
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
1066
|
+
${imgHtml}
|
|
1067
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
|
|
1068
|
+
</div>
|
|
1069
|
+
`;
|
|
1070
|
+
}
|
|
1071
|
+
return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
|
|
1072
|
+
}
|
|
1073
|
+
function convertBlock(node, mediaUrl) {
|
|
1074
|
+
const blockType = node.fields?.blockName;
|
|
1075
|
+
switch (blockType) {
|
|
1076
|
+
case "button":
|
|
1077
|
+
return convertButtonBlock(node.fields);
|
|
1078
|
+
case "divider":
|
|
1079
|
+
return convertDividerBlock(node.fields);
|
|
1080
|
+
default:
|
|
1081
|
+
if (node.children) {
|
|
1082
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
1083
|
+
}
|
|
1084
|
+
return "";
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function convertButtonBlock(fields) {
|
|
1088
|
+
const text = fields?.text || "Click here";
|
|
1089
|
+
const url = fields?.url || "#";
|
|
1090
|
+
const style = fields?.style || "primary";
|
|
1091
|
+
const styles2 = {
|
|
1092
|
+
primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
|
|
1093
|
+
secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
|
|
1094
|
+
outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
|
|
1095
|
+
};
|
|
1096
|
+
const buttonStyle = `${styles2[style] || styles2.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
|
|
1097
|
+
return `
|
|
1098
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
1099
|
+
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
|
|
1100
|
+
</div>
|
|
1101
|
+
`;
|
|
1102
|
+
}
|
|
1103
|
+
function convertDividerBlock(fields) {
|
|
1104
|
+
const style = fields?.style || "solid";
|
|
1105
|
+
const styles2 = {
|
|
1106
|
+
solid: "border-top: 1px solid #e5e7eb;",
|
|
1107
|
+
dashed: "border-top: 1px dashed #e5e7eb;",
|
|
1108
|
+
dotted: "border-top: 1px dotted #e5e7eb;"
|
|
1109
|
+
};
|
|
1110
|
+
return `<hr style="${styles2[style] || styles2.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
|
|
1111
|
+
}
|
|
1112
|
+
function getAlignment(format) {
|
|
1113
|
+
if (!format) return "left";
|
|
1114
|
+
if (format & 2) return "center";
|
|
1115
|
+
if (format & 3) return "right";
|
|
1116
|
+
if (format & 4) return "justify";
|
|
1117
|
+
return "left";
|
|
1118
|
+
}
|
|
1119
|
+
function escapeHtml(text) {
|
|
1120
|
+
const map = {
|
|
1121
|
+
"&": "&",
|
|
1122
|
+
"<": "<",
|
|
1123
|
+
">": ">",
|
|
1124
|
+
'"': """,
|
|
1125
|
+
"'": "'"
|
|
1126
|
+
};
|
|
1127
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
1128
|
+
}
|
|
1129
|
+
function wrapInEmailTemplate(content, preheader) {
|
|
1130
|
+
return `<!DOCTYPE html>
|
|
1131
|
+
<html lang="en">
|
|
1132
|
+
<head>
|
|
1133
|
+
<meta charset="UTF-8">
|
|
1134
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1135
|
+
<title>Email</title>
|
|
1136
|
+
<!--[if mso]>
|
|
1137
|
+
<noscript>
|
|
1138
|
+
<xml>
|
|
1139
|
+
<o:OfficeDocumentSettings>
|
|
1140
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
1141
|
+
</o:OfficeDocumentSettings>
|
|
1142
|
+
</xml>
|
|
1143
|
+
</noscript>
|
|
1144
|
+
<![endif]-->
|
|
1145
|
+
</head>
|
|
1146
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; background-color: #f3f4f6;">
|
|
1147
|
+
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
|
|
1148
|
+
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
|
|
1149
|
+
<tr>
|
|
1150
|
+
<td align="center" style="padding: 20px 0;">
|
|
1151
|
+
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
|
|
1152
|
+
<tr>
|
|
1153
|
+
<td style="padding: 40px 30px;">
|
|
1154
|
+
${content}
|
|
1155
|
+
</td>
|
|
1156
|
+
</tr>
|
|
1157
|
+
</table>
|
|
1158
|
+
</td>
|
|
1159
|
+
</tr>
|
|
1160
|
+
</table>
|
|
1161
|
+
</body>
|
|
1162
|
+
</html>`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/collections/Broadcasts.ts
|
|
1166
|
+
var createBroadcastsCollection = (pluginConfig) => {
|
|
1167
|
+
const hasProviders = !!(pluginConfig.providers?.broadcast || pluginConfig.providers?.resend);
|
|
1168
|
+
const customizations = pluginConfig.customizations?.broadcasts;
|
|
1169
|
+
return {
|
|
1170
|
+
slug: "broadcasts",
|
|
1171
|
+
labels: {
|
|
1172
|
+
singular: "Broadcast",
|
|
1173
|
+
plural: "Broadcasts"
|
|
1174
|
+
},
|
|
1175
|
+
admin: {
|
|
1176
|
+
useAsTitle: "subject",
|
|
1177
|
+
description: "Individual email campaigns sent to subscribers",
|
|
1178
|
+
defaultColumns: ["subject", "status", "sentAt", "recipientCount", "actions"]
|
|
1179
|
+
},
|
|
1180
|
+
fields: [
|
|
1181
|
+
{
|
|
1182
|
+
name: "subject",
|
|
1183
|
+
type: "text",
|
|
1184
|
+
required: true,
|
|
1185
|
+
admin: {
|
|
1186
|
+
description: "Email subject line"
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
// Add any additional fields from customizations after subject
|
|
1190
|
+
...customizations?.additionalFields || [],
|
|
1191
|
+
{
|
|
1192
|
+
type: "row",
|
|
1193
|
+
fields: [
|
|
1194
|
+
{
|
|
1195
|
+
name: "contentSection",
|
|
1196
|
+
type: "group",
|
|
1197
|
+
label: false,
|
|
1198
|
+
admin: {
|
|
1199
|
+
width: "50%",
|
|
1200
|
+
style: {
|
|
1201
|
+
paddingRight: "1rem"
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
fields: [
|
|
1205
|
+
{
|
|
1206
|
+
name: "preheader",
|
|
1207
|
+
type: "text",
|
|
1208
|
+
admin: {
|
|
1209
|
+
description: "Preview text shown in email clients"
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
// Apply content field customization if provided
|
|
1213
|
+
// Process blocks server-side to avoid client serialization issues
|
|
1214
|
+
(() => {
|
|
1215
|
+
const emailEditor = createEmailLexicalEditor(customizations?.customBlocks);
|
|
1216
|
+
const baseField = createEmailContentField({
|
|
1217
|
+
admin: { description: "Email content" },
|
|
1218
|
+
editor: emailEditor
|
|
1219
|
+
});
|
|
1220
|
+
return customizations?.fieldOverrides?.content ? customizations.fieldOverrides.content(baseField) : baseField;
|
|
1221
|
+
})()
|
|
1222
|
+
]
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
name: "previewSection",
|
|
1226
|
+
type: "group",
|
|
1227
|
+
label: false,
|
|
1228
|
+
admin: {
|
|
1229
|
+
width: "50%"
|
|
1230
|
+
},
|
|
1231
|
+
fields: [
|
|
1232
|
+
createBroadcastInlinePreviewField()
|
|
1233
|
+
]
|
|
1234
|
+
}
|
|
1235
|
+
]
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
name: "status",
|
|
1239
|
+
type: "select",
|
|
1240
|
+
required: true,
|
|
1241
|
+
defaultValue: "draft" /* DRAFT */,
|
|
1242
|
+
options: [
|
|
1243
|
+
{ label: "Draft", value: "draft" /* DRAFT */ },
|
|
1244
|
+
{ label: "Scheduled", value: "scheduled" /* SCHEDULED */ },
|
|
1245
|
+
{ label: "Sending", value: "sending" /* SENDING */ },
|
|
1246
|
+
{ label: "Sent", value: "sent" /* SENT */ },
|
|
1247
|
+
{ label: "Failed", value: "failed" /* FAILED */ },
|
|
1248
|
+
{ label: "Paused", value: "paused" /* PAUSED */ },
|
|
1249
|
+
{ label: "Canceled", value: "canceled" /* CANCELED */ }
|
|
1250
|
+
],
|
|
1251
|
+
admin: {
|
|
1252
|
+
readOnly: true,
|
|
1253
|
+
components: {
|
|
1254
|
+
Cell: "payload-plugin-newsletter/components#StatusBadge"
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
name: "settings",
|
|
1260
|
+
type: "group",
|
|
1261
|
+
fields: [
|
|
1262
|
+
{
|
|
1263
|
+
name: "trackOpens",
|
|
1264
|
+
type: "checkbox",
|
|
1265
|
+
defaultValue: true,
|
|
1266
|
+
admin: {
|
|
1267
|
+
description: "Track when recipients open this email"
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
name: "trackClicks",
|
|
1272
|
+
type: "checkbox",
|
|
1273
|
+
defaultValue: true,
|
|
1274
|
+
admin: {
|
|
1275
|
+
description: "Track when recipients click links"
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
name: "replyTo",
|
|
1280
|
+
type: "email",
|
|
1281
|
+
admin: {
|
|
1282
|
+
description: "Override the channel reply-to address for this broadcast"
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
]
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
name: "audienceIds",
|
|
1289
|
+
type: "array",
|
|
1290
|
+
fields: [
|
|
1291
|
+
{
|
|
1292
|
+
name: "audienceId",
|
|
1293
|
+
type: "text",
|
|
1294
|
+
required: true
|
|
1295
|
+
}
|
|
1296
|
+
],
|
|
1297
|
+
admin: {
|
|
1298
|
+
description: "Target specific audience segments",
|
|
1299
|
+
condition: () => {
|
|
1300
|
+
return hasProviders;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
name: "analytics",
|
|
1306
|
+
type: "group",
|
|
1307
|
+
admin: {
|
|
1308
|
+
readOnly: true,
|
|
1309
|
+
condition: (data) => data?.status === "sent" /* SENT */
|
|
1310
|
+
},
|
|
1311
|
+
fields: [
|
|
1312
|
+
{
|
|
1313
|
+
name: "recipientCount",
|
|
1314
|
+
type: "number",
|
|
1315
|
+
defaultValue: 0
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
name: "sent",
|
|
1319
|
+
type: "number",
|
|
1320
|
+
defaultValue: 0
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
name: "delivered",
|
|
1324
|
+
type: "number",
|
|
1325
|
+
defaultValue: 0
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
name: "opened",
|
|
1329
|
+
type: "number",
|
|
1330
|
+
defaultValue: 0
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
name: "clicked",
|
|
1334
|
+
type: "number",
|
|
1335
|
+
defaultValue: 0
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
name: "bounced",
|
|
1339
|
+
type: "number",
|
|
1340
|
+
defaultValue: 0
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
name: "complained",
|
|
1344
|
+
type: "number",
|
|
1345
|
+
defaultValue: 0
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
name: "unsubscribed",
|
|
1349
|
+
type: "number",
|
|
1350
|
+
defaultValue: 0
|
|
1351
|
+
}
|
|
1352
|
+
]
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: "sentAt",
|
|
1356
|
+
type: "date",
|
|
1357
|
+
admin: {
|
|
1358
|
+
readOnly: true,
|
|
1359
|
+
date: {
|
|
1360
|
+
displayFormat: "MMM d, yyyy h:mm a"
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
name: "scheduledAt",
|
|
1366
|
+
type: "date",
|
|
1367
|
+
admin: {
|
|
1368
|
+
condition: (data) => data?.status === "scheduled" /* SCHEDULED */,
|
|
1369
|
+
date: {
|
|
1370
|
+
displayFormat: "MMM d, yyyy h:mm a"
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
name: "providerId",
|
|
1376
|
+
type: "text",
|
|
1377
|
+
admin: {
|
|
1378
|
+
readOnly: true,
|
|
1379
|
+
description: "ID from the email provider",
|
|
1380
|
+
condition: (data) => hasProviders && data?.providerId
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
name: "providerData",
|
|
1385
|
+
type: "json",
|
|
1386
|
+
admin: {
|
|
1387
|
+
readOnly: true,
|
|
1388
|
+
condition: () => false
|
|
1389
|
+
// Hidden by default
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
// UI Field for custom actions in list view
|
|
1393
|
+
{
|
|
1394
|
+
name: "actions",
|
|
1395
|
+
type: "ui",
|
|
1396
|
+
admin: {
|
|
1397
|
+
components: {
|
|
1398
|
+
Cell: "payload-plugin-newsletter/components#ActionsCell",
|
|
1399
|
+
Field: "payload-plugin-newsletter/components#EmptyField"
|
|
1400
|
+
},
|
|
1401
|
+
disableListColumn: false
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
],
|
|
1405
|
+
hooks: {
|
|
1406
|
+
// Sync with provider on create
|
|
1407
|
+
afterChange: [
|
|
1408
|
+
async ({ doc, operation, req }) => {
|
|
1409
|
+
if (!hasProviders || operation !== "create") return doc;
|
|
1410
|
+
try {
|
|
1411
|
+
const providerConfig = pluginConfig.providers?.broadcast;
|
|
1412
|
+
if (!providerConfig) {
|
|
1413
|
+
req.payload.logger.error("Broadcast provider not configured");
|
|
1414
|
+
return doc;
|
|
1415
|
+
}
|
|
1416
|
+
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
1417
|
+
const provider = new BroadcastApiProvider2(providerConfig);
|
|
1418
|
+
const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
|
|
1419
|
+
const providerBroadcast = await provider.create({
|
|
1420
|
+
name: doc.subject,
|
|
1421
|
+
// Use subject as name since we removed the name field
|
|
1422
|
+
subject: doc.subject,
|
|
1423
|
+
preheader: doc.contentSection?.preheader,
|
|
1424
|
+
content: htmlContent,
|
|
1425
|
+
trackOpens: doc.settings?.trackOpens,
|
|
1426
|
+
trackClicks: doc.settings?.trackClicks,
|
|
1427
|
+
replyTo: doc.settings?.replyTo || providerConfig.replyTo,
|
|
1428
|
+
audienceIds: doc.audienceIds?.map((a) => a.audienceId)
|
|
1429
|
+
});
|
|
1430
|
+
await req.payload.update({
|
|
1431
|
+
collection: "broadcasts",
|
|
1432
|
+
id: doc.id,
|
|
1433
|
+
data: {
|
|
1434
|
+
providerId: providerBroadcast.id,
|
|
1435
|
+
providerData: providerBroadcast.providerData
|
|
1436
|
+
},
|
|
1437
|
+
req
|
|
1438
|
+
});
|
|
1439
|
+
return {
|
|
1440
|
+
...doc,
|
|
1441
|
+
providerId: providerBroadcast.id,
|
|
1442
|
+
providerData: providerBroadcast.providerData
|
|
1443
|
+
};
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
req.payload.logger.error("Failed to create broadcast in provider:", error);
|
|
1446
|
+
return doc;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
],
|
|
1450
|
+
// Sync updates with provider
|
|
1451
|
+
beforeChange: [
|
|
1452
|
+
async ({ data, originalDoc, operation, req }) => {
|
|
1453
|
+
if (!hasProviders || !originalDoc?.providerId || operation !== "update") return data;
|
|
1454
|
+
try {
|
|
1455
|
+
const providerConfig = pluginConfig.providers?.broadcast;
|
|
1456
|
+
if (!providerConfig) {
|
|
1457
|
+
req.payload.logger.error("Broadcast provider not configured");
|
|
1458
|
+
return data;
|
|
1459
|
+
}
|
|
1460
|
+
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
1461
|
+
const provider = new BroadcastApiProvider2(providerConfig);
|
|
1462
|
+
const capabilities = provider.getCapabilities();
|
|
1463
|
+
if (!capabilities.editableStatuses.includes(originalDoc.status)) {
|
|
1464
|
+
return data;
|
|
1465
|
+
}
|
|
1466
|
+
const updates = {};
|
|
1467
|
+
if (data.subject !== originalDoc.subject) {
|
|
1468
|
+
updates.name = data.subject;
|
|
1469
|
+
updates.subject = data.subject;
|
|
1470
|
+
}
|
|
1471
|
+
if (data.contentSection?.preheader !== originalDoc.contentSection?.preheader) updates.preheader = data.contentSection?.preheader;
|
|
1472
|
+
if (data.contentSection?.content !== originalDoc.contentSection?.content) {
|
|
1473
|
+
updates.content = await convertToEmailSafeHtml(data.contentSection?.content);
|
|
1474
|
+
}
|
|
1475
|
+
if (data.settings?.trackOpens !== originalDoc.settings?.trackOpens) {
|
|
1476
|
+
updates.trackOpens = data.settings.trackOpens;
|
|
1477
|
+
}
|
|
1478
|
+
if (data.settings?.trackClicks !== originalDoc.settings?.trackClicks) {
|
|
1479
|
+
updates.trackClicks = data.settings.trackClicks;
|
|
1480
|
+
}
|
|
1481
|
+
if (data.settings?.replyTo !== originalDoc.settings?.replyTo) {
|
|
1482
|
+
updates.replyTo = data.settings.replyTo || providerConfig.replyTo;
|
|
1483
|
+
}
|
|
1484
|
+
if (JSON.stringify(data.audienceIds) !== JSON.stringify(originalDoc.audienceIds)) {
|
|
1485
|
+
updates.audienceIds = data.audienceIds?.map((a) => a.audienceId);
|
|
1486
|
+
}
|
|
1487
|
+
if (Object.keys(updates).length > 0) {
|
|
1488
|
+
await provider.update(originalDoc.providerId, updates);
|
|
1489
|
+
}
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
req.payload.logger.error("Failed to update broadcast in provider:", error);
|
|
1492
|
+
}
|
|
1493
|
+
return data;
|
|
1494
|
+
}
|
|
1495
|
+
],
|
|
1496
|
+
// Handle deletion
|
|
1497
|
+
afterDelete: [
|
|
1498
|
+
async ({ doc, req }) => {
|
|
1499
|
+
if (!hasProviders || !doc?.providerId) return doc;
|
|
1500
|
+
try {
|
|
1501
|
+
const providerConfig = pluginConfig.providers?.broadcast;
|
|
1502
|
+
if (!providerConfig) {
|
|
1503
|
+
req.payload.logger.error("Broadcast provider not configured");
|
|
1504
|
+
return doc;
|
|
1505
|
+
}
|
|
1506
|
+
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
1507
|
+
const provider = new BroadcastApiProvider2(providerConfig);
|
|
1508
|
+
const capabilities = provider.getCapabilities();
|
|
1509
|
+
if (capabilities.editableStatuses.includes(doc.status)) {
|
|
1510
|
+
await provider.delete(doc.providerId);
|
|
1511
|
+
}
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
req.payload.logger.error("Failed to delete broadcast from provider:", error);
|
|
1514
|
+
}
|
|
1515
|
+
return doc;
|
|
1516
|
+
}
|
|
1517
|
+
]
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
// src/utils/access.ts
|
|
1523
|
+
var isAdmin = (user, config) => {
|
|
1524
|
+
if (!user || user.collection !== "users") {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
if (config?.access?.isAdmin) {
|
|
1528
|
+
return config.access.isAdmin(user);
|
|
1529
|
+
}
|
|
1530
|
+
if (user.roles?.includes("admin")) {
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
if (user.isAdmin === true) {
|
|
1534
|
+
return true;
|
|
1535
|
+
}
|
|
1536
|
+
if (user.role === "admin") {
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
if (user.admin === true) {
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
return false;
|
|
1543
|
+
};
|
|
1544
|
+
var adminOnly = (config) => ({ req }) => {
|
|
1545
|
+
const user = req.user;
|
|
1546
|
+
return isAdmin(user, config);
|
|
1547
|
+
};
|
|
1548
|
+
var adminOrSelf = (config) => ({ req, id }) => {
|
|
1549
|
+
const user = req.user;
|
|
1550
|
+
if (!user) {
|
|
1551
|
+
if (!id) {
|
|
1552
|
+
return {
|
|
1553
|
+
id: {
|
|
1554
|
+
equals: "unauthorized-no-access"
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
if (isAdmin(user, config)) {
|
|
1561
|
+
return true;
|
|
1562
|
+
}
|
|
1563
|
+
if (user.collection === "subscribers") {
|
|
1564
|
+
if (!id) {
|
|
1565
|
+
return {
|
|
1566
|
+
id: {
|
|
1567
|
+
equals: user.id
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
return id === user.id;
|
|
1572
|
+
}
|
|
1573
|
+
if (!id) {
|
|
1574
|
+
return {
|
|
1575
|
+
id: {
|
|
1576
|
+
equals: "unauthorized-no-access"
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
return false;
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
// src/emails/render.tsx
|
|
1584
|
+
var import_render = require("@react-email/render");
|
|
1585
|
+
|
|
1586
|
+
// src/emails/MagicLink.tsx
|
|
1587
|
+
var import_components = require("@react-email/components");
|
|
1588
|
+
|
|
1589
|
+
// src/emails/styles.ts
|
|
1590
|
+
var styles = {
|
|
1591
|
+
main: {
|
|
1592
|
+
backgroundColor: "#f6f9fc",
|
|
1593
|
+
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif'
|
|
1594
|
+
},
|
|
1595
|
+
container: {
|
|
1596
|
+
backgroundColor: "#ffffff",
|
|
1597
|
+
border: "1px solid #f0f0f0",
|
|
1598
|
+
borderRadius: "5px",
|
|
1599
|
+
margin: "0 auto",
|
|
1600
|
+
padding: "45px",
|
|
1601
|
+
marginBottom: "64px",
|
|
1602
|
+
maxWidth: "500px"
|
|
1603
|
+
},
|
|
1604
|
+
heading: {
|
|
1605
|
+
fontSize: "24px",
|
|
1606
|
+
letterSpacing: "-0.5px",
|
|
1607
|
+
lineHeight: "1.3",
|
|
1608
|
+
fontWeight: "600",
|
|
1609
|
+
color: "#484848",
|
|
1610
|
+
margin: "0 0 20px",
|
|
1611
|
+
padding: "0"
|
|
1612
|
+
},
|
|
1613
|
+
text: {
|
|
1614
|
+
fontSize: "16px",
|
|
1615
|
+
lineHeight: "26px",
|
|
1616
|
+
fontWeight: "400",
|
|
1617
|
+
color: "#484848",
|
|
1618
|
+
margin: "16px 0"
|
|
1619
|
+
},
|
|
1620
|
+
button: {
|
|
1621
|
+
backgroundColor: "#000000",
|
|
1622
|
+
borderRadius: "5px",
|
|
1623
|
+
color: "#fff",
|
|
1624
|
+
fontSize: "16px",
|
|
1625
|
+
fontWeight: "bold",
|
|
1626
|
+
textDecoration: "none",
|
|
1627
|
+
textAlign: "center",
|
|
1628
|
+
display: "block",
|
|
1629
|
+
width: "100%",
|
|
1630
|
+
padding: "14px 20px",
|
|
1631
|
+
margin: "30px 0"
|
|
1632
|
+
},
|
|
1633
|
+
link: {
|
|
1634
|
+
color: "#2754C5",
|
|
1635
|
+
fontSize: "14px",
|
|
1636
|
+
textDecoration: "underline",
|
|
1637
|
+
wordBreak: "break-all"
|
|
1638
|
+
},
|
|
1639
|
+
hr: {
|
|
1640
|
+
borderColor: "#e6ebf1",
|
|
1641
|
+
margin: "30px 0"
|
|
1642
|
+
},
|
|
1643
|
+
footer: {
|
|
1644
|
+
fontSize: "14px",
|
|
1645
|
+
lineHeight: "24px",
|
|
1646
|
+
color: "#9ca2ac",
|
|
1647
|
+
textAlign: "center",
|
|
1648
|
+
margin: "0"
|
|
1649
|
+
},
|
|
1650
|
+
code: {
|
|
1651
|
+
display: "inline-block",
|
|
1652
|
+
padding: "16px",
|
|
1653
|
+
width: "100%",
|
|
1654
|
+
backgroundColor: "#f4f4f4",
|
|
1655
|
+
borderRadius: "5px",
|
|
1656
|
+
border: "1px solid #eee",
|
|
1657
|
+
fontSize: "14px",
|
|
1658
|
+
fontFamily: "monospace",
|
|
1659
|
+
textAlign: "center",
|
|
1660
|
+
margin: "24px 0"
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// src/emails/MagicLink.tsx
|
|
1665
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
1666
|
+
var MagicLinkEmail = ({
|
|
1667
|
+
magicLink,
|
|
1668
|
+
email,
|
|
1669
|
+
siteName = "Newsletter",
|
|
1670
|
+
expiresIn = "24 hours"
|
|
1671
|
+
}) => {
|
|
1672
|
+
const previewText = `Sign in to ${siteName}`;
|
|
1673
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Html, { children: [
|
|
1674
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Head, {}),
|
|
1675
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Preview, { children: previewText }),
|
|
1676
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Container, { style: styles.container, children: [
|
|
1677
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.heading, children: [
|
|
1678
|
+
"Sign in to ",
|
|
1679
|
+
siteName
|
|
1680
|
+
] }),
|
|
1681
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.text, children: [
|
|
1682
|
+
"Hi ",
|
|
1683
|
+
email.split("@")[0],
|
|
1684
|
+
","
|
|
1685
|
+
] }),
|
|
1686
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.text, children: [
|
|
1687
|
+
"We received a request to sign in to your ",
|
|
1688
|
+
siteName,
|
|
1689
|
+
" account. Click the button below to complete your sign in:"
|
|
1690
|
+
] }),
|
|
1691
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Button, { href: magicLink, style: styles.button, children: [
|
|
1692
|
+
"Sign in to ",
|
|
1693
|
+
siteName
|
|
1694
|
+
] }),
|
|
1695
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Text, { style: styles.text, children: "Or copy and paste this URL into your browser:" }),
|
|
1696
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: styles.code, children: magicLink }),
|
|
1697
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Hr, { style: styles.hr }),
|
|
1698
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.footer, children: [
|
|
1699
|
+
"This link will expire in ",
|
|
1700
|
+
expiresIn,
|
|
1701
|
+
". If you didn't request this email, you can safely ignore it."
|
|
1702
|
+
] })
|
|
1703
|
+
] }) })
|
|
1704
|
+
] });
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
// src/emails/Welcome.tsx
|
|
1708
|
+
var import_components2 = require("@react-email/components");
|
|
1709
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1710
|
+
var WelcomeEmail = ({
|
|
1711
|
+
email,
|
|
1712
|
+
siteName = "Newsletter",
|
|
1713
|
+
preferencesUrl
|
|
1714
|
+
}) => {
|
|
1715
|
+
const previewText = `Welcome to ${siteName}!`;
|
|
1716
|
+
const firstName = email.split("@")[0];
|
|
1717
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Html, { children: [
|
|
1718
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Head, {}),
|
|
1719
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Preview, { children: previewText }),
|
|
1720
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Container, { style: styles.container, children: [
|
|
1721
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.heading, children: [
|
|
1722
|
+
"Welcome to ",
|
|
1723
|
+
siteName,
|
|
1724
|
+
"! \u{1F389}"
|
|
1725
|
+
] }),
|
|
1726
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
|
|
1727
|
+
"Hi ",
|
|
1728
|
+
firstName,
|
|
1729
|
+
","
|
|
1730
|
+
] }),
|
|
1731
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
|
|
1732
|
+
"Thanks for subscribing to ",
|
|
1733
|
+
siteName,
|
|
1734
|
+
"! We're excited to have you as part of our community."
|
|
1735
|
+
] }),
|
|
1736
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "You'll receive our newsletter based on your preferences. Speaking of which, you can update your preferences anytime:" }),
|
|
1737
|
+
preferencesUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Button, { href: preferencesUrl, style: styles.button, children: "Manage Preferences" }),
|
|
1738
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "Here's what you can expect from us:" }),
|
|
1739
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
|
|
1740
|
+
"\u2022 Regular updates based on your chosen frequency",
|
|
1741
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
|
|
1742
|
+
"\u2022 Content tailored to your interests",
|
|
1743
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
|
|
1744
|
+
"\u2022 Easy unsubscribe options in every email",
|
|
1745
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
|
|
1746
|
+
"\u2022 Your privacy respected always"
|
|
1747
|
+
] }),
|
|
1748
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Hr, { style: styles.hr }),
|
|
1749
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.footer, children: "If you have any questions, feel free to reply to this email. We're here to help!" })
|
|
1750
|
+
] }) })
|
|
1751
|
+
] });
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
// src/emails/SignIn.tsx
|
|
1755
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1756
|
+
var SignInEmail = (props) => {
|
|
1757
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MagicLinkEmail, { ...props });
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
// src/emails/render.tsx
|
|
1761
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1762
|
+
async function renderEmail(template, data, config) {
|
|
1763
|
+
try {
|
|
1764
|
+
if (config?.customTemplates) {
|
|
1765
|
+
const customTemplate = config.customTemplates[template];
|
|
1766
|
+
if (customTemplate) {
|
|
1767
|
+
const CustomComponent = customTemplate;
|
|
1768
|
+
return (0, import_render.render)(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CustomComponent, { ...data }));
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
switch (template) {
|
|
1772
|
+
case "magic-link": {
|
|
1773
|
+
const magicLinkData = data;
|
|
1774
|
+
return (0, import_render.render)(
|
|
1775
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1776
|
+
MagicLinkEmail,
|
|
1777
|
+
{
|
|
1778
|
+
magicLink: magicLinkData.magicLink || magicLinkData.verificationUrl || magicLinkData.magicLinkUrl || "",
|
|
1779
|
+
email: magicLinkData.email || "",
|
|
1780
|
+
siteName: magicLinkData.siteName,
|
|
1781
|
+
expiresIn: magicLinkData.expiresIn
|
|
1782
|
+
}
|
|
1783
|
+
)
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
case "signin": {
|
|
1787
|
+
const signinData = data;
|
|
1788
|
+
return (0, import_render.render)(
|
|
1789
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1790
|
+
SignInEmail,
|
|
1791
|
+
{
|
|
1792
|
+
magicLink: signinData.magicLink || signinData.verificationUrl || signinData.magicLinkUrl || "",
|
|
1793
|
+
email: signinData.email || "",
|
|
1794
|
+
siteName: signinData.siteName,
|
|
1795
|
+
expiresIn: signinData.expiresIn
|
|
1796
|
+
}
|
|
1797
|
+
)
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
case "welcome": {
|
|
1801
|
+
const welcomeData = data;
|
|
1802
|
+
return (0, import_render.render)(
|
|
1803
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1804
|
+
WelcomeEmail,
|
|
1805
|
+
{
|
|
1806
|
+
email: welcomeData.email || "",
|
|
1807
|
+
siteName: welcomeData.siteName,
|
|
1808
|
+
preferencesUrl: welcomeData.preferencesUrl
|
|
1809
|
+
}
|
|
1810
|
+
)
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
default:
|
|
1814
|
+
throw new Error(`Unknown email template: ${template}`);
|
|
1815
|
+
}
|
|
1816
|
+
} catch (error) {
|
|
1817
|
+
console.error(`Failed to render email template ${template}:`, error);
|
|
1818
|
+
throw error;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// src/collections/Subscribers.ts
|
|
1823
|
+
var createSubscribersCollection = (pluginConfig) => {
|
|
1824
|
+
const slug = pluginConfig.subscribersSlug || "subscribers";
|
|
1825
|
+
const defaultFields = [
|
|
1826
|
+
// Core fields
|
|
1827
|
+
{
|
|
1828
|
+
name: "email",
|
|
1829
|
+
type: "email",
|
|
1830
|
+
required: true,
|
|
1831
|
+
unique: true,
|
|
1832
|
+
admin: {
|
|
1833
|
+
description: "Subscriber email address"
|
|
1834
|
+
}
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
name: "name",
|
|
1838
|
+
type: "text",
|
|
1839
|
+
admin: {
|
|
1840
|
+
description: "Subscriber full name"
|
|
1841
|
+
}
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
name: "locale",
|
|
1845
|
+
type: "select",
|
|
1846
|
+
options: pluginConfig.i18n?.locales?.map((locale) => ({
|
|
1847
|
+
label: locale.toUpperCase(),
|
|
1848
|
+
value: locale
|
|
1849
|
+
})) || [
|
|
1850
|
+
{ label: "EN", value: "en" }
|
|
1851
|
+
],
|
|
1852
|
+
defaultValue: pluginConfig.i18n?.defaultLocale || "en",
|
|
1853
|
+
admin: {
|
|
1854
|
+
description: "Preferred language for communications"
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
// Authentication fields (hidden from admin UI)
|
|
1858
|
+
{
|
|
1859
|
+
name: "magicLinkToken",
|
|
1860
|
+
type: "text",
|
|
1861
|
+
hidden: true
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
name: "magicLinkTokenExpiry",
|
|
1865
|
+
type: "date",
|
|
1866
|
+
hidden: true
|
|
1867
|
+
},
|
|
1868
|
+
// Subscription status
|
|
1869
|
+
{
|
|
1870
|
+
name: "subscriptionStatus",
|
|
1871
|
+
type: "select",
|
|
1872
|
+
options: [
|
|
1873
|
+
{ label: "Active", value: "active" },
|
|
1874
|
+
{ label: "Unsubscribed", value: "unsubscribed" },
|
|
1875
|
+
{ label: "Pending", value: "pending" }
|
|
1876
|
+
],
|
|
1877
|
+
defaultValue: "pending",
|
|
1878
|
+
required: true,
|
|
1879
|
+
admin: {
|
|
1880
|
+
description: "Current subscription status"
|
|
1881
|
+
}
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
name: "unsubscribedAt",
|
|
1885
|
+
type: "date",
|
|
1886
|
+
admin: {
|
|
1887
|
+
condition: (data) => data?.subscriptionStatus === "unsubscribed",
|
|
1888
|
+
description: "When the user unsubscribed",
|
|
1889
|
+
readOnly: true
|
|
1890
|
+
}
|
|
1891
|
+
},
|
|
1892
|
+
// Email preferences
|
|
1893
|
+
{
|
|
1894
|
+
name: "emailPreferences",
|
|
1895
|
+
type: "group",
|
|
1896
|
+
fields: [
|
|
1897
|
+
{
|
|
1898
|
+
name: "newsletter",
|
|
1899
|
+
type: "checkbox",
|
|
1900
|
+
defaultValue: true,
|
|
1901
|
+
label: "Newsletter",
|
|
1902
|
+
admin: {
|
|
1903
|
+
description: "Receive regular newsletter updates"
|
|
1904
|
+
}
|
|
1905
|
+
},
|
|
1906
|
+
{
|
|
1907
|
+
name: "announcements",
|
|
1908
|
+
type: "checkbox",
|
|
1909
|
+
defaultValue: true,
|
|
1910
|
+
label: "Announcements",
|
|
1911
|
+
admin: {
|
|
1912
|
+
description: "Receive important announcements"
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
],
|
|
1916
|
+
admin: {
|
|
1917
|
+
description: "Email communication preferences"
|
|
1918
|
+
}
|
|
1919
|
+
},
|
|
1920
|
+
// Source tracking
|
|
1921
|
+
{
|
|
1922
|
+
name: "source",
|
|
1923
|
+
type: "text",
|
|
1924
|
+
admin: {
|
|
1925
|
+
description: "Where the subscriber signed up from"
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
];
|
|
1929
|
+
if (pluginConfig.features?.utmTracking?.enabled) {
|
|
1930
|
+
const utmFields = pluginConfig.features.utmTracking.fields || [
|
|
1931
|
+
"source",
|
|
1932
|
+
"medium",
|
|
1933
|
+
"campaign",
|
|
1934
|
+
"content",
|
|
1935
|
+
"term"
|
|
1936
|
+
];
|
|
1937
|
+
defaultFields.push({
|
|
1938
|
+
name: "utmParameters",
|
|
1939
|
+
type: "group",
|
|
1940
|
+
fields: utmFields.map((field) => ({
|
|
1941
|
+
name: field,
|
|
1942
|
+
type: "text",
|
|
1943
|
+
admin: {
|
|
1944
|
+
description: `UTM ${field} parameter`
|
|
1945
|
+
}
|
|
1946
|
+
})),
|
|
1947
|
+
admin: {
|
|
1948
|
+
description: "UTM tracking parameters"
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
defaultFields.push({
|
|
1953
|
+
name: "signupMetadata",
|
|
1954
|
+
type: "group",
|
|
1955
|
+
fields: [
|
|
1956
|
+
{
|
|
1957
|
+
name: "ipAddress",
|
|
1958
|
+
type: "text",
|
|
1959
|
+
admin: {
|
|
1960
|
+
readOnly: true
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
name: "userAgent",
|
|
1965
|
+
type: "text",
|
|
1966
|
+
admin: {
|
|
1967
|
+
readOnly: true
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
name: "referrer",
|
|
1972
|
+
type: "text",
|
|
1973
|
+
admin: {
|
|
1974
|
+
readOnly: true
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
name: "signupPage",
|
|
1979
|
+
type: "text",
|
|
1980
|
+
admin: {
|
|
1981
|
+
readOnly: true
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
],
|
|
1985
|
+
admin: {
|
|
1986
|
+
description: "Technical information about signup"
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
if (pluginConfig.features?.leadMagnets?.enabled) {
|
|
1990
|
+
defaultFields.push({
|
|
1991
|
+
name: "leadMagnet",
|
|
1992
|
+
type: "relationship",
|
|
1993
|
+
relationTo: pluginConfig.features.leadMagnets.collection || "media",
|
|
1994
|
+
admin: {
|
|
1995
|
+
description: "Lead magnet downloaded at signup"
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
let fields = defaultFields;
|
|
2000
|
+
if (pluginConfig.fields?.overrides) {
|
|
2001
|
+
fields = pluginConfig.fields.overrides({ defaultFields });
|
|
2002
|
+
}
|
|
2003
|
+
if (pluginConfig.fields?.additional) {
|
|
2004
|
+
fields = [...fields, ...pluginConfig.fields.additional];
|
|
2005
|
+
}
|
|
2006
|
+
const subscribersCollection = {
|
|
2007
|
+
slug,
|
|
2008
|
+
labels: {
|
|
2009
|
+
singular: "Subscriber",
|
|
2010
|
+
plural: "Subscribers"
|
|
2011
|
+
},
|
|
2012
|
+
admin: {
|
|
2013
|
+
useAsTitle: "email",
|
|
2014
|
+
defaultColumns: ["email", "name", "subscriptionStatus", "createdAt"],
|
|
2015
|
+
group: "Newsletter"
|
|
2016
|
+
},
|
|
2017
|
+
fields,
|
|
2018
|
+
hooks: {
|
|
2019
|
+
afterChange: [
|
|
2020
|
+
async ({ doc, req, operation, previousDoc }) => {
|
|
2021
|
+
if (operation === "create") {
|
|
2022
|
+
const emailService = req.payload.newsletterEmailService;
|
|
2023
|
+
console.log("[Newsletter Plugin] Creating subscriber:", {
|
|
2024
|
+
email: doc.email,
|
|
2025
|
+
hasEmailService: !!emailService
|
|
2026
|
+
});
|
|
2027
|
+
if (emailService) {
|
|
2028
|
+
try {
|
|
2029
|
+
await emailService.addContact(doc);
|
|
2030
|
+
console.log("[Newsletter Plugin] Successfully added contact to email service");
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
console.error("[Newsletter Plugin] Failed to add contact to email service:", error);
|
|
2033
|
+
}
|
|
2034
|
+
} else {
|
|
2035
|
+
console.warn("[Newsletter Plugin] No email service configured for subscriber creation");
|
|
2036
|
+
}
|
|
2037
|
+
if (doc.subscriptionStatus === "active" && emailService) {
|
|
2038
|
+
try {
|
|
2039
|
+
const settings = await req.payload.findGlobal({
|
|
2040
|
+
slug: pluginConfig.settingsSlug || "newsletter-settings"
|
|
2041
|
+
});
|
|
2042
|
+
const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
|
|
2043
|
+
const html = await renderEmail("welcome", {
|
|
2044
|
+
email: doc.email,
|
|
2045
|
+
siteName: settings?.brandSettings?.siteName || "Newsletter",
|
|
2046
|
+
preferencesUrl: `${serverURL}/account/preferences`
|
|
2047
|
+
// This could be customized
|
|
2048
|
+
}, pluginConfig);
|
|
2049
|
+
await emailService.send({
|
|
2050
|
+
to: doc.email,
|
|
2051
|
+
subject: settings?.brandSettings?.siteName ? `Welcome to ${settings.brandSettings.siteName}!` : "Welcome!",
|
|
2052
|
+
html
|
|
2053
|
+
});
|
|
2054
|
+
console.warn(`Welcome email sent to: ${doc.email}`);
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
console.error("Failed to send welcome email:", error);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (pluginConfig.hooks?.afterSubscribe) {
|
|
2060
|
+
await pluginConfig.hooks.afterSubscribe({ doc, req });
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
if (operation === "update" && previousDoc) {
|
|
2064
|
+
const emailService = req.payload.newsletterEmailService;
|
|
2065
|
+
if (doc.subscriptionStatus !== previousDoc.subscriptionStatus) {
|
|
2066
|
+
console.log("[Newsletter Plugin] Subscription status changed:", {
|
|
2067
|
+
email: doc.email,
|
|
2068
|
+
from: previousDoc.subscriptionStatus,
|
|
2069
|
+
to: doc.subscriptionStatus,
|
|
2070
|
+
hasEmailService: !!emailService
|
|
2071
|
+
});
|
|
2072
|
+
if (emailService) {
|
|
2073
|
+
try {
|
|
2074
|
+
await emailService.updateContact(doc);
|
|
2075
|
+
console.log("[Newsletter Plugin] Successfully updated contact in email service");
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
console.error("[Newsletter Plugin] Failed to update contact in email service:", error);
|
|
2078
|
+
}
|
|
2079
|
+
} else {
|
|
2080
|
+
console.warn("[Newsletter Plugin] No email service configured");
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (doc.subscriptionStatus === "unsubscribed" && previousDoc.subscriptionStatus !== "unsubscribed") {
|
|
2084
|
+
doc.unsubscribedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2085
|
+
if (pluginConfig.hooks?.afterUnsubscribe) {
|
|
2086
|
+
await pluginConfig.hooks.afterUnsubscribe({ doc, req });
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
],
|
|
2092
|
+
beforeDelete: [
|
|
2093
|
+
async ({ id, req }) => {
|
|
2094
|
+
const emailService = req.payload.newsletterEmailService;
|
|
2095
|
+
if (emailService) {
|
|
2096
|
+
try {
|
|
2097
|
+
const doc = await req.payload.findByID({
|
|
2098
|
+
collection: slug,
|
|
2099
|
+
id
|
|
2100
|
+
});
|
|
2101
|
+
await emailService.removeContact(doc.email);
|
|
2102
|
+
} catch {
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
]
|
|
2107
|
+
},
|
|
2108
|
+
access: {
|
|
2109
|
+
create: () => true,
|
|
2110
|
+
// Public can subscribe
|
|
2111
|
+
read: adminOrSelf(pluginConfig),
|
|
2112
|
+
update: adminOrSelf(pluginConfig),
|
|
2113
|
+
delete: adminOnly(pluginConfig)
|
|
2114
|
+
},
|
|
2115
|
+
timestamps: true
|
|
2116
|
+
};
|
|
2117
|
+
return subscribersCollection;
|
|
2118
|
+
};
|
|
2119
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2120
|
+
0 && (module.exports = {
|
|
2121
|
+
createBroadcastsCollection,
|
|
2122
|
+
createSubscribersCollection
|
|
2123
|
+
});
|
|
2124
|
+
//# sourceMappingURL=collections.cjs.map
|