@mobilizehub/payload-plugin 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/resend-adapter.js +8 -5
- package/dist/adapters/resend-adapter.js.map +1 -1
- package/dist/helpers/countries.d.ts +22 -0
- package/dist/helpers/countries.js +25 -0
- package/dist/helpers/countries.js.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +3 -0
- package/dist/helpers/index.js.map +1 -0
- package/package.json +6 -1
|
@@ -15,6 +15,13 @@ const WEBHOOK_EVENT_TO_ACTIVITY = {
|
|
|
15
15
|
'email.sent': 'sent'
|
|
16
16
|
};
|
|
17
17
|
async function sendResendEmail(apiKey, message, idempotencyKey) {
|
|
18
|
+
const headers = {
|
|
19
|
+
Authorization: `Bearer ${apiKey}`,
|
|
20
|
+
'Content-Type': 'application/json'
|
|
21
|
+
};
|
|
22
|
+
if (idempotencyKey) {
|
|
23
|
+
headers['Idempotency-Key'] = idempotencyKey;
|
|
24
|
+
}
|
|
18
25
|
const response = await fetch(RESEND_API_URL, {
|
|
19
26
|
body: JSON.stringify({
|
|
20
27
|
from: message.from,
|
|
@@ -22,11 +29,7 @@ async function sendResendEmail(apiKey, message, idempotencyKey) {
|
|
|
22
29
|
subject: message.subject,
|
|
23
30
|
to: message.to
|
|
24
31
|
}),
|
|
25
|
-
headers
|
|
26
|
-
Authorization: `Bearer ${apiKey}`,
|
|
27
|
-
'Content-Type': 'application/json',
|
|
28
|
-
'Idempotency-Key': idempotencyKey || ''
|
|
29
|
-
},
|
|
32
|
+
headers,
|
|
30
33
|
method: 'POST'
|
|
31
34
|
});
|
|
32
35
|
if (!response.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/resend-adapter.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport crypto from 'crypto'\n\nimport type { EmailActivityType, EmailAdapter, EmailMessage } from '../types/index.js'\n\nimport { formatFromAddress } from '../utils/email.js'\n\ntype ResendAdapterOptions = {\n apiKey: string\n defaultFromAddress: string\n defaultFromName: string\n render: ReturnType<EmailAdapter>['render']\n webhookSecret: string\n}\n\ntype ResendWebhookPayload = {\n created_at: string\n data: {\n [key: string]: unknown\n email_id?: string\n }\n type: string\n}\n\nconst RESEND_API_URL = 'https://api.resend.com/emails'\nconst WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\nconst WEBHOOK_EVENT_TO_ACTIVITY: Record<string, EmailActivityType> = {\n 'email.bounced': 'bounced',\n 'email.clicked': 'clicked',\n 'email.complained': 'complained',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delivery_delayed',\n 'email.failed': 'failed',\n 'email.opened': 'opened',\n 'email.received': 'received',\n 'email.sent': 'sent',\n}\n\nasync function sendResendEmail(\n apiKey: string,\n message: EmailMessage,\n idempotencyKey?: string,\n): Promise<{ providerId: string }> {\n const response = await fetch(RESEND_API_URL, {\n body: JSON.stringify({\n from: message.from,\n html: message.html,\n subject: message.subject,\n to: message.to,\n }),\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'Idempotency-Key': idempotencyKey || '',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Resend API error: ${response.status} - ${errorText}`)\n }\n\n const data = (await response.json()) as { id: string }\n\n return { providerId: data.id }\n}\n\ntype SvixHeaders = {\n id: string\n signature: string\n timestamp: string\n}\n\nfunction extractSvixHeaders(req: PayloadRequest): SvixHeaders {\n const id = req.headers.get('svix-id')\n const timestamp = req.headers.get('svix-timestamp')\n const signature = req.headers.get('svix-signature')\n\n if (!id || !timestamp || !signature) {\n throw new Error('Missing required Svix headers')\n }\n\n return { id, signature, timestamp }\n}\n\nfunction verifyTimestamp(timestamp: string): void {\n const timestampSeconds = parseInt(timestamp, 10)\n const nowSeconds = Math.floor(Date.now() / 1000)\n\n if (Math.abs(nowSeconds - timestampSeconds) > WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS) {\n throw new Error('Webhook timestamp is too old')\n }\n}\n\nfunction computeExpectedSignature(\n svixId: string,\n svixTimestamp: string,\n body: string,\n webhookSecret: string,\n): string {\n const signedContent = `${svixId}.${svixTimestamp}.${body}`\n\n // Resend webhook secrets are prefixed with \"whsec_\" and base64 encoded\n const secretParts = webhookSecret.split('_')\n const secretBase64 = secretParts.length > 1 ? secretParts[1] : secretParts[0]\n const secretBytes = Buffer.from(secretBase64, 'base64')\n\n return crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64')\n}\n\nfunction verifySignature(svixSignature: string, expectedSignature: string): boolean {\n // Svix signatures can be multiple, space-delimited with version prefix (e.g., \"v1,<sig>\")\n const signatures = svixSignature.split(' ').map((sig) => {\n const [version, signature] = sig.split(',')\n return { signature, version }\n })\n\n return signatures.some(({ signature, version }) => {\n if (version !== 'v1') {\n return false\n }\n\n try {\n return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature))\n } catch {\n return false\n }\n })\n}\n\nasync function verifyWebhookSignature(\n req: PayloadRequest,\n webhookSecret: string,\n): Promise<ResendWebhookPayload> {\n const headers = extractSvixHeaders(req)\n\n verifyTimestamp(headers.timestamp)\n\n if (!req.text) {\n throw new Error('Request does not have a text body method')\n }\n\n const body = await req.text()\n\n if (!body) {\n throw new Error('No body in request')\n }\n\n const expectedSignature = computeExpectedSignature(\n headers.id,\n headers.timestamp,\n body,\n webhookSecret,\n )\n\n if (!verifySignature(headers.signature, expectedSignature)) {\n throw new Error('Invalid webhook signature')\n }\n\n return JSON.parse(body) as ResendWebhookPayload\n}\n\nasync function findEmailByProviderId(\n payload: Payload,\n providerId: string,\n): Promise<{ activity?: unknown[]; id: number | string } | null> {\n const result = await payload.find({\n collection: 'emails',\n limit: 1,\n where: {\n providerId: { equals: providerId },\n },\n })\n\n return (result.docs[0] as { activity?: unknown[]; id: number | string }) || null\n}\n\nasync function addEmailActivity(\n payload: Payload,\n emailId: number | string,\n currentActivity: undefined | unknown[],\n activityType: EmailActivityType,\n): Promise<void> {\n const updatedActivity = [\n ...(currentActivity || []),\n {\n type: activityType,\n timestamp: new Date().toISOString(),\n },\n ]\n\n await payload.update({\n id: emailId,\n collection: 'emails',\n data: {\n activity: updatedActivity,\n },\n })\n}\n\nasync function handleWebhookEvent(\n payload: Payload,\n eventType: string,\n emailProviderId: string,\n logger: Payload['logger'],\n): Promise<void> {\n const activityType = WEBHOOK_EVENT_TO_ACTIVITY[eventType]\n\n if (!activityType) {\n logger.warn(`Unhandled webhook event type: ${eventType}`)\n return\n }\n\n const email = await findEmailByProviderId(payload, emailProviderId)\n\n if (!email) {\n logger.error(`No email record found for provider ID: ${emailProviderId}`)\n return\n }\n\n await addEmailActivity(payload, email.id, email.activity, activityType)\n\n // Log based on severity\n const warnEvents = ['email.bounced', 'email.complained', 'email.delivery_delayed']\n const errorEvents = ['email.failed']\n\n if (errorEvents.includes(eventType)) {\n logger.error(`Email ${activityType}: ${emailProviderId}`)\n } else if (warnEvents.includes(eventType)) {\n logger.warn(`Email ${activityType}: ${emailProviderId}`)\n } else {\n logger.info(`Email ${activityType}: ${emailProviderId}`)\n }\n}\n\n/**\n * Resend email adapter for MobilizeHub\n *\n * @example\n * ```typescript\n * import { resendAdapter } from '@mobilizehub/payload-plugin/adapters'\n *\n * const emailAdapter = resendAdapter({\n * apiKey: process.env.RESEND_API_KEY!,\n * webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,\n * defaultFromAddress: 'noreply@example.com',\n * defaultFromName: 'My App',\n * render: ({ html }) => html,\n * })\n *\n * // In your Payload config:\n * plugins: [\n * mobilizehub({\n * email: emailAdapter,\n * // ...\n * }),\n * ]\n * ```\n */\nexport const resendAdapter = (opts: ResendAdapterOptions): EmailAdapter => {\n return ({ payload }) => ({\n name: 'resend-mobilizehub-adapter',\n defaultFromAddress: opts.defaultFromAddress,\n defaultFromName: opts.defaultFromName,\n render: opts.render,\n\n sendEmail: async (message) => {\n const fromAddress =\n message.from || formatFromAddress(opts.defaultFromName, opts.defaultFromAddress)\n\n return sendResendEmail(\n opts.apiKey,\n {\n from: fromAddress,\n html: message.html,\n subject: message.subject,\n to: message.to,\n },\n message.idempotencyKey,\n )\n },\n\n webhookHandler: async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n try {\n const webhookPayload = await verifyWebhookSignature(req, opts.webhookSecret)\n\n const emailProviderId = webhookPayload.data.email_id\n\n if (!emailProviderId) {\n logger.warn(`Webhook event ${webhookPayload.type} has no email_id`)\n return\n }\n\n await handleWebhookEvent(payload, webhookPayload.type, emailProviderId, logger)\n } catch (error) {\n logger.error(\n `Resend webhook error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n throw error\n }\n },\n })\n}\n"],"names":["crypto","formatFromAddress","RESEND_API_URL","WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS","WEBHOOK_EVENT_TO_ACTIVITY","sendResendEmail","apiKey","message","idempotencyKey","response","fetch","body","JSON","stringify","from","html","subject","to","headers","Authorization","method","ok","errorText","text","Error","status","data","json","providerId","id","extractSvixHeaders","req","get","timestamp","signature","verifyTimestamp","timestampSeconds","parseInt","nowSeconds","Math","floor","Date","now","abs","computeExpectedSignature","svixId","svixTimestamp","webhookSecret","signedContent","secretParts","split","secretBase64","length","secretBytes","Buffer","createHmac","update","digest","verifySignature","svixSignature","expectedSignature","signatures","map","sig","version","some","timingSafeEqual","verifyWebhookSignature","parse","findEmailByProviderId","payload","result","find","collection","limit","where","equals","docs","addEmailActivity","emailId","currentActivity","activityType","updatedActivity","type","toISOString","activity","handleWebhookEvent","eventType","emailProviderId","logger","warn","email","error","warnEvents","errorEvents","includes","info","resendAdapter","opts","name","defaultFromAddress","defaultFromName","render","sendEmail","fromAddress","webhookHandler","webhookPayload","email_id"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/resend-adapter.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport crypto from 'crypto'\n\nimport type { EmailActivityType, EmailAdapter, EmailMessage } from '../types/index.js'\n\nimport { formatFromAddress } from '../utils/email.js'\n\ntype ResendAdapterOptions = {\n apiKey: string\n defaultFromAddress: string\n defaultFromName: string\n render: ReturnType<EmailAdapter>['render']\n webhookSecret: string\n}\n\ntype ResendWebhookPayload = {\n created_at: string\n data: {\n [key: string]: unknown\n email_id?: string\n }\n type: string\n}\n\nconst RESEND_API_URL = 'https://api.resend.com/emails'\nconst WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\nconst WEBHOOK_EVENT_TO_ACTIVITY: Record<string, EmailActivityType> = {\n 'email.bounced': 'bounced',\n 'email.clicked': 'clicked',\n 'email.complained': 'complained',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delivery_delayed',\n 'email.failed': 'failed',\n 'email.opened': 'opened',\n 'email.received': 'received',\n 'email.sent': 'sent',\n}\n\nasync function sendResendEmail(\n apiKey: string,\n message: EmailMessage,\n idempotencyKey?: string,\n): Promise<{ providerId: string }> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n }\n\n if (idempotencyKey) {\n headers['Idempotency-Key'] = idempotencyKey\n }\n\n const response = await fetch(RESEND_API_URL, {\n body: JSON.stringify({\n from: message.from,\n html: message.html,\n subject: message.subject,\n to: message.to,\n }),\n headers,\n method: 'POST',\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Resend API error: ${response.status} - ${errorText}`)\n }\n\n const data = (await response.json()) as { id: string }\n\n return { providerId: data.id }\n}\n\ntype SvixHeaders = {\n id: string\n signature: string\n timestamp: string\n}\n\nfunction extractSvixHeaders(req: PayloadRequest): SvixHeaders {\n const id = req.headers.get('svix-id')\n const timestamp = req.headers.get('svix-timestamp')\n const signature = req.headers.get('svix-signature')\n\n if (!id || !timestamp || !signature) {\n throw new Error('Missing required Svix headers')\n }\n\n return { id, signature, timestamp }\n}\n\nfunction verifyTimestamp(timestamp: string): void {\n const timestampSeconds = parseInt(timestamp, 10)\n const nowSeconds = Math.floor(Date.now() / 1000)\n\n if (Math.abs(nowSeconds - timestampSeconds) > WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS) {\n throw new Error('Webhook timestamp is too old')\n }\n}\n\nfunction computeExpectedSignature(\n svixId: string,\n svixTimestamp: string,\n body: string,\n webhookSecret: string,\n): string {\n const signedContent = `${svixId}.${svixTimestamp}.${body}`\n\n // Resend webhook secrets are prefixed with \"whsec_\" and base64 encoded\n const secretParts = webhookSecret.split('_')\n const secretBase64 = secretParts.length > 1 ? secretParts[1] : secretParts[0]\n const secretBytes = Buffer.from(secretBase64, 'base64')\n\n return crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64')\n}\n\nfunction verifySignature(svixSignature: string, expectedSignature: string): boolean {\n // Svix signatures can be multiple, space-delimited with version prefix (e.g., \"v1,<sig>\")\n const signatures = svixSignature.split(' ').map((sig) => {\n const [version, signature] = sig.split(',')\n return { signature, version }\n })\n\n return signatures.some(({ signature, version }) => {\n if (version !== 'v1') {\n return false\n }\n\n try {\n return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature))\n } catch {\n return false\n }\n })\n}\n\nasync function verifyWebhookSignature(\n req: PayloadRequest,\n webhookSecret: string,\n): Promise<ResendWebhookPayload> {\n const headers = extractSvixHeaders(req)\n\n verifyTimestamp(headers.timestamp)\n\n if (!req.text) {\n throw new Error('Request does not have a text body method')\n }\n\n const body = await req.text()\n\n if (!body) {\n throw new Error('No body in request')\n }\n\n const expectedSignature = computeExpectedSignature(\n headers.id,\n headers.timestamp,\n body,\n webhookSecret,\n )\n\n if (!verifySignature(headers.signature, expectedSignature)) {\n throw new Error('Invalid webhook signature')\n }\n\n return JSON.parse(body) as ResendWebhookPayload\n}\n\nasync function findEmailByProviderId(\n payload: Payload,\n providerId: string,\n): Promise<{ activity?: unknown[]; id: number | string } | null> {\n const result = await payload.find({\n collection: 'emails',\n limit: 1,\n where: {\n providerId: { equals: providerId },\n },\n })\n\n return (result.docs[0] as { activity?: unknown[]; id: number | string }) || null\n}\n\nasync function addEmailActivity(\n payload: Payload,\n emailId: number | string,\n currentActivity: undefined | unknown[],\n activityType: EmailActivityType,\n): Promise<void> {\n const updatedActivity = [\n ...(currentActivity || []),\n {\n type: activityType,\n timestamp: new Date().toISOString(),\n },\n ]\n\n await payload.update({\n id: emailId,\n collection: 'emails',\n data: {\n activity: updatedActivity,\n },\n })\n}\n\nasync function handleWebhookEvent(\n payload: Payload,\n eventType: string,\n emailProviderId: string,\n logger: Payload['logger'],\n): Promise<void> {\n const activityType = WEBHOOK_EVENT_TO_ACTIVITY[eventType]\n\n if (!activityType) {\n logger.warn(`Unhandled webhook event type: ${eventType}`)\n return\n }\n\n const email = await findEmailByProviderId(payload, emailProviderId)\n\n if (!email) {\n logger.error(`No email record found for provider ID: ${emailProviderId}`)\n return\n }\n\n await addEmailActivity(payload, email.id, email.activity, activityType)\n\n // Log based on severity\n const warnEvents = ['email.bounced', 'email.complained', 'email.delivery_delayed']\n const errorEvents = ['email.failed']\n\n if (errorEvents.includes(eventType)) {\n logger.error(`Email ${activityType}: ${emailProviderId}`)\n } else if (warnEvents.includes(eventType)) {\n logger.warn(`Email ${activityType}: ${emailProviderId}`)\n } else {\n logger.info(`Email ${activityType}: ${emailProviderId}`)\n }\n}\n\n/**\n * Resend email adapter for MobilizeHub\n *\n * @example\n * ```typescript\n * import { resendAdapter } from '@mobilizehub/payload-plugin/adapters'\n *\n * const emailAdapter = resendAdapter({\n * apiKey: process.env.RESEND_API_KEY!,\n * webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,\n * defaultFromAddress: 'noreply@example.com',\n * defaultFromName: 'My App',\n * render: ({ html }) => html,\n * })\n *\n * // In your Payload config:\n * plugins: [\n * mobilizehub({\n * email: emailAdapter,\n * // ...\n * }),\n * ]\n * ```\n */\nexport const resendAdapter = (opts: ResendAdapterOptions): EmailAdapter => {\n return ({ payload }) => ({\n name: 'resend-mobilizehub-adapter',\n defaultFromAddress: opts.defaultFromAddress,\n defaultFromName: opts.defaultFromName,\n render: opts.render,\n\n sendEmail: async (message) => {\n const fromAddress =\n message.from || formatFromAddress(opts.defaultFromName, opts.defaultFromAddress)\n\n return sendResendEmail(\n opts.apiKey,\n {\n from: fromAddress,\n html: message.html,\n subject: message.subject,\n to: message.to,\n },\n message.idempotencyKey,\n )\n },\n\n webhookHandler: async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n try {\n const webhookPayload = await verifyWebhookSignature(req, opts.webhookSecret)\n\n const emailProviderId = webhookPayload.data.email_id\n\n if (!emailProviderId) {\n logger.warn(`Webhook event ${webhookPayload.type} has no email_id`)\n return\n }\n\n await handleWebhookEvent(payload, webhookPayload.type, emailProviderId, logger)\n } catch (error) {\n logger.error(\n `Resend webhook error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n throw error\n }\n },\n })\n}\n"],"names":["crypto","formatFromAddress","RESEND_API_URL","WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS","WEBHOOK_EVENT_TO_ACTIVITY","sendResendEmail","apiKey","message","idempotencyKey","headers","Authorization","response","fetch","body","JSON","stringify","from","html","subject","to","method","ok","errorText","text","Error","status","data","json","providerId","id","extractSvixHeaders","req","get","timestamp","signature","verifyTimestamp","timestampSeconds","parseInt","nowSeconds","Math","floor","Date","now","abs","computeExpectedSignature","svixId","svixTimestamp","webhookSecret","signedContent","secretParts","split","secretBase64","length","secretBytes","Buffer","createHmac","update","digest","verifySignature","svixSignature","expectedSignature","signatures","map","sig","version","some","timingSafeEqual","verifyWebhookSignature","parse","findEmailByProviderId","payload","result","find","collection","limit","where","equals","docs","addEmailActivity","emailId","currentActivity","activityType","updatedActivity","type","toISOString","activity","handleWebhookEvent","eventType","emailProviderId","logger","warn","email","error","warnEvents","errorEvents","includes","info","resendAdapter","opts","name","defaultFromAddress","defaultFromName","render","sendEmail","fromAddress","webhookHandler","webhookPayload","email_id"],"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 @@
|
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobilizehub/payload-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|