payload-plugin-newsletter 0.20.7 → 0.21.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/CHANGELOG.md +27 -0
- package/README.md +35 -18
- package/dist/admin.d.ts +3 -1
- package/dist/admin.js +146 -1
- package/dist/collections.cjs +105 -0
- package/dist/collections.cjs.map +1 -1
- package/dist/collections.js +105 -0
- package/dist/collections.js.map +1 -1
- package/dist/server.d.ts +0 -25
- package/dist/server.js +590 -131
- package/dist/types.d.cts +1 -26
- package/dist/types.d.ts +1 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## [0.21.0] - 2025-01-31
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **Webhook Support**: Real-time webhook integration for Broadcast provider
|
|
5
|
+
- Automatic updates for subscriber events (subscribed/unsubscribed)
|
|
6
|
+
- Real-time broadcast status updates (scheduled, in_progress, sent, etc.)
|
|
7
|
+
- HMAC-SHA256 signature verification for security
|
|
8
|
+
- Webhook configuration UI in Newsletter Settings
|
|
9
|
+
- Manual webhook registration with setup instructions
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **BREAKING**: Removed polling-based unsubscribe synchronization
|
|
13
|
+
- Webhooks now provide real-time updates instead of scheduled polling
|
|
14
|
+
- Removed `unsubscribeSync` configuration option
|
|
15
|
+
- Removed `afterUnsubscribeSync` hook
|
|
16
|
+
- Removed sync job infrastructure
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
- Added webhook signature verification with timestamp validation
|
|
20
|
+
- Webhook secrets stored securely in Newsletter Settings
|
|
21
|
+
|
|
22
|
+
### Migration Guide
|
|
23
|
+
If you were using unsubscribe sync:
|
|
24
|
+
1. Remove any `unsubscribeSync` configuration from your plugin setup
|
|
25
|
+
2. Configure webhooks in your Broadcast dashboard (see README)
|
|
26
|
+
3. Add the webhook secret to your Newsletter Settings
|
|
27
|
+
|
|
1
28
|
## [0.20.7] - 2025-08-02
|
|
2
29
|
|
|
3
30
|
### Added
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A complete newsletter management plugin for [Payload CMS](https://github.com/pay
|
|
|
17
17
|
- 🌍 **Internationalization** - Multi-language support built-in
|
|
18
18
|
- 📊 **Analytics Ready** - UTM tracking and signup metadata collection
|
|
19
19
|
- ⚙️ **Admin UI Configuration** - Manage email settings through Payload admin panel
|
|
20
|
-
- 🔄 **
|
|
20
|
+
- 🔄 **Real-time Webhook Sync** - Receive subscriber and broadcast events from email services via webhooks
|
|
21
21
|
- 👁️ **Email Preview** - Real-time preview with desktop/mobile views (v0.9.0+)
|
|
22
22
|
- ✅ **Email Validation** - Built-in validation for email client compatibility (v0.9.0+)
|
|
23
23
|
- 📝 **Email-Safe Editor** - Rich text editor limited to email-compatible features (v0.9.0+)
|
|
@@ -654,27 +654,44 @@ This adds a "Newsletter Scheduling" group to your articles with:
|
|
|
654
654
|
- Audience segment selection
|
|
655
655
|
- Send status tracking
|
|
656
656
|
|
|
657
|
-
##
|
|
657
|
+
## Webhook Configuration (Broadcast)
|
|
658
658
|
|
|
659
|
-
The plugin supports
|
|
659
|
+
The plugin supports real-time webhook integration with Broadcast for instant updates:
|
|
660
660
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
661
|
+
### Automatic Updates
|
|
662
|
+
|
|
663
|
+
When configured, the plugin automatically receives and processes:
|
|
664
|
+
- **Subscriber Events**: `subscribed`, `unsubscribed`
|
|
665
|
+
- **Broadcast Events**: All status changes (`scheduled`, `in_progress`, `sent`, etc.)
|
|
666
|
+
|
|
667
|
+
### Setup Instructions
|
|
668
|
+
|
|
669
|
+
1. **Save your Newsletter Settings** in the Payload admin to generate a webhook URL
|
|
670
|
+
2. **Configure in Broadcast**:
|
|
671
|
+
- Go to your Broadcast dashboard → "Webhook Endpoints"
|
|
672
|
+
- Click "Add Webhook Endpoint"
|
|
673
|
+
- Paste the webhook URL from Payload
|
|
674
|
+
- Select events:
|
|
675
|
+
- Subscriber Events: `subscribed`, `unsubscribed`
|
|
676
|
+
- Broadcast Events: All
|
|
677
|
+
- Create the webhook and copy the webhook secret
|
|
678
|
+
3. **Add the webhook secret** to your Newsletter Settings in Payload
|
|
679
|
+
4. **Save and verify** the webhook connection
|
|
680
|
+
|
|
681
|
+
### Security
|
|
682
|
+
|
|
683
|
+
Webhooks are secured with:
|
|
684
|
+
- HMAC-SHA256 signature verification
|
|
685
|
+
- Timestamp validation (5-minute window)
|
|
686
|
+
- Secret key stored in Newsletter Settings
|
|
687
|
+
|
|
688
|
+
### Data Flow
|
|
670
689
|
|
|
671
|
-
|
|
672
|
-
-
|
|
673
|
-
-
|
|
674
|
-
- Supports both Broadcast and Resend providers
|
|
675
|
-
- Can run on a schedule or be triggered manually
|
|
690
|
+
- **Subscriber events** update the subscriber's status and metadata
|
|
691
|
+
- **Broadcast events** update the broadcast's status and delivery metrics
|
|
692
|
+
- All updates happen in real-time without polling
|
|
676
693
|
|
|
677
|
-
|
|
694
|
+
**Note**: Email engagement events (opens, clicks) remain in Broadcast for analytics.
|
|
678
695
|
|
|
679
696
|
## Email Providers
|
|
680
697
|
|
package/dist/admin.d.ts
CHANGED
|
@@ -22,4 +22,6 @@ interface EmailPreviewProps {
|
|
|
22
22
|
}
|
|
23
23
|
declare const EmailPreview: React.FC<EmailPreviewProps>;
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
declare const WebhookConfiguration: React.FC;
|
|
26
|
+
|
|
27
|
+
export { BroadcastInlinePreview, type BroadcastInlinePreviewProps, EmailPreview, type EmailPreviewProps, StatusBadge, type StatusBadgeProps, WebhookConfiguration };
|
package/dist/admin.js
CHANGED
|
@@ -188,8 +188,153 @@ var EmailPreview = ({
|
|
|
188
188
|
] })
|
|
189
189
|
] });
|
|
190
190
|
};
|
|
191
|
+
|
|
192
|
+
// src/admin/components/WebhookConfiguration.tsx
|
|
193
|
+
import { useState as useState2 } from "react";
|
|
194
|
+
import { useFormFields as useFormFields2 } from "@payloadcms/ui";
|
|
195
|
+
import { Button } from "@payloadcms/ui";
|
|
196
|
+
import { toast } from "@payloadcms/ui";
|
|
197
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
198
|
+
var WebhookConfiguration = () => {
|
|
199
|
+
const [showInstructions, setShowInstructions] = useState2(false);
|
|
200
|
+
const [verifying, setVerifying] = useState2(false);
|
|
201
|
+
const fields = useFormFields2(([fields2]) => ({
|
|
202
|
+
webhookUrl: fields2?.broadcastSettings?.webhookUrl,
|
|
203
|
+
webhookStatus: fields2?.broadcastSettings?.webhookStatus,
|
|
204
|
+
lastWebhookReceived: fields2?.broadcastSettings?.lastWebhookReceived
|
|
205
|
+
}));
|
|
206
|
+
const handleVerify = async () => {
|
|
207
|
+
setVerifying(true);
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetch("/api/newsletter/webhooks/verify", {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: {
|
|
212
|
+
"Content-Type": "application/json"
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
if (data.success) {
|
|
217
|
+
toast.success(data.message || "Webhook verified");
|
|
218
|
+
} else {
|
|
219
|
+
toast.error(data.error || "Verification failed");
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
toast.error("Failed to verify webhook");
|
|
223
|
+
} finally {
|
|
224
|
+
setVerifying(false);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const getStatusColor = (status) => {
|
|
228
|
+
switch (status) {
|
|
229
|
+
case "verified":
|
|
230
|
+
return "green";
|
|
231
|
+
case "configured":
|
|
232
|
+
return "yellow";
|
|
233
|
+
case "error":
|
|
234
|
+
return "red";
|
|
235
|
+
default:
|
|
236
|
+
return "gray";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const getStatusLabel = (status) => {
|
|
240
|
+
switch (status) {
|
|
241
|
+
case "verified":
|
|
242
|
+
return "Verified \u2713";
|
|
243
|
+
case "configured":
|
|
244
|
+
return "Configured";
|
|
245
|
+
case "error":
|
|
246
|
+
return "Error";
|
|
247
|
+
default:
|
|
248
|
+
return "Not Configured";
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
return /* @__PURE__ */ jsx4("div", { className: "field-type", children: /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "1rem" }, children: [
|
|
252
|
+
/* @__PURE__ */ jsx4("label", { className: "field-label", children: "Webhook Configuration" }),
|
|
253
|
+
/* @__PURE__ */ jsxs3("div", { style: {
|
|
254
|
+
background: "#f5f5f5",
|
|
255
|
+
padding: "1rem",
|
|
256
|
+
borderRadius: "4px",
|
|
257
|
+
marginTop: "0.5rem"
|
|
258
|
+
}, children: [
|
|
259
|
+
/* @__PURE__ */ jsxs3("div", { style: { marginBottom: "0.5rem" }, children: [
|
|
260
|
+
/* @__PURE__ */ jsx4("strong", { children: "Webhook URL:" }),
|
|
261
|
+
/* @__PURE__ */ jsx4("code", { style: {
|
|
262
|
+
display: "block",
|
|
263
|
+
padding: "0.5rem",
|
|
264
|
+
background: "#fff",
|
|
265
|
+
border: "1px solid #ddd",
|
|
266
|
+
borderRadius: "4px",
|
|
267
|
+
marginTop: "0.25rem",
|
|
268
|
+
fontSize: "0.875rem"
|
|
269
|
+
}, children: fields.webhookUrl || "Save settings to generate URL" })
|
|
270
|
+
] }),
|
|
271
|
+
/* @__PURE__ */ jsxs3("div", { style: { marginBottom: "0.5rem" }, children: [
|
|
272
|
+
/* @__PURE__ */ jsx4("strong", { children: "Status:" }),
|
|
273
|
+
" ",
|
|
274
|
+
/* @__PURE__ */ jsx4("span", { style: { color: getStatusColor(fields.webhookStatus || "not_configured") }, children: getStatusLabel(fields.webhookStatus || "not_configured") })
|
|
275
|
+
] }),
|
|
276
|
+
fields.lastWebhookReceived && /* @__PURE__ */ jsxs3("div", { style: { fontSize: "0.875rem", color: "#666" }, children: [
|
|
277
|
+
"Last event: ",
|
|
278
|
+
new Date(fields.lastWebhookReceived).toLocaleString()
|
|
279
|
+
] })
|
|
280
|
+
] }),
|
|
281
|
+
/* @__PURE__ */ jsxs3("div", { style: { marginTop: "1rem", display: "flex", gap: "0.5rem" }, children: [
|
|
282
|
+
/* @__PURE__ */ jsxs3(
|
|
283
|
+
Button,
|
|
284
|
+
{
|
|
285
|
+
onClick: () => setShowInstructions(!showInstructions),
|
|
286
|
+
buttonStyle: "secondary",
|
|
287
|
+
size: "small",
|
|
288
|
+
children: [
|
|
289
|
+
showInstructions ? "Hide" : "Show",
|
|
290
|
+
" Instructions"
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
),
|
|
294
|
+
/* @__PURE__ */ jsx4(
|
|
295
|
+
Button,
|
|
296
|
+
{
|
|
297
|
+
onClick: handleVerify,
|
|
298
|
+
buttonStyle: "primary",
|
|
299
|
+
size: "small",
|
|
300
|
+
disabled: verifying || !fields.webhookUrl,
|
|
301
|
+
children: verifying ? "Verifying..." : "Verify Webhook"
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
] }),
|
|
305
|
+
showInstructions && /* @__PURE__ */ jsxs3("div", { style: {
|
|
306
|
+
marginTop: "1rem",
|
|
307
|
+
padding: "1rem",
|
|
308
|
+
background: "#f0f8ff",
|
|
309
|
+
border: "1px solid #b0d4ff",
|
|
310
|
+
borderRadius: "4px"
|
|
311
|
+
}, children: [
|
|
312
|
+
/* @__PURE__ */ jsx4("h4", { style: { marginTop: 0 }, children: "Configure Broadcast Webhook" }),
|
|
313
|
+
/* @__PURE__ */ jsxs3("ol", { children: [
|
|
314
|
+
/* @__PURE__ */ jsx4("li", { children: "Copy the webhook URL above" }),
|
|
315
|
+
/* @__PURE__ */ jsx4("li", { children: "Go to your Broadcast dashboard" }),
|
|
316
|
+
/* @__PURE__ */ jsx4("li", { children: 'Navigate to "Webhook Endpoints"' }),
|
|
317
|
+
/* @__PURE__ */ jsx4("li", { children: 'Click "Add Webhook Endpoint"' }),
|
|
318
|
+
/* @__PURE__ */ jsx4("li", { children: "Paste the URL" }),
|
|
319
|
+
/* @__PURE__ */ jsxs3("li", { children: [
|
|
320
|
+
"Select these events:",
|
|
321
|
+
/* @__PURE__ */ jsxs3("ul", { children: [
|
|
322
|
+
/* @__PURE__ */ jsx4("li", { children: "Subscriber Events: subscribed, unsubscribed" }),
|
|
323
|
+
/* @__PURE__ */ jsx4("li", { children: "Broadcast Events: All" })
|
|
324
|
+
] })
|
|
325
|
+
] }),
|
|
326
|
+
/* @__PURE__ */ jsx4("li", { children: 'Click "Create Webhook"' }),
|
|
327
|
+
/* @__PURE__ */ jsx4("li", { children: "Copy the webhook secret shown" }),
|
|
328
|
+
/* @__PURE__ */ jsx4("li", { children: "Paste it in the Webhook Secret field below" }),
|
|
329
|
+
/* @__PURE__ */ jsx4("li", { children: "Save these settings" }),
|
|
330
|
+
/* @__PURE__ */ jsx4("li", { children: 'Click "Verify Webhook" to test the connection' })
|
|
331
|
+
] })
|
|
332
|
+
] })
|
|
333
|
+
] }) });
|
|
334
|
+
};
|
|
191
335
|
export {
|
|
192
336
|
BroadcastInlinePreview,
|
|
193
337
|
EmailPreview,
|
|
194
|
-
StatusBadge
|
|
338
|
+
StatusBadge,
|
|
339
|
+
WebhookConfiguration
|
|
195
340
|
};
|
package/dist/collections.cjs
CHANGED
|
@@ -2154,6 +2154,14 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
2154
2154
|
condition: (data) => hasProviders && data?.providerId
|
|
2155
2155
|
}
|
|
2156
2156
|
},
|
|
2157
|
+
{
|
|
2158
|
+
name: "externalId",
|
|
2159
|
+
type: "text",
|
|
2160
|
+
admin: {
|
|
2161
|
+
readOnly: true,
|
|
2162
|
+
description: "External ID for webhook integration"
|
|
2163
|
+
}
|
|
2164
|
+
},
|
|
2157
2165
|
{
|
|
2158
2166
|
name: "providerData",
|
|
2159
2167
|
type: "json",
|
|
@@ -2162,6 +2170,77 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
2162
2170
|
condition: () => false
|
|
2163
2171
|
// Hidden by default
|
|
2164
2172
|
}
|
|
2173
|
+
},
|
|
2174
|
+
// Webhook tracking fields
|
|
2175
|
+
{
|
|
2176
|
+
name: "webhookData",
|
|
2177
|
+
type: "group",
|
|
2178
|
+
label: "Webhook Data",
|
|
2179
|
+
admin: {
|
|
2180
|
+
condition: () => false
|
|
2181
|
+
// Hidden by default, used for webhook tracking
|
|
2182
|
+
},
|
|
2183
|
+
fields: [
|
|
2184
|
+
{
|
|
2185
|
+
name: "lastWebhookEvent",
|
|
2186
|
+
type: "text",
|
|
2187
|
+
admin: {
|
|
2188
|
+
readOnly: true
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
name: "lastWebhookEventAt",
|
|
2193
|
+
type: "date",
|
|
2194
|
+
admin: {
|
|
2195
|
+
readOnly: true
|
|
2196
|
+
}
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
name: "hasWarnings",
|
|
2200
|
+
type: "checkbox",
|
|
2201
|
+
defaultValue: false
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
name: "failureReason",
|
|
2205
|
+
type: "text"
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
name: "sentCount",
|
|
2209
|
+
type: "number"
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
name: "totalCount",
|
|
2213
|
+
type: "number"
|
|
2214
|
+
},
|
|
2215
|
+
{
|
|
2216
|
+
name: "failedCount",
|
|
2217
|
+
type: "number"
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
name: "remainingCount",
|
|
2221
|
+
type: "number"
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
name: "sendingStartedAt",
|
|
2225
|
+
type: "date"
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
name: "failedAt",
|
|
2229
|
+
type: "date"
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
name: "abortedAt",
|
|
2233
|
+
type: "date"
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
name: "abortReason",
|
|
2237
|
+
type: "text"
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
name: "pausedAt",
|
|
2241
|
+
type: "date"
|
|
2242
|
+
}
|
|
2243
|
+
]
|
|
2165
2244
|
}
|
|
2166
2245
|
],
|
|
2167
2246
|
hooks: {
|
|
@@ -2803,6 +2882,15 @@ var createSubscribersCollection = (pluginConfig) => {
|
|
|
2803
2882
|
type: "date",
|
|
2804
2883
|
hidden: true
|
|
2805
2884
|
},
|
|
2885
|
+
// External ID for webhook integration
|
|
2886
|
+
{
|
|
2887
|
+
name: "externalId",
|
|
2888
|
+
type: "text",
|
|
2889
|
+
admin: {
|
|
2890
|
+
description: "ID from email service provider",
|
|
2891
|
+
readOnly: true
|
|
2892
|
+
}
|
|
2893
|
+
},
|
|
2806
2894
|
// Subscription status
|
|
2807
2895
|
{
|
|
2808
2896
|
name: "subscriptionStatus",
|
|
@@ -2818,6 +2906,14 @@ var createSubscribersCollection = (pluginConfig) => {
|
|
|
2818
2906
|
description: "Current subscription status"
|
|
2819
2907
|
}
|
|
2820
2908
|
},
|
|
2909
|
+
{
|
|
2910
|
+
name: "subscribedAt",
|
|
2911
|
+
type: "date",
|
|
2912
|
+
admin: {
|
|
2913
|
+
description: "When the user subscribed",
|
|
2914
|
+
readOnly: true
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2821
2917
|
{
|
|
2822
2918
|
name: "unsubscribedAt",
|
|
2823
2919
|
type: "date",
|
|
@@ -2827,6 +2923,15 @@ var createSubscribersCollection = (pluginConfig) => {
|
|
|
2827
2923
|
readOnly: true
|
|
2828
2924
|
}
|
|
2829
2925
|
},
|
|
2926
|
+
{
|
|
2927
|
+
name: "unsubscribeReason",
|
|
2928
|
+
type: "text",
|
|
2929
|
+
admin: {
|
|
2930
|
+
condition: (data) => data?.subscriptionStatus === "unsubscribed",
|
|
2931
|
+
description: "Reason for unsubscribing",
|
|
2932
|
+
readOnly: true
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2830
2935
|
// Email preferences
|
|
2831
2936
|
{
|
|
2832
2937
|
name: "emailPreferences",
|