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 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 searchResponse = await fetch(
1264
- `${this.apiUrl}/api/v1/subscribers/find.json?email=${encodeURIComponent(contact.email)}`,
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: "PATCH",
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 searchResponse = await fetch(
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.21.4",
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",