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.
@@ -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;">&nbsp;</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
+ "&": "&amp;",
1122
+ "<": "&lt;",
1123
+ ">": "&gt;",
1124
+ '"': "&quot;",
1125
+ "'": "&#039;"
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