Skip to content

Commit e6cabee

Browse files
bukinoshitakrithika0827renovate[bot]dependabot[bot]vcapretz
authored
feat: Add Broadcasts (#445)
Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Krithika <[email protected]> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vitor Capretz <[email protected]> Co-authored-by: Vitor Capretz <[email protected]>
1 parent cfad2ce commit e6cabee

6 files changed

+402
-0
lines changed

src/broadcasts/broadcasts.spec.ts

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { enableFetchMocks } from 'jest-fetch-mock';
2+
import type { ErrorResponse } from '../interfaces';
3+
import { Resend } from '../resend';
4+
import type {
5+
CreateBroadcastOptions,
6+
CreateBroadcastResponseSuccess,
7+
} from './interfaces/create-broadcast-options.interface';
8+
9+
enableFetchMocks();
10+
11+
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
12+
13+
describe('Broadcasts', () => {
14+
afterEach(() => fetchMock.resetMocks());
15+
16+
describe('create', () => {
17+
it('missing `from`', async () => {
18+
const response: ErrorResponse = {
19+
name: 'missing_required_field',
20+
message: 'Missing `from` field.',
21+
};
22+
23+
fetchMock.mockOnce(JSON.stringify(response), {
24+
status: 422,
25+
headers: {
26+
'content-type': 'application/json',
27+
Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop',
28+
},
29+
});
30+
31+
const data = await resend.broadcasts.create({} as CreateBroadcastOptions);
32+
console.log({ data });
33+
expect(data).toMatchInlineSnapshot(`
34+
{
35+
"data": null,
36+
"error": {
37+
"message": "Missing \`from\` field.",
38+
"name": "missing_required_field",
39+
},
40+
}
41+
`);
42+
});
43+
44+
it('creates broadcast', async () => {
45+
const response: CreateBroadcastResponseSuccess = {
46+
id: '71cdfe68-cf79-473a-a9d7-21f91db6a526',
47+
};
48+
fetchMock.mockOnce(JSON.stringify(response), {
49+
status: 200,
50+
headers: {
51+
'content-type': 'application/json',
52+
Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop',
53+
},
54+
});
55+
56+
const payload: CreateBroadcastOptions = {
57+
58+
audienceId: '0192f4ed-c2e9-7112-9c13-b04a043e23ee',
59+
subject: 'Hello World',
60+
html: '<h1>Hello world</h1>',
61+
};
62+
63+
const data = await resend.broadcasts.create(payload);
64+
expect(data).toMatchInlineSnapshot(`
65+
{
66+
"data": {
67+
"id": "71cdfe68-cf79-473a-a9d7-21f91db6a526",
68+
},
69+
"error": null,
70+
}
71+
`);
72+
});
73+
74+
it('creates broadcast with multiple recipients', async () => {
75+
const response: CreateBroadcastResponseSuccess = {
76+
id: '124dc0f1-e36c-417c-a65c-e33773abc768',
77+
};
78+
79+
fetchMock.mockOnce(JSON.stringify(response), {
80+
status: 200,
81+
headers: {
82+
'content-type': 'application/json',
83+
Authorization: 'Bearer re_924b3rjh2387fbewf823',
84+
},
85+
});
86+
87+
const payload: CreateBroadcastOptions = {
88+
89+
audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917',
90+
subject: 'Hello World',
91+
text: 'Hello world',
92+
};
93+
const data = await resend.broadcasts.create(payload);
94+
expect(data).toMatchInlineSnapshot(`
95+
{
96+
"data": {
97+
"id": "124dc0f1-e36c-417c-a65c-e33773abc768",
98+
},
99+
"error": null,
100+
}
101+
`);
102+
});
103+
104+
it('creates broadcast with multiple replyTo emails', async () => {
105+
const response: CreateBroadcastResponseSuccess = {
106+
id: '124dc0f1-e36c-417c-a65c-e33773abc768',
107+
};
108+
109+
fetchMock.mockOnce(JSON.stringify(response), {
110+
status: 200,
111+
headers: {
112+
'content-type': 'application/json',
113+
Authorization: 'Bearer re_924b3rjh2387fbewf823',
114+
},
115+
});
116+
117+
const payload: CreateBroadcastOptions = {
118+
119+
audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917',
120+
121+
subject: 'Hello World',
122+
text: 'Hello world',
123+
};
124+
125+
const data = await resend.broadcasts.create(payload);
126+
expect(data).toMatchInlineSnapshot(`
127+
{
128+
"data": {
129+
"id": "124dc0f1-e36c-417c-a65c-e33773abc768",
130+
},
131+
"error": null,
132+
}
133+
`);
134+
});
135+
136+
it('throws an error when an ErrorResponse is returned', async () => {
137+
const response: ErrorResponse = {
138+
name: 'invalid_parameter',
139+
message:
140+
'Invalid `from` field. The email address needs to follow the `[email protected]` or `Name <[email protected]>` format',
141+
};
142+
143+
fetchMock.mockOnce(JSON.stringify(response), {
144+
status: 422,
145+
headers: {
146+
'content-type': 'application/json',
147+
Authorization: 'Bearer re_924b3rjh2387fbewf823',
148+
},
149+
});
150+
151+
const payload: CreateBroadcastOptions = {
152+
from: 'resend.com', // Invalid from address
153+
audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917',
154+
155+
subject: 'Hello World',
156+
text: 'Hello world',
157+
};
158+
159+
const result = resend.broadcasts.create(payload);
160+
161+
await expect(result).resolves.toMatchInlineSnapshot(`
162+
{
163+
"data": null,
164+
"error": {
165+
"message": "Invalid \`from\` field. The email address needs to follow the \`[email protected]\` or \`Name <[email protected]>\` format",
166+
"name": "invalid_parameter",
167+
},
168+
}
169+
`);
170+
});
171+
172+
it('returns an error when fetch fails', async () => {
173+
const originalEnv = process.env;
174+
process.env = {
175+
...originalEnv,
176+
RESEND_BASE_URL: 'http://invalidurl.noturl',
177+
};
178+
179+
const result = await resend.broadcasts.create({
180+
181+
audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917',
182+
subject: 'Hello World',
183+
text: 'Hello world',
184+
});
185+
186+
expect(result).toEqual(
187+
expect.objectContaining({
188+
data: null,
189+
error: {
190+
message: 'Unable to fetch data. The request could not be resolved.',
191+
name: 'application_error',
192+
},
193+
}),
194+
);
195+
process.env = originalEnv;
196+
});
197+
198+
it('returns an error when api responds with text payload', async () => {
199+
fetchMock.mockOnce('local_rate_limited', {
200+
status: 422,
201+
headers: {
202+
Authorization: 'Bearer re_924b3rjh2387fbewf823',
203+
},
204+
});
205+
206+
const result = await resend.broadcasts.create({
207+
208+
audienceId: '0192f4f1-d5f9-7110-8eb5-370552515917',
209+
subject: 'Hello World',
210+
text: 'Hello world',
211+
});
212+
213+
expect(result).toEqual(
214+
expect.objectContaining({
215+
data: null,
216+
error: {
217+
message:
218+
'Internal server error. We are unable to process your request right now, please try again later.',
219+
name: 'application_error',
220+
},
221+
}),
222+
);
223+
});
224+
});
225+
});

src/broadcasts/broadcasts.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type * as React from 'react';
2+
import type { Resend } from '../resend';
3+
import type {
4+
CreateBroadcastOptions,
5+
CreateBroadcastRequestOptions,
6+
} from './interfaces/create-broadcast-options.interface';
7+
import type {
8+
SendBroadcastOptions,
9+
SendBroadcastResponse,
10+
SendBroadcastResponseSuccess,
11+
} from './interfaces/send-broadcast-options.interface';
12+
13+
export class Broadcasts {
14+
private renderAsync?: (component: React.ReactElement) => Promise<string>;
15+
constructor(private readonly resend: Resend) {}
16+
17+
async create(
18+
payload: CreateBroadcastOptions,
19+
options: CreateBroadcastRequestOptions = {},
20+
): Promise<SendBroadcastResponse> {
21+
if (payload.react) {
22+
if (!this.renderAsync) {
23+
try {
24+
const { renderAsync } = await import('@react-email/render');
25+
this.renderAsync = renderAsync;
26+
} catch (error) {
27+
throw new Error(
28+
'Failed to render React component. Make sure to install `@react-email/render`',
29+
);
30+
}
31+
}
32+
33+
payload.html = await this.renderAsync(
34+
payload.react as React.ReactElement,
35+
);
36+
}
37+
38+
const data = await this.resend.post<SendBroadcastResponseSuccess>(
39+
'/broadcasts',
40+
{
41+
name: payload.name,
42+
audience_id: payload.audienceId,
43+
preview_text: payload.previewText,
44+
from: payload.from,
45+
html: payload.html,
46+
reply_to: payload.replyTo,
47+
subject: payload.subject,
48+
text: payload.text,
49+
},
50+
options,
51+
);
52+
53+
return data;
54+
}
55+
56+
async send(
57+
id: string,
58+
payload: SendBroadcastOptions,
59+
): Promise<SendBroadcastResponse> {
60+
const data = await this.resend.post<SendBroadcastResponseSuccess>(
61+
`/broadcasts/${id}/send`,
62+
{ scheduled_at: payload.scheduledAt },
63+
);
64+
65+
return data;
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type * as React from 'react';
2+
import type { PostOptions } from '../../common/interfaces';
3+
import type { RequireAtLeastOne } from '../../common/interfaces/require-at-least-one';
4+
import type { ErrorResponse } from '../../interfaces';
5+
6+
interface EmailRenderOptions {
7+
/**
8+
* The React component used to write the message.
9+
*
10+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
11+
*/
12+
react: React.ReactNode;
13+
/**
14+
* The HTML version of the message.
15+
*
16+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
17+
*/
18+
html: string;
19+
/**
20+
* The plain text version of the message.
21+
*
22+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
23+
*/
24+
text: string;
25+
}
26+
27+
interface CreateBroadcastBaseOptions {
28+
/**
29+
* The name of the broadcast
30+
*
31+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
32+
*/
33+
name?: string;
34+
/**
35+
* The id of the audience you want to send to
36+
*
37+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
38+
*/
39+
audienceId: string;
40+
/**
41+
* A short snippet of text displayed as a preview in recipients' inboxes, often shown below or beside the subject line.
42+
*
43+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
44+
*/
45+
previewText?: string;
46+
/**
47+
* Sender email address. To include a friendly name, use the format `"Your Name <[email protected]>"`
48+
*
49+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
50+
*/
51+
from: string;
52+
/**
53+
* Reply-to email address. For multiple addresses, send as an array of strings.
54+
*
55+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
56+
*/
57+
replyTo?: string | string[];
58+
/**
59+
* Email subject.
60+
*
61+
* @link https://resend.com/docs/api-reference/broadcasts/create#body-parameters
62+
*/
63+
subject: string;
64+
}
65+
66+
export type CreateBroadcastOptions = RequireAtLeastOne<EmailRenderOptions> &
67+
CreateBroadcastBaseOptions;
68+
69+
export interface CreateBroadcastRequestOptions extends PostOptions {}
70+
71+
export interface CreateBroadcastResponseSuccess {
72+
/** The ID of the newly sent broadcasts. */
73+
id: string;
74+
}
75+
76+
export interface CreateBroadcastResponse {
77+
data: CreateBroadcastResponseSuccess | null;
78+
error: ErrorResponse | null;
79+
}

0 commit comments

Comments
 (0)