@mobilizehub/payload-plugin 0.0.1 → 0.2.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.
Files changed (91) hide show
  1. package/dist/access/authenticated.d.ts +4 -0
  2. package/dist/access/authenticated.js +5 -0
  3. package/dist/access/authenticated.js.map +1 -0
  4. package/dist/access/authenticated.spec.d.ts +1 -0
  5. package/dist/access/authenticated.spec.js +33 -0
  6. package/dist/access/authenticated.spec.js.map +1 -0
  7. package/dist/adapters/index.d.ts +1 -0
  8. package/dist/adapters/index.js +3 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/resend-adapter.d.ts +34 -0
  11. package/dist/adapters/resend-adapter.js +219 -0
  12. package/dist/adapters/resend-adapter.js.map +1 -0
  13. package/dist/collections/broadcasts/generateBroadcastsCollection.d.ts +3 -0
  14. package/dist/collections/broadcasts/generateBroadcastsCollection.js +241 -0
  15. package/dist/collections/broadcasts/generateBroadcastsCollection.js.map +1 -0
  16. package/dist/collections/contacts/generateContactsCollection.d.ts +22 -0
  17. package/dist/collections/contacts/generateContactsCollection.js +124 -0
  18. package/dist/collections/contacts/generateContactsCollection.js.map +1 -0
  19. package/dist/collections/emails/generateEmailsCollection.d.ts +3 -0
  20. package/dist/collections/emails/generateEmailsCollection.js +204 -0
  21. package/dist/collections/emails/generateEmailsCollection.js.map +1 -0
  22. package/dist/collections/emails/hooks/sync-status-from-activity.d.ts +5 -0
  23. package/dist/collections/emails/hooks/sync-status-from-activity.js +64 -0
  24. package/dist/collections/emails/hooks/sync-status-from-activity.js.map +1 -0
  25. package/dist/collections/tags/generateTagsCollection.d.ts +3 -0
  26. package/dist/collections/tags/generateTagsCollection.js +29 -0
  27. package/dist/collections/tags/generateTagsCollection.js.map +1 -0
  28. package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.d.ts +2 -0
  29. package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.js +48 -0
  30. package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.js.map +1 -0
  31. package/dist/components/broadcast-metrics-card.d.ts +7 -0
  32. package/dist/components/broadcast-metrics-card.js +159 -0
  33. package/dist/components/broadcast-metrics-card.js.map +1 -0
  34. package/dist/components/broadcast-send-modal.d.ts +9 -0
  35. package/dist/components/broadcast-send-modal.js +51 -0
  36. package/dist/components/broadcast-send-modal.js.map +1 -0
  37. package/dist/components/broadcast-send-test-drawer.d.ts +7 -0
  38. package/dist/components/broadcast-send-test-drawer.js +154 -0
  39. package/dist/components/broadcast-send-test-drawer.js.map +1 -0
  40. package/dist/components/email-activity.d.ts +4 -0
  41. package/dist/components/email-activity.js +359 -0
  42. package/dist/components/email-activity.js.map +1 -0
  43. package/dist/components/email-preview.d.ts +2 -0
  44. package/dist/components/email-preview.js +95 -0
  45. package/dist/components/email-preview.js.map +1 -0
  46. package/dist/endpoints/sendBroadcastHandler.d.ts +9 -0
  47. package/dist/endpoints/sendBroadcastHandler.js +107 -0
  48. package/dist/endpoints/sendBroadcastHandler.js.map +1 -0
  49. package/dist/endpoints/sendTestBroadcastHandler.d.ts +10 -0
  50. package/dist/endpoints/sendTestBroadcastHandler.js +143 -0
  51. package/dist/endpoints/sendTestBroadcastHandler.js.map +1 -0
  52. package/dist/endpoints/unsubscribeHandler.d.ts +9 -0
  53. package/dist/endpoints/unsubscribeHandler.js +153 -0
  54. package/dist/endpoints/unsubscribeHandler.js.map +1 -0
  55. package/dist/exports/client.d.ts +3 -1
  56. package/dist/exports/client.js +3 -0
  57. package/dist/exports/client.js.map +1 -1
  58. package/dist/exports/rsc.d.ts +2 -1
  59. package/dist/exports/rsc.js +2 -0
  60. package/dist/exports/rsc.js.map +1 -1
  61. package/dist/index.d.ts +2 -3
  62. package/dist/index.js +51 -2
  63. package/dist/index.js.map +1 -1
  64. package/dist/react/index.d.ts +1 -0
  65. package/dist/react/index.js +3 -0
  66. package/dist/react/index.js.map +1 -0
  67. package/dist/react/unsubscribe.d.ts +6 -0
  68. package/dist/react/unsubscribe.js +16 -0
  69. package/dist/react/unsubscribe.js.map +1 -0
  70. package/dist/tasks/sendBroadcastsTask.d.ts +11 -0
  71. package/dist/tasks/sendBroadcastsTask.js +196 -0
  72. package/dist/tasks/sendBroadcastsTask.js.map +1 -0
  73. package/dist/tasks/sendEmailTask.d.ts +9 -0
  74. package/dist/tasks/sendEmailTask.js +167 -0
  75. package/dist/tasks/sendEmailTask.js.map +1 -0
  76. package/dist/types/index.d.ts +135 -0
  77. package/dist/types/index.js +3 -0
  78. package/dist/types/index.js.map +1 -0
  79. package/dist/utils/api-response.d.ts +72 -0
  80. package/dist/utils/api-response.js +66 -0
  81. package/dist/utils/api-response.js.map +1 -0
  82. package/dist/utils/email.d.ts +36 -0
  83. package/dist/utils/email.js +40 -0
  84. package/dist/utils/email.js.map +1 -0
  85. package/dist/utils/lexical.d.ts +13 -0
  86. package/dist/utils/lexical.js +27 -0
  87. package/dist/utils/lexical.js.map +1 -0
  88. package/dist/utils/unsubscribe-token.d.ts +67 -0
  89. package/dist/utils/unsubscribe-token.js +103 -0
  90. package/dist/utils/unsubscribe-token.js.map +1 -0
  91. package/package.json +25 -9
@@ -0,0 +1,143 @@
1
+ import { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js';
2
+ import { formatFromAddress } from '../utils/email.js';
3
+ import { parseLexicalContent } from '../utils/lexical.js';
4
+ /**
5
+ * Validates the request body contains valid broadcastId and testEmail fields.
6
+ */ function validateRequestBody(body) {
7
+ const { broadcastId, testEmail } = body;
8
+ if (typeof broadcastId !== 'number') {
9
+ return {
10
+ code: ErrorCodes.VALIDATION_ERROR,
11
+ error: 'broadcastId is required and must be a number',
12
+ success: false
13
+ };
14
+ }
15
+ if (typeof testEmail !== 'string' || testEmail.length === 0) {
16
+ return {
17
+ code: ErrorCodes.VALIDATION_ERROR,
18
+ error: 'testEmail is required',
19
+ success: false
20
+ };
21
+ }
22
+ if (!testEmail.includes('@')) {
23
+ return {
24
+ code: ErrorCodes.VALIDATION_ERROR,
25
+ error: 'testEmail must be a valid email address',
26
+ success: false
27
+ };
28
+ }
29
+ return {
30
+ broadcastId,
31
+ success: true,
32
+ testEmail
33
+ };
34
+ }
35
+ /**
36
+ * Validates that a broadcast has all required fields for sending.
37
+ */ function validateBroadcastFields(broadcast) {
38
+ if (!broadcast.content) {
39
+ return {
40
+ code: ErrorCodes.VALIDATION_ERROR,
41
+ error: 'Broadcast content is required',
42
+ success: false
43
+ };
44
+ }
45
+ if (!broadcast.fromName) {
46
+ return {
47
+ code: ErrorCodes.VALIDATION_ERROR,
48
+ error: 'Broadcast fromName is required',
49
+ success: false
50
+ };
51
+ }
52
+ if (!broadcast.fromAddress) {
53
+ return {
54
+ code: ErrorCodes.VALIDATION_ERROR,
55
+ error: 'Broadcast fromAddress is required',
56
+ success: false
57
+ };
58
+ }
59
+ if (!broadcast.subject) {
60
+ return {
61
+ code: ErrorCodes.VALIDATION_ERROR,
62
+ error: 'Broadcast subject is required',
63
+ success: false
64
+ };
65
+ }
66
+ return {
67
+ success: true
68
+ };
69
+ }
70
+ /**
71
+ * Fetches a broadcast, renders its content, and sends a test email.
72
+ * Test emails do not include an unsubscribe token.
73
+ */ async function sendTestEmail(req, broadcastId, testEmail, emailConfig) {
74
+ const { payload } = req;
75
+ const logger = payload.logger;
76
+ const broadcast = await payload.findByID({
77
+ id: broadcastId,
78
+ collection: 'broadcasts'
79
+ });
80
+ if (!broadcast) {
81
+ logger.error(`Broadcast with ID ${broadcastId} not found`);
82
+ return errorResponse(ErrorCodes.BROADCAST_NOT_FOUND, 'Broadcast not found', 404);
83
+ }
84
+ const validation = validateBroadcastFields(broadcast);
85
+ if (!validation.success) {
86
+ logger.error(`Broadcast ${broadcastId}: ${validation.error}`);
87
+ return errorResponse(validation.code, validation.error, 400);
88
+ }
89
+ const { render, sendEmail } = emailConfig(req);
90
+ const parsedContent = await parseLexicalContent(broadcast.content, payload.config);
91
+ const fromAddress = formatFromAddress(broadcast.fromName, broadcast.fromAddress);
92
+ const html = render({
93
+ from: fromAddress,
94
+ html: parsedContent.html,
95
+ markdown: parsedContent.markdown,
96
+ plainText: parsedContent.plainText,
97
+ subject: broadcast.subject,
98
+ to: testEmail,
99
+ token: ''
100
+ });
101
+ const emailInput = {
102
+ from: fromAddress,
103
+ html,
104
+ subject: broadcast.subject,
105
+ to: testEmail
106
+ };
107
+ await sendEmail(emailInput);
108
+ logger.info(`Test email sent to ${testEmail} for broadcast ${broadcastId}`);
109
+ return successResponse({
110
+ message: 'Test email sent successfully'
111
+ }, 200);
112
+ }
113
+ /**
114
+ * Creates the send-test-email endpoint handler.
115
+ *
116
+ * Allows authenticated users to send a test email for a broadcast to a
117
+ * specified email address. Validates the broadcast exists and has all
118
+ * required fields before rendering and sending.
119
+ */ export const sendTestEmailHandler = (config)=>{
120
+ return async (req)=>{
121
+ const { payload } = req;
122
+ const logger = payload.logger;
123
+ if (!req.json) {
124
+ return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400);
125
+ }
126
+ if (!req.user) {
127
+ return errorResponse(ErrorCodes.UNAUTHORIZED, 'Unauthorized', 401);
128
+ }
129
+ try {
130
+ const body = await req.json();
131
+ const validation = validateRequestBody(body);
132
+ if (!validation.success) {
133
+ return errorResponse(validation.code, validation.error, 400);
134
+ }
135
+ return await sendTestEmail(req, validation.broadcastId, validation.testEmail, config.email);
136
+ } catch (error) {
137
+ logger.error(error, 'Error sending test email');
138
+ return errorResponse(ErrorCodes.INTERNAL_ERROR, 'Error sending test email', 500);
139
+ }
140
+ };
141
+ };
142
+
143
+ //# sourceMappingURL=sendTestBroadcastHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/sendTestBroadcastHandler.ts"],"sourcesContent":["import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'\nimport type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport type { EmailMessage, MobilizehubPluginConfig } from '../types/index.js'\n\nimport { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js'\nimport { formatFromAddress } from '../utils/email.js'\nimport { parseLexicalContent } from '../utils/lexical.js'\n\ntype Broadcast = {\n content?: SerializedEditorState\n fromAddress?: string\n fromName?: string\n id: number | string\n subject?: string\n}\n\ntype SendTestEmailBody = {\n broadcastId?: unknown\n testEmail?: unknown\n}\n\ntype ValidationResult =\n | { broadcastId: number; success: true; testEmail: string }\n | { code: string; error: string; success: false }\n\n/**\n * Validates the request body contains valid broadcastId and testEmail fields.\n */\nfunction validateRequestBody(body: SendTestEmailBody): ValidationResult {\n const { broadcastId, testEmail } = body\n\n if (typeof broadcastId !== 'number') {\n return {\n code: ErrorCodes.VALIDATION_ERROR,\n error: 'broadcastId is required and must be a number',\n success: false,\n }\n }\n\n if (typeof testEmail !== 'string' || testEmail.length === 0) {\n return { code: ErrorCodes.VALIDATION_ERROR, error: 'testEmail is required', success: false }\n }\n\n if (!testEmail.includes('@')) {\n return {\n code: ErrorCodes.VALIDATION_ERROR,\n error: 'testEmail must be a valid email address',\n success: false,\n }\n }\n\n return { broadcastId, success: true, testEmail }\n}\n\n/**\n * Validates that a broadcast has all required fields for sending.\n */\nfunction validateBroadcastFields(\n broadcast: Broadcast,\n): { code: string; error: string; success: false } | { success: true } {\n if (!broadcast.content) {\n return {\n code: ErrorCodes.VALIDATION_ERROR,\n error: 'Broadcast content is required',\n success: false,\n }\n }\n\n if (!broadcast.fromName) {\n return {\n code: ErrorCodes.VALIDATION_ERROR,\n error: 'Broadcast fromName is required',\n success: false,\n }\n }\n\n if (!broadcast.fromAddress) {\n return {\n code: ErrorCodes.VALIDATION_ERROR,\n error: 'Broadcast fromAddress is required',\n success: false,\n }\n }\n\n if (!broadcast.subject) {\n return {\n code: ErrorCodes.VALIDATION_ERROR,\n error: 'Broadcast subject is required',\n success: false,\n }\n }\n\n return { success: true }\n}\n\n/**\n * Fetches a broadcast, renders its content, and sends a test email.\n * Test emails do not include an unsubscribe token.\n */\nasync function sendTestEmail(\n req: PayloadRequest,\n broadcastId: number,\n testEmail: string,\n emailConfig: MobilizehubPluginConfig['email'],\n): Promise<Response> {\n const { payload } = req\n const logger = payload.logger\n\n const broadcast = (await payload.findByID({\n id: broadcastId,\n collection: 'broadcasts',\n })) as Broadcast | null\n\n if (!broadcast) {\n logger.error(`Broadcast with ID ${broadcastId} not found`)\n return errorResponse(ErrorCodes.BROADCAST_NOT_FOUND, 'Broadcast not found', 404)\n }\n\n const validation = validateBroadcastFields(broadcast)\n if (!validation.success) {\n logger.error(`Broadcast ${broadcastId}: ${validation.error}`)\n return errorResponse(validation.code, validation.error, 400)\n }\n\n const { render, sendEmail } = emailConfig(req)\n\n const parsedContent = await parseLexicalContent(broadcast.content!, payload.config)\n\n const fromAddress = formatFromAddress(broadcast.fromName!, broadcast.fromAddress!)\n\n const html = render({\n from: fromAddress,\n html: parsedContent.html,\n markdown: parsedContent.markdown,\n plainText: parsedContent.plainText,\n subject: broadcast.subject!,\n to: testEmail,\n token: '',\n })\n\n const emailInput: Pick<EmailMessage, 'from' | 'html' | 'subject' | 'to'> = {\n from: fromAddress,\n html,\n subject: broadcast.subject!,\n to: testEmail,\n }\n\n await sendEmail(emailInput)\n\n logger.info(`Test email sent to ${testEmail} for broadcast ${broadcastId}`)\n\n return successResponse({ message: 'Test email sent successfully' }, 200)\n}\n\n/**\n * Creates the send-test-email endpoint handler.\n *\n * Allows authenticated users to send a test email for a broadcast to a\n * specified email address. Validates the broadcast exists and has all\n * required fields before rendering and sending.\n */\nexport const sendTestEmailHandler = (\n config: Pick<MobilizehubPluginConfig, 'email'>,\n): PayloadHandler => {\n return async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n if (!req.json) {\n return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400)\n }\n\n if (!req.user) {\n return errorResponse(ErrorCodes.UNAUTHORIZED, 'Unauthorized', 401)\n }\n\n try {\n const body = (await req.json()) as SendTestEmailBody\n\n const validation = validateRequestBody(body)\n if (!validation.success) {\n return errorResponse(validation.code, validation.error, 400)\n }\n\n return await sendTestEmail(req, validation.broadcastId, validation.testEmail, config.email)\n } catch (error) {\n logger.error(error as Error, 'Error sending test email')\n return errorResponse(ErrorCodes.INTERNAL_ERROR, 'Error sending test email', 500)\n }\n }\n}\n"],"names":["ErrorCodes","errorResponse","successResponse","formatFromAddress","parseLexicalContent","validateRequestBody","body","broadcastId","testEmail","code","VALIDATION_ERROR","error","success","length","includes","validateBroadcastFields","broadcast","content","fromName","fromAddress","subject","sendTestEmail","req","emailConfig","payload","logger","findByID","id","collection","BROADCAST_NOT_FOUND","validation","render","sendEmail","parsedContent","config","html","from","markdown","plainText","to","token","emailInput","info","message","sendTestEmailHandler","json","BAD_REQUEST","user","UNAUTHORIZED","email","INTERNAL_ERROR"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ /**
3
+ * Creates the unsubscribe endpoint handler.
4
+ *
5
+ * Processes unsubscribe requests by validating a signed token, looking up
6
+ * the associated email and contact, then setting the contact's emailOptIn
7
+ * to false. Handles already-unsubscribed contacts gracefully.
8
+ */
9
+ export declare const unsubscribeHandler: () => PayloadHandler;
@@ -0,0 +1,153 @@
1
+ import z from 'zod';
2
+ import { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js';
3
+ import { verifyUnsubscribeToken } from '../utils/unsubscribe-token.js';
4
+ /**
5
+ * Schema for validating unsubscribe request body.
6
+ */ const UnsubscribeBodySchema = z.object({
7
+ token: z.string().min(1, {
8
+ message: 'Token is required'
9
+ })
10
+ });
11
+ /**
12
+ * Validates the request body against the schema.
13
+ * Returns typed data on success, or error message on failure.
14
+ */ function validateRequestBody(body) {
15
+ const result = UnsubscribeBodySchema.safeParse(body);
16
+ if (!result.success) {
17
+ const firstError = result.error.issues[0]?.message || 'Invalid request body';
18
+ return {
19
+ error: firstError,
20
+ success: false
21
+ };
22
+ }
23
+ return {
24
+ data: result.data,
25
+ success: true
26
+ };
27
+ }
28
+ /**
29
+ * Checks if a token has expired by comparing the expiration date to now.
30
+ */ function isTokenExpired(expiresAt) {
31
+ return new Date(expiresAt) < new Date();
32
+ }
33
+ /**
34
+ * Fetches an unsubscribe token record by ID.
35
+ * Returns null if the record doesn't exist.
36
+ */ async function findUnsubscribeRecord(payload, tokenId) {
37
+ try {
38
+ const record = await payload.findByID({
39
+ id: tokenId,
40
+ collection: 'emailUnsubscribeTokens'
41
+ });
42
+ return record;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Fetches an email document by ID.
49
+ * Returns null if the email doesn't exist.
50
+ */ async function findEmailById(payload, emailId) {
51
+ try {
52
+ const email = await payload.findByID({
53
+ id: emailId,
54
+ collection: 'emails'
55
+ });
56
+ return email;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Finds a contact by their email address.
63
+ * Returns null if no matching contact exists.
64
+ */ async function findContactByEmail(payload, emailAddress) {
65
+ const result = await payload.find({
66
+ collection: 'contacts',
67
+ limit: 1,
68
+ where: {
69
+ email: {
70
+ equals: emailAddress
71
+ }
72
+ }
73
+ });
74
+ return result.docs[0] || null;
75
+ }
76
+ /**
77
+ * Sets a contact's emailOptIn to false to unsubscribe them from emails.
78
+ */ async function unsubscribeContact(payload, contactId) {
79
+ await payload.update({
80
+ id: contactId,
81
+ collection: 'contacts',
82
+ data: {
83
+ emailOptIn: false
84
+ }
85
+ });
86
+ }
87
+ /**
88
+ * Creates the unsubscribe endpoint handler.
89
+ *
90
+ * Processes unsubscribe requests by validating a signed token, looking up
91
+ * the associated email and contact, then setting the contact's emailOptIn
92
+ * to false. Handles already-unsubscribed contacts gracefully.
93
+ */ export const unsubscribeHandler = ()=>{
94
+ return async (req)=>{
95
+ const { payload } = req;
96
+ const logger = payload.logger;
97
+ if (!req.json) {
98
+ return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400);
99
+ }
100
+ try {
101
+ const body = await req.json();
102
+ const validation = validateRequestBody(body);
103
+ if (!validation.success) {
104
+ return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.error, 400);
105
+ }
106
+ const { token } = validation.data;
107
+ const verificationResult = verifyUnsubscribeToken({
108
+ token
109
+ });
110
+ if (!verificationResult) {
111
+ return errorResponse(ErrorCodes.TOKEN_INVALID, 'Invalid or expired token', 400);
112
+ }
113
+ const { tokenId } = verificationResult;
114
+ const unsubscribeRecord = await findUnsubscribeRecord(payload, tokenId);
115
+ if (!unsubscribeRecord) {
116
+ return errorResponse(ErrorCodes.TOKEN_INVALID, 'Invalid token', 400);
117
+ }
118
+ if (unsubscribeRecord.expiresAt && isTokenExpired(unsubscribeRecord.expiresAt)) {
119
+ return errorResponse(ErrorCodes.TOKEN_EXPIRED, 'Token has expired', 400);
120
+ }
121
+ if (!unsubscribeRecord.emailId) {
122
+ logger.error(`Unsubscribe record ${tokenId} has no emailId`);
123
+ return errorResponse(ErrorCodes.VALIDATION_ERROR, 'Invalid unsubscribe record', 400);
124
+ }
125
+ const email = await findEmailById(payload, unsubscribeRecord.emailId);
126
+ if (!email) {
127
+ logger.error(`Email ${unsubscribeRecord.emailId} not found for unsubscribe record ${tokenId}`);
128
+ return errorResponse(ErrorCodes.NOT_FOUND, 'Associated email not found', 404);
129
+ }
130
+ const contact = await findContactByEmail(payload, email.to);
131
+ if (!contact) {
132
+ logger.error(`Contact not found for email address ${email.to}`);
133
+ return errorResponse(ErrorCodes.CONTACT_NOT_FOUND, 'Associated contact not found', 404);
134
+ }
135
+ if (contact.emailOptIn === false) {
136
+ logger.info(`Contact ${contact.id} (${email.to}) is already unsubscribed`);
137
+ return successResponse({
138
+ message: 'Already unsubscribed'
139
+ }, 200);
140
+ }
141
+ await unsubscribeContact(payload, contact.id);
142
+ logger.info(`Contact ${contact.id} (${email.to}) successfully unsubscribed`);
143
+ return successResponse({
144
+ message: 'Successfully unsubscribed'
145
+ }, 200);
146
+ } catch (error) {
147
+ logger.error(error, 'Error processing unsubscribe request');
148
+ return errorResponse(ErrorCodes.INTERNAL_ERROR, 'Internal Server Error', 500);
149
+ }
150
+ };
151
+ };
152
+
153
+ //# sourceMappingURL=unsubscribeHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/unsubscribeHandler.ts"],"sourcesContent":["import type { Payload, PayloadHandler } from 'payload'\n\nimport z from 'zod'\n\nimport type { Contact, UnsubscribeTokenRecord } from '../types/index.js'\n\nimport { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js'\nimport { verifyUnsubscribeToken } from '../utils/unsubscribe-token.js'\n\n/**\n * Schema for validating unsubscribe request body.\n */\nconst UnsubscribeBodySchema = z.object({\n token: z.string().min(1, { message: 'Token is required' }),\n})\n\n/**\n * Validates the request body against the schema.\n * Returns typed data on success, or error message on failure.\n */\nfunction validateRequestBody(body: unknown) {\n const result = UnsubscribeBodySchema.safeParse(body)\n\n if (!result.success) {\n const firstError = result.error.issues[0]?.message || 'Invalid request body'\n return { error: firstError, success: false as const }\n }\n\n return { data: result.data, success: true as const }\n}\n\n/**\n * Checks if a token has expired by comparing the expiration date to now.\n */\nfunction isTokenExpired(expiresAt: string): boolean {\n return new Date(expiresAt) < new Date()\n}\n\n/**\n * Fetches an unsubscribe token record by ID.\n * Returns null if the record doesn't exist.\n */\nasync function findUnsubscribeRecord(\n payload: Payload,\n tokenId: string,\n): Promise<null | UnsubscribeTokenRecord> {\n try {\n const record = await payload.findByID({\n id: tokenId,\n collection: 'emailUnsubscribeTokens',\n })\n return record as null | UnsubscribeTokenRecord\n } catch {\n return null\n }\n}\n\n/**\n * Fetches an email document by ID.\n * Returns null if the email doesn't exist.\n */\nasync function findEmailById(\n payload: Payload,\n emailId: number | string,\n): Promise<{ to: string } | null> {\n try {\n const email = await payload.findByID({\n id: emailId,\n collection: 'emails',\n })\n return email as unknown as { to: string } | null\n } catch {\n return null\n }\n}\n\n/**\n * Finds a contact by their email address.\n * Returns null if no matching contact exists.\n */\nasync function findContactByEmail(payload: Payload, emailAddress: string): Promise<Contact | null> {\n const result = await payload.find({\n collection: 'contacts',\n limit: 1,\n where: {\n email: { equals: emailAddress },\n },\n })\n\n return (result.docs[0] as Contact) || null\n}\n\n/**\n * Sets a contact's emailOptIn to false to unsubscribe them from emails.\n */\nasync function unsubscribeContact(payload: Payload, contactId: number | string): Promise<void> {\n await payload.update({\n id: contactId,\n collection: 'contacts',\n data: {\n emailOptIn: false,\n },\n })\n}\n\n/**\n * Creates the unsubscribe endpoint handler.\n *\n * Processes unsubscribe requests by validating a signed token, looking up\n * the associated email and contact, then setting the contact's emailOptIn\n * to false. Handles already-unsubscribed contacts gracefully.\n */\nexport const unsubscribeHandler = (): PayloadHandler => {\n return async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n if (!req.json) {\n return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400)\n }\n\n try {\n const body = await req.json()\n\n const validation = validateRequestBody(body)\n if (!validation.success) {\n return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.error, 400)\n }\n\n const { token } = validation.data\n\n const verificationResult = verifyUnsubscribeToken({ token })\n\n if (!verificationResult) {\n return errorResponse(ErrorCodes.TOKEN_INVALID, 'Invalid or expired token', 400)\n }\n\n const { tokenId } = verificationResult\n\n const unsubscribeRecord = await findUnsubscribeRecord(payload, tokenId)\n\n if (!unsubscribeRecord) {\n return errorResponse(ErrorCodes.TOKEN_INVALID, 'Invalid token', 400)\n }\n\n if (unsubscribeRecord.expiresAt && isTokenExpired(unsubscribeRecord.expiresAt)) {\n return errorResponse(ErrorCodes.TOKEN_EXPIRED, 'Token has expired', 400)\n }\n\n if (!unsubscribeRecord.emailId) {\n logger.error(`Unsubscribe record ${tokenId} has no emailId`)\n return errorResponse(ErrorCodes.VALIDATION_ERROR, 'Invalid unsubscribe record', 400)\n }\n\n const email = await findEmailById(payload, unsubscribeRecord.emailId)\n\n if (!email) {\n logger.error(\n `Email ${unsubscribeRecord.emailId} not found for unsubscribe record ${tokenId}`,\n )\n return errorResponse(ErrorCodes.NOT_FOUND, 'Associated email not found', 404)\n }\n\n const contact = await findContactByEmail(payload, email.to)\n\n if (!contact) {\n logger.error(`Contact not found for email address ${email.to}`)\n return errorResponse(ErrorCodes.CONTACT_NOT_FOUND, 'Associated contact not found', 404)\n }\n\n if (contact.emailOptIn === false) {\n logger.info(`Contact ${contact.id} (${email.to}) is already unsubscribed`)\n return successResponse({ message: 'Already unsubscribed' }, 200)\n }\n\n await unsubscribeContact(payload, contact.id)\n\n logger.info(`Contact ${contact.id} (${email.to}) successfully unsubscribed`)\n\n return successResponse({ message: 'Successfully unsubscribed' }, 200)\n } catch (error) {\n logger.error(error as Error, 'Error processing unsubscribe request')\n return errorResponse(ErrorCodes.INTERNAL_ERROR, 'Internal Server Error', 500)\n }\n }\n}\n"],"names":["z","ErrorCodes","errorResponse","successResponse","verifyUnsubscribeToken","UnsubscribeBodySchema","object","token","string","min","message","validateRequestBody","body","result","safeParse","success","firstError","error","issues","data","isTokenExpired","expiresAt","Date","findUnsubscribeRecord","payload","tokenId","record","findByID","id","collection","findEmailById","emailId","email","findContactByEmail","emailAddress","find","limit","where","equals","docs","unsubscribeContact","contactId","update","emailOptIn","unsubscribeHandler","req","logger","json","BAD_REQUEST","validation","VALIDATION_ERROR","verificationResult","TOKEN_INVALID","unsubscribeRecord","TOKEN_EXPIRED","NOT_FOUND","contact","to","CONTACT_NOT_FOUND","info","INTERNAL_ERROR"],"mappings":"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"}
@@ -1 +1,3 @@
1
- export {};
1
+ export { SendBroadcastModal } from '../components/broadcast-send-modal.js';
2
+ export { SendTestBroadcastDrawer } from '../components/broadcast-send-test-drawer.js';
3
+ export { EmailPreviewField } from '../components/email-preview.js';
@@ -1,2 +1,5 @@
1
+ export { SendBroadcastModal } from '../components/broadcast-send-modal.js';
2
+ export { SendTestBroadcastDrawer } from '../components/broadcast-send-test-drawer.js';
3
+ export { EmailPreviewField } from '../components/email-preview.js';
1
4
 
2
5
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/client.ts"],"names":[],"mappings":""}
1
+ {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { SendBroadcastModal } from '../components/broadcast-send-modal.js'\nexport { SendTestBroadcastDrawer } from '../components/broadcast-send-test-drawer.js'\nexport { EmailPreviewField } from '../components/email-preview.js'\n"],"names":["SendBroadcastModal","SendTestBroadcastDrawer","EmailPreviewField"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,wCAAuC;AAC1E,SAASC,uBAAuB,QAAQ,8CAA6C;AACrF,SAASC,iBAAiB,QAAQ,iCAAgC"}
@@ -1 +1,2 @@
1
- export {};
1
+ export { MetricsCards } from '../components/broadcast-metrics-card.js';
2
+ export { EmailActivityField } from '../components/email-activity.js';
@@ -1,2 +1,4 @@
1
+ export { MetricsCards } from '../components/broadcast-metrics-card.js';
2
+ export { EmailActivityField } from '../components/email-activity.js';
1
3
 
2
4
  //# sourceMappingURL=rsc.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/rsc.ts"],"names":[],"mappings":""}
1
+ {"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["export { MetricsCards } from '../components/broadcast-metrics-card.js'\nexport { EmailActivityField } from '../components/email-activity.js'\n"],"names":["MetricsCards","EmailActivityField"],"mappings":"AAAA,SAASA,YAAY,QAAQ,0CAAyC;AACtE,SAASC,kBAAkB,QAAQ,kCAAiC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { Config } from 'payload';
2
- export type MobilizehubPluginConfig = {
3
- disabled?: boolean;
4
- };
2
+ import type { MobilizehubPluginConfig } from './types/index.js';
3
+ export * from './types/index.js';
5
4
  export declare const mobilizehubPlugin: (pluginOptions: MobilizehubPluginConfig) => (config: Config) => Config;
package/dist/index.js CHANGED
@@ -1,10 +1,59 @@
1
+ import { generateBroadcastsCollection } from './collections/broadcasts/generateBroadcastsCollection.js';
2
+ import { generateContactsCollection } from './collections/contacts/generateContactsCollection.js';
3
+ import { generateEmailsCollection } from './collections/emails/generateEmailsCollection.js';
4
+ import { generateTagsCollection } from './collections/tags/generateTagsCollection.js';
5
+ import { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js';
6
+ import { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js';
7
+ import { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js';
8
+ import { unsubscribeHandler } from './endpoints/unsubscribeHandler.js';
9
+ import { createSendBroadcastsTask } from './tasks/sendBroadcastsTask.js';
10
+ import { createSendEmailTask } from './tasks/sendEmailTask.js';
11
+ export * from './types/index.js';
1
12
  export const mobilizehubPlugin = (pluginOptions)=>(config)=>{
13
+ if (pluginOptions.disabled) {
14
+ return config;
15
+ }
2
16
  if (!config.collections) {
3
17
  config.collections = [];
4
18
  }
5
- if (pluginOptions.disabled) {
6
- return config;
19
+ config.collections.push(generateTagsCollection(pluginOptions), generateContactsCollection(pluginOptions), generateBroadcastsCollection(pluginOptions), generateEmailsCollection(pluginOptions), generateUnsubscribeTokensCollection());
20
+ if (!config.endpoints) {
21
+ config.endpoints = [];
22
+ }
23
+ const endpoints = [
24
+ {
25
+ handler: sendBroadcastHandler(),
26
+ method: 'post',
27
+ path: '/send-broadcast'
28
+ },
29
+ {
30
+ handler: sendTestEmailHandler(pluginOptions),
31
+ method: 'post',
32
+ path: '/send-test-email'
33
+ },
34
+ {
35
+ handler: unsubscribeHandler(),
36
+ method: 'post',
37
+ path: '/unsubscribe'
38
+ }
39
+ ];
40
+ config.endpoints = [
41
+ ...config.endpoints,
42
+ ...endpoints
43
+ ];
44
+ if (!config.jobs) {
45
+ config.jobs = {
46
+ tasks: []
47
+ };
7
48
  }
49
+ const tasks = [
50
+ createSendBroadcastsTask(pluginOptions),
51
+ createSendEmailTask(pluginOptions)
52
+ ];
53
+ config.jobs.tasks = [
54
+ ...config.jobs.tasks ?? [],
55
+ ...tasks
56
+ ];
8
57
  const incomingOnInit = config.onInit;
9
58
  config.onInit = async (payload)=>{
10
59
  if (incomingOnInit) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nexport type MobilizehubPluginConfig = {\n disabled?: boolean\n}\n\nexport const mobilizehubPlugin =\n (pluginOptions: MobilizehubPluginConfig) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n if (pluginOptions.disabled) {\n return config\n }\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n }\n\n return config\n }\n"],"names":["mobilizehubPlugin","pluginOptions","config","collections","disabled","incomingOnInit","onInit","payload"],"mappings":"AAMA,OAAO,MAAMA,oBACX,CAACC,gBACD,CAACC;QACC,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEA,IAAIF,cAAcG,QAAQ,EAAE;YAC1B,OAAOF;QACT;QAEA,MAAMG,iBAAiBH,OAAOI,MAAM;QAEpCJ,OAAOI,MAAM,GAAG,OAAOC;YACrB,IAAIF,gBAAgB;gBAClB,MAAMA,eAAeE;YACvB;QACF;QAEA,OAAOL;IACT,EAAC"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config, Endpoint, TaskConfig } from 'payload'\n\nimport type { MobilizehubPluginConfig } from './types/index.js'\n\nimport { generateBroadcastsCollection } from './collections/broadcasts/generateBroadcastsCollection.js'\nimport { generateContactsCollection } from './collections/contacts/generateContactsCollection.js'\nimport { generateEmailsCollection } from './collections/emails/generateEmailsCollection.js'\nimport { generateTagsCollection } from './collections/tags/generateTagsCollection.js'\nimport { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js'\nimport { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js'\nimport { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js'\nimport { unsubscribeHandler } from './endpoints/unsubscribeHandler.js'\nimport { createSendBroadcastsTask } from './tasks/sendBroadcastsTask.js'\nimport { createSendEmailTask } from './tasks/sendEmailTask.js'\n\nexport * from './types/index.js'\n\nexport const mobilizehubPlugin =\n (pluginOptions: MobilizehubPluginConfig) =>\n (config: Config): Config => {\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n config.collections.push(\n generateTagsCollection(pluginOptions),\n generateContactsCollection(pluginOptions),\n generateBroadcastsCollection(pluginOptions),\n generateEmailsCollection(pluginOptions),\n generateUnsubscribeTokensCollection(),\n )\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n const endpoints: Endpoint[] = [\n {\n handler: sendBroadcastHandler(),\n method: 'post',\n path: '/send-broadcast',\n },\n {\n handler: sendTestEmailHandler(pluginOptions),\n method: 'post',\n path: '/send-test-email',\n },\n {\n handler: unsubscribeHandler(),\n method: 'post',\n path: '/unsubscribe',\n },\n ]\n\n config.endpoints = [...config.endpoints, ...endpoints]\n\n if (!config.jobs) {\n config.jobs = {\n tasks: [],\n }\n }\n\n const tasks: TaskConfig[] = [\n createSendBroadcastsTask(pluginOptions),\n createSendEmailTask(pluginOptions),\n ]\n\n config.jobs.tasks = [...(config.jobs.tasks ?? []), ...tasks]\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n }\n\n return config\n }\n"],"names":["generateBroadcastsCollection","generateContactsCollection","generateEmailsCollection","generateTagsCollection","generateUnsubscribeTokensCollection","sendBroadcastHandler","sendTestEmailHandler","unsubscribeHandler","createSendBroadcastsTask","createSendEmailTask","mobilizehubPlugin","pluginOptions","config","disabled","collections","push","endpoints","handler","method","path","jobs","tasks","incomingOnInit","onInit","payload"],"mappings":"AAIA,SAASA,4BAA4B,QAAQ,2DAA0D;AACvG,SAASC,0BAA0B,QAAQ,uDAAsD;AACjG,SAASC,wBAAwB,QAAQ,mDAAkD;AAC3F,SAASC,sBAAsB,QAAQ,+CAA8C;AACrF,SAASC,mCAAmC,QAAQ,gEAA+D;AACnH,SAASC,oBAAoB,QAAQ,sCAAqC;AAC1E,SAASC,oBAAoB,QAAQ,0CAAyC;AAC9E,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,cAAc,mBAAkB;AAEhC,OAAO,MAAMC,oBACX,CAACC,gBACD,CAACC;QACC,IAAID,cAAcE,QAAQ,EAAE;YAC1B,OAAOD;QACT;QAEA,IAAI,CAACA,OAAOE,WAAW,EAAE;YACvBF,OAAOE,WAAW,GAAG,EAAE;QACzB;QAEAF,OAAOE,WAAW,CAACC,IAAI,CACrBZ,uBAAuBQ,gBACvBV,2BAA2BU,gBAC3BX,6BAA6BW,gBAC7BT,yBAAyBS,gBACzBP;QAGF,IAAI,CAACQ,OAAOI,SAAS,EAAE;YACrBJ,OAAOI,SAAS,GAAG,EAAE;QACvB;QAEA,MAAMA,YAAwB;YAC5B;gBACEC,SAASZ;gBACTa,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASX,qBAAqBK;gBAC9BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASV;gBACTW,QAAQ;gBACRC,MAAM;YACR;SACD;QAEDP,OAAOI,SAAS,GAAG;eAAIJ,OAAOI,SAAS;eAAKA;SAAU;QAEtD,IAAI,CAACJ,OAAOQ,IAAI,EAAE;YAChBR,OAAOQ,IAAI,GAAG;gBACZC,OAAO,EAAE;YACX;QACF;QAEA,MAAMA,QAAsB;YAC1Bb,yBAAyBG;YACzBF,oBAAoBE;SACrB;QAEDC,OAAOQ,IAAI,CAACC,KAAK,GAAG;eAAKT,OAAOQ,IAAI,CAACC,KAAK,IAAI,EAAE;eAAMA;SAAM;QAE5D,MAAMC,iBAAiBV,OAAOW,MAAM;QAEpCX,OAAOW,MAAM,GAAG,OAAOC;YACrB,IAAIF,gBAAgB;gBAClB,MAAMA,eAAeE;YACvB;QACF;QAEA,OAAOZ;IACT,EAAC"}
@@ -0,0 +1 @@
1
+ export { confirmUnsubscribe } from './unsubscribe.js';
@@ -0,0 +1,3 @@
1
+ export { confirmUnsubscribe } from './unsubscribe.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.ts"],"sourcesContent":["export { confirmUnsubscribe } from './unsubscribe.js'\n"],"names":["confirmUnsubscribe"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,mBAAkB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Confirms an unsubscribe request by sending the token to the backend.
3
+ */
4
+ export declare function confirmUnsubscribe({ token }: {
5
+ token: string;
6
+ }): Promise<any>;
@@ -0,0 +1,16 @@
1
+ const apiUrl = '/api/unsubscribe';
2
+ /**
3
+ * Confirms an unsubscribe request by sending the token to the backend.
4
+ */ export async function confirmUnsubscribe({ token }) {
5
+ return fetch(apiUrl, {
6
+ body: JSON.stringify({
7
+ token
8
+ }),
9
+ headers: {
10
+ 'Content-Type': 'application/json'
11
+ },
12
+ method: 'POST'
13
+ }).then((res)=>res.json());
14
+ }
15
+
16
+ //# sourceMappingURL=unsubscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/unsubscribe.ts"],"sourcesContent":["const apiUrl = '/api/unsubscribe'\n\n/**\n * Confirms an unsubscribe request by sending the token to the backend.\n */\nexport async function confirmUnsubscribe({ token }: { token: string }) {\n return fetch(apiUrl, {\n body: JSON.stringify({ token }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n }).then((res) => res.json())\n}\n"],"names":["apiUrl","confirmUnsubscribe","token","fetch","body","JSON","stringify","headers","method","then","res","json"],"mappings":"AAAA,MAAMA,SAAS;AAEf;;CAEC,GACD,OAAO,eAAeC,mBAAmB,EAAEC,KAAK,EAAqB;IACnE,OAAOC,MAAMH,QAAQ;QACnBI,MAAMC,KAAKC,SAAS,CAAC;YAAEJ;QAAM;QAC7BK,SAAS;YACP,gBAAgB;QAClB;QACAC,QAAQ;IACV,GAAGC,IAAI,CAAC,CAACC,MAAQA,IAAIC,IAAI;AAC3B"}
@@ -0,0 +1,11 @@
1
+ import type { TaskConfig } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../types/index.js';
3
+ /**
4
+ * Creates the send-broadcasts scheduled task.
5
+ *
6
+ * Processes broadcasts by polling for documents with status 'sending' and
7
+ * queuing batches of send-email jobs. Each invocation processes one broadcast
8
+ * and one batch of contacts, allowing the task to be distributed across
9
+ * multiple schedule intervals.
10
+ */
11
+ export declare const createSendBroadcastsTask: (pluginConfig: MobilizehubPluginConfig) => TaskConfig;