payload-plugin-newsletter 0.20.1 → 0.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/dist/admin.js +104 -0
- package/dist/broadcast-VMCYSZRY.js +6 -0
- package/dist/chunk-XVMYJQRQ.js +490 -0
- package/dist/client.d.ts +131 -15
- package/dist/client.js +1 -1
- package/dist/server.d.ts +735 -0
- package/dist/{index.js → server.js} +30 -654
- package/package.json +19 -28
- package/dist/client.cjs +0 -891
- package/dist/client.cjs.map +0 -1
- package/dist/client.d.cts +0 -53
- package/dist/client.js.map +0 -1
- package/dist/components.cjs +0 -2460
- package/dist/components.cjs.map +0 -1
- package/dist/components.d.cts +0 -66
- package/dist/components.d.ts +0 -66
- package/dist/components.js +0 -2418
- package/dist/components.js.map +0 -1
- package/dist/index.cjs +0 -5545
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -90
- package/dist/index.d.ts +0 -90
- package/dist/index.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
+
## [0.20.3] - 2025-07-31
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- **CRITICAL: Admin Bundle Server Dependencies** - Eliminated all Node.js server dependencies from admin bundle
|
|
5
|
+
- Reorganized source code into strict server/, client/, admin/, shared/ separation
|
|
6
|
+
- Created pure React admin components that don't depend on Node.js built-ins
|
|
7
|
+
- Updated build system with strict external dependencies for admin bundle
|
|
8
|
+
- Added bundle validation script to prevent server dependencies in browser bundles
|
|
9
|
+
- Fixed Next.js App Router compatibility issues with worker_threads, node:assert, and pino dependencies
|
|
10
|
+
|
|
11
|
+
### Breaking Changes (None)
|
|
12
|
+
- Import paths remain unchanged - full backward compatibility maintained
|
|
13
|
+
- All existing functionality preserved with improved bundle separation
|
|
14
|
+
|
|
15
|
+
### Technical Changes
|
|
16
|
+
- Reorganized source structure: server/, client/, admin/, shared/ directories
|
|
17
|
+
- Created browser-only admin components without server dependencies
|
|
18
|
+
- Updated tsup config to strictly external Node.js modules in admin bundle
|
|
19
|
+
- Added automated bundle validation in build process
|
|
20
|
+
- Reduced admin bundle size by eliminating server code
|
|
21
|
+
|
|
22
|
+
### Validation
|
|
23
|
+
- ✅ Admin bundle contains no worker_threads, node:assert, pino dependencies
|
|
24
|
+
- ✅ Client bundle contains no server-side modules
|
|
25
|
+
- ✅ Server bundle maintains full server functionality
|
|
26
|
+
- ✅ All existing APIs and components work unchanged
|
|
27
|
+
|
|
28
|
+
## [0.20.2] - 2025-07-31
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **CRITICAL: Next.js App Router createContext Error** - Complete architectural fix for Next.js App Router compatibility
|
|
32
|
+
- Separated server and client entry points to prevent React context issues during server-side initialization
|
|
33
|
+
- Created dedicated `server.ts`, `client.ts`, and `admin.ts` entry points
|
|
34
|
+
- Fixed server-side React context imports that caused `createContext is not a function` errors
|
|
35
|
+
- Updated package.json exports to properly route to server-safe and client-safe bundles
|
|
36
|
+
- Implemented server-safe plugin configuration storage using simple object store
|
|
37
|
+
- Updated build system to generate separate bundles with proper "use client" directives
|
|
38
|
+
|
|
39
|
+
### Breaking Changes (Minor)
|
|
40
|
+
- **Import Path Changes**:
|
|
41
|
+
- Admin components: `import { BroadcastInlinePreview } from 'payload-plugin-newsletter/admin'` (was `/components`)
|
|
42
|
+
- Client components: `import { NewsletterForm } from 'payload-plugin-newsletter/client'`
|
|
43
|
+
- Server exports: `import { newsletterPlugin } from 'payload-plugin-newsletter'` (unchanged)
|
|
44
|
+
- **Context API Changes**:
|
|
45
|
+
- Server-side config access through `getPluginConfig()` instead of React context
|
|
46
|
+
- Client-side React context requires explicit `PluginConfigProvider` setup
|
|
47
|
+
|
|
48
|
+
### Technical Changes
|
|
49
|
+
- Added separate entry points: `src/server.ts`, `src/client.ts`, `src/admin.ts`
|
|
50
|
+
- Created `ServerContext.ts` for server-safe configuration storage
|
|
51
|
+
- Created `ClientContext.tsx` for client-side React context
|
|
52
|
+
- Updated tsup config to build multiple entry points with proper directives
|
|
53
|
+
- Fixed plugin initialization to use server-safe config storage
|
|
54
|
+
- Updated package.json exports to route to appropriate bundles
|
|
55
|
+
|
|
1
56
|
## [0.20.1] - 2025-07-31
|
|
2
57
|
|
|
3
58
|
### Fixed
|
package/dist/admin.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/admin/components/BroadcastInlinePreview.tsx
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
var BroadcastInlinePreview = ({
|
|
7
|
+
field: _field,
|
|
8
|
+
data: _data,
|
|
9
|
+
..._props
|
|
10
|
+
}) => {
|
|
11
|
+
return /* @__PURE__ */ jsx("div", { className: "broadcast-preview", children: /* @__PURE__ */ jsxs("div", { style: { padding: "1rem", border: "1px solid #e0e0e0", borderRadius: "4px" }, children: [
|
|
12
|
+
/* @__PURE__ */ jsx("h3", { children: "Email Preview" }),
|
|
13
|
+
/* @__PURE__ */ jsx("p", { children: "This is a simplified preview component for the admin bundle." }),
|
|
14
|
+
/* @__PURE__ */ jsx("p", { children: "Full preview functionality will be available in the complete admin interface." })
|
|
15
|
+
] }) });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/admin/components/StatusBadge.tsx
|
|
19
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
20
|
+
var StatusBadge = (props) => {
|
|
21
|
+
const status = props.cellData || "draft";
|
|
22
|
+
const getStatusColor = (status2) => {
|
|
23
|
+
switch (status2) {
|
|
24
|
+
case "sent":
|
|
25
|
+
return "#22c55e";
|
|
26
|
+
case "scheduled":
|
|
27
|
+
return "#3b82f6";
|
|
28
|
+
case "draft":
|
|
29
|
+
return "#6b7280";
|
|
30
|
+
case "failed":
|
|
31
|
+
return "#ef4444";
|
|
32
|
+
default:
|
|
33
|
+
return "#6b7280";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const getStatusLabel = (status2) => {
|
|
37
|
+
switch (status2) {
|
|
38
|
+
case "sent":
|
|
39
|
+
return "Sent";
|
|
40
|
+
case "scheduled":
|
|
41
|
+
return "Scheduled";
|
|
42
|
+
case "draft":
|
|
43
|
+
return "Draft";
|
|
44
|
+
case "failed":
|
|
45
|
+
return "Failed";
|
|
46
|
+
default:
|
|
47
|
+
return status2;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return /* @__PURE__ */ jsx2(
|
|
51
|
+
"span",
|
|
52
|
+
{
|
|
53
|
+
style: {
|
|
54
|
+
display: "inline-block",
|
|
55
|
+
padding: "4px 8px",
|
|
56
|
+
borderRadius: "12px",
|
|
57
|
+
fontSize: "12px",
|
|
58
|
+
fontWeight: "500",
|
|
59
|
+
color: "#fff",
|
|
60
|
+
backgroundColor: getStatusColor(status),
|
|
61
|
+
textTransform: "capitalize"
|
|
62
|
+
},
|
|
63
|
+
children: getStatusLabel(status)
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/admin/components/EmailPreview.tsx
|
|
69
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
70
|
+
var EmailPreview = ({
|
|
71
|
+
content,
|
|
72
|
+
subject,
|
|
73
|
+
preheader
|
|
74
|
+
}) => {
|
|
75
|
+
return /* @__PURE__ */ jsxs2("div", { className: "email-preview", style: { padding: "1rem" }, children: [
|
|
76
|
+
/* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem" }, children: [
|
|
77
|
+
/* @__PURE__ */ jsx3("strong", { children: "Subject:" }),
|
|
78
|
+
" ",
|
|
79
|
+
subject || "No subject"
|
|
80
|
+
] }),
|
|
81
|
+
preheader && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem", color: "#666" }, children: [
|
|
82
|
+
/* @__PURE__ */ jsx3("strong", { children: "Preheader:" }),
|
|
83
|
+
" ",
|
|
84
|
+
preheader
|
|
85
|
+
] }),
|
|
86
|
+
/* @__PURE__ */ jsxs2("div", { style: {
|
|
87
|
+
border: "1px solid #e0e0e0",
|
|
88
|
+
borderRadius: "4px",
|
|
89
|
+
padding: "1rem",
|
|
90
|
+
backgroundColor: "#f9f9f9"
|
|
91
|
+
}, children: [
|
|
92
|
+
/* @__PURE__ */ jsx3("div", { children: "Email content will be rendered here" }),
|
|
93
|
+
content && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "1rem", fontSize: "14px", color: "#666" }, children: [
|
|
94
|
+
"Content type: ",
|
|
95
|
+
typeof content
|
|
96
|
+
] })
|
|
97
|
+
] })
|
|
98
|
+
] });
|
|
99
|
+
};
|
|
100
|
+
export {
|
|
101
|
+
BroadcastInlinePreview,
|
|
102
|
+
EmailPreview,
|
|
103
|
+
StatusBadge
|
|
104
|
+
};
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// src/types/newsletter.ts
|
|
2
|
+
var NewsletterProviderError = class extends Error {
|
|
3
|
+
constructor(message, code, provider, details) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.provider = provider;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = "NewsletterProviderError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/types/broadcast.ts
|
|
13
|
+
var BroadcastProviderError = class extends Error {
|
|
14
|
+
constructor(message, code, provider, details) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.provider = provider;
|
|
18
|
+
this.details = details;
|
|
19
|
+
this.name = "BroadcastProviderError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/types/providers.ts
|
|
24
|
+
var BaseBroadcastProvider = class {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Schedule a broadcast - default implementation throws not supported
|
|
30
|
+
*/
|
|
31
|
+
async schedule(_id, _scheduledAt) {
|
|
32
|
+
const capabilities = this.getCapabilities();
|
|
33
|
+
if (!capabilities.supportsScheduling) {
|
|
34
|
+
throw new BroadcastProviderError(
|
|
35
|
+
"Scheduling is not supported by this provider",
|
|
36
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
37
|
+
this.name
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
throw new Error("Method not implemented");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Cancel scheduled broadcast - default implementation throws not supported
|
|
44
|
+
*/
|
|
45
|
+
async cancelSchedule(_id) {
|
|
46
|
+
const capabilities = this.getCapabilities();
|
|
47
|
+
if (!capabilities.supportsScheduling) {
|
|
48
|
+
throw new BroadcastProviderError(
|
|
49
|
+
"Scheduling is not supported by this provider",
|
|
50
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
51
|
+
this.name
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Method not implemented");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get analytics - default implementation returns zeros
|
|
58
|
+
*/
|
|
59
|
+
async getAnalytics(_id) {
|
|
60
|
+
const capabilities = this.getCapabilities();
|
|
61
|
+
if (!capabilities.supportsAnalytics) {
|
|
62
|
+
throw new BroadcastProviderError(
|
|
63
|
+
"Analytics are not supported by this provider",
|
|
64
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
65
|
+
this.name
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
sent: 0,
|
|
70
|
+
delivered: 0,
|
|
71
|
+
opened: 0,
|
|
72
|
+
clicked: 0,
|
|
73
|
+
bounced: 0,
|
|
74
|
+
complained: 0,
|
|
75
|
+
unsubscribed: 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Helper method to validate required fields
|
|
80
|
+
*/
|
|
81
|
+
validateRequiredFields(data, fields) {
|
|
82
|
+
const missing = fields.filter((field) => !data[field]);
|
|
83
|
+
if (missing.length > 0) {
|
|
84
|
+
throw new BroadcastProviderError(
|
|
85
|
+
`Missing required fields: ${missing.join(", ")}`,
|
|
86
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
87
|
+
this.name
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Helper method to check if a status transition is allowed
|
|
93
|
+
*/
|
|
94
|
+
canEditInStatus(status) {
|
|
95
|
+
const capabilities = this.getCapabilities();
|
|
96
|
+
return capabilities.editableStatuses.includes(status);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Helper to build pagination response
|
|
100
|
+
*/
|
|
101
|
+
buildListResponse(items, total, options = {}) {
|
|
102
|
+
const limit = options.limit || 20;
|
|
103
|
+
const offset = options.offset || 0;
|
|
104
|
+
return {
|
|
105
|
+
items,
|
|
106
|
+
total,
|
|
107
|
+
limit,
|
|
108
|
+
offset,
|
|
109
|
+
hasMore: offset + items.length < total
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/providers/broadcast/broadcast.ts
|
|
115
|
+
var BroadcastApiProvider = class extends BaseBroadcastProvider {
|
|
116
|
+
constructor(config) {
|
|
117
|
+
super(config);
|
|
118
|
+
this.name = "broadcast";
|
|
119
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
120
|
+
this.token = config.token;
|
|
121
|
+
if (!this.token) {
|
|
122
|
+
throw new BroadcastProviderError(
|
|
123
|
+
"Broadcast API token is required",
|
|
124
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
125
|
+
this.name
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Broadcast Management Methods
|
|
130
|
+
async list(options) {
|
|
131
|
+
try {
|
|
132
|
+
const params = new URLSearchParams();
|
|
133
|
+
if (options?.limit) params.append("limit", options.limit.toString());
|
|
134
|
+
if (options?.offset) params.append("offset", options.offset.toString());
|
|
135
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts?${params}`, {
|
|
136
|
+
method: "GET",
|
|
137
|
+
headers: {
|
|
138
|
+
"Authorization": `Bearer ${this.token}`,
|
|
139
|
+
"Content-Type": "application/json"
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const error = await response.text();
|
|
144
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
145
|
+
}
|
|
146
|
+
const data = await response.json();
|
|
147
|
+
const broadcasts = data.data.map((broadcast) => this.transformBroadcastFromApi(broadcast));
|
|
148
|
+
return this.buildListResponse(broadcasts, data.total, options);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new BroadcastProviderError(
|
|
151
|
+
`Failed to list broadcasts: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
152
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
153
|
+
this.name,
|
|
154
|
+
error
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async get(id) {
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
161
|
+
method: "GET",
|
|
162
|
+
headers: {
|
|
163
|
+
"Authorization": `Bearer ${this.token}`,
|
|
164
|
+
"Content-Type": "application/json"
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
if (response.status === 404) {
|
|
169
|
+
throw new BroadcastProviderError(
|
|
170
|
+
`Broadcast not found: ${id}`,
|
|
171
|
+
"NOT_FOUND" /* NOT_FOUND */,
|
|
172
|
+
this.name
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const error = await response.text();
|
|
176
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
177
|
+
}
|
|
178
|
+
const broadcast = await response.json();
|
|
179
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
182
|
+
throw new BroadcastProviderError(
|
|
183
|
+
`Failed to get broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
184
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
185
|
+
this.name,
|
|
186
|
+
error
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async create(data) {
|
|
191
|
+
try {
|
|
192
|
+
this.validateRequiredFields(data, ["name", "subject", "content"]);
|
|
193
|
+
const requestBody = {
|
|
194
|
+
broadcast: {
|
|
195
|
+
name: data.name,
|
|
196
|
+
subject: data.subject,
|
|
197
|
+
preheader: data.preheader,
|
|
198
|
+
body: data.content,
|
|
199
|
+
html_body: true,
|
|
200
|
+
track_opens: data.trackOpens ?? true,
|
|
201
|
+
track_clicks: data.trackClicks ?? true,
|
|
202
|
+
reply_to: data.replyTo,
|
|
203
|
+
segment_ids: data.audienceIds
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
console.log("[BroadcastApiProvider] Creating broadcast:", {
|
|
207
|
+
url: `${this.apiUrl}/api/v1/broadcasts`,
|
|
208
|
+
method: "POST",
|
|
209
|
+
hasToken: !!this.token,
|
|
210
|
+
tokenLength: this.token?.length,
|
|
211
|
+
body: JSON.stringify(requestBody, null, 2)
|
|
212
|
+
});
|
|
213
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"Authorization": `Bearer ${this.token}`,
|
|
217
|
+
"Content-Type": "application/json"
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify(requestBody)
|
|
220
|
+
});
|
|
221
|
+
console.log("[BroadcastApiProvider] Response status:", response.status);
|
|
222
|
+
console.log("[BroadcastApiProvider] Response headers:", Object.fromEntries(response.headers.entries()));
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
const errorText = await response.text();
|
|
225
|
+
console.error("[BroadcastApiProvider] Error response body:", errorText);
|
|
226
|
+
let errorDetails;
|
|
227
|
+
try {
|
|
228
|
+
errorDetails = JSON.parse(errorText);
|
|
229
|
+
console.error("[BroadcastApiProvider] Parsed error:", errorDetails);
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
throw new Error(`Broadcast API error: ${response.status} - ${errorText}`);
|
|
233
|
+
}
|
|
234
|
+
const result = await response.json();
|
|
235
|
+
return this.get(result.id.toString());
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
238
|
+
throw new BroadcastProviderError(
|
|
239
|
+
`Failed to create broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
240
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
241
|
+
this.name,
|
|
242
|
+
error
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async update(id, data) {
|
|
247
|
+
try {
|
|
248
|
+
const existing = await this.get(id);
|
|
249
|
+
if (!this.canEditInStatus(existing.sendStatus)) {
|
|
250
|
+
throw new BroadcastProviderError(
|
|
251
|
+
`Cannot update broadcast in status: ${existing.sendStatus}`,
|
|
252
|
+
"INVALID_STATUS" /* INVALID_STATUS */,
|
|
253
|
+
this.name
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
257
|
+
method: "PATCH",
|
|
258
|
+
headers: {
|
|
259
|
+
"Authorization": `Bearer ${this.token}`,
|
|
260
|
+
"Content-Type": "application/json"
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
broadcast: {
|
|
264
|
+
name: data.name,
|
|
265
|
+
subject: data.subject,
|
|
266
|
+
preheader: data.preheader,
|
|
267
|
+
body: data.content,
|
|
268
|
+
track_opens: data.trackOpens,
|
|
269
|
+
track_clicks: data.trackClicks,
|
|
270
|
+
reply_to: data.replyTo,
|
|
271
|
+
segment_ids: data.audienceIds
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
const error = await response.text();
|
|
277
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
278
|
+
}
|
|
279
|
+
const broadcast = await response.json();
|
|
280
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
283
|
+
throw new BroadcastProviderError(
|
|
284
|
+
`Failed to update broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
285
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
286
|
+
this.name,
|
|
287
|
+
error
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async delete(id) {
|
|
292
|
+
try {
|
|
293
|
+
const existing = await this.get(id);
|
|
294
|
+
if (!this.canEditInStatus(existing.sendStatus)) {
|
|
295
|
+
throw new BroadcastProviderError(
|
|
296
|
+
`Cannot delete broadcast in status: ${existing.sendStatus}`,
|
|
297
|
+
"INVALID_STATUS" /* INVALID_STATUS */,
|
|
298
|
+
this.name
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
302
|
+
method: "DELETE",
|
|
303
|
+
headers: {
|
|
304
|
+
"Authorization": `Bearer ${this.token}`,
|
|
305
|
+
"Content-Type": "application/json"
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
const error = await response.text();
|
|
310
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
314
|
+
throw new BroadcastProviderError(
|
|
315
|
+
`Failed to delete broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
316
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
317
|
+
this.name,
|
|
318
|
+
error
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async send(id, options) {
|
|
323
|
+
try {
|
|
324
|
+
if (options?.testMode && options.testRecipients?.length) {
|
|
325
|
+
throw new BroadcastProviderError(
|
|
326
|
+
"Test send is not yet implemented for Broadcast provider",
|
|
327
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
328
|
+
this.name
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}/send_broadcast`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: {
|
|
334
|
+
"Authorization": `Bearer ${this.token}`,
|
|
335
|
+
"Content-Type": "application/json"
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
segment_ids: options?.audienceIds
|
|
339
|
+
})
|
|
340
|
+
});
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
const error = await response.text();
|
|
343
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
344
|
+
}
|
|
345
|
+
const result = await response.json();
|
|
346
|
+
return this.get(result.id.toString());
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof BroadcastProviderError) throw error;
|
|
349
|
+
throw new BroadcastProviderError(
|
|
350
|
+
`Failed to send broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
351
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
352
|
+
this.name,
|
|
353
|
+
error
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async schedule(id, scheduledAt) {
|
|
358
|
+
try {
|
|
359
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
360
|
+
method: "PATCH",
|
|
361
|
+
headers: {
|
|
362
|
+
"Authorization": `Bearer ${this.token}`,
|
|
363
|
+
"Content-Type": "application/json"
|
|
364
|
+
},
|
|
365
|
+
body: JSON.stringify({
|
|
366
|
+
broadcast: {
|
|
367
|
+
scheduled_send_at: scheduledAt.toISOString(),
|
|
368
|
+
// TODO: Handle timezone properly
|
|
369
|
+
scheduled_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
});
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
const error = await response.text();
|
|
375
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
376
|
+
}
|
|
377
|
+
const broadcast = await response.json();
|
|
378
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
throw new BroadcastProviderError(
|
|
381
|
+
`Failed to schedule broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
382
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
383
|
+
this.name,
|
|
384
|
+
error
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async cancelSchedule(id) {
|
|
389
|
+
try {
|
|
390
|
+
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
|
|
391
|
+
method: "PATCH",
|
|
392
|
+
headers: {
|
|
393
|
+
"Authorization": `Bearer ${this.token}`,
|
|
394
|
+
"Content-Type": "application/json"
|
|
395
|
+
},
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
broadcast: {
|
|
398
|
+
scheduled_send_at: null,
|
|
399
|
+
scheduled_timezone: null
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
});
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
const error = await response.text();
|
|
405
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
406
|
+
}
|
|
407
|
+
const broadcast = await response.json();
|
|
408
|
+
return this.transformBroadcastFromApi(broadcast);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
throw new BroadcastProviderError(
|
|
411
|
+
`Failed to cancel scheduled broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
412
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
|
|
413
|
+
this.name,
|
|
414
|
+
error
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async getAnalytics(_id) {
|
|
419
|
+
throw new BroadcastProviderError(
|
|
420
|
+
"Analytics API not yet implemented for Broadcast provider",
|
|
421
|
+
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
|
|
422
|
+
this.name
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
getCapabilities() {
|
|
426
|
+
return {
|
|
427
|
+
supportsScheduling: true,
|
|
428
|
+
supportsSegmentation: true,
|
|
429
|
+
supportsAnalytics: false,
|
|
430
|
+
// Not documented yet
|
|
431
|
+
supportsABTesting: false,
|
|
432
|
+
supportsTemplates: false,
|
|
433
|
+
supportsPersonalization: true,
|
|
434
|
+
supportsMultipleChannels: false,
|
|
435
|
+
supportsChannelSegmentation: false,
|
|
436
|
+
editableStatuses: ["draft" /* DRAFT */, "scheduled" /* SCHEDULED */],
|
|
437
|
+
supportedContentTypes: ["html", "text"]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async validateConfiguration() {
|
|
441
|
+
try {
|
|
442
|
+
await this.list({ limit: 1 });
|
|
443
|
+
return true;
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
transformBroadcastFromApi(broadcast) {
|
|
449
|
+
return {
|
|
450
|
+
id: broadcast.id.toString(),
|
|
451
|
+
name: broadcast.name,
|
|
452
|
+
subject: broadcast.subject,
|
|
453
|
+
preheader: broadcast.preheader,
|
|
454
|
+
content: broadcast.body,
|
|
455
|
+
sendStatus: this.mapBroadcastStatus(broadcast.status),
|
|
456
|
+
trackOpens: broadcast.track_opens,
|
|
457
|
+
trackClicks: broadcast.track_clicks,
|
|
458
|
+
replyTo: broadcast.reply_to,
|
|
459
|
+
recipientCount: broadcast.total_recipients,
|
|
460
|
+
sentAt: broadcast.sent_at ? new Date(broadcast.sent_at) : void 0,
|
|
461
|
+
scheduledAt: broadcast.scheduled_send_at ? new Date(broadcast.scheduled_send_at) : void 0,
|
|
462
|
+
createdAt: new Date(broadcast.created_at),
|
|
463
|
+
updatedAt: new Date(broadcast.updated_at),
|
|
464
|
+
providerData: { broadcast },
|
|
465
|
+
providerId: broadcast.id.toString(),
|
|
466
|
+
providerType: "broadcast"
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
mapBroadcastStatus(status) {
|
|
470
|
+
const statusMap = {
|
|
471
|
+
"draft": "draft" /* DRAFT */,
|
|
472
|
+
"scheduled": "scheduled" /* SCHEDULED */,
|
|
473
|
+
"queueing": "sending" /* SENDING */,
|
|
474
|
+
"sending": "sending" /* SENDING */,
|
|
475
|
+
"sent": "sent" /* SENT */,
|
|
476
|
+
"failed": "failed" /* FAILED */,
|
|
477
|
+
"partial_failure": "failed" /* FAILED */,
|
|
478
|
+
"paused": "paused" /* PAUSED */,
|
|
479
|
+
"aborted": "canceled" /* CANCELED */
|
|
480
|
+
};
|
|
481
|
+
return statusMap[status] || "draft" /* DRAFT */;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export {
|
|
486
|
+
NewsletterProviderError,
|
|
487
|
+
BroadcastProviderError,
|
|
488
|
+
BaseBroadcastProvider,
|
|
489
|
+
BroadcastApiProvider
|
|
490
|
+
};
|