payload-plugin-newsletter 0.21.4 → 0.23.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/dist/server.js +134 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## [0.23.0] - 2025-08-07
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **Complete webhook sync for Broadcast subscribers**: Added support for three new webhook events
|
|
5
|
+
- `subscriber.created`: Syncs new subscribers created in Broadcast interface to Payload
|
|
6
|
+
- `subscriber.updated`: Syncs subscriber updates from Broadcast to Payload
|
|
7
|
+
- `subscriber.deleted`: Removes subscribers from Payload when deleted in Broadcast
|
|
8
|
+
- **Bidirectional sync**: Now provides complete two-way synchronization between Broadcast and Payload
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Enhanced webhook event handling to support all subscriber lifecycle events
|
|
12
|
+
- Improved logging for better webhook debugging
|
|
13
|
+
|
|
14
|
+
## [0.22.0] - 2025-08-07
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Improved Broadcast sync**: Now uses proper subscribe/unsubscribe endpoints
|
|
18
|
+
- Uses `/api/v1/subscribers/unsubscribe.json` for unsubscribing (sets `unsubscribed_at` timestamp)
|
|
19
|
+
- Uses `/api/v1/subscribers.json` for both new subscriptions and resubscriptions
|
|
20
|
+
- Removed language tagging as per user preference
|
|
21
|
+
- Simplified resubscription flow - no special error handling needed
|
|
22
|
+
- **Better compliance tracking**: Broadcast now properly tracks unsubscribe timestamps for compliance
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Fixed Broadcast provider to properly handle subscription status changes
|
|
26
|
+
- Removed unnecessary contact existence checks (Broadcast API handles this gracefully)
|
|
27
|
+
|
|
1
28
|
## [0.21.4] - 2025-08-07
|
|
2
29
|
|
|
3
30
|
### Fixed
|
package/dist/server.js
CHANGED
|
@@ -1240,13 +1240,11 @@ var BroadcastProvider = class {
|
|
|
1240
1240
|
email: contact.email,
|
|
1241
1241
|
first_name: firstName || void 0,
|
|
1242
1242
|
last_name: lastName || void 0,
|
|
1243
|
-
tags: [`lang:${contact.locale || "en"}`],
|
|
1244
|
-
is_active: contact.subscriptionStatus === "active",
|
|
1245
1243
|
source: contact.source
|
|
1246
1244
|
}
|
|
1247
1245
|
})
|
|
1248
1246
|
});
|
|
1249
|
-
if (!response.ok) {
|
|
1247
|
+
if (!response.ok && response.status !== 201) {
|
|
1250
1248
|
const error = await response.text();
|
|
1251
1249
|
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
1252
1250
|
}
|
|
@@ -1260,44 +1258,39 @@ var BroadcastProvider = class {
|
|
|
1260
1258
|
}
|
|
1261
1259
|
async updateContact(contact) {
|
|
1262
1260
|
try {
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1261
|
+
const [firstName, ...lastNameParts] = (contact.name || "").split(" ");
|
|
1262
|
+
const lastName = lastNameParts.join(" ");
|
|
1263
|
+
if (contact.subscriptionStatus === "unsubscribed") {
|
|
1264
|
+
const response2 = await fetch(`${this.apiUrl}/api/v1/subscribers/unsubscribe.json`, {
|
|
1265
|
+
method: "POST",
|
|
1266
1266
|
headers: {
|
|
1267
|
-
"Authorization": `Bearer ${this.token}
|
|
1268
|
-
|
|
1267
|
+
"Authorization": `Bearer ${this.token}`,
|
|
1268
|
+
"Content-Type": "application/json"
|
|
1269
|
+
},
|
|
1270
|
+
body: JSON.stringify({ email: contact.email })
|
|
1271
|
+
});
|
|
1272
|
+
if (!response2.ok) {
|
|
1273
|
+
const error = await response2.text();
|
|
1274
|
+
throw new Error(`Broadcast API error: ${response2.status} - ${error}`);
|
|
1269
1275
|
}
|
|
1270
|
-
);
|
|
1271
|
-
if (!searchResponse.ok) {
|
|
1272
|
-
await this.addContact(contact);
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
const existingContact = await searchResponse.json();
|
|
1276
|
-
if (!existingContact || !existingContact.id) {
|
|
1277
|
-
await this.addContact(contact);
|
|
1278
1276
|
return;
|
|
1279
1277
|
}
|
|
1280
|
-
const [firstName, ...lastNameParts] = (contact.name || "").split(" ");
|
|
1281
|
-
const lastName = lastNameParts.join(" ");
|
|
1282
1278
|
const response = await fetch(`${this.apiUrl}/api/v1/subscribers.json`, {
|
|
1283
|
-
method: "
|
|
1279
|
+
method: "POST",
|
|
1284
1280
|
headers: {
|
|
1285
1281
|
"Authorization": `Bearer ${this.token}`,
|
|
1286
1282
|
"Content-Type": "application/json"
|
|
1287
1283
|
},
|
|
1288
1284
|
body: JSON.stringify({
|
|
1289
|
-
email: contact.email,
|
|
1290
|
-
// Email at root level to identify the subscriber
|
|
1291
1285
|
subscriber: {
|
|
1286
|
+
email: contact.email,
|
|
1292
1287
|
first_name: firstName || void 0,
|
|
1293
1288
|
last_name: lastName || void 0,
|
|
1294
|
-
tags: [`lang:${contact.locale || "en"}`],
|
|
1295
|
-
is_active: contact.subscriptionStatus === "active",
|
|
1296
1289
|
source: contact.source
|
|
1297
1290
|
}
|
|
1298
1291
|
})
|
|
1299
1292
|
});
|
|
1300
|
-
if (!response.ok) {
|
|
1293
|
+
if (!response.ok && response.status !== 201) {
|
|
1301
1294
|
const error = await response.text();
|
|
1302
1295
|
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
1303
1296
|
}
|
|
@@ -1311,22 +1304,7 @@ var BroadcastProvider = class {
|
|
|
1311
1304
|
}
|
|
1312
1305
|
async removeContact(email) {
|
|
1313
1306
|
try {
|
|
1314
|
-
const
|
|
1315
|
-
`${this.apiUrl}/api/v1/subscribers/find.json?email=${encodeURIComponent(email)}`,
|
|
1316
|
-
{
|
|
1317
|
-
headers: {
|
|
1318
|
-
"Authorization": `Bearer ${this.token}`
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
);
|
|
1322
|
-
if (!searchResponse.ok) {
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
const contact = await searchResponse.json();
|
|
1326
|
-
if (!contact || !contact.id) {
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
const response = await fetch(`${this.apiUrl}/api/v1/subscribers/deactivate.json`, {
|
|
1307
|
+
const response = await fetch(`${this.apiUrl}/api/v1/subscribers/unsubscribe.json`, {
|
|
1330
1308
|
method: "POST",
|
|
1331
1309
|
headers: {
|
|
1332
1310
|
"Authorization": `Bearer ${this.token}`,
|
|
@@ -2464,6 +2442,9 @@ function verifyBroadcastWebhookSignature(payload, signature, timestamp, secret)
|
|
|
2464
2442
|
// src/types/webhooks.ts
|
|
2465
2443
|
var WEBHOOK_EVENT_TYPES = {
|
|
2466
2444
|
// Subscriber events
|
|
2445
|
+
SUBSCRIBER_CREATED: "subscriber.created",
|
|
2446
|
+
SUBSCRIBER_UPDATED: "subscriber.updated",
|
|
2447
|
+
SUBSCRIBER_DELETED: "subscriber.deleted",
|
|
2467
2448
|
SUBSCRIBER_SUBSCRIBED: "subscriber.subscribed",
|
|
2468
2449
|
SUBSCRIBER_UNSUBSCRIBED: "subscriber.unsubscribed",
|
|
2469
2450
|
// Broadcast events
|
|
@@ -2491,6 +2472,15 @@ async function handleSubscriberEvent(event, req, config) {
|
|
|
2491
2472
|
const { payload } = req;
|
|
2492
2473
|
const subscribersSlug = config.subscribersSlug || "subscribers";
|
|
2493
2474
|
switch (event.type) {
|
|
2475
|
+
case WEBHOOK_EVENT_TYPES.SUBSCRIBER_CREATED:
|
|
2476
|
+
await handleSubscriberCreated(event, payload, subscribersSlug);
|
|
2477
|
+
break;
|
|
2478
|
+
case WEBHOOK_EVENT_TYPES.SUBSCRIBER_UPDATED:
|
|
2479
|
+
await handleSubscriberUpdated(event, payload, subscribersSlug);
|
|
2480
|
+
break;
|
|
2481
|
+
case WEBHOOK_EVENT_TYPES.SUBSCRIBER_DELETED:
|
|
2482
|
+
await handleSubscriberDeleted(event, payload, subscribersSlug);
|
|
2483
|
+
break;
|
|
2494
2484
|
case WEBHOOK_EVENT_TYPES.SUBSCRIBER_SUBSCRIBED:
|
|
2495
2485
|
await handleSubscriberSubscribed(event, payload, subscribersSlug);
|
|
2496
2486
|
break;
|
|
@@ -2579,6 +2569,110 @@ async function handleSubscriberUnsubscribed(event, payload, subscribersSlug) {
|
|
|
2579
2569
|
throw error;
|
|
2580
2570
|
}
|
|
2581
2571
|
}
|
|
2572
|
+
async function handleSubscriberCreated(event, payload, subscribersSlug) {
|
|
2573
|
+
const { data } = event;
|
|
2574
|
+
try {
|
|
2575
|
+
const existing = await payload.find({
|
|
2576
|
+
collection: subscribersSlug,
|
|
2577
|
+
where: {
|
|
2578
|
+
email: {
|
|
2579
|
+
equals: data.email
|
|
2580
|
+
}
|
|
2581
|
+
},
|
|
2582
|
+
limit: 1
|
|
2583
|
+
});
|
|
2584
|
+
if (existing.docs.length > 0) {
|
|
2585
|
+
const subscriber = existing.docs[0];
|
|
2586
|
+
await payload.update({
|
|
2587
|
+
collection: subscribersSlug,
|
|
2588
|
+
id: subscriber.id,
|
|
2589
|
+
data: {
|
|
2590
|
+
name: data.name || subscriber.name,
|
|
2591
|
+
externalId: data.id,
|
|
2592
|
+
source: data.source || subscriber.source,
|
|
2593
|
+
// Don't change subscription status on created event
|
|
2594
|
+
...data.attributes && { attributes: data.attributes }
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
console.log("[Subscriber Handler] Updated existing subscriber on created event:", data.email);
|
|
2598
|
+
} else {
|
|
2599
|
+
await payload.create({
|
|
2600
|
+
collection: subscribersSlug,
|
|
2601
|
+
data: {
|
|
2602
|
+
email: data.email,
|
|
2603
|
+
name: data.name,
|
|
2604
|
+
subscriptionStatus: "pending",
|
|
2605
|
+
// New subscribers start as pending
|
|
2606
|
+
externalId: data.id,
|
|
2607
|
+
source: data.source,
|
|
2608
|
+
attributes: data.attributes || {}
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
console.log("[Subscriber Handler] Created new subscriber:", data.email);
|
|
2612
|
+
}
|
|
2613
|
+
} catch (error) {
|
|
2614
|
+
console.error("[Subscriber Handler] Error handling created event:", error);
|
|
2615
|
+
throw error;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
async function handleSubscriberUpdated(event, payload, subscribersSlug) {
|
|
2619
|
+
const { data } = event;
|
|
2620
|
+
try {
|
|
2621
|
+
const existing = await payload.find({
|
|
2622
|
+
collection: subscribersSlug,
|
|
2623
|
+
where: {
|
|
2624
|
+
email: {
|
|
2625
|
+
equals: data.email
|
|
2626
|
+
}
|
|
2627
|
+
},
|
|
2628
|
+
limit: 1
|
|
2629
|
+
});
|
|
2630
|
+
if (existing.docs.length > 0) {
|
|
2631
|
+
const subscriber = existing.docs[0];
|
|
2632
|
+
await payload.update({
|
|
2633
|
+
collection: subscribersSlug,
|
|
2634
|
+
id: subscriber.id,
|
|
2635
|
+
data: {
|
|
2636
|
+
name: data.name || subscriber.name,
|
|
2637
|
+
...data.attributes && { attributes: data.attributes }
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
console.log("[Subscriber Handler] Updated subscriber:", data.email);
|
|
2641
|
+
} else {
|
|
2642
|
+
console.warn("[Subscriber Handler] Subscriber not found for update:", data.email);
|
|
2643
|
+
}
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
console.error("[Subscriber Handler] Error handling updated event:", error);
|
|
2646
|
+
throw error;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
async function handleSubscriberDeleted(event, payload, subscribersSlug) {
|
|
2650
|
+
const { data } = event;
|
|
2651
|
+
try {
|
|
2652
|
+
const existing = await payload.find({
|
|
2653
|
+
collection: subscribersSlug,
|
|
2654
|
+
where: {
|
|
2655
|
+
email: {
|
|
2656
|
+
equals: data.email
|
|
2657
|
+
}
|
|
2658
|
+
},
|
|
2659
|
+
limit: 1
|
|
2660
|
+
});
|
|
2661
|
+
if (existing.docs.length > 0) {
|
|
2662
|
+
const subscriber = existing.docs[0];
|
|
2663
|
+
await payload.delete({
|
|
2664
|
+
collection: subscribersSlug,
|
|
2665
|
+
id: subscriber.id
|
|
2666
|
+
});
|
|
2667
|
+
console.log("[Subscriber Handler] Deleted subscriber:", data.email);
|
|
2668
|
+
} else {
|
|
2669
|
+
console.warn("[Subscriber Handler] Subscriber not found for deletion:", data.email);
|
|
2670
|
+
}
|
|
2671
|
+
} catch (error) {
|
|
2672
|
+
console.error("[Subscriber Handler] Error handling deleted event:", error);
|
|
2673
|
+
throw error;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2582
2676
|
|
|
2583
2677
|
// src/webhooks/handlers/broadcast.ts
|
|
2584
2678
|
var STATUS_MAP = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-plugin-newsletter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|