payload-plugin-newsletter 0.14.3 → 0.15.0

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