@mobilizehub/payload-plugin 0.6.2 → 0.8.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 (32) hide show
  1. package/dist/collections/petition-signatures/generatePetitionSignaturesCollection.d.ts +3 -0
  2. package/dist/collections/petition-signatures/generatePetitionSignaturesCollection.js +79 -0
  3. package/dist/collections/petition-signatures/generatePetitionSignaturesCollection.js.map +1 -0
  4. package/dist/collections/petition-signatures/hooks/processPetitionSignature.d.ts +11 -0
  5. package/dist/collections/petition-signatures/hooks/processPetitionSignature.js +153 -0
  6. package/dist/collections/petition-signatures/hooks/processPetitionSignature.js.map +1 -0
  7. package/dist/collections/petition-signatures/hooks/sendAutoresponse.d.ts +16 -0
  8. package/dist/collections/petition-signatures/hooks/sendAutoresponse.js +109 -0
  9. package/dist/collections/petition-signatures/hooks/sendAutoresponse.js.map +1 -0
  10. package/dist/collections/petitions/generatePetitionsCollection.d.ts +3 -0
  11. package/dist/collections/petitions/generatePetitionsCollection.js +252 -0
  12. package/dist/collections/petitions/generatePetitionsCollection.js.map +1 -0
  13. package/dist/endpoints/petitionSignatureHandler.d.ts +14 -0
  14. package/dist/endpoints/petitionSignatureHandler.js +146 -0
  15. package/dist/endpoints/petitionSignatureHandler.js.map +1 -0
  16. package/dist/helpers/countries.d.ts +22 -0
  17. package/dist/helpers/countries.js +25 -0
  18. package/dist/helpers/countries.js.map +1 -0
  19. package/dist/helpers/index.d.ts +1 -0
  20. package/dist/helpers/index.js +3 -0
  21. package/dist/helpers/index.js.map +1 -0
  22. package/dist/index.js +9 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/react/index.d.ts +1 -0
  25. package/dist/react/index.js +1 -0
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/react/sign-petition.d.ts +57 -0
  28. package/dist/react/sign-petition.js +54 -0
  29. package/dist/react/sign-petition.js.map +1 -0
  30. package/dist/types/index.d.ts +8 -0
  31. package/dist/types/index.js.map +1 -1
  32. package/package.json +6 -1
@@ -0,0 +1,3 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../../types/index.js';
3
+ export declare const generatePetitionSignaturesCollection: (petitionSignaturesConfig: MobilizehubPluginConfig) => CollectionConfig;
@@ -0,0 +1,79 @@
1
+ import { authenticated } from '../../access/authenticated.js';
2
+ import { createProcessPetitionSignatureHook } from './hooks/processPetitionSignature.js';
3
+ import { createSendPetitionAutoresponseHook } from './hooks/sendAutoresponse.js';
4
+ export const generatePetitionSignaturesCollection = (petitionSignaturesConfig)=>{
5
+ const defaultFields = [
6
+ {
7
+ name: 'petition',
8
+ type: 'relationship',
9
+ admin: {
10
+ position: 'sidebar',
11
+ readOnly: true
12
+ },
13
+ relationTo: petitionSignaturesConfig.petitionsOverrides?.slug || 'petitions',
14
+ required: true
15
+ },
16
+ {
17
+ name: 'contact',
18
+ type: 'relationship',
19
+ admin: {
20
+ position: 'sidebar',
21
+ readOnly: true
22
+ },
23
+ relationTo: petitionSignaturesConfig.contactsOverrides?.slug || 'contacts'
24
+ },
25
+ {
26
+ name: 'createdAt',
27
+ type: 'date',
28
+ admin: {
29
+ position: 'sidebar',
30
+ readOnly: true
31
+ }
32
+ },
33
+ {
34
+ name: 'data',
35
+ type: 'json',
36
+ admin: {
37
+ description: 'The raw data submitted with the petition signature.'
38
+ }
39
+ }
40
+ ];
41
+ const config = {
42
+ ...petitionSignaturesConfig.petitionSignaturesOverrides || {},
43
+ slug: petitionSignaturesConfig.petitionSignaturesOverrides?.slug || 'petitionSignatures',
44
+ access: {
45
+ // Don't allow creation via admin or API - only via endpoint
46
+ create: ()=>false,
47
+ // Only authenticated users can read
48
+ read: authenticated,
49
+ // Prevent updates
50
+ update: ()=>false,
51
+ ...petitionSignaturesConfig.petitionSignaturesOverrides?.access || {}
52
+ },
53
+ admin: {
54
+ ...petitionSignaturesConfig.petitionSignaturesOverrides?.admin || {},
55
+ defaultColumns: petitionSignaturesConfig.petitionSignaturesOverrides?.admin?.defaultColumns || [
56
+ 'id',
57
+ 'petition',
58
+ 'contact',
59
+ 'createdAt'
60
+ ],
61
+ hidden: petitionSignaturesConfig.petitionSignaturesOverrides?.admin?.hidden || true
62
+ },
63
+ fields: petitionSignaturesConfig.petitionSignaturesOverrides?.fields ? petitionSignaturesConfig.petitionSignaturesOverrides.fields({
64
+ defaultFields
65
+ }) : defaultFields,
66
+ hooks: {
67
+ afterChange: [
68
+ createSendPetitionAutoresponseHook(petitionSignaturesConfig)
69
+ ],
70
+ beforeChange: [
71
+ createProcessPetitionSignatureHook(petitionSignaturesConfig)
72
+ ],
73
+ ...petitionSignaturesConfig.petitionSignaturesOverrides?.hooks || {}
74
+ }
75
+ };
76
+ return config;
77
+ };
78
+
79
+ //# sourceMappingURL=generatePetitionSignaturesCollection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/collections/petition-signatures/generatePetitionSignaturesCollection.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../types/index.js'\n\nimport { authenticated } from '../../access/authenticated.js'\nimport { createProcessPetitionSignatureHook } from './hooks/processPetitionSignature.js'\nimport { createSendPetitionAutoresponseHook } from './hooks/sendAutoresponse.js'\n\nexport const generatePetitionSignaturesCollection = (\n petitionSignaturesConfig: MobilizehubPluginConfig,\n) => {\n const defaultFields: Field[] = [\n {\n name: 'petition',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: petitionSignaturesConfig.petitionsOverrides?.slug || 'petitions',\n required: true,\n },\n {\n name: 'contact',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: petitionSignaturesConfig.contactsOverrides?.slug || 'contacts',\n },\n {\n name: 'createdAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n },\n {\n name: 'data',\n type: 'json',\n admin: {\n description: 'The raw data submitted with the petition signature.',\n },\n },\n ]\n\n const config: CollectionConfig = {\n ...(petitionSignaturesConfig.petitionSignaturesOverrides || {}),\n slug: petitionSignaturesConfig.petitionSignaturesOverrides?.slug || 'petitionSignatures',\n access: {\n // Don't allow creation via admin or API - only via endpoint\n create: () => false,\n // Only authenticated users can read\n read: authenticated,\n // Prevent updates\n update: () => false,\n ...(petitionSignaturesConfig.petitionSignaturesOverrides?.access || {}),\n },\n admin: {\n ...(petitionSignaturesConfig.petitionSignaturesOverrides?.admin || {}),\n defaultColumns: petitionSignaturesConfig.petitionSignaturesOverrides?.admin\n ?.defaultColumns || ['id', 'petition', 'contact', 'createdAt'],\n hidden: petitionSignaturesConfig.petitionSignaturesOverrides?.admin?.hidden || true,\n },\n fields: petitionSignaturesConfig.petitionSignaturesOverrides?.fields\n ? petitionSignaturesConfig.petitionSignaturesOverrides.fields({ defaultFields })\n : defaultFields,\n hooks: {\n afterChange: [createSendPetitionAutoresponseHook(petitionSignaturesConfig)],\n beforeChange: [createProcessPetitionSignatureHook(petitionSignaturesConfig)],\n ...(petitionSignaturesConfig.petitionSignaturesOverrides?.hooks || {}),\n },\n }\n\n return config\n}\n"],"names":["authenticated","createProcessPetitionSignatureHook","createSendPetitionAutoresponseHook","generatePetitionSignaturesCollection","petitionSignaturesConfig","defaultFields","name","type","admin","position","readOnly","relationTo","petitionsOverrides","slug","required","contactsOverrides","description","config","petitionSignaturesOverrides","access","create","read","update","defaultColumns","hidden","fields","hooks","afterChange","beforeChange"],"mappings":"AAIA,SAASA,aAAa,QAAQ,gCAA+B;AAC7D,SAASC,kCAAkC,QAAQ,sCAAqC;AACxF,SAASC,kCAAkC,QAAQ,8BAA6B;AAEhF,OAAO,MAAMC,uCAAuC,CAClDC;IAEA,MAAMC,gBAAyB;QAC7B;YACEC,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAYP,yBAAyBQ,kBAAkB,EAAEC,QAAQ;YACjEC,UAAU;QACZ;QACA;YACER,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAYP,yBAAyBW,iBAAiB,EAAEF,QAAQ;QAClE;QACA;YACEP,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;QACF;QACA;YACEJ,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLQ,aAAa;YACf;QACF;KACD;IAED,MAAMC,SAA2B;QAC/B,GAAIb,yBAAyBc,2BAA2B,IAAI,CAAC,CAAC;QAC9DL,MAAMT,yBAAyBc,2BAA2B,EAAEL,QAAQ;QACpEM,QAAQ;YACN,4DAA4D;YAC5DC,QAAQ,IAAM;YACd,oCAAoC;YACpCC,MAAMrB;YACN,kBAAkB;YAClBsB,QAAQ,IAAM;YACd,GAAIlB,yBAAyBc,2BAA2B,EAAEC,UAAU,CAAC,CAAC;QACxE;QACAX,OAAO;YACL,GAAIJ,yBAAyBc,2BAA2B,EAAEV,SAAS,CAAC,CAAC;YACrEe,gBAAgBnB,yBAAyBc,2BAA2B,EAAEV,OAClEe,kBAAkB;gBAAC;gBAAM;gBAAY;gBAAW;aAAY;YAChEC,QAAQpB,yBAAyBc,2BAA2B,EAAEV,OAAOgB,UAAU;QACjF;QACAC,QAAQrB,yBAAyBc,2BAA2B,EAAEO,SAC1DrB,yBAAyBc,2BAA2B,CAACO,MAAM,CAAC;YAAEpB;QAAc,KAC5EA;QACJqB,OAAO;YACLC,aAAa;gBAACzB,mCAAmCE;aAA0B;YAC3EwB,cAAc;gBAAC3B,mCAAmCG;aAA0B;YAC5E,GAAIA,yBAAyBc,2BAA2B,EAAEQ,SAAS,CAAC,CAAC;QACvE;IACF;IAEA,OAAOT;AACT,EAAC"}
@@ -0,0 +1,11 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../../../types/index.js';
3
+ /**
4
+ * Creates the petition signature processing hook.
5
+ *
6
+ * This hook:
7
+ * 1. Creates or updates a contact based on signature data
8
+ * 2. Applies petition tags to the contact
9
+ * 3. Links the signature to the contact by returning modified data
10
+ */
11
+ export declare const createProcessPetitionSignatureHook: (pluginConfig: MobilizehubPluginConfig) => CollectionBeforeChangeHook;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Contact field mapping from petition signature data to contact fields.
3
+ */ const CONTACT_FIELD_MAP = [
4
+ 'email',
5
+ 'emailOptIn',
6
+ 'firstName',
7
+ 'lastName',
8
+ 'mobileNumber',
9
+ 'mobileOptIn',
10
+ 'address',
11
+ 'city',
12
+ 'state',
13
+ 'zip',
14
+ 'country'
15
+ ];
16
+ /**
17
+ * Extracts contact-related fields from petition signature data.
18
+ */ function extractContactData(signatureData) {
19
+ const contactData = {};
20
+ for (const field of CONTACT_FIELD_MAP){
21
+ if (signatureData[field] !== undefined) {
22
+ contactData[field] = signatureData[field];
23
+ }
24
+ }
25
+ return contactData;
26
+ }
27
+ /**
28
+ * Creates the petition signature processing hook.
29
+ *
30
+ * This hook:
31
+ * 1. Creates or updates a contact based on signature data
32
+ * 2. Applies petition tags to the contact
33
+ * 3. Links the signature to the contact by returning modified data
34
+ */ export const createProcessPetitionSignatureHook = (pluginConfig)=>{
35
+ const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts';
36
+ const petitionsSlug = pluginConfig.petitionsOverrides?.slug || 'petitions';
37
+ return async ({ data, operation, req })=>{
38
+ // Only process on creation
39
+ if (operation !== 'create') {
40
+ return data;
41
+ }
42
+ const { payload } = req;
43
+ const logger = payload.logger;
44
+ // Parse signature data
45
+ const signatureData = data.data;
46
+ if (!signatureData) {
47
+ logger.warn('Petition signature has no data');
48
+ return data;
49
+ }
50
+ // Email is required for contact creation
51
+ const email = signatureData.email;
52
+ if (!email || typeof email !== 'string') {
53
+ logger.info('Petition signature has no email, skipping contact creation');
54
+ return data;
55
+ }
56
+ try {
57
+ // Extract contact fields from signature
58
+ const contactData = extractContactData(signatureData);
59
+ // Find or create contact
60
+ const existingContactResult = await payload.find({
61
+ collection: contactsSlug,
62
+ limit: 1,
63
+ where: {
64
+ email: {
65
+ equals: email
66
+ }
67
+ }
68
+ });
69
+ const existingContact = existingContactResult.docs[0];
70
+ let contactId;
71
+ if (existingContact) {
72
+ // Update existing contact (merge data, don't overwrite with empty values)
73
+ const updateData = {};
74
+ for (const [key, value] of Object.entries(contactData)){
75
+ if (value !== undefined && value !== null && value !== '') {
76
+ updateData[key] = value;
77
+ }
78
+ }
79
+ await payload.update({
80
+ id: existingContact.id,
81
+ collection: contactsSlug,
82
+ data: updateData
83
+ });
84
+ contactId = existingContact.id;
85
+ logger.info(`Updated contact ${contactId} from petition signature`);
86
+ } else {
87
+ // Create new contact
88
+ const newContact = await payload.create({
89
+ collection: contactsSlug,
90
+ data: contactData
91
+ });
92
+ contactId = newContact.id;
93
+ logger.info(`Created contact ${contactId} from petition signature`);
94
+ }
95
+ const petitionId = data.petition;
96
+ const petitionIdValue = typeof petitionId === 'object' ? petitionId.id : petitionId;
97
+ if (!petitionIdValue) {
98
+ logger.error('Petition ID not found for petition signature');
99
+ throw new Error('Petition ID not found for petition signature');
100
+ }
101
+ // Get petition to check for tags
102
+ const petition = await payload.findByID({
103
+ id: petitionIdValue,
104
+ collection: petitionsSlug,
105
+ depth: 0
106
+ });
107
+ // Apply petition tags to contact
108
+ if (petition && Array.isArray(petition.tags) && petition.tags.length > 0) {
109
+ // Get petition tag IDs
110
+ const petitionTagIds = petition.tags.map((t)=>typeof t === 'object' ? t.id : t);
111
+ // Fetch current contact to get up-to-date tags
112
+ const currentContact = await payload.findByID({
113
+ id: contactId,
114
+ collection: contactsSlug,
115
+ depth: 0
116
+ });
117
+ // Get existing tag IDs (normalize to IDs)
118
+ const existingTagIds = (currentContact.tags || []).map((t)=>typeof t === 'object' ? t.id : t);
119
+ // Find only new tags that don't already exist
120
+ const newTagIds = petitionTagIds.filter((tagId)=>!existingTagIds.includes(tagId));
121
+ if (newTagIds.length > 0) {
122
+ // Merge tags
123
+ const mergedTags = [
124
+ ...existingTagIds,
125
+ ...newTagIds
126
+ ];
127
+ // Update contact with merged tags
128
+ await payload.update({
129
+ id: contactId,
130
+ collection: contactsSlug,
131
+ data: {
132
+ tags: mergedTags
133
+ }
134
+ });
135
+ logger.info(`Applied ${newTagIds.length} new tags to contact ${contactId}`);
136
+ } else {
137
+ logger.info(`Contact ${contactId} already has all petition tags`);
138
+ }
139
+ }
140
+ // Return data with contact linked - no separate update needed
141
+ return {
142
+ ...data,
143
+ contact: contactId
144
+ };
145
+ } catch (error) {
146
+ logger.error(error, 'Error processing petition signature');
147
+ // Don't throw - we don't want to fail the signature
148
+ return data;
149
+ }
150
+ };
151
+ };
152
+
153
+ //# sourceMappingURL=processPetitionSignature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/collections/petition-signatures/hooks/processPetitionSignature.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../../types/index.js'\n\n/**\n * Contact field mapping from petition signature data to contact fields.\n */\nconst CONTACT_FIELD_MAP = [\n 'email',\n 'emailOptIn',\n 'firstName',\n 'lastName',\n 'mobileNumber',\n 'mobileOptIn',\n 'address',\n 'city',\n 'state',\n 'zip',\n 'country',\n] as const\n\ntype ContactFieldName = (typeof CONTACT_FIELD_MAP)[number]\n\n/**\n * Extracts contact-related fields from petition signature data.\n */\nfunction extractContactData(\n signatureData: Record<string, unknown>,\n): Partial<Record<ContactFieldName, unknown>> {\n const contactData: Partial<Record<ContactFieldName, unknown>> = {}\n\n for (const field of CONTACT_FIELD_MAP) {\n if (signatureData[field] !== undefined) {\n contactData[field] = signatureData[field]\n }\n }\n\n return contactData\n}\n\n/**\n * Creates the petition signature processing hook.\n *\n * This hook:\n * 1. Creates or updates a contact based on signature data\n * 2. Applies petition tags to the contact\n * 3. Links the signature to the contact by returning modified data\n */\nexport const createProcessPetitionSignatureHook = (\n pluginConfig: MobilizehubPluginConfig,\n): CollectionBeforeChangeHook => {\n const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts'\n const petitionsSlug = pluginConfig.petitionsOverrides?.slug || 'petitions'\n\n return async ({ data, operation, req }) => {\n // Only process on creation\n if (operation !== 'create') {\n return data\n }\n\n const { payload } = req\n const logger = payload.logger\n\n // Parse signature data\n const signatureData = data.data as Record<string, unknown> | undefined\n\n if (!signatureData) {\n logger.warn('Petition signature has no data')\n return data\n }\n\n // Email is required for contact creation\n const email = signatureData.email as string | undefined\n\n if (!email || typeof email !== 'string') {\n logger.info('Petition signature has no email, skipping contact creation')\n return data\n }\n\n try {\n // Extract contact fields from signature\n const contactData = extractContactData(signatureData)\n\n // Find or create contact\n const existingContactResult = await payload.find({\n collection: contactsSlug,\n limit: 1,\n where: { email: { equals: email } },\n })\n\n const existingContact = existingContactResult.docs[0] as\n | { id: number | string; tags?: (number | string)[] }\n | undefined\n\n let contactId: number | string\n\n if (existingContact) {\n // Update existing contact (merge data, don't overwrite with empty values)\n const updateData: Record<string, unknown> = {}\n\n for (const [key, value] of Object.entries(contactData)) {\n if (value !== undefined && value !== null && value !== '') {\n updateData[key] = value\n }\n }\n\n await payload.update({\n id: existingContact.id,\n collection: contactsSlug,\n data: updateData,\n })\n\n contactId = existingContact.id\n logger.info(`Updated contact ${contactId} from petition signature`)\n } else {\n // Create new contact\n const newContact = await payload.create({\n collection: contactsSlug,\n data: contactData,\n })\n\n contactId = newContact.id\n logger.info(`Created contact ${contactId} from petition signature`)\n }\n\n const petitionId = data.petition as { id: number | string } | number | string\n const petitionIdValue = typeof petitionId === 'object' ? petitionId.id : petitionId\n\n if (!petitionIdValue) {\n logger.error('Petition ID not found for petition signature')\n throw new Error('Petition ID not found for petition signature')\n }\n\n // Get petition to check for tags\n const petition = await payload.findByID({\n id: petitionIdValue,\n collection: petitionsSlug,\n depth: 0,\n })\n\n // Apply petition tags to contact\n if (petition && Array.isArray(petition.tags) && petition.tags.length > 0) {\n // Get petition tag IDs\n const petitionTagIds = petition.tags.map((t: { id: number | string } | number | string) =>\n typeof t === 'object' ? t.id : t,\n )\n\n // Fetch current contact to get up-to-date tags\n const currentContact = await payload.findByID({\n id: contactId,\n collection: contactsSlug,\n depth: 0,\n })\n\n // Get existing tag IDs (normalize to IDs)\n const existingTagIds = (\n (currentContact.tags as ({ id: number | string } | number | string)[]) || []\n ).map((t) => (typeof t === 'object' ? t.id : t))\n\n // Find only new tags that don't already exist\n const newTagIds = petitionTagIds.filter(\n (tagId: number | string) => !existingTagIds.includes(tagId),\n )\n\n if (newTagIds.length > 0) {\n // Merge tags\n const mergedTags = [...existingTagIds, ...newTagIds]\n\n // Update contact with merged tags\n await payload.update({\n id: contactId,\n collection: contactsSlug,\n data: { tags: mergedTags },\n })\n\n logger.info(`Applied ${newTagIds.length} new tags to contact ${contactId}`)\n } else {\n logger.info(`Contact ${contactId} already has all petition tags`)\n }\n }\n\n // Return data with contact linked - no separate update needed\n return {\n ...data,\n contact: contactId,\n }\n } catch (error) {\n logger.error(error as Error, 'Error processing petition signature')\n // Don't throw - we don't want to fail the signature\n return data\n }\n }\n}\n"],"names":["CONTACT_FIELD_MAP","extractContactData","signatureData","contactData","field","undefined","createProcessPetitionSignatureHook","pluginConfig","contactsSlug","contactsOverrides","slug","petitionsSlug","petitionsOverrides","data","operation","req","payload","logger","warn","email","info","existingContactResult","find","collection","limit","where","equals","existingContact","docs","contactId","updateData","key","value","Object","entries","update","id","newContact","create","petitionId","petition","petitionIdValue","error","Error","findByID","depth","Array","isArray","tags","length","petitionTagIds","map","t","currentContact","existingTagIds","newTagIds","filter","tagId","includes","mergedTags","contact"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { CollectionAfterChangeHook } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../../../types/index.js';
3
+ /**
4
+ * Creates the autoresponse email hook for petition signatures.
5
+ *
6
+ * This hook sends an automatic confirmation email to the petition signer
7
+ * when autoresponse is enabled on the petition. It:
8
+ * 1. Checks if autoresponse is enabled on the petition
9
+ * 2. Validates required autoresponse fields
10
+ * 3. Parses Lexical content to HTML
11
+ * 4. Renders through email template
12
+ * 5. Sends the email
13
+ *
14
+ * Errors are logged but never thrown to avoid failing the signature.
15
+ */
16
+ export declare const createSendPetitionAutoresponseHook: (pluginConfig: MobilizehubPluginConfig) => CollectionAfterChangeHook;
@@ -0,0 +1,109 @@
1
+ import { formatFromAddress } from '../../../utils/email.js';
2
+ import { parseLexicalContent } from '../../../utils/lexical.js';
3
+ /**
4
+ * Creates the autoresponse email hook for petition signatures.
5
+ *
6
+ * This hook sends an automatic confirmation email to the petition signer
7
+ * when autoresponse is enabled on the petition. It:
8
+ * 1. Checks if autoresponse is enabled on the petition
9
+ * 2. Validates required autoresponse fields
10
+ * 3. Parses Lexical content to HTML
11
+ * 4. Renders through email template
12
+ * 5. Sends the email
13
+ *
14
+ * Errors are logged but never thrown to avoid failing the signature.
15
+ */ export const createSendPetitionAutoresponseHook = (pluginConfig)=>{
16
+ const petitionsSlug = pluginConfig.petitionsOverrides?.slug || 'petitions';
17
+ const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts';
18
+ return async ({ doc, operation, req })=>{
19
+ // Only process on create
20
+ if (operation !== 'create') {
21
+ return doc;
22
+ }
23
+ const { payload } = req;
24
+ const logger = payload.logger;
25
+ try {
26
+ // Get petition ID from signature
27
+ const petitionId = doc.petition;
28
+ const petitionIdValue = typeof petitionId === 'object' ? petitionId.id : petitionId;
29
+ if (!petitionIdValue) {
30
+ return doc;
31
+ }
32
+ // Fetch petition to get autoresponse configuration
33
+ const petition = await payload.findByID({
34
+ id: petitionIdValue,
35
+ collection: petitionsSlug
36
+ });
37
+ if (!petition) {
38
+ return doc;
39
+ }
40
+ // Check if autoresponse is enabled
41
+ const autoresponse = petition.autoresponse;
42
+ if (!autoresponse?.enabled) {
43
+ return doc;
44
+ }
45
+ // Get contact email
46
+ const contactId = doc.contact;
47
+ if (!contactId) {
48
+ logger.warn('Petition signature has no linked contact, skipping autoresponse');
49
+ return doc;
50
+ }
51
+ const contactIdValue = typeof contactId === 'object' ? contactId.id : contactId;
52
+ const contact = await payload.findByID({
53
+ id: contactIdValue,
54
+ collection: contactsSlug
55
+ });
56
+ const contactEmail = contact?.email;
57
+ if (!contactEmail) {
58
+ logger.warn(`Contact ${contactIdValue} has no email, skipping autoresponse`);
59
+ return doc;
60
+ }
61
+ // Validate required autoresponse fields
62
+ const { content, fromAddress, fromName, previewText, replyTo, subject } = autoresponse;
63
+ if (!subject) {
64
+ logger.warn(`Petition ${petitionIdValue} autoresponse has no subject, skipping`);
65
+ return doc;
66
+ }
67
+ if (!content) {
68
+ logger.warn(`Petition ${petitionIdValue} autoresponse has no content, skipping`);
69
+ return doc;
70
+ }
71
+ if (!fromName || !fromAddress) {
72
+ logger.warn(`Petition ${petitionIdValue} autoresponse has no from address, skipping`);
73
+ return doc;
74
+ }
75
+ // Get email adapter
76
+ const { render, sendEmail } = pluginConfig.email(req);
77
+ // Parse Lexical content
78
+ const parsedContent = await parseLexicalContent(content, payload.config);
79
+ // Format from address
80
+ const formattedFromAddress = formatFromAddress(fromName, fromAddress);
81
+ // Render through email template
82
+ const html = render({
83
+ from: formattedFromAddress,
84
+ html: parsedContent.html,
85
+ markdown: parsedContent.markdown,
86
+ plainText: parsedContent.plainText,
87
+ previewText,
88
+ replyTo,
89
+ subject,
90
+ to: contactEmail,
91
+ token: ''
92
+ });
93
+ // Send the email
94
+ await sendEmail({
95
+ from: formattedFromAddress,
96
+ html,
97
+ subject,
98
+ to: contactEmail
99
+ });
100
+ logger.info(`Sent autoresponse to ${contactEmail} for petition ${petitionIdValue}`);
101
+ } catch (error) {
102
+ logger.error(error, 'Error sending autoresponse email');
103
+ // Don't throw - signature should still succeed
104
+ }
105
+ return doc;
106
+ };
107
+ };
108
+
109
+ //# sourceMappingURL=sendAutoresponse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/collections/petition-signatures/hooks/sendAutoresponse.ts"],"sourcesContent":["import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../../types/index.js'\n\nimport { formatFromAddress } from '../../../utils/email.js'\nimport { parseLexicalContent } from '../../../utils/lexical.js'\n\n/**\n * Autoresponse configuration from petition.\n */\ntype AutoresponseConfig = {\n content?: SerializedEditorState\n enabled?: boolean\n fromAddress?: string\n fromName?: string\n previewText?: string\n replyTo?: string\n subject?: string\n}\n\n/**\n * Creates the autoresponse email hook for petition signatures.\n *\n * This hook sends an automatic confirmation email to the petition signer\n * when autoresponse is enabled on the petition. It:\n * 1. Checks if autoresponse is enabled on the petition\n * 2. Validates required autoresponse fields\n * 3. Parses Lexical content to HTML\n * 4. Renders through email template\n * 5. Sends the email\n *\n * Errors are logged but never thrown to avoid failing the signature.\n */\nexport const createSendPetitionAutoresponseHook = (\n pluginConfig: MobilizehubPluginConfig,\n): CollectionAfterChangeHook => {\n const petitionsSlug = pluginConfig.petitionsOverrides?.slug || 'petitions'\n const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts'\n\n return async ({ doc, operation, req }) => {\n // Only process on create\n if (operation !== 'create') {\n return doc\n }\n\n const { payload } = req\n const logger = payload.logger\n\n try {\n // Get petition ID from signature\n const petitionId = doc.petition as { id: number | string } | number | string\n const petitionIdValue = typeof petitionId === 'object' ? petitionId.id : petitionId\n\n if (!petitionIdValue) {\n return doc\n }\n\n // Fetch petition to get autoresponse configuration\n const petition = await payload.findByID({\n id: petitionIdValue,\n collection: petitionsSlug,\n })\n\n if (!petition) {\n return doc\n }\n\n // Check if autoresponse is enabled\n const autoresponse = petition.autoresponse as AutoresponseConfig | undefined\n\n if (!autoresponse?.enabled) {\n return doc\n }\n\n // Get contact email\n const contactId = doc.contact as { id: number | string } | number | string | undefined\n\n if (!contactId) {\n logger.warn('Petition signature has no linked contact, skipping autoresponse')\n return doc\n }\n\n const contactIdValue = typeof contactId === 'object' ? contactId.id : contactId\n\n const contact = await payload.findByID({\n id: contactIdValue,\n collection: contactsSlug,\n })\n\n const contactEmail = contact?.email as string | undefined\n\n if (!contactEmail) {\n logger.warn(`Contact ${contactIdValue} has no email, skipping autoresponse`)\n return doc\n }\n\n // Validate required autoresponse fields\n const { content, fromAddress, fromName, previewText, replyTo, subject } = autoresponse\n\n if (!subject) {\n logger.warn(`Petition ${petitionIdValue} autoresponse has no subject, skipping`)\n return doc\n }\n\n if (!content) {\n logger.warn(`Petition ${petitionIdValue} autoresponse has no content, skipping`)\n return doc\n }\n\n if (!fromName || !fromAddress) {\n logger.warn(`Petition ${petitionIdValue} autoresponse has no from address, skipping`)\n return doc\n }\n\n // Get email adapter\n const { render, sendEmail } = pluginConfig.email(req)\n\n // Parse Lexical content\n const parsedContent = await parseLexicalContent(content, payload.config)\n\n // Format from address\n const formattedFromAddress = formatFromAddress(fromName, fromAddress)\n\n // Render through email template\n const html = render({\n from: formattedFromAddress,\n html: parsedContent.html,\n markdown: parsedContent.markdown,\n plainText: parsedContent.plainText,\n previewText,\n replyTo,\n subject,\n to: contactEmail,\n token: '', // No unsubscribe token for autoresponse\n })\n\n // Send the email\n await sendEmail({\n from: formattedFromAddress,\n html,\n subject,\n to: contactEmail,\n })\n\n logger.info(`Sent autoresponse to ${contactEmail} for petition ${petitionIdValue}`)\n } catch (error) {\n logger.error(error as Error, 'Error sending autoresponse email')\n // Don't throw - signature should still succeed\n }\n\n return doc\n }\n}\n"],"names":["formatFromAddress","parseLexicalContent","createSendPetitionAutoresponseHook","pluginConfig","petitionsSlug","petitionsOverrides","slug","contactsSlug","contactsOverrides","doc","operation","req","payload","logger","petitionId","petition","petitionIdValue","id","findByID","collection","autoresponse","enabled","contactId","contact","warn","contactIdValue","contactEmail","email","content","fromAddress","fromName","previewText","replyTo","subject","render","sendEmail","parsedContent","config","formattedFromAddress","html","from","markdown","plainText","to","token","info","error"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../../types/index.js';
3
+ export declare const generatePetitionsCollection: (petitionsConfig: MobilizehubPluginConfig) => CollectionConfig;
@@ -0,0 +1,252 @@
1
+ import { createContactFieldsConfig } from '../../fields/contact-fields.js';
2
+ import { createNameField } from '../../fields/name.js';
3
+ import { createPublishedAtField } from '../../fields/publishedAt.js';
4
+ import { createSlugField } from '../../fields/slug.js';
5
+ import { createStatusField } from '../../fields/status.js';
6
+ export const generatePetitionsCollection = (petitionsConfig)=>{
7
+ const defaultFields = [
8
+ createStatusField(),
9
+ {
10
+ type: 'tabs',
11
+ tabs: [
12
+ {
13
+ fields: [
14
+ createNameField(),
15
+ createSlugField(),
16
+ createPublishedAtField()
17
+ ],
18
+ label: 'Settings'
19
+ },
20
+ {
21
+ fields: [
22
+ {
23
+ name: 'headline',
24
+ type: 'text',
25
+ label: 'Headline'
26
+ },
27
+ {
28
+ name: 'content',
29
+ type: 'richText',
30
+ label: 'Content'
31
+ },
32
+ {
33
+ name: 'target',
34
+ type: 'text',
35
+ admin: {
36
+ description: 'Who or what is this petition addressed to?'
37
+ },
38
+ label: 'Petition Target'
39
+ },
40
+ {
41
+ name: 'ask',
42
+ type: 'textarea',
43
+ admin: {
44
+ description: 'What is this petition asking for or demanding?'
45
+ },
46
+ label: 'Petition Ask'
47
+ },
48
+ {
49
+ name: 'goal',
50
+ type: 'number',
51
+ admin: {
52
+ description: 'The target number of signatures for this petition.'
53
+ },
54
+ label: 'Signature Goal',
55
+ min: 1
56
+ }
57
+ ],
58
+ label: 'Content'
59
+ },
60
+ {
61
+ fields: [
62
+ {
63
+ name: 'legend',
64
+ type: 'text',
65
+ localized: true
66
+ },
67
+ createContactFieldsConfig(),
68
+ {
69
+ name: 'submitButtonLabel',
70
+ type: 'text',
71
+ defaultValue: 'Sign Petition',
72
+ localized: true
73
+ },
74
+ {
75
+ name: 'confirmationType',
76
+ type: 'radio',
77
+ admin: {
78
+ description: 'Choose whether to display an on-page message or redirect to a different page after they sign the petition.',
79
+ layout: 'horizontal'
80
+ },
81
+ defaultValue: 'message',
82
+ options: [
83
+ {
84
+ label: 'Message',
85
+ value: 'message'
86
+ },
87
+ {
88
+ label: 'Redirect',
89
+ value: 'redirect'
90
+ }
91
+ ]
92
+ },
93
+ {
94
+ name: 'confirmationMessage',
95
+ type: 'richText',
96
+ admin: {
97
+ condition: (_, siblingData)=>siblingData?.confirmationType === 'message'
98
+ },
99
+ localized: true
100
+ },
101
+ {
102
+ name: 'type',
103
+ type: 'radio',
104
+ admin: {
105
+ condition: (_, siblingData)=>siblingData?.confirmationType === 'redirect',
106
+ layout: 'horizontal'
107
+ },
108
+ defaultValue: 'reference',
109
+ options: [
110
+ {
111
+ label: 'Internal link',
112
+ value: 'reference'
113
+ },
114
+ {
115
+ label: 'Custom URL',
116
+ value: 'custom'
117
+ }
118
+ ]
119
+ },
120
+ {
121
+ name: 'reference',
122
+ type: 'relationship',
123
+ admin: {
124
+ condition: (_, siblingData)=>siblingData?.confirmationType === 'redirect' && siblingData?.type === 'reference'
125
+ },
126
+ label: 'Document to link to',
127
+ maxDepth: 2,
128
+ relationTo: [
129
+ 'pages',
130
+ 'petitions'
131
+ ],
132
+ required: true
133
+ },
134
+ {
135
+ name: 'url',
136
+ type: 'text',
137
+ admin: {
138
+ condition: (_, siblingData)=>siblingData?.confirmationType === 'redirect' && siblingData?.type === 'custom'
139
+ },
140
+ label: 'URL to redirect to',
141
+ required: true
142
+ },
143
+ {
144
+ name: 'tags',
145
+ type: 'relationship',
146
+ admin: {
147
+ description: 'Tag all contacts who sign this petition with these tags.'
148
+ },
149
+ hasMany: true,
150
+ relationTo: 'tags'
151
+ },
152
+ {
153
+ name: 'autoresponse',
154
+ type: 'group',
155
+ fields: [
156
+ {
157
+ name: 'enabled',
158
+ type: 'checkbox',
159
+ admin: {
160
+ style: {
161
+ marginTop: '1.5rem'
162
+ }
163
+ },
164
+ defaultValue: false,
165
+ label: 'Automatically send an autoresponse email to the signer'
166
+ },
167
+ {
168
+ name: 'fromName',
169
+ type: 'text',
170
+ defaultValue: ({ req })=>petitionsConfig.email(req).defaultFromName || '',
171
+ label: 'Name',
172
+ required: true
173
+ },
174
+ {
175
+ name: 'fromAddress',
176
+ type: 'text',
177
+ admin: {
178
+ description: 'The from address is set in the email configuration.',
179
+ readOnly: true
180
+ },
181
+ defaultValue: ({ req })=>petitionsConfig.email(req).defaultFromAddress || '',
182
+ label: 'Address',
183
+ required: true
184
+ },
185
+ {
186
+ name: 'replyTo',
187
+ type: 'text',
188
+ localized: true
189
+ },
190
+ {
191
+ name: 'subject',
192
+ type: 'text',
193
+ localized: true
194
+ },
195
+ {
196
+ name: 'previewText',
197
+ type: 'text',
198
+ localized: true
199
+ },
200
+ {
201
+ name: 'content',
202
+ type: 'richText',
203
+ localized: true
204
+ }
205
+ ]
206
+ }
207
+ ],
208
+ label: 'Form'
209
+ },
210
+ {
211
+ fields: [
212
+ {
213
+ name: 'petitionSignatures',
214
+ type: 'join',
215
+ collection: 'petitionSignatures',
216
+ on: 'petition'
217
+ }
218
+ ],
219
+ label: 'Signatures'
220
+ }
221
+ ]
222
+ }
223
+ ];
224
+ const config = {
225
+ ...petitionsConfig.petitionsOverrides || {},
226
+ slug: petitionsConfig.petitionsOverrides?.slug || 'petitions',
227
+ access: {
228
+ read: ()=>true,
229
+ ...petitionsConfig.petitionsOverrides?.access || {}
230
+ },
231
+ admin: {
232
+ ...petitionsConfig.petitionsOverrides?.admin || {},
233
+ defaultColumns: petitionsConfig.petitionsOverrides?.admin?.defaultColumns || [
234
+ 'id',
235
+ 'name',
236
+ 'slug',
237
+ 'status'
238
+ ],
239
+ hidden: petitionsConfig.petitionsOverrides?.admin?.hidden || false,
240
+ useAsTitle: petitionsConfig.petitionsOverrides?.admin?.useAsTitle || 'name'
241
+ },
242
+ fields: petitionsConfig.petitionsOverrides?.fields ? petitionsConfig.petitionsOverrides.fields({
243
+ defaultFields
244
+ }) : defaultFields,
245
+ hooks: {
246
+ ...petitionsConfig.petitionsOverrides?.hooks || {}
247
+ }
248
+ };
249
+ return config;
250
+ };
251
+
252
+ //# sourceMappingURL=generatePetitionsCollection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/collections/petitions/generatePetitionsCollection.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../types/index.js'\n\nimport { createContactFieldsConfig } from '../../fields/contact-fields.js'\nimport { createNameField } from '../../fields/name.js'\nimport { createPublishedAtField } from '../../fields/publishedAt.js'\nimport { createSlugField } from '../../fields/slug.js'\nimport { createStatusField } from '../../fields/status.js'\n\nexport const generatePetitionsCollection = (petitionsConfig: MobilizehubPluginConfig) => {\n const defaultFields: Field[] = [\n createStatusField(),\n {\n type: 'tabs',\n tabs: [\n {\n fields: [createNameField(), createSlugField(), createPublishedAtField()],\n label: 'Settings',\n },\n {\n fields: [\n {\n name: 'headline',\n type: 'text',\n label: 'Headline',\n },\n {\n name: 'content',\n type: 'richText',\n label: 'Content',\n },\n {\n name: 'target',\n type: 'text',\n admin: {\n description: 'Who or what is this petition addressed to?',\n },\n label: 'Petition Target',\n },\n {\n name: 'ask',\n type: 'textarea',\n admin: {\n description: 'What is this petition asking for or demanding?',\n },\n label: 'Petition Ask',\n },\n {\n name: 'goal',\n type: 'number',\n admin: {\n description: 'The target number of signatures for this petition.',\n },\n label: 'Signature Goal',\n min: 1,\n },\n ],\n label: 'Content',\n },\n {\n fields: [\n {\n name: 'legend',\n type: 'text',\n localized: true,\n },\n createContactFieldsConfig(),\n {\n name: 'submitButtonLabel',\n type: 'text',\n defaultValue: 'Sign Petition',\n localized: true,\n },\n {\n name: 'confirmationType',\n type: 'radio',\n admin: {\n description:\n 'Choose whether to display an on-page message or redirect to a different page after they sign the petition.',\n layout: 'horizontal',\n },\n defaultValue: 'message',\n options: [\n {\n label: 'Message',\n value: 'message',\n },\n {\n label: 'Redirect',\n value: 'redirect',\n },\n ],\n },\n {\n name: 'confirmationMessage',\n type: 'richText',\n admin: {\n condition: (_, siblingData) => siblingData?.confirmationType === 'message',\n },\n localized: true,\n },\n {\n name: 'type',\n type: 'radio',\n admin: {\n condition: (_, siblingData) => siblingData?.confirmationType === 'redirect',\n layout: 'horizontal',\n },\n defaultValue: 'reference',\n options: [\n {\n label: 'Internal link',\n value: 'reference',\n },\n {\n label: 'Custom URL',\n value: 'custom',\n },\n ],\n },\n {\n name: 'reference',\n type: 'relationship',\n admin: {\n condition: (_, siblingData) =>\n siblingData?.confirmationType === 'redirect' && siblingData?.type === 'reference',\n },\n label: 'Document to link to',\n maxDepth: 2,\n relationTo: ['pages', 'petitions'],\n required: true,\n },\n {\n name: 'url',\n type: 'text',\n admin: {\n condition: (_, siblingData) =>\n siblingData?.confirmationType === 'redirect' && siblingData?.type === 'custom',\n },\n label: 'URL to redirect to',\n required: true,\n },\n {\n name: 'tags',\n type: 'relationship',\n admin: {\n description: 'Tag all contacts who sign this petition with these tags.',\n },\n hasMany: true,\n relationTo: 'tags',\n },\n {\n name: 'autoresponse',\n type: 'group',\n fields: [\n {\n name: 'enabled',\n type: 'checkbox',\n admin: {\n style: {\n marginTop: '1.5rem',\n },\n },\n defaultValue: false,\n label: 'Automatically send an autoresponse email to the signer',\n },\n {\n name: 'fromName',\n type: 'text',\n defaultValue: ({ req }) => petitionsConfig.email(req).defaultFromName || '',\n label: 'Name',\n required: true,\n },\n {\n name: 'fromAddress',\n type: 'text',\n admin: {\n description: 'The from address is set in the email configuration.',\n readOnly: true,\n },\n defaultValue: ({ req }) => petitionsConfig.email(req).defaultFromAddress || '',\n label: 'Address',\n required: true,\n },\n {\n name: 'replyTo',\n type: 'text',\n localized: true,\n },\n {\n name: 'subject',\n type: 'text',\n localized: true,\n },\n {\n name: 'previewText',\n type: 'text',\n localized: true,\n },\n {\n name: 'content',\n type: 'richText',\n localized: true,\n },\n ],\n },\n ],\n label: 'Form',\n },\n {\n fields: [\n {\n name: 'petitionSignatures',\n type: 'join',\n collection: 'petitionSignatures',\n on: 'petition',\n },\n ],\n label: 'Signatures',\n },\n ],\n },\n ]\n\n const config: CollectionConfig = {\n ...(petitionsConfig.petitionsOverrides || {}),\n slug: petitionsConfig.petitionsOverrides?.slug || 'petitions',\n access: {\n read: () => true,\n ...(petitionsConfig.petitionsOverrides?.access || {}),\n },\n admin: {\n ...(petitionsConfig.petitionsOverrides?.admin || {}),\n defaultColumns: petitionsConfig.petitionsOverrides?.admin?.defaultColumns || [\n 'id',\n 'name',\n 'slug',\n 'status',\n ],\n hidden: petitionsConfig.petitionsOverrides?.admin?.hidden || false,\n useAsTitle: petitionsConfig.petitionsOverrides?.admin?.useAsTitle || 'name',\n },\n fields: petitionsConfig.petitionsOverrides?.fields\n ? petitionsConfig.petitionsOverrides.fields({ defaultFields })\n : defaultFields,\n hooks: {\n ...(petitionsConfig.petitionsOverrides?.hooks || {}),\n },\n }\n\n return config\n}\n"],"names":["createContactFieldsConfig","createNameField","createPublishedAtField","createSlugField","createStatusField","generatePetitionsCollection","petitionsConfig","defaultFields","type","tabs","fields","label","name","admin","description","min","localized","defaultValue","layout","options","value","condition","_","siblingData","confirmationType","maxDepth","relationTo","required","hasMany","style","marginTop","req","email","defaultFromName","readOnly","defaultFromAddress","collection","on","config","petitionsOverrides","slug","access","read","defaultColumns","hidden","useAsTitle","hooks"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../types/index.js';
3
+ /**
4
+ * Creates the public petition signature endpoint handler.
5
+ *
6
+ * Accepts petition signatures from frontend applications, validates the data,
7
+ * creates a petition signature record, and returns the appropriate confirmation.
8
+ *
9
+ * This endpoint is public (no authentication required) but validates:
10
+ * - Petition exists and is published
11
+ * - Required fields are present
12
+ * - Email format is valid (if provided)
13
+ */
14
+ export declare const petitionSignatureHandler: (pluginConfig: MobilizehubPluginConfig) => PayloadHandler;
@@ -0,0 +1,146 @@
1
+ import z from 'zod';
2
+ import { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js';
3
+ /**
4
+ * Schema for petition signature request body.
5
+ */ const PetitionSignatureBodySchema = z.object({
6
+ data: z.record(z.string(), z.unknown()),
7
+ petitionId: z.union([
8
+ z.string(),
9
+ z.number()
10
+ ])
11
+ });
12
+ /**
13
+ * Validates signature data against petition field configuration.
14
+ */ function validateSignatureData(data, contactFields) {
15
+ const errors = [];
16
+ if (!contactFields || contactFields.length === 0) {
17
+ return {
18
+ errors: [],
19
+ valid: true
20
+ };
21
+ }
22
+ for (const field of contactFields){
23
+ if (field.required) {
24
+ const value = data[field.blockType];
25
+ if (value === undefined || value === null || value === '') {
26
+ errors.push(`${field.blockType} is required`);
27
+ }
28
+ }
29
+ }
30
+ // Validate email format if present
31
+ if (data.email && typeof data.email === 'string') {
32
+ const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
33
+ if (!emailRegex.test(data.email)) {
34
+ errors.push('Invalid email format');
35
+ }
36
+ }
37
+ return {
38
+ errors,
39
+ valid: errors.length === 0
40
+ };
41
+ }
42
+ /**
43
+ * Fetches and validates a petition exists and is published.
44
+ */ async function getPublishedPetition(payload, petitionId, collectionSlug) {
45
+ try {
46
+ const petition = await payload.findByID({
47
+ id: petitionId,
48
+ collection: collectionSlug
49
+ });
50
+ if (!petition) {
51
+ return null;
52
+ }
53
+ // Only allow signatures to published petitions
54
+ if (petition.status !== 'published') {
55
+ return null;
56
+ }
57
+ return petition;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Builds the confirmation response based on petition settings.
64
+ */ function buildConfirmationResponse(petition) {
65
+ if (petition.confirmationType === 'redirect' && petition.reference) {
66
+ let redirectUrl;
67
+ if (petition.url) {
68
+ redirectUrl = petition.url;
69
+ } else if (typeof petition.reference.value === 'object' && petition.reference.value.slug) {
70
+ redirectUrl = `/${petition.reference.value.slug}`;
71
+ }
72
+ if (redirectUrl) {
73
+ return {
74
+ type: 'redirect',
75
+ redirect: redirectUrl
76
+ };
77
+ }
78
+ }
79
+ return {
80
+ type: 'message',
81
+ message: petition.confirmationMessage || 'Thank you for signing this petition.'
82
+ };
83
+ }
84
+ /**
85
+ * Creates the public petition signature endpoint handler.
86
+ *
87
+ * Accepts petition signatures from frontend applications, validates the data,
88
+ * creates a petition signature record, and returns the appropriate confirmation.
89
+ *
90
+ * This endpoint is public (no authentication required) but validates:
91
+ * - Petition exists and is published
92
+ * - Required fields are present
93
+ * - Email format is valid (if provided)
94
+ */ export const petitionSignatureHandler = (pluginConfig)=>{
95
+ const petitionsSlug = pluginConfig.petitionsOverrides?.slug || 'petitions';
96
+ const petitionSignaturesSlug = pluginConfig.petitionSignaturesOverrides?.slug || 'petitionSignatures';
97
+ return async (req)=>{
98
+ const { payload } = req;
99
+ const logger = payload.logger;
100
+ if (!req.json) {
101
+ return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400);
102
+ }
103
+ try {
104
+ const body = await req.json();
105
+ // Validate request body structure
106
+ const parseResult = PetitionSignatureBodySchema.safeParse(body);
107
+ if (!parseResult.success) {
108
+ const firstError = parseResult.error.issues[0]?.message || 'Invalid request body';
109
+ return errorResponse(ErrorCodes.VALIDATION_ERROR, firstError, 400);
110
+ }
111
+ const { data, petitionId } = parseResult.data;
112
+ // Fetch and validate petition
113
+ const petition = await getPublishedPetition(payload, petitionId, petitionsSlug);
114
+ if (!petition) {
115
+ return errorResponse(ErrorCodes.NOT_FOUND, 'Petition not found or not published', 404);
116
+ }
117
+ // Validate signature data against petition fields
118
+ const validation = validateSignatureData(data, petition.contactFields);
119
+ if (!validation.valid) {
120
+ return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.errors.join(', '), 400);
121
+ }
122
+ // Create the petition signature
123
+ // Note: The beforeChange hook will handle contact creation/update
124
+ const signature = await payload.create({
125
+ collection: petitionSignaturesSlug,
126
+ data: {
127
+ data,
128
+ petition: petition.id
129
+ },
130
+ // Use internal context to bypass access control
131
+ overrideAccess: true
132
+ });
133
+ // Build confirmation response
134
+ const confirmation = buildConfirmationResponse(petition);
135
+ return successResponse({
136
+ confirmation,
137
+ signatureId: signature.id
138
+ }, 201);
139
+ } catch (error) {
140
+ logger.error(error, 'Error processing petition signature');
141
+ return errorResponse(ErrorCodes.INTERNAL_ERROR, error instanceof Error ? error.message : 'Failed to process signature', 500);
142
+ }
143
+ };
144
+ };
145
+
146
+ //# sourceMappingURL=petitionSignatureHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/petitionSignatureHandler.ts"],"sourcesContent":["import type { CollectionSlug, Payload, PayloadHandler } from 'payload'\n\nimport z from 'zod'\n\nimport type { MobilizehubPluginConfig } from '../types/index.js'\n\nimport { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js'\n\n/**\n * Schema for petition signature request body.\n */\nconst PetitionSignatureBodySchema = z.object({\n data: z.record(z.string(), z.unknown()),\n petitionId: z.union([z.string(), z.number()]),\n})\n\n/**\n * Petition type for validation.\n */\ntype PetitionDocument = {\n confirmationMessage?: unknown\n confirmationType?: 'message' | 'redirect'\n contactFields?: Array<{\n blockType: string\n required?: boolean\n }>\n id: number | string\n reference?: {\n relationTo: 'pages' | 'petitions'\n value: { slug?: string } | number | string\n }\n status?: 'draft' | 'published'\n url?: string\n}\n\n/**\n * Validates signature data against petition field configuration.\n */\nfunction validateSignatureData(\n data: Record<string, unknown>,\n contactFields: PetitionDocument['contactFields'],\n): { errors: string[]; valid: boolean } {\n const errors: string[] = []\n\n if (!contactFields || contactFields.length === 0) {\n return { errors: [], valid: true }\n }\n\n for (const field of contactFields) {\n if (field.required) {\n const value = data[field.blockType]\n if (value === undefined || value === null || value === '') {\n errors.push(`${field.blockType} is required`)\n }\n }\n }\n\n // Validate email format if present\n if (data.email && typeof data.email === 'string') {\n const emailRegex = /^[^\\s@]+@[^\\s@][^\\s.@]*\\.[^\\s@]+$/\n if (!emailRegex.test(data.email)) {\n errors.push('Invalid email format')\n }\n }\n\n return {\n errors,\n valid: errors.length === 0,\n }\n}\n\n/**\n * Fetches and validates a petition exists and is published.\n */\nasync function getPublishedPetition(\n payload: Payload,\n petitionId: number | string,\n collectionSlug: CollectionSlug,\n): Promise<null | PetitionDocument> {\n try {\n const petition = await payload.findByID({\n id: petitionId,\n collection: collectionSlug,\n })\n\n if (!petition) {\n return null\n }\n\n // Only allow signatures to published petitions\n if ((petition as PetitionDocument).status !== 'published') {\n return null\n }\n\n return petition as PetitionDocument\n } catch {\n return null\n }\n}\n\n/**\n * Builds the confirmation response based on petition settings.\n */\nfunction buildConfirmationResponse(petition: PetitionDocument): {\n message?: unknown\n redirect?: string\n type: 'message' | 'redirect'\n} {\n if (petition.confirmationType === 'redirect' && petition.reference) {\n let redirectUrl: string | undefined\n\n if (petition.url) {\n redirectUrl = petition.url\n } else if (typeof petition.reference.value === 'object' && petition.reference.value.slug) {\n redirectUrl = `/${petition.reference.value.slug}`\n }\n\n if (redirectUrl) {\n return { type: 'redirect', redirect: redirectUrl }\n }\n }\n\n return {\n type: 'message',\n message: petition.confirmationMessage || 'Thank you for signing this petition.',\n }\n}\n\n/**\n * Creates the public petition signature endpoint handler.\n *\n * Accepts petition signatures from frontend applications, validates the data,\n * creates a petition signature record, and returns the appropriate confirmation.\n *\n * This endpoint is public (no authentication required) but validates:\n * - Petition exists and is published\n * - Required fields are present\n * - Email format is valid (if provided)\n */\nexport const petitionSignatureHandler = (\n pluginConfig: MobilizehubPluginConfig,\n): PayloadHandler => {\n const petitionsSlug = pluginConfig.petitionsOverrides?.slug || 'petitions'\n const petitionSignaturesSlug =\n pluginConfig.petitionSignaturesOverrides?.slug || 'petitionSignatures'\n\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 // Validate request body structure\n const parseResult = PetitionSignatureBodySchema.safeParse(body)\n\n if (!parseResult.success) {\n const firstError = parseResult.error.issues[0]?.message || 'Invalid request body'\n return errorResponse(ErrorCodes.VALIDATION_ERROR, firstError, 400)\n }\n\n const { data, petitionId } = parseResult.data\n\n // Fetch and validate petition\n const petition = await getPublishedPetition(payload, petitionId, petitionsSlug)\n\n if (!petition) {\n return errorResponse(ErrorCodes.NOT_FOUND, 'Petition not found or not published', 404)\n }\n\n // Validate signature data against petition fields\n const validation = validateSignatureData(data, petition.contactFields)\n\n if (!validation.valid) {\n return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.errors.join(', '), 400)\n }\n\n // Create the petition signature\n // Note: The beforeChange hook will handle contact creation/update\n const signature = await payload.create({\n collection: petitionSignaturesSlug,\n data: {\n data,\n petition: petition.id as number,\n },\n // Use internal context to bypass access control\n overrideAccess: true,\n })\n\n // Build confirmation response\n const confirmation = buildConfirmationResponse(petition)\n\n return successResponse(\n {\n confirmation,\n signatureId: signature.id,\n },\n 201,\n )\n } catch (error) {\n logger.error(error as Error, 'Error processing petition signature')\n return errorResponse(\n ErrorCodes.INTERNAL_ERROR,\n error instanceof Error ? error.message : 'Failed to process signature',\n 500,\n )\n }\n }\n}\n"],"names":["z","ErrorCodes","errorResponse","successResponse","PetitionSignatureBodySchema","object","data","record","string","unknown","petitionId","union","number","validateSignatureData","contactFields","errors","length","valid","field","required","value","blockType","undefined","push","email","emailRegex","test","getPublishedPetition","payload","collectionSlug","petition","findByID","id","collection","status","buildConfirmationResponse","confirmationType","reference","redirectUrl","url","slug","type","redirect","message","confirmationMessage","petitionSignatureHandler","pluginConfig","petitionsSlug","petitionsOverrides","petitionSignaturesSlug","petitionSignaturesOverrides","req","logger","json","BAD_REQUEST","body","parseResult","safeParse","success","firstError","error","issues","VALIDATION_ERROR","NOT_FOUND","validation","join","signature","create","overrideAccess","confirmation","signatureId","INTERNAL_ERROR","Error"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Array of country options with ISO country codes and official names.
3
+ * Can be used to populate country select inputs on the frontend.
4
+ *
5
+ * @example
6
+ * ```tsx
7
+ * import { countries } from '@mobilizehub/payload-plugin/helpers'
8
+ *
9
+ * <select>
10
+ * {countries.map((country) => (
11
+ * <option key={country.value} value={country.value}>
12
+ * {country.label}
13
+ * </option>
14
+ * ))}
15
+ * </select>
16
+ * ```
17
+ */
18
+ export declare const countries: {
19
+ label: string;
20
+ value: string;
21
+ }[];
22
+ export type Country = (typeof countries)[number];
@@ -0,0 +1,25 @@
1
+ import iso from 'i18n-iso-countries';
2
+ /**
3
+ * Array of country options with ISO country codes and official names.
4
+ * Can be used to populate country select inputs on the frontend.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { countries } from '@mobilizehub/payload-plugin/helpers'
9
+ *
10
+ * <select>
11
+ * {countries.map((country) => (
12
+ * <option key={country.value} value={country.value}>
13
+ * {country.label}
14
+ * </option>
15
+ * ))}
16
+ * </select>
17
+ * ```
18
+ */ export const countries = Object.entries(iso.getNames('en', {
19
+ select: 'official'
20
+ })).map(([code, name])=>({
21
+ label: name,
22
+ value: code
23
+ }));
24
+
25
+ //# sourceMappingURL=countries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/helpers/countries.ts"],"sourcesContent":["import iso from 'i18n-iso-countries'\n\n/**\n * Array of country options with ISO country codes and official names.\n * Can be used to populate country select inputs on the frontend.\n *\n * @example\n * ```tsx\n * import { countries } from '@mobilizehub/payload-plugin/helpers'\n *\n * <select>\n * {countries.map((country) => (\n * <option key={country.value} value={country.value}>\n * {country.label}\n * </option>\n * ))}\n * </select>\n * ```\n */\nexport const countries = Object.entries(iso.getNames('en', { select: 'official' })).map(\n ([code, name]) => ({\n label: name,\n value: code,\n }),\n)\n\nexport type Country = (typeof countries)[number]\n"],"names":["iso","countries","Object","entries","getNames","select","map","code","name","label","value"],"mappings":"AAAA,OAAOA,SAAS,qBAAoB;AAEpC;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,YAAYC,OAAOC,OAAO,CAACH,IAAII,QAAQ,CAAC,MAAM;IAAEC,QAAQ;AAAW,IAAIC,GAAG,CACrF,CAAC,CAACC,MAAMC,KAAK,GAAM,CAAA;QACjBC,OAAOD;QACPE,OAAOH;IACT,CAAA,GACD"}
@@ -0,0 +1 @@
1
+ export { countries, type Country } from './countries.js';
@@ -0,0 +1,3 @@
1
+ export { countries } from './countries.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/helpers/index.ts"],"sourcesContent":["export { countries, type Country } from './countries.js'\n"],"names":["countries"],"mappings":"AAAA,SAASA,SAAS,QAAsB,iBAAgB"}
package/dist/index.js CHANGED
@@ -4,10 +4,13 @@ import { generateEmailsCollection } from './collections/emails/generateEmailsCol
4
4
  import { generateFormSubmissionsCollection } from './collections/form-submissions/generateFormSubmissionsCollection.js';
5
5
  import { generateFormsCollection } from './collections/forms/generateFormsCollection.js';
6
6
  import { generatePagesCollection } from './collections/pages/generatePagesCollection.js';
7
+ import { generatePetitionSignaturesCollection } from './collections/petition-signatures/generatePetitionSignaturesCollection.js';
8
+ import { generatePetitionsCollection } from './collections/petitions/generatePetitionsCollection.js';
7
9
  import { generateTagsCollection } from './collections/tags/generateTagsCollection.js';
8
10
  import { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js';
9
11
  import { emailWebhookHandler } from './endpoints/emailWebhookHandler.js';
10
12
  import { formSubmissionHandler } from './endpoints/formSubmissionHandler.js';
13
+ import { petitionSignatureHandler } from './endpoints/petitionSignatureHandler.js';
11
14
  import { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js';
12
15
  import { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js';
13
16
  import { unsubscribeHandler } from './endpoints/unsubscribeHandler.js';
@@ -18,7 +21,7 @@ export const mobilizehubPlugin = (pluginOptions)=>(config)=>{
18
21
  if (!config.collections) {
19
22
  config.collections = [];
20
23
  }
21
- config.collections.push(generateTagsCollection(pluginOptions), generateContactsCollection(pluginOptions), generateBroadcastsCollection(pluginOptions), generateEmailsCollection(pluginOptions), generatePagesCollection(pluginOptions), generateUnsubscribeTokensCollection(), generateFormSubmissionsCollection(pluginOptions), generateFormsCollection(pluginOptions));
24
+ config.collections.push(generateTagsCollection(pluginOptions), generateContactsCollection(pluginOptions), generateBroadcastsCollection(pluginOptions), generateEmailsCollection(pluginOptions), generatePagesCollection(pluginOptions), generateUnsubscribeTokensCollection(), generateFormSubmissionsCollection(pluginOptions), generateFormsCollection(pluginOptions), generatePetitionSignaturesCollection(pluginOptions), generatePetitionsCollection(pluginOptions));
22
25
  if (pluginOptions.disabled) {
23
26
  return config;
24
27
  }
@@ -50,6 +53,11 @@ export const mobilizehubPlugin = (pluginOptions)=>(config)=>{
50
53
  handler: formSubmissionHandler(pluginOptions),
51
54
  method: 'post',
52
55
  path: '/forms.createSubmission'
56
+ },
57
+ {
58
+ handler: petitionSignatureHandler(pluginOptions),
59
+ method: 'post',
60
+ path: '/petitions.createSignature'
53
61
  }
54
62
  ];
55
63
  config.endpoints = [
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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 { generateFormSubmissionsCollection } from './collections/form-submissions/generateFormSubmissionsCollection.js'\nimport { generateFormsCollection } from './collections/forms/generateFormsCollection.js'\nimport { generatePagesCollection } from './collections/pages/generatePagesCollection.js'\nimport { generateTagsCollection } from './collections/tags/generateTagsCollection.js'\nimport { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js'\nimport { emailWebhookHandler } from './endpoints/emailWebhookHandler.js'\nimport { formSubmissionHandler } from './endpoints/formSubmissionHandler.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 (!config.collections) {\n config.collections = []\n }\n\n config.collections.push(\n generateTagsCollection(pluginOptions),\n generateContactsCollection(pluginOptions),\n generateBroadcastsCollection(pluginOptions),\n generateEmailsCollection(pluginOptions),\n generatePagesCollection(pluginOptions),\n generateUnsubscribeTokensCollection(),\n generateFormSubmissionsCollection(pluginOptions),\n generateFormsCollection(pluginOptions),\n )\n\n if (pluginOptions.disabled) {\n return config\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 handler: emailWebhookHandler(pluginOptions),\n method: 'post',\n path: '/webhooks/email',\n },\n {\n handler: formSubmissionHandler(pluginOptions),\n method: 'post',\n path: '/forms.createSubmission',\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","generateFormSubmissionsCollection","generateFormsCollection","generatePagesCollection","generateTagsCollection","generateUnsubscribeTokensCollection","emailWebhookHandler","formSubmissionHandler","sendBroadcastHandler","sendTestEmailHandler","unsubscribeHandler","createSendBroadcastsTask","createSendEmailTask","mobilizehubPlugin","pluginOptions","config","collections","push","disabled","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,iCAAiC,QAAQ,sEAAqE;AACvH,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,sBAAsB,QAAQ,+CAA8C;AACrF,SAASC,mCAAmC,QAAQ,gEAA+D;AACnH,SAASC,mBAAmB,QAAQ,qCAAoC;AACxE,SAASC,qBAAqB,QAAQ,uCAAsC;AAC5E,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,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEAD,OAAOC,WAAW,CAACC,IAAI,CACrBb,uBAAuBU,gBACvBf,2BAA2Be,gBAC3BhB,6BAA6BgB,gBAC7Bd,yBAAyBc,gBACzBX,wBAAwBW,gBACxBT,uCACAJ,kCAAkCa,gBAClCZ,wBAAwBY;QAG1B,IAAIA,cAAcI,QAAQ,EAAE;YAC1B,OAAOH;QACT;QAEA,IAAI,CAACA,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;YACA;gBACEF,SAASd,oBAAoBQ;gBAC7BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASb,sBAAsBO;gBAC/BO,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"}
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 { generateFormSubmissionsCollection } from './collections/form-submissions/generateFormSubmissionsCollection.js'\nimport { generateFormsCollection } from './collections/forms/generateFormsCollection.js'\nimport { generatePagesCollection } from './collections/pages/generatePagesCollection.js'\nimport { generatePetitionSignaturesCollection } from './collections/petition-signatures/generatePetitionSignaturesCollection.js'\nimport { generatePetitionsCollection } from './collections/petitions/generatePetitionsCollection.js'\nimport { generateTagsCollection } from './collections/tags/generateTagsCollection.js'\nimport { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js'\nimport { emailWebhookHandler } from './endpoints/emailWebhookHandler.js'\nimport { formSubmissionHandler } from './endpoints/formSubmissionHandler.js'\nimport { petitionSignatureHandler } from './endpoints/petitionSignatureHandler.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 (!config.collections) {\n config.collections = []\n }\n\n config.collections.push(\n generateTagsCollection(pluginOptions),\n generateContactsCollection(pluginOptions),\n generateBroadcastsCollection(pluginOptions),\n generateEmailsCollection(pluginOptions),\n generatePagesCollection(pluginOptions),\n generateUnsubscribeTokensCollection(),\n generateFormSubmissionsCollection(pluginOptions),\n generateFormsCollection(pluginOptions),\n generatePetitionSignaturesCollection(pluginOptions),\n generatePetitionsCollection(pluginOptions),\n )\n\n if (pluginOptions.disabled) {\n return config\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 handler: emailWebhookHandler(pluginOptions),\n method: 'post',\n path: '/webhooks/email',\n },\n {\n handler: formSubmissionHandler(pluginOptions),\n method: 'post',\n path: '/forms.createSubmission',\n },\n {\n handler: petitionSignatureHandler(pluginOptions),\n method: 'post',\n path: '/petitions.createSignature',\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","generateFormSubmissionsCollection","generateFormsCollection","generatePagesCollection","generatePetitionSignaturesCollection","generatePetitionsCollection","generateTagsCollection","generateUnsubscribeTokensCollection","emailWebhookHandler","formSubmissionHandler","petitionSignatureHandler","sendBroadcastHandler","sendTestEmailHandler","unsubscribeHandler","createSendBroadcastsTask","createSendEmailTask","mobilizehubPlugin","pluginOptions","config","collections","push","disabled","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,iCAAiC,QAAQ,sEAAqE;AACvH,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,oCAAoC,QAAQ,4EAA2E;AAChI,SAASC,2BAA2B,QAAQ,yDAAwD;AACpG,SAASC,sBAAsB,QAAQ,+CAA8C;AACrF,SAASC,mCAAmC,QAAQ,gEAA+D;AACnH,SAASC,mBAAmB,QAAQ,qCAAoC;AACxE,SAASC,qBAAqB,QAAQ,uCAAsC;AAC5E,SAASC,wBAAwB,QAAQ,0CAAyC;AAClF,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,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEAD,OAAOC,WAAW,CAACC,IAAI,CACrBd,uBAAuBW,gBACvBlB,2BAA2BkB,gBAC3BnB,6BAA6BmB,gBAC7BjB,yBAAyBiB,gBACzBd,wBAAwBc,gBACxBV,uCACAN,kCAAkCgB,gBAClCf,wBAAwBe,gBACxBb,qCAAqCa,gBACrCZ,4BAA4BY;QAG9B,IAAIA,cAAcI,QAAQ,EAAE;YAC1B,OAAOH;QACT;QAEA,IAAI,CAACA,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;YACA;gBACEF,SAASf,oBAAoBS;gBAC7BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASd,sBAAsBQ;gBAC/BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASb,yBAAyBO;gBAClCO,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"}
@@ -1,2 +1,3 @@
1
+ export { signPetition } from './sign-petition.js';
1
2
  export { submitForm } from './submit-form.js';
2
3
  export { confirmUnsubscribe } from './unsubscribe.js';
@@ -1,3 +1,4 @@
1
+ export { signPetition } from './sign-petition.js';
1
2
  export { submitForm } from './submit-form.js';
2
3
  export { confirmUnsubscribe } from './unsubscribe.js';
3
4
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.ts"],"sourcesContent":["export { submitForm } from './submit-form.js'\nexport { confirmUnsubscribe } from './unsubscribe.js'\n"],"names":["submitForm","confirmUnsubscribe"],"mappings":"AAAA,SAASA,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,kBAAkB,QAAQ,mBAAkB"}
1
+ {"version":3,"sources":["../../src/react/index.ts"],"sourcesContent":["export { signPetition } from './sign-petition.js'\nexport { submitForm } from './submit-form.js'\nexport { confirmUnsubscribe } from './unsubscribe.js'\n"],"names":["signPetition","submitForm","confirmUnsubscribe"],"mappings":"AAAA,SAASA,YAAY,QAAQ,qBAAoB;AACjD,SAASC,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,kBAAkB,QAAQ,mBAAkB"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Options for handling petition signature responses.
3
+ */
4
+ type SignPetitionOptions = {
5
+ /**
6
+ * Called when the petition returns a message confirmation.
7
+ * @param message - The confirmation message (may be a RichText object)
8
+ */
9
+ onMessage?: (message: unknown) => void;
10
+ /**
11
+ * Called when the petition should redirect after signing.
12
+ * @param redirect - The URL to redirect to
13
+ */
14
+ onRedirect?: (redirect: string) => void;
15
+ };
16
+ /**
17
+ * Signs a petition via the backend API.
18
+ *
19
+ * @param args - Petition signature arguments
20
+ * @param args.petitionId - The ID of the petition to sign
21
+ * @param args.data - The signature data (contact info)
22
+ * @param args.opts - Optional callbacks for handling the response
23
+ * @returns The signature data including confirmation and signatureId
24
+ * @throws Error if the signature fails
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * const handleSign = async (formData: Record<string, unknown>) => {
29
+ * try {
30
+ * const result = await signPetition({
31
+ * petitionId: '1',
32
+ * data: formData,
33
+ * opts: {
34
+ * onRedirect: (url) => router.push(url),
35
+ * onMessage: (message) => setConfirmation(message),
36
+ * },
37
+ * })
38
+ * console.log('Signature ID:', result.signatureId)
39
+ * } catch (error) {
40
+ * console.error('Petition signature failed:', error)
41
+ * }
42
+ * }
43
+ * ```
44
+ */
45
+ export declare function signPetition(args: {
46
+ data: Record<string, unknown>;
47
+ opts?: SignPetitionOptions;
48
+ petitionId: number | string;
49
+ }): Promise<{
50
+ confirmation: {
51
+ message?: unknown;
52
+ redirect?: string;
53
+ type: "message" | "redirect";
54
+ };
55
+ signatureId: number | string;
56
+ } | undefined>;
57
+ export {};
@@ -0,0 +1,54 @@
1
+ const apiUrl = '/api/petitions.createSignature';
2
+ /**
3
+ * Signs a petition via the backend API.
4
+ *
5
+ * @param args - Petition signature arguments
6
+ * @param args.petitionId - The ID of the petition to sign
7
+ * @param args.data - The signature data (contact info)
8
+ * @param args.opts - Optional callbacks for handling the response
9
+ * @returns The signature data including confirmation and signatureId
10
+ * @throws Error if the signature fails
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const handleSign = async (formData: Record<string, unknown>) => {
15
+ * try {
16
+ * const result = await signPetition({
17
+ * petitionId: '1',
18
+ * data: formData,
19
+ * opts: {
20
+ * onRedirect: (url) => router.push(url),
21
+ * onMessage: (message) => setConfirmation(message),
22
+ * },
23
+ * })
24
+ * console.log('Signature ID:', result.signatureId)
25
+ * } catch (error) {
26
+ * console.error('Petition signature failed:', error)
27
+ * }
28
+ * }
29
+ * ```
30
+ */ export async function signPetition(args) {
31
+ const response = await fetch(apiUrl, {
32
+ body: JSON.stringify({
33
+ data: args.data,
34
+ petitionId: args.petitionId
35
+ }),
36
+ headers: {
37
+ 'Content-Type': 'application/json'
38
+ },
39
+ method: 'POST'
40
+ });
41
+ const result = await response.json();
42
+ if (!result.success) {
43
+ throw new Error(result.error?.message || 'Signature failed');
44
+ }
45
+ if (result.data?.confirmation.type === 'redirect' && result.data.confirmation.redirect) {
46
+ args.opts?.onRedirect?.(result.data.confirmation.redirect);
47
+ }
48
+ if (result.data?.confirmation.type === 'message') {
49
+ args.opts?.onMessage?.(result.data.confirmation.message);
50
+ }
51
+ return result.data;
52
+ }
53
+
54
+ //# sourceMappingURL=sign-petition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/sign-petition.ts"],"sourcesContent":["const apiUrl = '/api/petitions.createSignature'\n\n/**\n * Response type from the petition signature API.\n */\ntype PetitionSignatureResponse = {\n data?: {\n confirmation: {\n message?: unknown\n redirect?: string\n type: 'message' | 'redirect'\n }\n signatureId: number | string\n }\n error?: {\n code: string\n message: string\n }\n success: boolean\n}\n\n/**\n * Options for handling petition signature responses.\n */\ntype SignPetitionOptions = {\n /**\n * Called when the petition returns a message confirmation.\n * @param message - The confirmation message (may be a RichText object)\n */\n onMessage?: (message: unknown) => void\n /**\n * Called when the petition should redirect after signing.\n * @param redirect - The URL to redirect to\n */\n onRedirect?: (redirect: string) => void\n}\n\n/**\n * Signs a petition via the backend API.\n *\n * @param args - Petition signature arguments\n * @param args.petitionId - The ID of the petition to sign\n * @param args.data - The signature data (contact info)\n * @param args.opts - Optional callbacks for handling the response\n * @returns The signature data including confirmation and signatureId\n * @throws Error if the signature fails\n *\n * @example\n * ```tsx\n * const handleSign = async (formData: Record<string, unknown>) => {\n * try {\n * const result = await signPetition({\n * petitionId: '1',\n * data: formData,\n * opts: {\n * onRedirect: (url) => router.push(url),\n * onMessage: (message) => setConfirmation(message),\n * },\n * })\n * console.log('Signature ID:', result.signatureId)\n * } catch (error) {\n * console.error('Petition signature failed:', error)\n * }\n * }\n * ```\n */\nexport async function signPetition(args: {\n data: Record<string, unknown>\n opts?: SignPetitionOptions\n petitionId: number | string\n}) {\n const response = await fetch(apiUrl, {\n body: JSON.stringify({ data: args.data, petitionId: args.petitionId }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n const result: PetitionSignatureResponse = await response.json()\n\n if (!result.success) {\n throw new Error(result.error?.message || 'Signature failed')\n }\n\n if (result.data?.confirmation.type === 'redirect' && result.data.confirmation.redirect) {\n args.opts?.onRedirect?.(result.data.confirmation.redirect)\n }\n\n if (result.data?.confirmation.type === 'message') {\n args.opts?.onMessage?.(result.data.confirmation.message)\n }\n\n return result.data\n}\n"],"names":["apiUrl","signPetition","args","response","fetch","body","JSON","stringify","data","petitionId","headers","method","result","json","success","Error","error","message","confirmation","type","redirect","opts","onRedirect","onMessage"],"mappings":"AAAA,MAAMA,SAAS;AAqCf;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,eAAeC,aAAaC,IAIlC;IACC,MAAMC,WAAW,MAAMC,MAAMJ,QAAQ;QACnCK,MAAMC,KAAKC,SAAS,CAAC;YAAEC,MAAMN,KAAKM,IAAI;YAAEC,YAAYP,KAAKO,UAAU;QAAC;QACpEC,SAAS;YACP,gBAAgB;QAClB;QACAC,QAAQ;IACV;IAEA,MAAMC,SAAoC,MAAMT,SAASU,IAAI;IAE7D,IAAI,CAACD,OAAOE,OAAO,EAAE;QACnB,MAAM,IAAIC,MAAMH,OAAOI,KAAK,EAAEC,WAAW;IAC3C;IAEA,IAAIL,OAAOJ,IAAI,EAAEU,aAAaC,SAAS,cAAcP,OAAOJ,IAAI,CAACU,YAAY,CAACE,QAAQ,EAAE;QACtFlB,KAAKmB,IAAI,EAAEC,aAAaV,OAAOJ,IAAI,CAACU,YAAY,CAACE,QAAQ;IAC3D;IAEA,IAAIR,OAAOJ,IAAI,EAAEU,aAAaC,SAAS,WAAW;QAChDjB,KAAKmB,IAAI,EAAEE,YAAYX,OAAOJ,IAAI,CAACU,YAAY,CAACD,OAAO;IACzD;IAEA,OAAOL,OAAOJ,IAAI;AACpB"}
@@ -148,6 +148,14 @@ export type MobilizehubPluginConfig = {
148
148
  pagesOverrides?: {
149
149
  blocks?: BlocksOverride;
150
150
  } & CollectionOverride;
151
+ /**
152
+ * Overrides for the petition signatures collection
153
+ */
154
+ petitionSignaturesOverrides?: CollectionOverride;
155
+ /**
156
+ * Overrides for the petitions collection
157
+ */
158
+ petitionsOverrides?: CollectionOverride;
151
159
  /**
152
160
  * Overrides for the tags collection
153
161
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["import type { BasePayload, Block, CollectionConfig, Field, PayloadRequest } from 'payload'\n\nexport type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\nexport type CollectionOverride = { fields?: FieldsOverride } & Partial<\n Omit<CollectionConfig, 'fields'>\n>\n\nexport type BlocksOverride = (args: { defaultBlocks: Block[] }) => Block[]\n\n/**\n * Contact type\n */\nexport type Contact = {\n createdAt?: string\n email?: string\n emailOptIn: boolean\n firstName?: string\n id: number | string\n lastName?: string\n tags?: {\n createdAt?: string\n id: number | string\n name?: string\n updatedAt?: string\n }[]\n updatedAt?: string\n}\n\n/**\n * Unsubscribe token input structure\n */\nexport interface UnsubscribeTokenInput {\n timestamp: number\n tokenId: string\n}\n\n/**\n * Unsubscribe token record structure\n */\nexport type UnsubscribeTokenRecord = {\n emailId?: number | string\n expiresAt?: string\n id: string\n}\n\n/**\n * Email activity types\n */\nexport type EmailStatus =\n | 'bounced'\n | 'complained'\n | 'delivered'\n | 'failed'\n | 'queued'\n | 'sent'\n | 'unsubscribed'\n\n/**\n * Email activity types from providers\n */\nexport type EmailActivityType =\n | 'bounced'\n | 'clicked'\n | 'complained'\n | 'delivered'\n | 'delivery_delayed'\n | 'failed'\n | 'opened'\n | 'received'\n | 'sent'\n\n/**\n * Email message structure\n */\nexport type EmailMessage = {\n from: string\n html: string\n idempotencyKey?: string\n markdown?: string\n plainText?: string\n previewText?: string\n replyTo?: string\n subject: string\n to: string\n token?: string\n}\n\n/**\n * Result of a webhook call\n */\nexport type WebhookResult = {\n body?: unknown\n status: number\n}\n\n/**\n * Email adapter interface for sending emails\n */\nexport type EmailAdapter = ({ payload }: { payload: BasePayload }) => {\n defaultFromAddress: string\n defaultFromName: string\n name: string\n render: (args: EmailMessage) => string\n sendEmail: (args: EmailMessage) => Promise<{ providerId: string } | void>\n webhookHandler?: (req: PayloadRequest) => Promise<void | WebhookResult>\n}\n\nexport type MobilizehubPluginConfig = {\n /**\n * Broadcasts task configuration\n */\n broadcastConfig?: {\n /**\n * Batch size for processing contacts in the broadcasts task.\n * Higher values process faster but use more memory.\n * @default 100\n */\n batchSize?: number\n /**\n * Optional custom queue name for the broadcasts task\n * @default 'send-broadcasts'\n */\n broadcastQueueName?: string\n /**\n * Optional custom queue name for the email sending task\n * @default 'send-email'\n */\n emailQueueName?: string\n /**\n * Cron schedule for the broadcasts task\n * On schedule the task will run to process and send pending broadcasts\n * @default '5 * * * *' (every 5 minutes)\n */\n taskSchedule?: string\n }\n /**\n * Overrides for the broadcasts collection\n */\n broadcastsOverrides?: CollectionOverride\n /**\n * Overrides for the contacts collection\n */\n contactsOverrides?: CollectionOverride\n /**\n * Disable the plugin\n */\n disabled?: boolean\n /**\n * Email adapter for sending emails\n */\n email: EmailAdapter\n /**\n * Overrides for the emails collection\n */\n emailsOverrides?: CollectionOverride\n /**\n * Overrides for the forms collection\n */\n formsOverrides?: CollectionOverride\n /**\n * Overrides for the form submissions collection\n */\n formSubmissionsOverrides?: CollectionOverride\n /**\n * Overrides for the pages collection\n */\n pagesOverrides?: { blocks?: BlocksOverride } & CollectionOverride\n /**\n * Overrides for the tags collection\n */\n tagsOverrides?: CollectionOverride\n}\n"],"names":[],"mappings":"AA4GA,WAgEC"}
1
+ {"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["import type { BasePayload, Block, CollectionConfig, Field, PayloadRequest } from 'payload'\n\nexport type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\nexport type CollectionOverride = { fields?: FieldsOverride } & Partial<\n Omit<CollectionConfig, 'fields'>\n>\n\nexport type BlocksOverride = (args: { defaultBlocks: Block[] }) => Block[]\n\n/**\n * Contact type\n */\nexport type Contact = {\n createdAt?: string\n email?: string\n emailOptIn: boolean\n firstName?: string\n id: number | string\n lastName?: string\n tags?: {\n createdAt?: string\n id: number | string\n name?: string\n updatedAt?: string\n }[]\n updatedAt?: string\n}\n\n/**\n * Unsubscribe token input structure\n */\nexport interface UnsubscribeTokenInput {\n timestamp: number\n tokenId: string\n}\n\n/**\n * Unsubscribe token record structure\n */\nexport type UnsubscribeTokenRecord = {\n emailId?: number | string\n expiresAt?: string\n id: string\n}\n\n/**\n * Email activity types\n */\nexport type EmailStatus =\n | 'bounced'\n | 'complained'\n | 'delivered'\n | 'failed'\n | 'queued'\n | 'sent'\n | 'unsubscribed'\n\n/**\n * Email activity types from providers\n */\nexport type EmailActivityType =\n | 'bounced'\n | 'clicked'\n | 'complained'\n | 'delivered'\n | 'delivery_delayed'\n | 'failed'\n | 'opened'\n | 'received'\n | 'sent'\n\n/**\n * Email message structure\n */\nexport type EmailMessage = {\n from: string\n html: string\n idempotencyKey?: string\n markdown?: string\n plainText?: string\n previewText?: string\n replyTo?: string\n subject: string\n to: string\n token?: string\n}\n\n/**\n * Result of a webhook call\n */\nexport type WebhookResult = {\n body?: unknown\n status: number\n}\n\n/**\n * Email adapter interface for sending emails\n */\nexport type EmailAdapter = ({ payload }: { payload: BasePayload }) => {\n defaultFromAddress: string\n defaultFromName: string\n name: string\n render: (args: EmailMessage) => string\n sendEmail: (args: EmailMessage) => Promise<{ providerId: string } | void>\n webhookHandler?: (req: PayloadRequest) => Promise<void | WebhookResult>\n}\n\nexport type MobilizehubPluginConfig = {\n /**\n * Broadcasts task configuration\n */\n broadcastConfig?: {\n /**\n * Batch size for processing contacts in the broadcasts task.\n * Higher values process faster but use more memory.\n * @default 100\n */\n batchSize?: number\n /**\n * Optional custom queue name for the broadcasts task\n * @default 'send-broadcasts'\n */\n broadcastQueueName?: string\n /**\n * Optional custom queue name for the email sending task\n * @default 'send-email'\n */\n emailQueueName?: string\n /**\n * Cron schedule for the broadcasts task\n * On schedule the task will run to process and send pending broadcasts\n * @default '5 * * * *' (every 5 minutes)\n */\n taskSchedule?: string\n }\n /**\n * Overrides for the broadcasts collection\n */\n broadcastsOverrides?: CollectionOverride\n /**\n * Overrides for the contacts collection\n */\n contactsOverrides?: CollectionOverride\n /**\n * Disable the plugin\n */\n disabled?: boolean\n /**\n * Email adapter for sending emails\n */\n email: EmailAdapter\n /**\n * Overrides for the emails collection\n */\n emailsOverrides?: CollectionOverride\n /**\n * Overrides for the forms collection\n */\n formsOverrides?: CollectionOverride\n /**\n * Overrides for the form submissions collection\n */\n formSubmissionsOverrides?: CollectionOverride\n /**\n * Overrides for the pages collection\n */\n pagesOverrides?: { blocks?: BlocksOverride } & CollectionOverride\n /**\n * Overrides for the petition signatures collection\n */\n petitionSignaturesOverrides?: CollectionOverride\n /**\n * Overrides for the petitions collection\n */\n petitionsOverrides?: CollectionOverride\n /**\n * Overrides for the tags collection\n */\n tagsOverrides?: CollectionOverride\n}\n"],"names":[],"mappings":"AA4GA,WAwEC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobilizehub/payload-plugin",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Edvocacy plugin for Payload",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -30,6 +30,11 @@
30
30
  "import": "./dist/adapters/index.js",
31
31
  "types": "./dist/adapters/index.d.ts",
32
32
  "default": "./dist/adapters/index.js"
33
+ },
34
+ "./helpers": {
35
+ "import": "./dist/helpers/index.js",
36
+ "types": "./dist/helpers/index.d.ts",
37
+ "default": "./dist/helpers/index.js"
33
38
  }
34
39
  },
35
40
  "main": "./dist/index.js",