payload-plugin-newsletter 0.25.12 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Field, Block, RichTextField, Config, Endpoint, CollectionConfig, GlobalConfig } from 'payload';
1
+ import { Field, Block, RichTextField, Payload, Config, Endpoint, CollectionConfig, GlobalConfig } from 'payload';
2
+ import { SerializedEditorState } from 'lexical';
2
3
 
3
4
  /**
4
5
  * Core types for broadcast management functionality
@@ -632,6 +633,96 @@ interface SurveyQuestion {
632
633
  required?: boolean;
633
634
  }
634
635
 
636
+ /**
637
+ * Recursively populates media fields in Lexical content.
638
+ * Resolves Media IDs to full media objects with URLs.
639
+ *
640
+ * @param content - Lexical editor state content
641
+ * @param payload - Payload instance for database queries
642
+ * @param config - Newsletter plugin configuration
643
+ * @returns Populated content with resolved media objects
644
+ */
645
+ declare function populateMediaFields(content: unknown, payload: Payload, config: NewsletterPluginConfig): Promise<unknown>;
646
+
647
+ /**
648
+ * Options for generating broadcast preview HTML
649
+ */
650
+ interface PreviewOptions {
651
+ /** Email subject line */
652
+ subject: string;
653
+ /** Email preheader text (preview text in inbox) */
654
+ preheader?: string;
655
+ /** Whether to wrap content in email template (default: true) */
656
+ wrapInTemplate?: boolean;
657
+ /** Base URL for media files (optional, derived from payload.config.serverURL if not provided) */
658
+ mediaUrl?: string;
659
+ /** Additional document data to pass to custom wrapper */
660
+ documentData?: Record<string, unknown>;
661
+ }
662
+ /**
663
+ * Result from preview generation
664
+ */
665
+ interface PreviewResult {
666
+ /** Generated HTML content */
667
+ html: string;
668
+ /** Subject line used */
669
+ subject: string;
670
+ /** Preheader text used (null if not provided) */
671
+ preheader: string | null;
672
+ }
673
+ /**
674
+ * Generates preview HTML from broadcast content.
675
+ *
676
+ * This utility handles the full preview generation pipeline:
677
+ * 1. Populates media fields (resolves Media IDs to full objects with URLs)
678
+ * 2. Converts Lexical content to email-safe HTML
679
+ * 3. Optionally wraps in email template
680
+ *
681
+ * @param content - Lexical editor state content from broadcast
682
+ * @param payload - Payload instance for database queries
683
+ * @param config - Newsletter plugin configuration
684
+ * @param options - Preview generation options
685
+ * @returns Preview result with HTML, subject, and preheader
686
+ *
687
+ * @example
688
+ * ```typescript
689
+ * import { generateBroadcastPreviewHtml } from 'payload-plugin-newsletter'
690
+ *
691
+ * const preview = await generateBroadcastPreviewHtml(
692
+ * broadcast.content,
693
+ * payload,
694
+ * pluginConfig,
695
+ * {
696
+ * subject: broadcast.subject,
697
+ * preheader: broadcast.preheader,
698
+ * wrapInTemplate: true,
699
+ * }
700
+ * )
701
+ *
702
+ * // preview.html contains the rendered email HTML
703
+ * ```
704
+ */
705
+ declare function generateBroadcastPreviewHtml(content: SerializedEditorState | null | undefined, payload: Payload, config: NewsletterPluginConfig, options: PreviewOptions): Promise<PreviewResult>;
706
+
707
+ /**
708
+ * Converts Lexical editor state to email-safe HTML
709
+ */
710
+ declare function convertToEmailSafeHtml(editorState: SerializedEditorState | undefined | null, options?: {
711
+ wrapInTemplate?: boolean;
712
+ preheader?: string;
713
+ mediaUrl?: string;
714
+ customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>;
715
+ payload?: any;
716
+ populateFields?: string[] | ((blockType: string) => string[]);
717
+ customWrapper?: (content: string, options?: {
718
+ preheader?: string;
719
+ subject?: string;
720
+ documentData?: Record<string, any>;
721
+ }) => string | Promise<string>;
722
+ subject?: string;
723
+ documentData?: Record<string, any>;
724
+ }): Promise<string>;
725
+
635
726
  declare module 'payload' {
636
727
  interface BasePayload {
637
728
  newsletterEmailService?: any;
@@ -710,4 +801,4 @@ declare class BroadcastProvider implements EmailProvider {
710
801
  removeContact(email: string): Promise<void>;
711
802
  }
712
803
 
713
- export { BroadcastProvider, type NewsletterPluginConfig, ResendProvider, type Subscriber, createBroadcastsCollection, createNewsletterSettingsGlobal, createPreferencesEndpoint, createSubscribeEndpoint, createSubscribersCollection, createUnsubscribeEndpoint, createVerifyMagicLinkEndpoint, newsletterPlugin };
804
+ export { BroadcastProvider, type NewsletterPluginConfig, type PreviewOptions, type PreviewResult, ResendProvider, type Subscriber, convertToEmailSafeHtml, createBroadcastsCollection, createNewsletterSettingsGlobal, createPreferencesEndpoint, createSubscribeEndpoint, createSubscribersCollection, createUnsubscribeEndpoint, createVerifyMagicLinkEndpoint, generateBroadcastPreviewHtml, newsletterPlugin, populateMediaFields };
package/dist/server.js CHANGED
@@ -2763,10 +2763,20 @@ async function updateBroadcastStatus(event, status, payload, broadcastsSlug) {
2763
2763
  return;
2764
2764
  }
2765
2765
  const broadcast = existing.docs[0];
2766
+ const newWebhookEvent = {
2767
+ eventType: event.type,
2768
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
2769
+ eventPayload: event.data
2770
+ };
2771
+ const existingEvents = broadcast.webhookData?.webhookEvents || [];
2772
+ const webhookEvents = [...existingEvents.slice(-9), newWebhookEvent];
2766
2773
  const updateData = {
2767
- status,
2768
- lastWebhookEvent: event.type,
2769
- lastWebhookEventAt: event.occurred_at
2774
+ sendStatus: status,
2775
+ // CRITICAL: Top-level field - 'sendStatus' not 'status'
2776
+ "webhookData.lastWebhookEvent": event.type,
2777
+ "webhookData.lastWebhookEventAt": event.occurred_at,
2778
+ "webhookData.webhookEvents": webhookEvents
2779
+ // Log event for debugging
2770
2780
  };
2771
2781
  switch (event.type) {
2772
2782
  case WEBHOOK_EVENT_TYPES.BROADCAST_SCHEDULED:
@@ -2774,42 +2784,42 @@ async function updateBroadcastStatus(event, status, payload, broadcastsSlug) {
2774
2784
  break;
2775
2785
  case WEBHOOK_EVENT_TYPES.BROADCAST_SENDING: {
2776
2786
  const sendingEvent = event;
2777
- updateData.sentCount = sendingEvent.data.sent_count;
2778
- updateData.totalCount = sendingEvent.data.total_count;
2787
+ updateData["webhookData.sentCount"] = sendingEvent.data.sent_count;
2788
+ updateData["webhookData.totalCount"] = sendingEvent.data.total_count;
2779
2789
  break;
2780
2790
  }
2781
2791
  case WEBHOOK_EVENT_TYPES.BROADCAST_SENT: {
2782
2792
  const sentEvent = event;
2783
- updateData.sentCount = sentEvent.data.sent_count;
2793
+ updateData["webhookData.sentCount"] = sentEvent.data.sent_count;
2784
2794
  updateData.sentAt = sentEvent.data.completed_at;
2785
- updateData.sendingStartedAt = sentEvent.data.started_at;
2795
+ updateData["webhookData.sendingStartedAt"] = sentEvent.data.started_at;
2786
2796
  break;
2787
2797
  }
2788
2798
  case WEBHOOK_EVENT_TYPES.BROADCAST_FAILED: {
2789
2799
  const failedEvent = event;
2790
- updateData.failureReason = failedEvent.data.error;
2791
- updateData.failedAt = failedEvent.data.failed_at;
2800
+ updateData["webhookData.failureReason"] = failedEvent.data.error;
2801
+ updateData["webhookData.failedAt"] = failedEvent.data.failed_at;
2792
2802
  break;
2793
2803
  }
2794
2804
  case WEBHOOK_EVENT_TYPES.BROADCAST_PARTIAL_FAILURE: {
2795
2805
  const partialFailureEvent = event;
2796
- updateData.sentCount = partialFailureEvent.data.sent_count;
2797
- updateData.failedCount = partialFailureEvent.data.failed_count;
2798
- updateData.totalCount = partialFailureEvent.data.total_count;
2799
- updateData.hasWarnings = true;
2806
+ updateData["webhookData.sentCount"] = partialFailureEvent.data.sent_count;
2807
+ updateData["webhookData.failedCount"] = partialFailureEvent.data.failed_count;
2808
+ updateData["webhookData.totalCount"] = partialFailureEvent.data.total_count;
2809
+ updateData["webhookData.hasWarnings"] = true;
2800
2810
  break;
2801
2811
  }
2802
2812
  case WEBHOOK_EVENT_TYPES.BROADCAST_ABORTED: {
2803
2813
  const abortedEvent = event;
2804
- updateData.abortedAt = abortedEvent.data.aborted_at;
2805
- updateData.abortReason = abortedEvent.data.reason;
2814
+ updateData["webhookData.abortedAt"] = abortedEvent.data.aborted_at;
2815
+ updateData["webhookData.abortReason"] = abortedEvent.data.reason;
2806
2816
  break;
2807
2817
  }
2808
2818
  case WEBHOOK_EVENT_TYPES.BROADCAST_PAUSED: {
2809
2819
  const pausedEvent = event;
2810
- updateData.pausedAt = pausedEvent.data.paused_at;
2811
- updateData.sentCount = pausedEvent.data.sent_count;
2812
- updateData.remainingCount = pausedEvent.data.remaining_count;
2820
+ updateData["webhookData.pausedAt"] = pausedEvent.data.paused_at;
2821
+ updateData["webhookData.sentCount"] = pausedEvent.data.sent_count;
2822
+ updateData["webhookData.remainingCount"] = pausedEvent.data.remaining_count;
2813
2823
  break;
2814
2824
  }
2815
2825
  }
@@ -4821,6 +4831,33 @@ var createBroadcastsCollection = (pluginConfig) => {
4821
4831
  {
4822
4832
  name: "pausedAt",
4823
4833
  type: "date"
4834
+ },
4835
+ {
4836
+ name: "webhookEvents",
4837
+ type: "array",
4838
+ label: "Webhook Event Log",
4839
+ maxRows: 10,
4840
+ admin: {
4841
+ readOnly: true,
4842
+ description: "Recent webhook events for debugging"
4843
+ },
4844
+ fields: [
4845
+ {
4846
+ name: "eventType",
4847
+ type: "text",
4848
+ admin: { readOnly: true }
4849
+ },
4850
+ {
4851
+ name: "receivedAt",
4852
+ type: "date",
4853
+ admin: { readOnly: true }
4854
+ },
4855
+ {
4856
+ name: "eventPayload",
4857
+ type: "json",
4858
+ admin: { readOnly: true }
4859
+ }
4860
+ ]
4824
4861
  }
4825
4862
  ]
4826
4863
  }
@@ -5289,6 +5326,28 @@ var setPluginConfig = (config) => {
5289
5326
  // src/utilities/session.ts
5290
5327
  import jwt2 from "jsonwebtoken";
5291
5328
 
5329
+ // src/utils/preview.ts
5330
+ async function generateBroadcastPreviewHtml(content, payload, config, options) {
5331
+ const mediaUrl = options.mediaUrl ?? (payload.config.serverURL ? `${payload.config.serverURL}/api/media` : "/api/media");
5332
+ const emailPreviewConfig = config.customizations?.broadcasts?.emailPreview;
5333
+ payload.logger?.info("Populating media fields for preview generation...");
5334
+ const populatedContent = await populateMediaFields(content, payload, config);
5335
+ const html = await convertToEmailSafeHtml(populatedContent, {
5336
+ wrapInTemplate: options.wrapInTemplate ?? emailPreviewConfig?.wrapInTemplate ?? true,
5337
+ preheader: options.preheader,
5338
+ subject: options.subject,
5339
+ mediaUrl,
5340
+ documentData: options.documentData,
5341
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter,
5342
+ customWrapper: emailPreviewConfig?.customWrapper
5343
+ });
5344
+ return {
5345
+ html,
5346
+ subject: options.subject,
5347
+ preheader: options.preheader ?? null
5348
+ };
5349
+ }
5350
+
5292
5351
  // src/index.ts
5293
5352
  var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
5294
5353
  const config = {
@@ -5443,6 +5502,7 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
5443
5502
  export {
5444
5503
  BroadcastProvider,
5445
5504
  ResendProvider,
5505
+ convertToEmailSafeHtml,
5446
5506
  createBroadcastsCollection,
5447
5507
  createNewsletterSettingsGlobal,
5448
5508
  createPreferencesEndpoint,
@@ -5450,5 +5510,7 @@ export {
5450
5510
  createSubscribersCollection,
5451
5511
  createUnsubscribeEndpoint,
5452
5512
  createVerifyMagicLinkEndpoint,
5453
- newsletterPlugin
5513
+ generateBroadcastPreviewHtml,
5514
+ newsletterPlugin,
5515
+ populateMediaFields
5454
5516
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-plugin-newsletter",
3
- "version": "0.25.12",
3
+ "version": "0.26.0",
4
4
  "description": "Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",