payload-plugin-newsletter 0.17.1 → 0.17.2

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/index.cjs CHANGED
@@ -2873,268 +2873,658 @@ var createSignoutEndpoint = (_config) => {
2873
2873
  };
2874
2874
  };
2875
2875
 
2876
- // src/endpoints/broadcasts/send.ts
2877
- init_types();
2876
+ // src/endpoints/broadcasts/index.ts
2877
+ var createBroadcastManagementEndpoints = (config) => {
2878
+ return [];
2879
+ };
2878
2880
 
2879
- // src/utils/auth.ts
2880
- async function getAuthenticatedUser(req) {
2881
- try {
2882
- const me = await req.payload.find({
2883
- collection: "users",
2884
- where: {
2885
- id: {
2886
- equals: "me"
2887
- // Special value in Payload to get current user
2888
- }
2889
- },
2890
- limit: 1,
2891
- depth: 0
2892
- });
2893
- return me.docs[0] || null;
2894
- } catch {
2895
- return null;
2896
- }
2897
- }
2898
- async function requireAdmin(req, config) {
2899
- const user = await getAuthenticatedUser(req);
2900
- if (!user) {
2901
- return {
2902
- authorized: false,
2903
- error: "Authentication required"
2904
- };
2905
- }
2906
- if (!isAdmin(user, config)) {
2907
- return {
2908
- authorized: false,
2909
- error: "Admin access required"
2910
- };
2881
+ // src/endpoints/index.ts
2882
+ function createNewsletterEndpoints(config) {
2883
+ const endpoints = [
2884
+ createSubscribeEndpoint(config),
2885
+ createUnsubscribeEndpoint(config)
2886
+ ];
2887
+ if (config.auth?.enabled !== false) {
2888
+ endpoints.push(
2889
+ createVerifyMagicLinkEndpoint(config),
2890
+ createPreferencesEndpoint(config),
2891
+ createUpdatePreferencesEndpoint(config),
2892
+ createSigninEndpoint(config),
2893
+ createMeEndpoint(config),
2894
+ createSignoutEndpoint(config)
2895
+ );
2911
2896
  }
2912
- return {
2913
- authorized: true,
2914
- user
2915
- };
2897
+ endpoints.push(...createBroadcastManagementEndpoints(config));
2898
+ return endpoints;
2916
2899
  }
2917
2900
 
2918
- // src/utils/getBroadcastConfig.ts
2919
- async function getBroadcastConfig(req, pluginConfig) {
2920
- try {
2921
- const settings = await req.payload.findGlobal({
2922
- slug: pluginConfig.settingsSlug || "newsletter-settings",
2923
- req
2924
- });
2925
- if (settings?.provider === "broadcast" && settings?.broadcastSettings) {
2926
- return {
2927
- apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || "",
2928
- token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || "",
2929
- fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || "",
2930
- fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || "",
2931
- replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo
2932
- };
2901
+ // src/fields/newsletterScheduling.ts
2902
+ function createNewsletterSchedulingFields(config) {
2903
+ const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
2904
+ const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
2905
+ const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
2906
+ const fields = [
2907
+ {
2908
+ name: groupName,
2909
+ type: "group",
2910
+ label: "Newsletter Scheduling",
2911
+ admin: {
2912
+ condition: (data, { user }) => user?.collection === "users"
2913
+ // Only show for admin users
2914
+ },
2915
+ fields: [
2916
+ {
2917
+ name: "scheduled",
2918
+ type: "checkbox",
2919
+ label: "Schedule for Newsletter",
2920
+ defaultValue: false,
2921
+ admin: {
2922
+ description: "Schedule this content to be sent as a newsletter"
2923
+ }
2924
+ },
2925
+ {
2926
+ name: "scheduledDate",
2927
+ type: "date",
2928
+ label: "Send Date",
2929
+ required: true,
2930
+ admin: {
2931
+ date: {
2932
+ pickerAppearance: "dayAndTime"
2933
+ },
2934
+ condition: (data) => data?.[groupName]?.scheduled,
2935
+ description: "When to send this newsletter"
2936
+ }
2937
+ },
2938
+ {
2939
+ name: "sentDate",
2940
+ type: "date",
2941
+ label: "Sent Date",
2942
+ admin: {
2943
+ readOnly: true,
2944
+ condition: (data) => data?.[groupName]?.sendStatus === "sent",
2945
+ description: "When this newsletter was sent"
2946
+ }
2947
+ },
2948
+ {
2949
+ name: "sendStatus",
2950
+ type: "select",
2951
+ label: "Status",
2952
+ options: [
2953
+ { label: "Draft", value: "draft" },
2954
+ { label: "Scheduled", value: "scheduled" },
2955
+ { label: "Sending", value: "sending" },
2956
+ { label: "Sent", value: "sent" },
2957
+ { label: "Failed", value: "failed" }
2958
+ ],
2959
+ defaultValue: "draft",
2960
+ admin: {
2961
+ readOnly: true,
2962
+ description: "Current send status"
2963
+ }
2964
+ },
2965
+ {
2966
+ name: "emailSubject",
2967
+ type: "text",
2968
+ label: "Email Subject",
2969
+ required: true,
2970
+ admin: {
2971
+ condition: (data) => data?.[groupName]?.scheduled,
2972
+ description: "Subject line for the newsletter email"
2973
+ }
2974
+ },
2975
+ {
2976
+ name: "preheader",
2977
+ type: "text",
2978
+ label: "Email Preheader",
2979
+ admin: {
2980
+ condition: (data) => data?.[groupName]?.scheduled,
2981
+ description: "Preview text that appears after the subject line"
2982
+ }
2983
+ },
2984
+ {
2985
+ name: "segments",
2986
+ type: "select",
2987
+ label: "Target Segments",
2988
+ hasMany: true,
2989
+ options: [
2990
+ { label: "All Subscribers", value: "all" },
2991
+ ...config.i18n?.locales?.map((locale) => ({
2992
+ label: `${locale.toUpperCase()} Subscribers`,
2993
+ value: locale
2994
+ })) || []
2995
+ ],
2996
+ defaultValue: ["all"],
2997
+ admin: {
2998
+ condition: (data) => data?.[groupName]?.scheduled,
2999
+ description: "Which subscriber segments to send to"
3000
+ }
3001
+ },
3002
+ {
3003
+ name: "testEmails",
3004
+ type: "array",
3005
+ label: "Test Email Recipients",
3006
+ admin: {
3007
+ condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
3008
+ description: "Send test emails before scheduling"
3009
+ },
3010
+ fields: [
3011
+ {
3012
+ name: "email",
3013
+ type: "email",
3014
+ required: true
3015
+ }
3016
+ ]
3017
+ }
3018
+ ]
2933
3019
  }
2934
- return pluginConfig.providers?.broadcast || null;
2935
- } catch (error) {
2936
- req.payload.logger.error("Failed to get broadcast config from settings:", error);
2937
- return pluginConfig.providers?.broadcast || null;
3020
+ ];
3021
+ if (createMarkdownField) {
3022
+ fields.push(createMarkdownFieldInternal({
3023
+ name: `${contentField}Markdown`,
3024
+ richTextField: contentField,
3025
+ label: "Email Content (Markdown)",
3026
+ admin: {
3027
+ position: "sidebar",
3028
+ condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
3029
+ description: "Markdown version for email rendering",
3030
+ readOnly: true
3031
+ }
3032
+ }));
2938
3033
  }
3034
+ return fields;
2939
3035
  }
2940
-
2941
- // src/endpoints/broadcasts/send.ts
2942
- var createSendBroadcastEndpoint = (config, collectionSlug) => {
3036
+ function createMarkdownFieldInternal(config) {
2943
3037
  return {
2944
- path: `/${collectionSlug}/:id/send`,
2945
- method: "post",
2946
- handler: async (req) => {
2947
- try {
2948
- const auth = await requireAdmin(req, config);
2949
- if (!auth.authorized) {
2950
- return Response.json({
2951
- success: false,
2952
- error: auth.error
2953
- }, { status: 401 });
3038
+ name: config.name,
3039
+ type: "textarea",
3040
+ label: config.label || "Markdown",
3041
+ admin: {
3042
+ ...config.admin,
3043
+ description: config.admin?.description || "Auto-generated from rich text content"
3044
+ },
3045
+ hooks: {
3046
+ afterRead: [
3047
+ async ({ data }) => {
3048
+ if (data?.[config.richTextField]) {
3049
+ try {
3050
+ const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
3051
+ return convertLexicalToMarkdown({
3052
+ data: data[config.richTextField]
3053
+ });
3054
+ } catch {
3055
+ return "";
3056
+ }
3057
+ }
3058
+ return "";
2954
3059
  }
2955
- if (!config.features?.newsletterManagement?.enabled) {
2956
- return Response.json({
2957
- success: false,
2958
- error: "Broadcast management is not enabled"
2959
- }, { status: 400 });
3060
+ ],
3061
+ beforeChange: [
3062
+ () => {
3063
+ return null;
2960
3064
  }
2961
- const url = new URL(req.url || "", `http://localhost`);
2962
- const pathParts = url.pathname.split("/");
2963
- const id = pathParts[pathParts.length - 2];
2964
- if (!id) {
2965
- return Response.json({
2966
- success: false,
2967
- error: "Broadcast ID is required"
2968
- }, { status: 400 });
2969
- }
2970
- const data = await (req.json?.() || Promise.resolve({}));
2971
- const broadcastDoc = await req.payload.findByID({
2972
- collection: collectionSlug,
2973
- id,
2974
- user: auth.user
2975
- });
2976
- if (!broadcastDoc || !broadcastDoc.providerId) {
2977
- return Response.json({
2978
- success: false,
2979
- error: "Broadcast not found or not synced with provider"
2980
- }, { status: 404 });
2981
- }
2982
- const providerConfig = await getBroadcastConfig(req, config);
2983
- if (!providerConfig || !providerConfig.token) {
2984
- return Response.json({
2985
- success: false,
2986
- error: "Broadcast provider not configured in Newsletter Settings or environment variables"
2987
- }, { status: 500 });
2988
- }
2989
- const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
2990
- const provider = new BroadcastApiProvider2(providerConfig);
2991
- const broadcast = await provider.send(broadcastDoc.providerId, data);
2992
- await req.payload.update({
2993
- collection: collectionSlug,
2994
- id,
2995
- data: {
2996
- sendStatus: "sending" /* SENDING */,
2997
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
2998
- },
2999
- user: auth.user
3000
- });
3001
- return Response.json({
3002
- success: true,
3003
- message: "Broadcast sent successfully",
3004
- broadcast
3005
- });
3006
- } catch (error) {
3007
- console.error("Failed to send broadcast:", error);
3008
- if (error instanceof NewsletterProviderError) {
3009
- return Response.json({
3010
- success: false,
3011
- error: error.message,
3012
- code: error.code
3013
- }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
3014
- }
3015
- return Response.json({
3016
- success: false,
3017
- error: "Failed to send broadcast"
3018
- }, { status: 500 });
3019
- }
3065
+ ]
3020
3066
  }
3021
3067
  };
3022
- };
3068
+ }
3023
3069
 
3024
- // src/endpoints/broadcasts/schedule.ts
3025
- init_types();
3026
- var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
3070
+ // src/jobs/sync-unsubscribes.ts
3071
+ var createUnsubscribeSyncJob = (pluginConfig) => {
3027
3072
  return {
3028
- path: `/${collectionSlug}/:id/schedule`,
3029
- method: "post",
3030
- handler: async (req) => {
3073
+ slug: "sync-unsubscribes",
3074
+ label: "Sync Unsubscribes from Email Service",
3075
+ handler: async ({ req }) => {
3076
+ const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
3077
+ const emailService = req.payload.newsletterEmailService;
3078
+ if (!emailService) {
3079
+ console.error("Email service not configured");
3080
+ return {
3081
+ output: {
3082
+ syncedCount: 0
3083
+ }
3084
+ };
3085
+ }
3086
+ let syncedCount = 0;
3031
3087
  try {
3032
- const auth = await requireAdmin(req, config);
3033
- if (!auth.authorized) {
3034
- return Response.json({
3035
- success: false,
3036
- error: auth.error
3037
- }, { status: 401 });
3038
- }
3039
- if (!config.features?.newsletterManagement?.enabled) {
3040
- return Response.json({
3041
- success: false,
3042
- error: "Broadcast management is not enabled"
3043
- }, { status: 400 });
3044
- }
3045
- const url = new URL(req.url || "", `http://localhost`);
3046
- const pathParts = url.pathname.split("/");
3047
- const id = pathParts[pathParts.length - 2];
3048
- if (!id) {
3049
- return Response.json({
3050
- success: false,
3051
- error: "Broadcast ID is required"
3052
- }, { status: 400 });
3053
- }
3054
- const data = await (req.json?.() || Promise.resolve({}));
3055
- const { scheduledAt } = data;
3056
- if (!scheduledAt) {
3057
- return Response.json({
3058
- success: false,
3059
- error: "scheduledAt is required"
3060
- }, { status: 400 });
3061
- }
3062
- const scheduledDate = new Date(scheduledAt);
3063
- if (isNaN(scheduledDate.getTime())) {
3064
- return Response.json({
3065
- success: false,
3066
- error: "Invalid scheduledAt date"
3067
- }, { status: 400 });
3068
- }
3069
- if (scheduledDate <= /* @__PURE__ */ new Date()) {
3070
- return Response.json({
3071
- success: false,
3072
- error: "scheduledAt must be in the future"
3073
- }, { status: 400 });
3088
+ if (emailService.getProvider() === "broadcast") {
3089
+ console.warn("Starting Broadcast unsubscribe sync...");
3090
+ const broadcastConfig = pluginConfig.providers?.broadcast;
3091
+ if (!broadcastConfig) {
3092
+ throw new Error("Broadcast configuration not found");
3093
+ }
3094
+ const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
3095
+ const token = broadcastConfig.token;
3096
+ let page = 1;
3097
+ let hasMore = true;
3098
+ while (hasMore) {
3099
+ const response = await fetch(
3100
+ `${apiUrl}/api/v1/subscribers.json?page=${page}`,
3101
+ {
3102
+ headers: {
3103
+ "Authorization": `Bearer ${token}`
3104
+ }
3105
+ }
3106
+ );
3107
+ if (!response.ok) {
3108
+ throw new Error(`Broadcast API error: ${response.status}`);
3109
+ }
3110
+ const data = await response.json();
3111
+ const broadcastSubscribers = data.subscribers || [];
3112
+ for (const broadcastSub of broadcastSubscribers) {
3113
+ const payloadSubscribers = await req.payload.find({
3114
+ collection: subscribersSlug,
3115
+ where: {
3116
+ email: {
3117
+ equals: broadcastSub.email
3118
+ }
3119
+ },
3120
+ limit: 1
3121
+ });
3122
+ if (payloadSubscribers.docs.length > 0) {
3123
+ const payloadSub = payloadSubscribers.docs[0];
3124
+ const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
3125
+ const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
3126
+ if (broadcastUnsubscribed && !payloadUnsubscribed) {
3127
+ await req.payload.update({
3128
+ collection: subscribersSlug,
3129
+ id: payloadSub.id,
3130
+ data: {
3131
+ subscriptionStatus: "unsubscribed",
3132
+ unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
3133
+ }
3134
+ });
3135
+ syncedCount++;
3136
+ console.warn(`Unsubscribed: ${broadcastSub.email}`);
3137
+ }
3138
+ }
3139
+ }
3140
+ if (data.pagination && data.pagination.current < data.pagination.total_pages) {
3141
+ page++;
3142
+ } else {
3143
+ hasMore = false;
3144
+ }
3145
+ }
3146
+ console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
3074
3147
  }
3075
- const broadcastDoc = await req.payload.findByID({
3076
- collection: collectionSlug,
3077
- id,
3078
- user: auth.user
3079
- });
3080
- if (!broadcastDoc || !broadcastDoc.providerId) {
3081
- return Response.json({
3082
- success: false,
3083
- error: "Broadcast not found or not synced with provider"
3084
- }, { status: 404 });
3148
+ if (emailService.getProvider() === "resend") {
3149
+ console.warn("Starting Resend unsubscribe sync...");
3150
+ const resendConfig = pluginConfig.providers?.resend;
3151
+ if (!resendConfig) {
3152
+ throw new Error("Resend configuration not found");
3153
+ }
3154
+ console.warn("Resend polling implementation needed - webhooks recommended");
3085
3155
  }
3086
- const providerConfig = config.providers?.broadcast;
3087
- if (!providerConfig) {
3088
- return Response.json({
3089
- success: false,
3090
- error: "Broadcast provider not configured"
3091
- }, { status: 500 });
3156
+ if (pluginConfig.hooks?.afterUnsubscribeSync) {
3157
+ await pluginConfig.hooks.afterUnsubscribeSync({
3158
+ req,
3159
+ syncedCount
3160
+ });
3092
3161
  }
3093
- const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
3094
- const provider = new BroadcastApiProvider2(providerConfig);
3095
- const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
3096
- await req.payload.update({
3097
- collection: collectionSlug,
3098
- id,
3099
- data: {
3100
- sendStatus: "scheduled" /* SCHEDULED */,
3101
- scheduledAt: scheduledDate.toISOString()
3102
- },
3103
- user: auth.user
3104
- });
3105
- return Response.json({
3106
- success: true,
3107
- message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
3108
- broadcast
3109
- });
3110
3162
  } catch (error) {
3111
- console.error("Failed to schedule broadcast:", error);
3112
- if (error instanceof NewsletterProviderError) {
3113
- return Response.json({
3114
- success: false,
3115
- error: error.message,
3116
- code: error.code
3117
- }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
3118
- }
3119
- return Response.json({
3120
- success: false,
3121
- error: "Failed to schedule broadcast"
3122
- }, { status: 500 });
3163
+ console.error("Unsubscribe sync error:", error);
3164
+ throw error;
3123
3165
  }
3166
+ return {
3167
+ output: {
3168
+ syncedCount
3169
+ }
3170
+ };
3124
3171
  }
3125
3172
  };
3126
3173
  };
3127
3174
 
3128
- // src/utils/emailSafeHtml.ts
3129
- var import_isomorphic_dompurify2 = __toESM(require("isomorphic-dompurify"), 1);
3130
- var EMAIL_SAFE_CONFIG = {
3131
- ALLOWED_TAGS: [
3132
- "p",
3133
- "br",
3134
- "strong",
3135
- "b",
3136
- "em",
3137
- "i",
3175
+ // src/collections/Broadcasts.ts
3176
+ init_types();
3177
+
3178
+ // src/fields/emailContent.ts
3179
+ var import_richtext_lexical = require("@payloadcms/richtext-lexical");
3180
+
3181
+ // src/utils/blockValidation.ts
3182
+ var EMAIL_INCOMPATIBLE_TYPES = [
3183
+ "chart",
3184
+ "dataTable",
3185
+ "interactive",
3186
+ "streamable",
3187
+ "video",
3188
+ "iframe",
3189
+ "form",
3190
+ "carousel",
3191
+ "tabs",
3192
+ "accordion",
3193
+ "map"
3194
+ ];
3195
+ var validateEmailBlocks = (blocks) => {
3196
+ blocks.forEach((block) => {
3197
+ if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
3198
+ console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
3199
+ }
3200
+ const hasComplexFields = block.fields?.some((field) => {
3201
+ const complexTypes = ["code", "json", "richText", "blocks", "array"];
3202
+ return complexTypes.includes(field.type);
3203
+ });
3204
+ if (hasComplexFields) {
3205
+ console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
3206
+ }
3207
+ });
3208
+ };
3209
+ var createEmailSafeBlocks = (customBlocks = []) => {
3210
+ validateEmailBlocks(customBlocks);
3211
+ const baseBlocks = [
3212
+ {
3213
+ slug: "button",
3214
+ fields: [
3215
+ {
3216
+ name: "text",
3217
+ type: "text",
3218
+ label: "Button Text",
3219
+ required: true
3220
+ },
3221
+ {
3222
+ name: "url",
3223
+ type: "text",
3224
+ label: "Button URL",
3225
+ required: true,
3226
+ admin: {
3227
+ description: "Enter the full URL (including https://)"
3228
+ }
3229
+ },
3230
+ {
3231
+ name: "style",
3232
+ type: "select",
3233
+ label: "Button Style",
3234
+ defaultValue: "primary",
3235
+ options: [
3236
+ { label: "Primary", value: "primary" },
3237
+ { label: "Secondary", value: "secondary" },
3238
+ { label: "Outline", value: "outline" }
3239
+ ]
3240
+ }
3241
+ ],
3242
+ interfaceName: "EmailButton",
3243
+ labels: {
3244
+ singular: "Button",
3245
+ plural: "Buttons"
3246
+ }
3247
+ },
3248
+ {
3249
+ slug: "divider",
3250
+ fields: [
3251
+ {
3252
+ name: "style",
3253
+ type: "select",
3254
+ label: "Divider Style",
3255
+ defaultValue: "solid",
3256
+ options: [
3257
+ { label: "Solid", value: "solid" },
3258
+ { label: "Dashed", value: "dashed" },
3259
+ { label: "Dotted", value: "dotted" }
3260
+ ]
3261
+ }
3262
+ ],
3263
+ interfaceName: "EmailDivider",
3264
+ labels: {
3265
+ singular: "Divider",
3266
+ plural: "Dividers"
3267
+ }
3268
+ }
3269
+ ];
3270
+ return [
3271
+ ...baseBlocks,
3272
+ ...customBlocks
3273
+ ];
3274
+ };
3275
+
3276
+ // src/fields/emailContent.ts
3277
+ var createEmailSafeFeatures = (additionalBlocks) => {
3278
+ const baseBlocks = [
3279
+ {
3280
+ slug: "button",
3281
+ fields: [
3282
+ {
3283
+ name: "text",
3284
+ type: "text",
3285
+ label: "Button Text",
3286
+ required: true
3287
+ },
3288
+ {
3289
+ name: "url",
3290
+ type: "text",
3291
+ label: "Button URL",
3292
+ required: true,
3293
+ admin: {
3294
+ description: "Enter the full URL (including https://)"
3295
+ }
3296
+ },
3297
+ {
3298
+ name: "style",
3299
+ type: "select",
3300
+ label: "Button Style",
3301
+ defaultValue: "primary",
3302
+ options: [
3303
+ { label: "Primary", value: "primary" },
3304
+ { label: "Secondary", value: "secondary" },
3305
+ { label: "Outline", value: "outline" }
3306
+ ]
3307
+ }
3308
+ ],
3309
+ interfaceName: "EmailButton",
3310
+ labels: {
3311
+ singular: "Button",
3312
+ plural: "Buttons"
3313
+ }
3314
+ },
3315
+ {
3316
+ slug: "divider",
3317
+ fields: [
3318
+ {
3319
+ name: "style",
3320
+ type: "select",
3321
+ label: "Divider Style",
3322
+ defaultValue: "solid",
3323
+ options: [
3324
+ { label: "Solid", value: "solid" },
3325
+ { label: "Dashed", value: "dashed" },
3326
+ { label: "Dotted", value: "dotted" }
3327
+ ]
3328
+ }
3329
+ ],
3330
+ interfaceName: "EmailDivider",
3331
+ labels: {
3332
+ singular: "Divider",
3333
+ plural: "Dividers"
3334
+ }
3335
+ }
3336
+ ];
3337
+ const allBlocks = [
3338
+ ...baseBlocks,
3339
+ ...additionalBlocks || []
3340
+ ];
3341
+ return [
3342
+ // Toolbars
3343
+ (0, import_richtext_lexical.FixedToolbarFeature)(),
3344
+ // Fixed toolbar at the top
3345
+ (0, import_richtext_lexical.InlineToolbarFeature)(),
3346
+ // Floating toolbar when text is selected
3347
+ // Basic text formatting
3348
+ (0, import_richtext_lexical.BoldFeature)(),
3349
+ (0, import_richtext_lexical.ItalicFeature)(),
3350
+ (0, import_richtext_lexical.UnderlineFeature)(),
3351
+ (0, import_richtext_lexical.StrikethroughFeature)(),
3352
+ // Links with enhanced configuration
3353
+ (0, import_richtext_lexical.LinkFeature)({
3354
+ fields: [
3355
+ {
3356
+ name: "url",
3357
+ type: "text",
3358
+ required: true,
3359
+ admin: {
3360
+ description: "Enter the full URL (including https://)"
3361
+ }
3362
+ },
3363
+ {
3364
+ name: "newTab",
3365
+ type: "checkbox",
3366
+ label: "Open in new tab",
3367
+ defaultValue: false
3368
+ }
3369
+ ]
3370
+ }),
3371
+ // Lists
3372
+ (0, import_richtext_lexical.OrderedListFeature)(),
3373
+ (0, import_richtext_lexical.UnorderedListFeature)(),
3374
+ // Headings - limited to h1, h2, h3 for email compatibility
3375
+ (0, import_richtext_lexical.HeadingFeature)({
3376
+ enabledHeadingSizes: ["h1", "h2", "h3"]
3377
+ }),
3378
+ // Basic paragraph and alignment
3379
+ (0, import_richtext_lexical.ParagraphFeature)(),
3380
+ (0, import_richtext_lexical.AlignFeature)(),
3381
+ // Blockquotes
3382
+ (0, import_richtext_lexical.BlockquoteFeature)(),
3383
+ // Upload feature for images
3384
+ (0, import_richtext_lexical.UploadFeature)({
3385
+ collections: {
3386
+ media: {
3387
+ fields: [
3388
+ {
3389
+ name: "caption",
3390
+ type: "text",
3391
+ admin: {
3392
+ description: "Optional caption for the image"
3393
+ }
3394
+ },
3395
+ {
3396
+ name: "altText",
3397
+ type: "text",
3398
+ label: "Alt Text",
3399
+ required: true,
3400
+ admin: {
3401
+ description: "Alternative text for accessibility and when image cannot be displayed"
3402
+ }
3403
+ }
3404
+ ]
3405
+ }
3406
+ }
3407
+ }),
3408
+ // Custom blocks for email-specific content
3409
+ (0, import_richtext_lexical.BlocksFeature)({
3410
+ blocks: allBlocks
3411
+ })
3412
+ ];
3413
+ };
3414
+ var createEmailLexicalEditor = (customBlocks = []) => {
3415
+ const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
3416
+ return (0, import_richtext_lexical.lexicalEditor)({
3417
+ features: [
3418
+ // Toolbars
3419
+ (0, import_richtext_lexical.FixedToolbarFeature)(),
3420
+ (0, import_richtext_lexical.InlineToolbarFeature)(),
3421
+ // Basic text formatting
3422
+ (0, import_richtext_lexical.BoldFeature)(),
3423
+ (0, import_richtext_lexical.ItalicFeature)(),
3424
+ (0, import_richtext_lexical.UnderlineFeature)(),
3425
+ (0, import_richtext_lexical.StrikethroughFeature)(),
3426
+ // Links with enhanced configuration
3427
+ (0, import_richtext_lexical.LinkFeature)({
3428
+ fields: [
3429
+ {
3430
+ name: "url",
3431
+ type: "text",
3432
+ required: true,
3433
+ admin: {
3434
+ description: "Enter the full URL (including https://)"
3435
+ }
3436
+ },
3437
+ {
3438
+ name: "newTab",
3439
+ type: "checkbox",
3440
+ label: "Open in new tab",
3441
+ defaultValue: false
3442
+ }
3443
+ ]
3444
+ }),
3445
+ // Lists
3446
+ (0, import_richtext_lexical.OrderedListFeature)(),
3447
+ (0, import_richtext_lexical.UnorderedListFeature)(),
3448
+ // Headings - limited to h1, h2, h3 for email compatibility
3449
+ (0, import_richtext_lexical.HeadingFeature)({
3450
+ enabledHeadingSizes: ["h1", "h2", "h3"]
3451
+ }),
3452
+ // Basic paragraph and alignment
3453
+ (0, import_richtext_lexical.ParagraphFeature)(),
3454
+ (0, import_richtext_lexical.AlignFeature)(),
3455
+ // Blockquotes
3456
+ (0, import_richtext_lexical.BlockquoteFeature)(),
3457
+ // Upload feature for images
3458
+ (0, import_richtext_lexical.UploadFeature)({
3459
+ collections: {
3460
+ media: {
3461
+ fields: [
3462
+ {
3463
+ name: "caption",
3464
+ type: "text",
3465
+ admin: {
3466
+ description: "Optional caption for the image"
3467
+ }
3468
+ },
3469
+ {
3470
+ name: "altText",
3471
+ type: "text",
3472
+ label: "Alt Text",
3473
+ required: true,
3474
+ admin: {
3475
+ description: "Alternative text for accessibility and when image cannot be displayed"
3476
+ }
3477
+ }
3478
+ ]
3479
+ }
3480
+ }
3481
+ }),
3482
+ // Email-safe blocks (processed server-side)
3483
+ (0, import_richtext_lexical.BlocksFeature)({
3484
+ blocks: emailSafeBlocks
3485
+ })
3486
+ ]
3487
+ });
3488
+ };
3489
+ var emailSafeFeatures = createEmailSafeFeatures();
3490
+ var createEmailContentField = (overrides) => {
3491
+ const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
3492
+ return {
3493
+ name: "content",
3494
+ type: "richText",
3495
+ required: true,
3496
+ editor,
3497
+ admin: {
3498
+ description: "Email content with limited formatting for compatibility",
3499
+ ...overrides?.admin
3500
+ },
3501
+ ...overrides
3502
+ };
3503
+ };
3504
+
3505
+ // src/fields/broadcastInlinePreview.ts
3506
+ var createBroadcastInlinePreviewField = () => {
3507
+ return {
3508
+ name: "broadcastInlinePreview",
3509
+ type: "ui",
3510
+ admin: {
3511
+ components: {
3512
+ Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
3513
+ }
3514
+ }
3515
+ };
3516
+ };
3517
+
3518
+ // src/utils/emailSafeHtml.ts
3519
+ var import_isomorphic_dompurify2 = __toESM(require("isomorphic-dompurify"), 1);
3520
+ var EMAIL_SAFE_CONFIG = {
3521
+ ALLOWED_TAGS: [
3522
+ "p",
3523
+ "br",
3524
+ "strong",
3525
+ "b",
3526
+ "em",
3527
+ "i",
3138
3528
  "u",
3139
3529
  "strike",
3140
3530
  "s",
@@ -3364,858 +3754,459 @@ async function convertBlock(node, mediaUrl, customBlockConverter) {
3364
3754
  node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
3365
3755
  );
3366
3756
  return childParts.join("");
3367
- }
3368
- return "";
3369
- }
3370
- }
3371
- function convertButtonBlock(fields) {
3372
- const text = fields?.text || "Click here";
3373
- const url = fields?.url || "#";
3374
- const style = fields?.style || "primary";
3375
- const styles2 = {
3376
- primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
3377
- secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
3378
- outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
3379
- };
3380
- const buttonStyle = `${styles2[style] || styles2.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
3381
- return `
3382
- <div style="margin: 0 0 16px 0; text-align: center;">
3383
- <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
3384
- </div>
3385
- `;
3386
- }
3387
- function convertDividerBlock(fields) {
3388
- const style = fields?.style || "solid";
3389
- const styles2 = {
3390
- solid: "border-top: 1px solid #e5e7eb;",
3391
- dashed: "border-top: 1px dashed #e5e7eb;",
3392
- dotted: "border-top: 1px dotted #e5e7eb;"
3393
- };
3394
- return `<hr style="${styles2[style] || styles2.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
3395
- }
3396
- function getAlignment(format) {
3397
- if (!format) return "left";
3398
- if (format & 2) return "center";
3399
- if (format & 3) return "right";
3400
- if (format & 4) return "justify";
3401
- return "left";
3402
- }
3403
- function escapeHtml(text) {
3404
- const map = {
3405
- "&": "&amp;",
3406
- "<": "&lt;",
3407
- ">": "&gt;",
3408
- '"': "&quot;",
3409
- "'": "&#039;"
3410
- };
3411
- return text.replace(/[&<>"']/g, (m) => map[m]);
3412
- }
3413
- function wrapInEmailTemplate(content, preheader) {
3414
- return `<!DOCTYPE html>
3415
- <html lang="en">
3416
- <head>
3417
- <meta charset="UTF-8">
3418
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
3419
- <title>Email</title>
3420
- <!--[if mso]>
3421
- <noscript>
3422
- <xml>
3423
- <o:OfficeDocumentSettings>
3424
- <o:PixelsPerInch>96</o:PixelsPerInch>
3425
- </o:OfficeDocumentSettings>
3426
- </xml>
3427
- </noscript>
3428
- <![endif]-->
3429
- </head>
3430
- <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; background-color: #f3f4f6;">
3431
- ${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
3432
- <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
3433
- <tr>
3434
- <td align="center" style="padding: 20px 0;">
3435
- <table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
3436
- <tr>
3437
- <td style="padding: 40px 30px;">
3438
- ${content}
3439
- </td>
3440
- </tr>
3441
- </table>
3442
- </td>
3443
- </tr>
3444
- </table>
3445
- </body>
3446
- </html>`;
3447
- }
3448
-
3449
- // src/endpoints/broadcasts/test.ts
3450
- var createTestBroadcastEndpoint = (config, collectionSlug) => {
3451
- return {
3452
- path: `/${collectionSlug}/:id/test`,
3453
- method: "post",
3454
- handler: async (req) => {
3455
- try {
3456
- const auth = await requireAdmin(req, config);
3457
- if (!auth.authorized) {
3458
- return Response.json({
3459
- success: false,
3460
- error: auth.error
3461
- }, { status: 401 });
3462
- }
3463
- const url = new URL(req.url || "", `http://localhost`);
3464
- const pathParts = url.pathname.split("/");
3465
- const id = pathParts[pathParts.length - 2];
3466
- if (!id) {
3467
- return Response.json({
3468
- success: false,
3469
- error: "Broadcast ID is required"
3470
- }, { status: 400 });
3471
- }
3472
- const data = await (req.json?.() || Promise.resolve({}));
3473
- const testEmail = data.email || auth.user.email;
3474
- if (!testEmail) {
3475
- return Response.json({
3476
- success: false,
3477
- error: "No email address available for test send"
3478
- }, { status: 400 });
3479
- }
3480
- const broadcast = await req.payload.findByID({
3481
- collection: collectionSlug,
3482
- id,
3483
- user: auth.user
3484
- });
3485
- if (!broadcast) {
3486
- return Response.json({
3487
- success: false,
3488
- error: "Broadcast not found"
3489
- }, { status: 404 });
3490
- }
3491
- const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
3492
- wrapInTemplate: true,
3493
- preheader: broadcast.preheader,
3494
- customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
3495
- });
3496
- const emailService = req.payload.newsletterEmailService;
3497
- if (!emailService) {
3498
- return Response.json({
3499
- success: false,
3500
- error: "Email service is not configured"
3501
- }, { status: 500 });
3502
- }
3503
- const providerConfig = config.providers.default === "resend" ? config.providers.resend : config.providers.broadcast;
3504
- const fromEmail = providerConfig?.fromAddress || providerConfig?.fromEmail || "noreply@example.com";
3505
- const fromName = providerConfig?.fromName || "Newsletter";
3506
- const replyTo = broadcast.settings?.replyTo || providerConfig?.replyTo;
3507
- await emailService.send({
3508
- to: testEmail,
3509
- from: fromEmail,
3510
- fromName,
3511
- replyTo,
3512
- subject: `[TEST] ${broadcast.subject}`,
3513
- html: htmlContent,
3514
- trackOpens: false,
3515
- trackClicks: false
3516
- });
3517
- return Response.json({
3518
- success: true,
3519
- message: `Test email sent to ${testEmail}`
3520
- });
3521
- } catch (error) {
3522
- console.error("Failed to send test broadcast:", error);
3523
- return Response.json({
3524
- success: false,
3525
- error: "Failed to send test email"
3526
- }, { status: 500 });
3527
- }
3528
- }
3529
- };
3530
- };
3531
-
3532
- // src/endpoints/broadcasts/preview.ts
3533
- var createBroadcastPreviewEndpoint = (config, collectionSlug) => {
3534
- return {
3535
- path: `/${collectionSlug}/preview`,
3536
- method: "post",
3537
- handler: async (req) => {
3538
- try {
3539
- const data = await (req.json?.() || Promise.resolve({}));
3540
- const { content, preheader, subject } = data;
3541
- if (!content) {
3542
- return Response.json({
3543
- success: false,
3544
- error: "Content is required for preview"
3545
- }, { status: 400 });
3546
- }
3547
- const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
3548
- const htmlContent = await convertToEmailSafeHtml(content, {
3549
- wrapInTemplate: true,
3550
- preheader,
3551
- mediaUrl,
3552
- customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
3553
- });
3554
- return Response.json({
3555
- success: true,
3556
- preview: {
3557
- subject: subject || "Preview",
3558
- preheader: preheader || "",
3559
- html: htmlContent
3560
- }
3561
- });
3562
- } catch (error) {
3563
- console.error("Failed to generate email preview:", error);
3564
- return Response.json({
3565
- success: false,
3566
- error: "Failed to generate email preview"
3567
- }, { status: 500 });
3568
- }
3569
- }
3570
- };
3571
- };
3572
-
3573
- // src/endpoints/broadcasts/index.ts
3574
- var createBroadcastManagementEndpoints = (config) => {
3575
- if (!config.features?.newsletterManagement?.enabled) {
3576
- return [];
3757
+ }
3758
+ return "";
3577
3759
  }
3578
- const collectionSlug = config.features.newsletterManagement.collections?.broadcasts || "broadcasts";
3579
- return [
3580
- createSendBroadcastEndpoint(config, collectionSlug),
3581
- createScheduleBroadcastEndpoint(config, collectionSlug),
3582
- createTestBroadcastEndpoint(config, collectionSlug),
3583
- createBroadcastPreviewEndpoint(config, collectionSlug)
3584
- ];
3585
- };
3760
+ }
3761
+ function convertButtonBlock(fields) {
3762
+ const text = fields?.text || "Click here";
3763
+ const url = fields?.url || "#";
3764
+ const style = fields?.style || "primary";
3765
+ const styles2 = {
3766
+ primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
3767
+ secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
3768
+ outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
3769
+ };
3770
+ const buttonStyle = `${styles2[style] || styles2.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
3771
+ return `
3772
+ <div style="margin: 0 0 16px 0; text-align: center;">
3773
+ <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
3774
+ </div>
3775
+ `;
3776
+ }
3777
+ function convertDividerBlock(fields) {
3778
+ const style = fields?.style || "solid";
3779
+ const styles2 = {
3780
+ solid: "border-top: 1px solid #e5e7eb;",
3781
+ dashed: "border-top: 1px dashed #e5e7eb;",
3782
+ dotted: "border-top: 1px dotted #e5e7eb;"
3783
+ };
3784
+ return `<hr style="${styles2[style] || styles2.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
3785
+ }
3786
+ function getAlignment(format) {
3787
+ if (!format) return "left";
3788
+ if (format & 2) return "center";
3789
+ if (format & 3) return "right";
3790
+ if (format & 4) return "justify";
3791
+ return "left";
3792
+ }
3793
+ function escapeHtml(text) {
3794
+ const map = {
3795
+ "&": "&amp;",
3796
+ "<": "&lt;",
3797
+ ">": "&gt;",
3798
+ '"': "&quot;",
3799
+ "'": "&#039;"
3800
+ };
3801
+ return text.replace(/[&<>"']/g, (m) => map[m]);
3802
+ }
3803
+ function wrapInEmailTemplate(content, preheader) {
3804
+ return `<!DOCTYPE html>
3805
+ <html lang="en">
3806
+ <head>
3807
+ <meta charset="UTF-8">
3808
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3809
+ <title>Email</title>
3810
+ <!--[if mso]>
3811
+ <noscript>
3812
+ <xml>
3813
+ <o:OfficeDocumentSettings>
3814
+ <o:PixelsPerInch>96</o:PixelsPerInch>
3815
+ </o:OfficeDocumentSettings>
3816
+ </xml>
3817
+ </noscript>
3818
+ <![endif]-->
3819
+ </head>
3820
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; background-color: #f3f4f6;">
3821
+ ${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
3822
+ <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
3823
+ <tr>
3824
+ <td align="center" style="padding: 20px 0;">
3825
+ <table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
3826
+ <tr>
3827
+ <td style="padding: 40px 30px;">
3828
+ ${content}
3829
+ </td>
3830
+ </tr>
3831
+ </table>
3832
+ </td>
3833
+ </tr>
3834
+ </table>
3835
+ </body>
3836
+ </html>`;
3837
+ }
3586
3838
 
3587
- // src/endpoints/index.ts
3588
- function createNewsletterEndpoints(config) {
3589
- const endpoints = [
3590
- createSubscribeEndpoint(config),
3591
- createUnsubscribeEndpoint(config)
3592
- ];
3593
- if (config.auth?.enabled !== false) {
3594
- endpoints.push(
3595
- createVerifyMagicLinkEndpoint(config),
3596
- createPreferencesEndpoint(config),
3597
- createUpdatePreferencesEndpoint(config),
3598
- createSigninEndpoint(config),
3599
- createMeEndpoint(config),
3600
- createSignoutEndpoint(config)
3601
- );
3839
+ // src/utils/getBroadcastConfig.ts
3840
+ async function getBroadcastConfig(req, pluginConfig) {
3841
+ try {
3842
+ const settings = await req.payload.findGlobal({
3843
+ slug: pluginConfig.settingsSlug || "newsletter-settings",
3844
+ req
3845
+ });
3846
+ if (settings?.provider === "broadcast" && settings?.broadcastSettings) {
3847
+ return {
3848
+ apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || "",
3849
+ token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || "",
3850
+ fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || "",
3851
+ fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || "",
3852
+ replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo
3853
+ };
3854
+ }
3855
+ return pluginConfig.providers?.broadcast || null;
3856
+ } catch (error) {
3857
+ req.payload.logger.error("Failed to get broadcast config from settings:", error);
3858
+ return pluginConfig.providers?.broadcast || null;
3602
3859
  }
3603
- endpoints.push(...createBroadcastManagementEndpoints(config));
3604
- return endpoints;
3605
3860
  }
3606
3861
 
3607
- // src/fields/newsletterScheduling.ts
3608
- function createNewsletterSchedulingFields(config) {
3609
- const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
3610
- const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
3611
- const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
3612
- const fields = [
3613
- {
3614
- name: groupName,
3615
- type: "group",
3616
- label: "Newsletter Scheduling",
3617
- admin: {
3618
- condition: (data, { user }) => user?.collection === "users"
3619
- // Only show for admin users
3620
- },
3621
- fields: [
3622
- {
3623
- name: "scheduled",
3624
- type: "checkbox",
3625
- label: "Schedule for Newsletter",
3626
- defaultValue: false,
3627
- admin: {
3628
- description: "Schedule this content to be sent as a newsletter"
3629
- }
3630
- },
3631
- {
3632
- name: "scheduledDate",
3633
- type: "date",
3634
- label: "Send Date",
3635
- required: true,
3636
- admin: {
3637
- date: {
3638
- pickerAppearance: "dayAndTime"
3639
- },
3640
- condition: (data) => data?.[groupName]?.scheduled,
3641
- description: "When to send this newsletter"
3642
- }
3643
- },
3644
- {
3645
- name: "sentDate",
3646
- type: "date",
3647
- label: "Sent Date",
3648
- admin: {
3649
- readOnly: true,
3650
- condition: (data) => data?.[groupName]?.sendStatus === "sent",
3651
- description: "When this newsletter was sent"
3652
- }
3653
- },
3654
- {
3655
- name: "sendStatus",
3656
- type: "select",
3657
- label: "Status",
3658
- options: [
3659
- { label: "Draft", value: "draft" },
3660
- { label: "Scheduled", value: "scheduled" },
3661
- { label: "Sending", value: "sending" },
3662
- { label: "Sent", value: "sent" },
3663
- { label: "Failed", value: "failed" }
3664
- ],
3665
- defaultValue: "draft",
3666
- admin: {
3667
- readOnly: true,
3668
- description: "Current send status"
3669
- }
3670
- },
3671
- {
3672
- name: "emailSubject",
3673
- type: "text",
3674
- label: "Email Subject",
3675
- required: true,
3676
- admin: {
3677
- condition: (data) => data?.[groupName]?.scheduled,
3678
- description: "Subject line for the newsletter email"
3679
- }
3680
- },
3681
- {
3682
- name: "preheader",
3683
- type: "text",
3684
- label: "Email Preheader",
3685
- admin: {
3686
- condition: (data) => data?.[groupName]?.scheduled,
3687
- description: "Preview text that appears after the subject line"
3688
- }
3689
- },
3690
- {
3691
- name: "segments",
3692
- type: "select",
3693
- label: "Target Segments",
3694
- hasMany: true,
3695
- options: [
3696
- { label: "All Subscribers", value: "all" },
3697
- ...config.i18n?.locales?.map((locale) => ({
3698
- label: `${locale.toUpperCase()} Subscribers`,
3699
- value: locale
3700
- })) || []
3701
- ],
3702
- defaultValue: ["all"],
3703
- admin: {
3704
- condition: (data) => data?.[groupName]?.scheduled,
3705
- description: "Which subscriber segments to send to"
3706
- }
3707
- },
3708
- {
3709
- name: "testEmails",
3710
- type: "array",
3711
- label: "Test Email Recipients",
3712
- admin: {
3713
- condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
3714
- description: "Send test emails before scheduling"
3715
- },
3716
- fields: [
3717
- {
3718
- name: "email",
3719
- type: "email",
3720
- required: true
3721
- }
3722
- ]
3723
- }
3724
- ]
3725
- }
3726
- ];
3727
- if (createMarkdownField) {
3728
- fields.push(createMarkdownFieldInternal({
3729
- name: `${contentField}Markdown`,
3730
- richTextField: contentField,
3731
- label: "Email Content (Markdown)",
3732
- admin: {
3733
- position: "sidebar",
3734
- condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
3735
- description: "Markdown version for email rendering",
3736
- readOnly: true
3737
- }
3738
- }));
3862
+ // src/endpoints/broadcasts/send.ts
3863
+ init_types();
3864
+
3865
+ // src/utils/auth.ts
3866
+ async function getAuthenticatedUser(req) {
3867
+ try {
3868
+ const me = await req.payload.find({
3869
+ collection: "users",
3870
+ where: {
3871
+ id: {
3872
+ equals: "me"
3873
+ // Special value in Payload to get current user
3874
+ }
3875
+ },
3876
+ limit: 1,
3877
+ depth: 0
3878
+ });
3879
+ return me.docs[0] || null;
3880
+ } catch {
3881
+ return null;
3739
3882
  }
3740
- return fields;
3741
3883
  }
3742
- function createMarkdownFieldInternal(config) {
3884
+ async function requireAdmin(req, config) {
3885
+ const user = await getAuthenticatedUser(req);
3886
+ if (!user) {
3887
+ return {
3888
+ authorized: false,
3889
+ error: "Authentication required"
3890
+ };
3891
+ }
3892
+ if (!isAdmin(user, config)) {
3893
+ return {
3894
+ authorized: false,
3895
+ error: "Admin access required"
3896
+ };
3897
+ }
3743
3898
  return {
3744
- name: config.name,
3745
- type: "textarea",
3746
- label: config.label || "Markdown",
3747
- admin: {
3748
- ...config.admin,
3749
- description: config.admin?.description || "Auto-generated from rich text content"
3750
- },
3751
- hooks: {
3752
- afterRead: [
3753
- async ({ data }) => {
3754
- if (data?.[config.richTextField]) {
3755
- try {
3756
- const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
3757
- return convertLexicalToMarkdown({
3758
- data: data[config.richTextField]
3759
- });
3760
- } catch {
3761
- return "";
3762
- }
3763
- }
3764
- return "";
3765
- }
3766
- ],
3767
- beforeChange: [
3768
- () => {
3769
- return null;
3770
- }
3771
- ]
3772
- }
3899
+ authorized: true,
3900
+ user
3773
3901
  };
3774
3902
  }
3775
3903
 
3776
- // src/jobs/sync-unsubscribes.ts
3777
- var createUnsubscribeSyncJob = (pluginConfig) => {
3904
+ // src/endpoints/broadcasts/send.ts
3905
+ var createSendBroadcastEndpoint = (config, collectionSlug) => {
3778
3906
  return {
3779
- slug: "sync-unsubscribes",
3780
- label: "Sync Unsubscribes from Email Service",
3781
- handler: async ({ req }) => {
3782
- const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
3783
- const emailService = req.payload.newsletterEmailService;
3784
- if (!emailService) {
3785
- console.error("Email service not configured");
3786
- return {
3787
- output: {
3788
- syncedCount: 0
3789
- }
3790
- };
3791
- }
3792
- let syncedCount = 0;
3907
+ path: `/${collectionSlug}/:id/send`,
3908
+ method: "post",
3909
+ handler: async (req) => {
3793
3910
  try {
3794
- if (emailService.getProvider() === "broadcast") {
3795
- console.warn("Starting Broadcast unsubscribe sync...");
3796
- const broadcastConfig = pluginConfig.providers?.broadcast;
3797
- if (!broadcastConfig) {
3798
- throw new Error("Broadcast configuration not found");
3799
- }
3800
- const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
3801
- const token = broadcastConfig.token;
3802
- let page = 1;
3803
- let hasMore = true;
3804
- while (hasMore) {
3805
- const response = await fetch(
3806
- `${apiUrl}/api/v1/subscribers.json?page=${page}`,
3807
- {
3808
- headers: {
3809
- "Authorization": `Bearer ${token}`
3810
- }
3811
- }
3812
- );
3813
- if (!response.ok) {
3814
- throw new Error(`Broadcast API error: ${response.status}`);
3815
- }
3816
- const data = await response.json();
3817
- const broadcastSubscribers = data.subscribers || [];
3818
- for (const broadcastSub of broadcastSubscribers) {
3819
- const payloadSubscribers = await req.payload.find({
3820
- collection: subscribersSlug,
3821
- where: {
3822
- email: {
3823
- equals: broadcastSub.email
3824
- }
3825
- },
3826
- limit: 1
3827
- });
3828
- if (payloadSubscribers.docs.length > 0) {
3829
- const payloadSub = payloadSubscribers.docs[0];
3830
- const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
3831
- const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
3832
- if (broadcastUnsubscribed && !payloadUnsubscribed) {
3833
- await req.payload.update({
3834
- collection: subscribersSlug,
3835
- id: payloadSub.id,
3836
- data: {
3837
- subscriptionStatus: "unsubscribed",
3838
- unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
3839
- }
3840
- });
3841
- syncedCount++;
3842
- console.warn(`Unsubscribed: ${broadcastSub.email}`);
3843
- }
3844
- }
3845
- }
3846
- if (data.pagination && data.pagination.current < data.pagination.total_pages) {
3847
- page++;
3848
- } else {
3849
- hasMore = false;
3850
- }
3851
- }
3852
- console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
3853
- }
3854
- if (emailService.getProvider() === "resend") {
3855
- console.warn("Starting Resend unsubscribe sync...");
3856
- const resendConfig = pluginConfig.providers?.resend;
3857
- if (!resendConfig) {
3858
- throw new Error("Resend configuration not found");
3859
- }
3860
- console.warn("Resend polling implementation needed - webhooks recommended");
3911
+ const auth = await requireAdmin(req, config);
3912
+ if (!auth.authorized) {
3913
+ return Response.json({
3914
+ success: false,
3915
+ error: auth.error
3916
+ }, { status: 401 });
3861
3917
  }
3862
- if (pluginConfig.hooks?.afterUnsubscribeSync) {
3863
- await pluginConfig.hooks.afterUnsubscribeSync({
3864
- req,
3865
- syncedCount
3866
- });
3918
+ if (!config.features?.newsletterManagement?.enabled) {
3919
+ return Response.json({
3920
+ success: false,
3921
+ error: "Broadcast management is not enabled"
3922
+ }, { status: 400 });
3867
3923
  }
3868
- } catch (error) {
3869
- console.error("Unsubscribe sync error:", error);
3870
- throw error;
3871
- }
3872
- return {
3873
- output: {
3874
- syncedCount
3924
+ const url = new URL(req.url || "", `http://localhost`);
3925
+ const pathParts = url.pathname.split("/");
3926
+ const id = pathParts[pathParts.length - 2];
3927
+ if (!id) {
3928
+ return Response.json({
3929
+ success: false,
3930
+ error: "Broadcast ID is required"
3931
+ }, { status: 400 });
3875
3932
  }
3876
- };
3877
- }
3878
- };
3879
- };
3880
-
3881
- // src/collections/Broadcasts.ts
3882
- init_types();
3883
-
3884
- // src/fields/emailContent.ts
3885
- var import_richtext_lexical = require("@payloadcms/richtext-lexical");
3886
-
3887
- // src/utils/blockValidation.ts
3888
- var EMAIL_INCOMPATIBLE_TYPES = [
3889
- "chart",
3890
- "dataTable",
3891
- "interactive",
3892
- "streamable",
3893
- "video",
3894
- "iframe",
3895
- "form",
3896
- "carousel",
3897
- "tabs",
3898
- "accordion",
3899
- "map"
3900
- ];
3901
- var validateEmailBlocks = (blocks) => {
3902
- blocks.forEach((block) => {
3903
- if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
3904
- console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
3905
- }
3906
- const hasComplexFields = block.fields?.some((field) => {
3907
- const complexTypes = ["code", "json", "richText", "blocks", "array"];
3908
- return complexTypes.includes(field.type);
3909
- });
3910
- if (hasComplexFields) {
3911
- console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
3912
- }
3913
- });
3914
- };
3915
- var createEmailSafeBlocks = (customBlocks = []) => {
3916
- validateEmailBlocks(customBlocks);
3917
- const baseBlocks = [
3918
- {
3919
- slug: "button",
3920
- fields: [
3921
- {
3922
- name: "text",
3923
- type: "text",
3924
- label: "Button Text",
3925
- required: true
3926
- },
3927
- {
3928
- name: "url",
3929
- type: "text",
3930
- label: "Button URL",
3931
- required: true,
3932
- admin: {
3933
- description: "Enter the full URL (including https://)"
3934
- }
3935
- },
3936
- {
3937
- name: "style",
3938
- type: "select",
3939
- label: "Button Style",
3940
- defaultValue: "primary",
3941
- options: [
3942
- { label: "Primary", value: "primary" },
3943
- { label: "Secondary", value: "secondary" },
3944
- { label: "Outline", value: "outline" }
3945
- ]
3933
+ const data = await (req.json?.() || Promise.resolve({}));
3934
+ const broadcastDoc = await req.payload.findByID({
3935
+ collection: collectionSlug,
3936
+ id,
3937
+ user: auth.user
3938
+ });
3939
+ if (!broadcastDoc || !broadcastDoc.providerId) {
3940
+ return Response.json({
3941
+ success: false,
3942
+ error: "Broadcast not found or not synced with provider"
3943
+ }, { status: 404 });
3946
3944
  }
3947
- ],
3948
- interfaceName: "EmailButton",
3949
- labels: {
3950
- singular: "Button",
3951
- plural: "Buttons"
3952
- }
3953
- },
3954
- {
3955
- slug: "divider",
3956
- fields: [
3957
- {
3958
- name: "style",
3959
- type: "select",
3960
- label: "Divider Style",
3961
- defaultValue: "solid",
3962
- options: [
3963
- { label: "Solid", value: "solid" },
3964
- { label: "Dashed", value: "dashed" },
3965
- { label: "Dotted", value: "dotted" }
3966
- ]
3945
+ const providerConfig = await getBroadcastConfig(req, config);
3946
+ if (!providerConfig || !providerConfig.token) {
3947
+ return Response.json({
3948
+ success: false,
3949
+ error: "Broadcast provider not configured in Newsletter Settings or environment variables"
3950
+ }, { status: 500 });
3967
3951
  }
3968
- ],
3969
- interfaceName: "EmailDivider",
3970
- labels: {
3971
- singular: "Divider",
3972
- plural: "Dividers"
3952
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
3953
+ const provider = new BroadcastApiProvider2(providerConfig);
3954
+ const broadcast = await provider.send(broadcastDoc.providerId, data);
3955
+ await req.payload.update({
3956
+ collection: collectionSlug,
3957
+ id,
3958
+ data: {
3959
+ sendStatus: "sending" /* SENDING */,
3960
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
3961
+ },
3962
+ user: auth.user
3963
+ });
3964
+ return Response.json({
3965
+ success: true,
3966
+ message: "Broadcast sent successfully",
3967
+ broadcast
3968
+ });
3969
+ } catch (error) {
3970
+ console.error("Failed to send broadcast:", error);
3971
+ if (error instanceof NewsletterProviderError) {
3972
+ return Response.json({
3973
+ success: false,
3974
+ error: error.message,
3975
+ code: error.code
3976
+ }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
3977
+ }
3978
+ return Response.json({
3979
+ success: false,
3980
+ error: "Failed to send broadcast"
3981
+ }, { status: 500 });
3973
3982
  }
3974
3983
  }
3975
- ];
3976
- return [
3977
- ...baseBlocks,
3978
- ...customBlocks
3979
- ];
3984
+ };
3980
3985
  };
3981
3986
 
3982
- // src/fields/emailContent.ts
3983
- var createEmailSafeFeatures = (additionalBlocks) => {
3984
- const baseBlocks = [
3985
- {
3986
- slug: "button",
3987
- fields: [
3988
- {
3989
- name: "text",
3990
- type: "text",
3991
- label: "Button Text",
3992
- required: true
3993
- },
3994
- {
3995
- name: "url",
3996
- type: "text",
3997
- label: "Button URL",
3998
- required: true,
3999
- admin: {
4000
- description: "Enter the full URL (including https://)"
4001
- }
4002
- },
4003
- {
4004
- name: "style",
4005
- type: "select",
4006
- label: "Button Style",
4007
- defaultValue: "primary",
4008
- options: [
4009
- { label: "Primary", value: "primary" },
4010
- { label: "Secondary", value: "secondary" },
4011
- { label: "Outline", value: "outline" }
4012
- ]
3987
+ // src/endpoints/broadcasts/schedule.ts
3988
+ init_types();
3989
+ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
3990
+ return {
3991
+ path: `/${collectionSlug}/:id/schedule`,
3992
+ method: "post",
3993
+ handler: async (req) => {
3994
+ try {
3995
+ const auth = await requireAdmin(req, config);
3996
+ if (!auth.authorized) {
3997
+ return Response.json({
3998
+ success: false,
3999
+ error: auth.error
4000
+ }, { status: 401 });
4013
4001
  }
4014
- ],
4015
- interfaceName: "EmailButton",
4016
- labels: {
4017
- singular: "Button",
4018
- plural: "Buttons"
4019
- }
4020
- },
4021
- {
4022
- slug: "divider",
4023
- fields: [
4024
- {
4025
- name: "style",
4026
- type: "select",
4027
- label: "Divider Style",
4028
- defaultValue: "solid",
4029
- options: [
4030
- { label: "Solid", value: "solid" },
4031
- { label: "Dashed", value: "dashed" },
4032
- { label: "Dotted", value: "dotted" }
4033
- ]
4002
+ if (!config.features?.newsletterManagement?.enabled) {
4003
+ return Response.json({
4004
+ success: false,
4005
+ error: "Broadcast management is not enabled"
4006
+ }, { status: 400 });
4034
4007
  }
4035
- ],
4036
- interfaceName: "EmailDivider",
4037
- labels: {
4038
- singular: "Divider",
4039
- plural: "Dividers"
4040
- }
4041
- }
4042
- ];
4043
- const allBlocks = [
4044
- ...baseBlocks,
4045
- ...additionalBlocks || []
4046
- ];
4047
- return [
4048
- // Toolbars
4049
- (0, import_richtext_lexical.FixedToolbarFeature)(),
4050
- // Fixed toolbar at the top
4051
- (0, import_richtext_lexical.InlineToolbarFeature)(),
4052
- // Floating toolbar when text is selected
4053
- // Basic text formatting
4054
- (0, import_richtext_lexical.BoldFeature)(),
4055
- (0, import_richtext_lexical.ItalicFeature)(),
4056
- (0, import_richtext_lexical.UnderlineFeature)(),
4057
- (0, import_richtext_lexical.StrikethroughFeature)(),
4058
- // Links with enhanced configuration
4059
- (0, import_richtext_lexical.LinkFeature)({
4060
- fields: [
4061
- {
4062
- name: "url",
4063
- type: "text",
4064
- required: true,
4065
- admin: {
4066
- description: "Enter the full URL (including https://)"
4067
- }
4068
- },
4069
- {
4070
- name: "newTab",
4071
- type: "checkbox",
4072
- label: "Open in new tab",
4073
- defaultValue: false
4008
+ const url = new URL(req.url || "", `http://localhost`);
4009
+ const pathParts = url.pathname.split("/");
4010
+ const id = pathParts[pathParts.length - 2];
4011
+ if (!id) {
4012
+ return Response.json({
4013
+ success: false,
4014
+ error: "Broadcast ID is required"
4015
+ }, { status: 400 });
4074
4016
  }
4075
- ]
4076
- }),
4077
- // Lists
4078
- (0, import_richtext_lexical.OrderedListFeature)(),
4079
- (0, import_richtext_lexical.UnorderedListFeature)(),
4080
- // Headings - limited to h1, h2, h3 for email compatibility
4081
- (0, import_richtext_lexical.HeadingFeature)({
4082
- enabledHeadingSizes: ["h1", "h2", "h3"]
4083
- }),
4084
- // Basic paragraph and alignment
4085
- (0, import_richtext_lexical.ParagraphFeature)(),
4086
- (0, import_richtext_lexical.AlignFeature)(),
4087
- // Blockquotes
4088
- (0, import_richtext_lexical.BlockquoteFeature)(),
4089
- // Upload feature for images
4090
- (0, import_richtext_lexical.UploadFeature)({
4091
- collections: {
4092
- media: {
4093
- fields: [
4094
- {
4095
- name: "caption",
4096
- type: "text",
4097
- admin: {
4098
- description: "Optional caption for the image"
4099
- }
4100
- },
4101
- {
4102
- name: "altText",
4103
- type: "text",
4104
- label: "Alt Text",
4105
- required: true,
4106
- admin: {
4107
- description: "Alternative text for accessibility and when image cannot be displayed"
4108
- }
4109
- }
4110
- ]
4017
+ const data = await (req.json?.() || Promise.resolve({}));
4018
+ const { scheduledAt } = data;
4019
+ if (!scheduledAt) {
4020
+ return Response.json({
4021
+ success: false,
4022
+ error: "scheduledAt is required"
4023
+ }, { status: 400 });
4024
+ }
4025
+ const scheduledDate = new Date(scheduledAt);
4026
+ if (isNaN(scheduledDate.getTime())) {
4027
+ return Response.json({
4028
+ success: false,
4029
+ error: "Invalid scheduledAt date"
4030
+ }, { status: 400 });
4031
+ }
4032
+ if (scheduledDate <= /* @__PURE__ */ new Date()) {
4033
+ return Response.json({
4034
+ success: false,
4035
+ error: "scheduledAt must be in the future"
4036
+ }, { status: 400 });
4037
+ }
4038
+ const broadcastDoc = await req.payload.findByID({
4039
+ collection: collectionSlug,
4040
+ id,
4041
+ user: auth.user
4042
+ });
4043
+ if (!broadcastDoc || !broadcastDoc.providerId) {
4044
+ return Response.json({
4045
+ success: false,
4046
+ error: "Broadcast not found or not synced with provider"
4047
+ }, { status: 404 });
4111
4048
  }
4112
- }
4113
- }),
4114
- // Custom blocks for email-specific content
4115
- (0, import_richtext_lexical.BlocksFeature)({
4116
- blocks: allBlocks
4117
- })
4118
- ];
4119
- };
4120
- var createEmailLexicalEditor = (customBlocks = []) => {
4121
- const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
4122
- return (0, import_richtext_lexical.lexicalEditor)({
4123
- features: [
4124
- // Toolbars
4125
- (0, import_richtext_lexical.FixedToolbarFeature)(),
4126
- (0, import_richtext_lexical.InlineToolbarFeature)(),
4127
- // Basic text formatting
4128
- (0, import_richtext_lexical.BoldFeature)(),
4129
- (0, import_richtext_lexical.ItalicFeature)(),
4130
- (0, import_richtext_lexical.UnderlineFeature)(),
4131
- (0, import_richtext_lexical.StrikethroughFeature)(),
4132
- // Links with enhanced configuration
4133
- (0, import_richtext_lexical.LinkFeature)({
4134
- fields: [
4135
- {
4136
- name: "url",
4137
- type: "text",
4138
- required: true,
4139
- admin: {
4140
- description: "Enter the full URL (including https://)"
4141
- }
4049
+ const providerConfig = config.providers?.broadcast;
4050
+ if (!providerConfig) {
4051
+ return Response.json({
4052
+ success: false,
4053
+ error: "Broadcast provider not configured"
4054
+ }, { status: 500 });
4055
+ }
4056
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
4057
+ const provider = new BroadcastApiProvider2(providerConfig);
4058
+ const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
4059
+ await req.payload.update({
4060
+ collection: collectionSlug,
4061
+ id,
4062
+ data: {
4063
+ sendStatus: "scheduled" /* SCHEDULED */,
4064
+ scheduledAt: scheduledDate.toISOString()
4142
4065
  },
4143
- {
4144
- name: "newTab",
4145
- type: "checkbox",
4146
- label: "Open in new tab",
4147
- defaultValue: false
4148
- }
4149
- ]
4150
- }),
4151
- // Lists
4152
- (0, import_richtext_lexical.OrderedListFeature)(),
4153
- (0, import_richtext_lexical.UnorderedListFeature)(),
4154
- // Headings - limited to h1, h2, h3 for email compatibility
4155
- (0, import_richtext_lexical.HeadingFeature)({
4156
- enabledHeadingSizes: ["h1", "h2", "h3"]
4157
- }),
4158
- // Basic paragraph and alignment
4159
- (0, import_richtext_lexical.ParagraphFeature)(),
4160
- (0, import_richtext_lexical.AlignFeature)(),
4161
- // Blockquotes
4162
- (0, import_richtext_lexical.BlockquoteFeature)(),
4163
- // Upload feature for images
4164
- (0, import_richtext_lexical.UploadFeature)({
4165
- collections: {
4166
- media: {
4167
- fields: [
4168
- {
4169
- name: "caption",
4170
- type: "text",
4171
- admin: {
4172
- description: "Optional caption for the image"
4173
- }
4174
- },
4175
- {
4176
- name: "altText",
4177
- type: "text",
4178
- label: "Alt Text",
4179
- required: true,
4180
- admin: {
4181
- description: "Alternative text for accessibility and when image cannot be displayed"
4182
- }
4183
- }
4184
- ]
4185
- }
4066
+ user: auth.user
4067
+ });
4068
+ return Response.json({
4069
+ success: true,
4070
+ message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
4071
+ broadcast
4072
+ });
4073
+ } catch (error) {
4074
+ console.error("Failed to schedule broadcast:", error);
4075
+ if (error instanceof NewsletterProviderError) {
4076
+ return Response.json({
4077
+ success: false,
4078
+ error: error.message,
4079
+ code: error.code
4080
+ }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
4186
4081
  }
4187
- }),
4188
- // Email-safe blocks (processed server-side)
4189
- (0, import_richtext_lexical.BlocksFeature)({
4190
- blocks: emailSafeBlocks
4191
- })
4192
- ]
4193
- });
4082
+ return Response.json({
4083
+ success: false,
4084
+ error: "Failed to schedule broadcast"
4085
+ }, { status: 500 });
4086
+ }
4087
+ }
4088
+ };
4194
4089
  };
4195
- var emailSafeFeatures = createEmailSafeFeatures();
4196
- var createEmailContentField = (overrides) => {
4197
- const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
4090
+
4091
+ // src/endpoints/broadcasts/test.ts
4092
+ var createTestBroadcastEndpoint = (config, collectionSlug) => {
4198
4093
  return {
4199
- name: "content",
4200
- type: "richText",
4201
- required: true,
4202
- editor,
4203
- admin: {
4204
- description: "Email content with limited formatting for compatibility",
4205
- ...overrides?.admin
4206
- },
4207
- ...overrides
4094
+ path: `/${collectionSlug}/:id/test`,
4095
+ method: "post",
4096
+ handler: async (req) => {
4097
+ try {
4098
+ const auth = await requireAdmin(req, config);
4099
+ if (!auth.authorized) {
4100
+ return Response.json({
4101
+ success: false,
4102
+ error: auth.error
4103
+ }, { status: 401 });
4104
+ }
4105
+ const url = new URL(req.url || "", `http://localhost`);
4106
+ const pathParts = url.pathname.split("/");
4107
+ const id = pathParts[pathParts.length - 2];
4108
+ if (!id) {
4109
+ return Response.json({
4110
+ success: false,
4111
+ error: "Broadcast ID is required"
4112
+ }, { status: 400 });
4113
+ }
4114
+ const data = await (req.json?.() || Promise.resolve({}));
4115
+ const testEmail = data.email || auth.user.email;
4116
+ if (!testEmail) {
4117
+ return Response.json({
4118
+ success: false,
4119
+ error: "No email address available for test send"
4120
+ }, { status: 400 });
4121
+ }
4122
+ const broadcast = await req.payload.findByID({
4123
+ collection: collectionSlug,
4124
+ id,
4125
+ user: auth.user
4126
+ });
4127
+ if (!broadcast) {
4128
+ return Response.json({
4129
+ success: false,
4130
+ error: "Broadcast not found"
4131
+ }, { status: 404 });
4132
+ }
4133
+ const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
4134
+ wrapInTemplate: true,
4135
+ preheader: broadcast.preheader,
4136
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
4137
+ });
4138
+ const emailService = req.payload.newsletterEmailService;
4139
+ if (!emailService) {
4140
+ return Response.json({
4141
+ success: false,
4142
+ error: "Email service is not configured"
4143
+ }, { status: 500 });
4144
+ }
4145
+ const providerConfig = config.providers.default === "resend" ? config.providers.resend : config.providers.broadcast;
4146
+ const fromEmail = providerConfig?.fromAddress || providerConfig?.fromEmail || "noreply@example.com";
4147
+ const fromName = providerConfig?.fromName || "Newsletter";
4148
+ const replyTo = broadcast.settings?.replyTo || providerConfig?.replyTo;
4149
+ await emailService.send({
4150
+ to: testEmail,
4151
+ from: fromEmail,
4152
+ fromName,
4153
+ replyTo,
4154
+ subject: `[TEST] ${broadcast.subject}`,
4155
+ html: htmlContent,
4156
+ trackOpens: false,
4157
+ trackClicks: false
4158
+ });
4159
+ return Response.json({
4160
+ success: true,
4161
+ message: `Test email sent to ${testEmail}`
4162
+ });
4163
+ } catch (error) {
4164
+ console.error("Failed to send test broadcast:", error);
4165
+ return Response.json({
4166
+ success: false,
4167
+ error: "Failed to send test email"
4168
+ }, { status: 500 });
4169
+ }
4170
+ }
4208
4171
  };
4209
4172
  };
4210
4173
 
4211
- // src/fields/broadcastInlinePreview.ts
4212
- var createBroadcastInlinePreviewField = () => {
4174
+ // src/endpoints/broadcasts/preview.ts
4175
+ var createBroadcastPreviewEndpoint = (config, collectionSlug) => {
4213
4176
  return {
4214
- name: "broadcastInlinePreview",
4215
- type: "ui",
4216
- admin: {
4217
- components: {
4218
- Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
4177
+ path: `/${collectionSlug}/preview`,
4178
+ method: "post",
4179
+ handler: async (req) => {
4180
+ try {
4181
+ const data = await (req.json?.() || Promise.resolve({}));
4182
+ const { content, preheader, subject } = data;
4183
+ if (!content) {
4184
+ return Response.json({
4185
+ success: false,
4186
+ error: "Content is required for preview"
4187
+ }, { status: 400 });
4188
+ }
4189
+ const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
4190
+ const htmlContent = await convertToEmailSafeHtml(content, {
4191
+ wrapInTemplate: true,
4192
+ preheader,
4193
+ mediaUrl,
4194
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
4195
+ });
4196
+ return Response.json({
4197
+ success: true,
4198
+ preview: {
4199
+ subject: subject || "Preview",
4200
+ preheader: preheader || "",
4201
+ html: htmlContent
4202
+ }
4203
+ });
4204
+ } catch (error) {
4205
+ console.error("Failed to generate email preview:", error);
4206
+ return Response.json({
4207
+ success: false,
4208
+ error: "Failed to generate email preview"
4209
+ }, { status: 500 });
4219
4210
  }
4220
4211
  }
4221
4212
  };
@@ -4225,8 +4216,9 @@ var createBroadcastInlinePreviewField = () => {
4225
4216
  var createBroadcastsCollection = (pluginConfig) => {
4226
4217
  const hasProviders = !!(pluginConfig.providers?.broadcast || pluginConfig.providers?.resend);
4227
4218
  const customizations = pluginConfig.customizations?.broadcasts;
4219
+ const collectionSlug = "broadcasts";
4228
4220
  return {
4229
- slug: "broadcasts",
4221
+ slug: collectionSlug,
4230
4222
  access: {
4231
4223
  read: () => true,
4232
4224
  // Public read access
@@ -4255,6 +4247,12 @@ var createBroadcastsCollection = (pluginConfig) => {
4255
4247
  description: "Individual email campaigns sent to subscribers",
4256
4248
  defaultColumns: ["subject", "_status", "sendStatus", "sentAt", "recipientCount"]
4257
4249
  },
4250
+ endpoints: [
4251
+ createSendBroadcastEndpoint(pluginConfig, collectionSlug),
4252
+ createScheduleBroadcastEndpoint(pluginConfig, collectionSlug),
4253
+ createTestBroadcastEndpoint(pluginConfig, collectionSlug),
4254
+ createBroadcastPreviewEndpoint(pluginConfig, collectionSlug)
4255
+ ],
4258
4256
  fields: [
4259
4257
  {
4260
4258
  name: "subject",