waha-shared 1.0.527 → 1.0.529

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.
@@ -0,0 +1,153 @@
1
+ import { type ErrorResponse as ResendErrorResponse } from 'resend';
2
+ import { type DripEmailId, type DripLanguageId, type DripSequenceId } from '../types/resend';
3
+ /**
4
+ * Rate limits and server errors. Network failures (no status code) aren't
5
+ * retried: the request may have landed, and repeating a create or an event
6
+ * send could start an automation twice.
7
+ */
8
+ export declare function isRetryable(error: Pick<ResendErrorResponse, 'statusCode'>): boolean;
9
+ /** From: https://resend.com/audience/topics */
10
+ export declare enum Topics {
11
+ Newsletter = "83d566a5-a42d-47e9-85e7-b43fb5b10298"
12
+ }
13
+ export declare enum Segments {
14
+ /**
15
+ * App donors who asked for the thank-you sequence (plus donors carried over
16
+ * from AC).
17
+ */
18
+ Donor = "74ee171a-f00b-4a10-94b6-ef8508635ee1",
19
+ /** Everyone who created an app account (enrolled in the onboarding drip). */
20
+ AppAccounts = "07e0f635-7e85-4d96-9c31-12a7e34f6869"
21
+ }
22
+ /** From: https://resend.com/automations/events */
23
+ export declare enum Events {
24
+ AppDonorSequence = "donor_thank_you_sequence",
25
+ NewsletterWelcome = "newsletter_welcome"
26
+ }
27
+ /** From: https://resend.com/audience/properties */
28
+ export declare enum Properties {
29
+ /** Set by the donor automation when it runs, so it runs once per contact. */
30
+ AppDonorSequence = "donor_thank_you_sequence",
31
+ /**
32
+ * Set by the welcome automation when it runs, so it runs once per contact.
33
+ */
34
+ NewsletterWelcome = "newsletter_welcome",
35
+ /** The onboarding drip a contact was enrolled in, so it runs once each. */
36
+ NewAccountSequence = "drip_sequence",
37
+ /** The language a contact's onboarding drip was sent in. */
38
+ Language = "language"
39
+ }
40
+ /**
41
+ * Adds a language id to a base id to create a unique drip identifier. A
42
+ * sequence's id gives its trigger event (and automation) name; an email's id
43
+ * gives its template alias.
44
+ */
45
+ export declare function dripId(baseId: DripSequenceId | DripEmailId, languageId: DripLanguageId): string;
46
+ export declare function isDripLanguage(value: string): value is DripLanguageId;
47
+ /** Splits a single "name" form field into first and last name. */
48
+ export declare function splitName(name?: string): {
49
+ firstName?: string;
50
+ lastName?: string;
51
+ };
52
+ /**
53
+ * Returns the existing contact for an email, or creates one. Existing contacts
54
+ * are returned untouched, so their newsletter choice and account-wide
55
+ * unsubscribe are never overridden here. New contacts start off the
56
+ * newsletter.
57
+ */
58
+ export declare function upsertResendContact({ token, email, name, }: {
59
+ token: string;
60
+ email: string;
61
+ name?: string;
62
+ }): Promise<{
63
+ id: string;
64
+ created: boolean;
65
+ }>;
66
+ /**
67
+ * Creates a contact for an email the caller already knows isn't one, skipping
68
+ * the lookup {@link upsertResendContact} makes. The contact starts off the
69
+ * newsletter. Returns the new contact's id.
70
+ */
71
+ export declare function createResendContact({ token, email, name, }: {
72
+ token: string;
73
+ email: string;
74
+ name?: string;
75
+ }): Promise<string>;
76
+ /**
77
+ * Every contact's email, lowercased, or only those in one segment when
78
+ * `segmentId` is given. Read-only.
79
+ */
80
+ export declare function listContactEmails(token: string, segmentId?: string): Promise<Set<string>>;
81
+ /** Every segment's id and name. Read-only. */
82
+ export declare function listSegments(token: string): Promise<{
83
+ id: string;
84
+ name: string;
85
+ }[]>;
86
+ /** Creates a segment and returns its id. */
87
+ export declare function createSegment(token: string, name: string): Promise<string>;
88
+ /**
89
+ * Adds or removes the contact for an email in a segment. Adding someone already
90
+ * in it is harmless, and so is removing someone who isn't.
91
+ */
92
+ export declare function setSegmentMembership({ token, email, segmentId, member, }: {
93
+ token: string;
94
+ email: string;
95
+ segmentId: string;
96
+ member: boolean;
97
+ }): Promise<void>;
98
+ /**
99
+ * Deletes the contact for an email, taking its topic and segment memberships
100
+ * with it. A missing contact is already deleted, so it isn't an error.
101
+ */
102
+ export declare function deleteResendContact({ token, email, }: {
103
+ token: string;
104
+ email: string;
105
+ }): Promise<void>;
106
+ /**
107
+ * An explicit newsletter signup. This is fresh consent, so it opts the contact
108
+ * back in even if they opted out of the newsletter or unsubscribed from
109
+ * everything before. Bounce suppressions still apply. Also triggers the welcome
110
+ * email, which the automation sends only once per contact.
111
+ */
112
+ export declare function subscribeToNewsletter({ token, email, name, }: {
113
+ token: string;
114
+ email: string;
115
+ name?: string;
116
+ }): Promise<void>;
117
+ /**
118
+ * Starts the donor thank-you sequence for a donor who asked for it and adds
119
+ * them to the Donor segment. A donor who ticked the newsletter box is also
120
+ * subscribed to it, without the welcome email. Donors who don't ask for the
121
+ * sequence aren't added to Resend at all.
122
+ */
123
+ export declare function enrollInDonorSequence({ token, email, name, newsletter, }: {
124
+ token: string;
125
+ email: string;
126
+ name?: string;
127
+ newsletter?: boolean;
128
+ }): Promise<void>;
129
+ export type DripEnrollment = {
130
+ status: 'enrolled';
131
+ } | {
132
+ status: 'already_enrolled';
133
+ sequence: string;
134
+ };
135
+ export interface EnrollInDripSequenceParams {
136
+ token: string;
137
+ email: string;
138
+ firstName?: string;
139
+ sequence: DripSequenceId;
140
+ languageId: DripLanguageId;
141
+ /** Whether the user ticked the newsletter box when signing in. */
142
+ newsletter?: boolean;
143
+ }
144
+ /**
145
+ * Enrolls an app user in a localized drip sequence: adds them to the App
146
+ * Accounts segment and fires the event that starts that sequence and
147
+ * language's automation (provisioned by waha-website's
148
+ * `scripts/emailDrip/push.ts`). A contact already enrolled in any sequence is
149
+ * left alone, so each person gets at most one drip. A user who ticked the
150
+ * newsletter box is also subscribed to it: without the welcome email when
151
+ * their drip starts now, and as an explicit signup when it already ran.
152
+ */
153
+ export declare function enrollInDripSequence({ token, email, firstName, sequence, languageId, newsletter, }: EnrollInDripSequenceParams): Promise<DripEnrollment>;
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Properties = exports.Events = exports.Segments = exports.Topics = void 0;
4
+ exports.isRetryable = isRetryable;
5
+ exports.dripId = dripId;
6
+ exports.isDripLanguage = isDripLanguage;
7
+ exports.splitName = splitName;
8
+ exports.upsertResendContact = upsertResendContact;
9
+ exports.createResendContact = createResendContact;
10
+ exports.listContactEmails = listContactEmails;
11
+ exports.listSegments = listSegments;
12
+ exports.createSegment = createSegment;
13
+ exports.setSegmentMembership = setSegmentMembership;
14
+ exports.deleteResendContact = deleteResendContact;
15
+ exports.subscribeToNewsletter = subscribeToNewsletter;
16
+ exports.enrollInDonorSequence = enrollInDonorSequence;
17
+ exports.enrollInDripSequence = enrollInDripSequence;
18
+ const resend_1 = require("resend");
19
+ const resend_2 = require("../types/resend");
20
+ const utils_1 = require("./utils");
21
+ /**
22
+ * Rate limits and server errors. Network failures (no status code) aren't
23
+ * retried: the request may have landed, and repeating a create or an event
24
+ * send could start an automation twice.
25
+ */
26
+ function isRetryable(error) {
27
+ const status = error.statusCode;
28
+ return status !== null && (status === 429 || status >= 500);
29
+ }
30
+ /** Makes a Resend call, retrying transient failures twice with backoff. */
31
+ async function withRetry(call) {
32
+ for (let attempt = 1;; attempt++) {
33
+ const result = await call();
34
+ if (!result.error || !isRetryable(result.error) || attempt === 3)
35
+ return result;
36
+ await new Promise((resolve) => setTimeout(resolve, attempt * 500));
37
+ }
38
+ }
39
+ /**
40
+ * The Resend SDK resolves to `{ data, error }` instead of throwing, so an
41
+ * unchecked call fails silently and the next step gets `undefined`.
42
+ */
43
+ function verify(result, context) {
44
+ if (result.error)
45
+ throw new Error(`${context}: ${result.error.message}`);
46
+ if (!result.data)
47
+ throw new Error(`${context}: Resend returned no data.`);
48
+ return result.data;
49
+ }
50
+ /** From: https://resend.com/audience/topics */
51
+ var Topics;
52
+ (function (Topics) {
53
+ Topics["Newsletter"] = "83d566a5-a42d-47e9-85e7-b43fb5b10298";
54
+ })(Topics || (exports.Topics = Topics = {}));
55
+ /* From: https://resend.com/audience/segments */
56
+ var Segments;
57
+ (function (Segments) {
58
+ /**
59
+ * App donors who asked for the thank-you sequence (plus donors carried over
60
+ * from AC).
61
+ */
62
+ Segments["Donor"] = "74ee171a-f00b-4a10-94b6-ef8508635ee1";
63
+ /** Everyone who created an app account (enrolled in the onboarding drip). */
64
+ Segments["AppAccounts"] = "07e0f635-7e85-4d96-9c31-12a7e34f6869";
65
+ })(Segments || (exports.Segments = Segments = {}));
66
+ /** From: https://resend.com/automations/events */
67
+ var Events;
68
+ (function (Events) {
69
+ Events["AppDonorSequence"] = "donor_thank_you_sequence";
70
+ Events["NewsletterWelcome"] = "newsletter_welcome";
71
+ // Drip sequence events are per language and thus generated dynamically, so
72
+ // they aren't in this enum.
73
+ })(Events || (exports.Events = Events = {}));
74
+ /** From: https://resend.com/audience/properties */
75
+ var Properties;
76
+ (function (Properties) {
77
+ /** Set by the donor automation when it runs, so it runs once per contact. */
78
+ Properties["AppDonorSequence"] = "donor_thank_you_sequence";
79
+ /**
80
+ * Set by the welcome automation when it runs, so it runs once per contact.
81
+ */
82
+ Properties["NewsletterWelcome"] = "newsletter_welcome";
83
+ /** The onboarding drip a contact was enrolled in, so it runs once each. */
84
+ Properties["NewAccountSequence"] = "drip_sequence";
85
+ /** The language a contact's onboarding drip was sent in. */
86
+ Properties["Language"] = "language";
87
+ })(Properties || (exports.Properties = Properties = {}));
88
+ /**
89
+ * Adds a language id to a base id to create a unique drip identifier. A
90
+ * sequence's id gives its trigger event (and automation) name; an email's id
91
+ * gives its template alias.
92
+ */
93
+ function dripId(baseId, languageId) {
94
+ return `${baseId}_${languageId}`;
95
+ }
96
+ function isDripLanguage(value) {
97
+ return resend_2.DRIP_LANGUAGES.includes(value);
98
+ }
99
+ /** Splits a single "name" form field into first and last name. */
100
+ function splitName(name) {
101
+ const [firstName, ...rest] = (name ?? '').trim().split(/\s+/);
102
+ return {
103
+ firstName: firstName || undefined,
104
+ lastName: rest.join(' ') || undefined,
105
+ };
106
+ }
107
+ /**
108
+ * Returns the existing contact for an email, or creates one. Existing contacts
109
+ * are returned untouched, so their newsletter choice and account-wide
110
+ * unsubscribe are never overridden here. New contacts start off the
111
+ * newsletter.
112
+ */
113
+ async function upsertResendContact({ token, email, name, }) {
114
+ const resend = new resend_1.Resend(token);
115
+ const normalized = (0, utils_1.lower)(email);
116
+ const existing = await withRetry(() => resend.contacts.get({ email: normalized }));
117
+ if (existing.data)
118
+ return { id: existing.data.id, created: false };
119
+ if (existing.error?.name !== 'not_found')
120
+ verify(existing, `Looking up contact ${normalized}`);
121
+ return {
122
+ id: await createResendContact({ token, email, name }),
123
+ created: true,
124
+ };
125
+ }
126
+ /**
127
+ * Creates a contact for an email the caller already knows isn't one, skipping
128
+ * the lookup {@link upsertResendContact} makes. The contact starts off the
129
+ * newsletter. Returns the new contact's id.
130
+ */
131
+ async function createResendContact({ token, email, name, }) {
132
+ const resend = new resend_1.Resend(token);
133
+ const normalized = (0, utils_1.lower)(email);
134
+ return verify(await withRetry(() => resend.contacts.create({ email: normalized, ...splitName(name) })), `Creating contact ${normalized}`).id;
135
+ }
136
+ /**
137
+ * Every contact's email, lowercased, or only those in one segment when
138
+ * `segmentId` is given. Read-only.
139
+ */
140
+ async function listContactEmails(token, segmentId) {
141
+ const resend = new resend_1.Resend(token);
142
+ const emails = new Set();
143
+ for (let after;;) {
144
+ const page = verify(await withRetry(() => resend.contacts.list({
145
+ limit: 100,
146
+ ...(segmentId && { segmentId }),
147
+ ...(after && { after }),
148
+ })), `Listing contacts${segmentId ? ` in segment ${segmentId}` : ''} after ${after ?? 'the start'}`);
149
+ for (const contact of page.data)
150
+ emails.add((0, utils_1.lower)(contact.email));
151
+ const last = page.data[page.data.length - 1];
152
+ if (!page.has_more || !last)
153
+ return emails;
154
+ after = last.id;
155
+ }
156
+ }
157
+ /** Every segment's id and name. Read-only. */
158
+ async function listSegments(token) {
159
+ const resend = new resend_1.Resend(token);
160
+ const segments = [];
161
+ for (let after;;) {
162
+ const page = verify(await withRetry(() => resend.segments.list({ limit: 100, ...(after && { after }) })), `Listing segments after ${after ?? 'the start'}`);
163
+ for (const { id, name } of page.data)
164
+ segments.push({ id, name });
165
+ const last = page.data[page.data.length - 1];
166
+ if (!page.has_more || !last)
167
+ return segments;
168
+ after = last.id;
169
+ }
170
+ }
171
+ /** Creates a segment and returns its id. */
172
+ async function createSegment(token, name) {
173
+ const resend = new resend_1.Resend(token);
174
+ return verify(await withRetry(() => resend.segments.create({ name })), `Creating segment "${name}"`).id;
175
+ }
176
+ /**
177
+ * Adds or removes the contact for an email in a segment. Adding someone already
178
+ * in it is harmless, and so is removing someone who isn't.
179
+ */
180
+ async function setSegmentMembership({ token, email, segmentId, member, }) {
181
+ const resend = new resend_1.Resend(token);
182
+ const options = { email: (0, utils_1.lower)(email), segmentId };
183
+ if (member) {
184
+ verify(await withRetry(() => resend.contacts.segments.add(options)), `Adding ${options.email} to segment ${segmentId}`);
185
+ return;
186
+ }
187
+ const result = await withRetry(() => resend.contacts.segments.remove(options));
188
+ if (result.error?.name === 'not_found')
189
+ return;
190
+ verify(result, `Removing ${options.email} from segment ${segmentId}`);
191
+ }
192
+ /**
193
+ * Deletes the contact for an email, taking its topic and segment memberships
194
+ * with it. A missing contact is already deleted, so it isn't an error.
195
+ */
196
+ async function deleteResendContact({ token, email, }) {
197
+ const resend = new resend_1.Resend(token);
198
+ const normalized = (0, utils_1.lower)(email);
199
+ const result = await withRetry(() => resend.contacts.remove({ email: normalized }));
200
+ if (result.error?.name === 'not_found')
201
+ return;
202
+ verify(result, `Deleting contact ${normalized}`);
203
+ }
204
+ /**
205
+ * An explicit newsletter signup. This is fresh consent, so it opts the contact
206
+ * back in even if they opted out of the newsletter or unsubscribed from
207
+ * everything before. Bounce suppressions still apply. Also triggers the welcome
208
+ * email, which the automation sends only once per contact.
209
+ */
210
+ async function subscribeToNewsletter({ token, email, name, }) {
211
+ const resend = new resend_1.Resend(token);
212
+ // Upsert the contact.
213
+ const contact = await upsertResendContact({ token, email, name });
214
+ // If contact exists, resubscribe them in case they previously unsubscribed.
215
+ if (!contact.created)
216
+ verify(await withRetry(() => resend.contacts.update({ id: contact.id, unsubscribed: false })), `Resubscribing contact ${contact.id}`);
217
+ // Add them to the newsletter topic.
218
+ verify(await withRetry(() => resend.contacts.topics.update({
219
+ id: contact.id,
220
+ topics: [{ id: Topics.Newsletter, subscription: 'opt_in' }],
221
+ })), `Subscribing contact ${contact.id} to the newsletter`);
222
+ // Send them a welcome email.
223
+ verify(await withRetry(() => resend.events.send({
224
+ event: Events.NewsletterWelcome,
225
+ contactId: contact.id,
226
+ })), `Sending ${Events.NewsletterWelcome} for contact ${contact.id}`);
227
+ }
228
+ /** Adds a contact to a segment. Adding someone already in it is harmless. */
229
+ async function addToSegment(resend, contactId, segmentId) {
230
+ verify(await withRetry(() => resend.contacts.segments.add({ contactId, segmentId })), `Adding contact ${contactId} to segment ${segmentId}`);
231
+ }
232
+ /**
233
+ * Opts a contact who ticked the newsletter box alongside another signup (an
234
+ * app account or a donation) in to the newsletter, without the welcome email:
235
+ * the sequence they're starting already welcomes them. For a contact that
236
+ * signup just created, the welcome's run-once property is set so a later
237
+ * explicit signup doesn't send it either. An existing contact's is left alone,
238
+ * since it may record a welcome they really got.
239
+ *
240
+ * Never throws: the sequence has already started, so a failure here is logged
241
+ * instead of failing the request.
242
+ */
243
+ async function subscribeAlongsideSequence(resend, contactId, created) {
244
+ const subscribed = await withRetry(() => resend.contacts.topics.update({
245
+ id: contactId,
246
+ topics: [{ id: Topics.Newsletter, subscription: 'opt_in' }],
247
+ }));
248
+ if (subscribed.error) {
249
+ console.error(`Subscribing contact ${contactId} to the newsletter failed:`, subscribed.error);
250
+ return;
251
+ }
252
+ if (!created)
253
+ return;
254
+ const marked = await withRetry(() => resend.contacts.update({
255
+ id: contactId,
256
+ properties: {
257
+ [Properties.NewsletterWelcome]: `skipped|${new Date().toISOString()}`,
258
+ },
259
+ }));
260
+ if (marked.error)
261
+ console.error(`Marking contact ${contactId} welcomed failed:`, marked.error);
262
+ }
263
+ /**
264
+ * Starts the donor thank-you sequence for a donor who asked for it and adds
265
+ * them to the Donor segment. A donor who ticked the newsletter box is also
266
+ * subscribed to it, without the welcome email. Donors who don't ask for the
267
+ * sequence aren't added to Resend at all.
268
+ */
269
+ async function enrollInDonorSequence({ token, email, name, newsletter, }) {
270
+ const resend = new resend_1.Resend(token);
271
+ const { id, created } = await upsertResendContact({ token, email, name });
272
+ await addToSegment(resend, id, Segments.Donor);
273
+ verify(await withRetry(() => resend.events.send({
274
+ event: Events.AppDonorSequence,
275
+ contactId: id,
276
+ })), `Sending ${Events.AppDonorSequence} for contact ${id}`);
277
+ if (newsletter)
278
+ await subscribeAlongsideSequence(resend, id, created);
279
+ }
280
+ /**
281
+ * Enrolls an app user in a localized drip sequence: adds them to the App
282
+ * Accounts segment and fires the event that starts that sequence and
283
+ * language's automation (provisioned by waha-website's
284
+ * `scripts/emailDrip/push.ts`). A contact already enrolled in any sequence is
285
+ * left alone, so each person gets at most one drip. A user who ticked the
286
+ * newsletter box is also subscribed to it: without the welcome email when
287
+ * their drip starts now, and as an explicit signup when it already ran.
288
+ */
289
+ async function enrollInDripSequence({ token, email, firstName, sequence, languageId, newsletter, }) {
290
+ const resend = new resend_1.Resend(token);
291
+ const normalized = (0, utils_1.lower)(email);
292
+ const existing = await withRetry(() => resend.contacts.get({ email: normalized }));
293
+ if (existing.error?.name !== 'not_found' && !existing.data)
294
+ verify(existing, `Looking up contact ${normalized}`);
295
+ const contact = existing.data;
296
+ const enrolledIn = contact?.properties?.[Properties.NewAccountSequence]?.value;
297
+ if (enrolledIn) {
298
+ // No drip starts to welcome them, so this is an ordinary signup.
299
+ if (newsletter)
300
+ await subscribeToNewsletter({ token, email, name: firstName }).catch((e) => console.error(`Subscribing ${normalized} to the newsletter failed:`, e));
301
+ return { status: 'already_enrolled', sequence: `${enrolledIn}` };
302
+ }
303
+ /*
304
+ * The sequence property is deliberately not set yet. Marking a contact
305
+ * enrolled before the event below fires would strand them if it failed: the
306
+ * check above would skip them forever though their drip never started.
307
+ */
308
+ const properties = { [Properties.Language]: languageId };
309
+ const id = contact
310
+ ? verify(await withRetry(() => resend.contacts.update({ id: contact.id, firstName, properties })), `Updating contact ${contact.id}`).id
311
+ : verify(await withRetry(() => resend.contacts.create({ email: normalized, firstName, properties })), `Creating contact ${normalized}`).id;
312
+ await addToSegment(resend, id, Segments.AppAccounts);
313
+ const event = dripId(sequence, languageId);
314
+ verify(await withRetry(() => resend.events.send({ event, contactId: id })), `Sending ${event} for contact ${id}`);
315
+ /*
316
+ * The drip has started, so a failure here only weakens the guard against a
317
+ * second enrollment. It's logged rather than failing a request that
318
+ * otherwise succeeded.
319
+ */
320
+ const marked = await withRetry(() => resend.contacts.update({
321
+ id,
322
+ properties: { [Properties.NewAccountSequence]: sequence },
323
+ }));
324
+ if (marked.error)
325
+ console.error(`Marking contact ${id} enrolled failed:`, marked.error);
326
+ if (newsletter)
327
+ await subscribeAlongsideSequence(resend, id, !contact);
328
+ return { status: 'enrolled' };
329
+ }
@@ -7,3 +7,4 @@ export declare function getErrorInfo(e: unknown): {
7
7
  code?: string;
8
8
  message?: string;
9
9
  };
10
+ export declare function lower(string: string): string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dirname = exports.first = exports.basename = exports.join = void 0;
4
4
  exports.getErrorInfo = getErrorInfo;
5
+ exports.lower = lower;
5
6
  const join = (...parts) => parts.join('/').replace(/\/+/g, '/');
6
7
  exports.join = join;
7
8
  const basename = (p) => p.slice(p.lastIndexOf('/') + 1);
@@ -21,3 +22,6 @@ function getErrorInfo(e) {
21
22
  const message = e instanceof Error ? e.message : undefined;
22
23
  return { code, message };
23
24
  }
25
+ function lower(string) {
26
+ return string.trim().toLowerCase();
27
+ }
@@ -18,11 +18,7 @@ export declare const SubscribeDripBody: z.ZodObject<{
18
18
  drip_believer: "drip_believer";
19
19
  drip_seeker: "drip_seeker";
20
20
  }>;
21
- languageId: z.ZodEnum<{
22
- eng: "eng";
23
- ara: "ara";
24
- swe: "swe";
25
- }>;
21
+ languageId: z.ZodString;
26
22
  firstName: z.ZodOptional<z.ZodString>;
27
23
  }, z.core.$strip>;
28
24
  export type SubscribeDripBody = z.infer<typeof SubscribeDripBody>;
@@ -12,6 +12,6 @@ exports.DRIP_LANGUAGES = ['eng', 'ara', 'swe'];
12
12
  exports.DripLanguageId = zod_1.default.enum(exports.DRIP_LANGUAGES);
13
13
  exports.SubscribeDripBody = zod_1.default.object({
14
14
  sequence: exports.DripSequenceId,
15
- languageId: exports.DripLanguageId,
15
+ languageId: zod_1.default.string(),
16
16
  firstName: zod_1.default.string().optional(),
17
17
  });
@@ -55,6 +55,12 @@ export declare const LanguageInfoStatuses: z.ZodObject<{
55
55
  testing: "testing";
56
56
  launched: "launched";
57
57
  }>;
58
+ drip: z.ZodOptional<z.ZodEnum<{
59
+ notStarted: "notStarted";
60
+ inProgress: "inProgress";
61
+ testing: "testing";
62
+ launched: "launched";
63
+ }>>;
58
64
  dmCourse: z.ZodEnum<{
59
65
  notStarted: "notStarted";
60
66
  inProgress: "inProgress";
@@ -1,7 +1,6 @@
1
1
  import z from 'zod';
2
2
  export declare const WahaShareContent: z.ZodEnum<{
3
3
  lesson: "lesson";
4
- note: "note";
5
4
  lessonLink: "lessonLink";
6
5
  setLink: "setLink";
7
6
  app: "app";
@@ -21,6 +20,7 @@ export declare const WahaShareContent: z.ZodEnum<{
21
20
  scriptureAudio: "scriptureAudio";
22
21
  inviteGroup: "inviteGroup";
23
22
  offlineVersionLink: "offlineVersionLink";
23
+ note: "note";
24
24
  customSet: "customSet";
25
25
  microLesson: "microLesson";
26
26
  }>;
@@ -29,7 +29,6 @@ export declare const ShareLogEvent: z.ZodObject<{
29
29
  time: z.ZodNumber;
30
30
  content: z.ZodEnum<{
31
31
  lesson: "lesson";
32
- note: "note";
33
32
  lessonLink: "lessonLink";
34
33
  setLink: "setLink";
35
34
  app: "app";
@@ -49,6 +48,7 @@ export declare const ShareLogEvent: z.ZodObject<{
49
48
  scriptureAudio: "scriptureAudio";
50
49
  inviteGroup: "inviteGroup";
51
50
  offlineVersionLink: "offlineVersionLink";
51
+ note: "note";
52
52
  customSet: "customSet";
53
53
  microLesson: "microLesson";
54
54
  }>;
@@ -0,0 +1,60 @@
1
+ import z from 'zod';
2
+ import type { Status } from '../data/languages/languages.zod';
3
+ /** The two onboarding drip audiences a new signup can be enrolled in. */
4
+ export declare const DripSequenceId: z.ZodEnum<{
5
+ drip_believer: "drip_believer";
6
+ drip_seeker: "drip_seeker";
7
+ }>;
8
+ export type DripSequenceId = z.infer<typeof DripSequenceId>;
9
+ /** Ids of languages whose `status.drip` in languages.json is one of these. */
10
+ export declare function dripLanguagesWithStatus(...statuses: Status[]): string[];
11
+ /**
12
+ * Languages the onboarding drip is live in: provisioned in Resend and enrolled
13
+ * into on signup. Driven by `status.drip` in languages.json.
14
+ */
15
+ export declare const DRIP_LANGUAGES: string[];
16
+ export declare const DripLanguageId: z.ZodString;
17
+ export type DripLanguageId = z.infer<typeof DripLanguageId>;
18
+ /**
19
+ * A single drip email's id: its sequence's id plus its position. The same as
20
+ * the file name for the email's source .md file.
21
+ */
22
+ export type DripEmailId = `${DripSequenceId}_${number}`;
23
+ /** `POST /api/submit-subscribe` (form data): an explicit newsletter signup. */
24
+ export declare const NewsletterSignupBody: z.ZodObject<{
25
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
26
+ name: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>;
28
+ export type NewsletterSignupBody = z.infer<typeof NewsletterSignupBody>;
29
+ /** `POST /api/submit-contact` (form data): the website's contact form. */
30
+ export declare const ContactFormBody: z.ZodObject<{
31
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
32
+ name: z.ZodString;
33
+ message: z.ZodString;
34
+ }, z.core.$strip>;
35
+ export type ContactFormBody = z.infer<typeof ContactFormBody>;
36
+ /**
37
+ * `POST /api/add-to-donation-sequence` (JSON): starts the donor thank-you
38
+ * drip.
39
+ */
40
+ export declare const DonorSequenceBody: z.ZodObject<{
41
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
42
+ name: z.ZodOptional<z.ZodString>;
43
+ newsletter: z.ZodOptional<z.ZodBoolean>;
44
+ }, z.core.$strip>;
45
+ export type DonorSequenceBody = z.infer<typeof DonorSequenceBody>;
46
+ /**
47
+ * `POST /api/subscribe-drip` (JSON, Firebase ID token). Any language is
48
+ * accepted so callers don't need to know which ones have a drip; unsupported
49
+ * ones are a no-op.
50
+ */
51
+ export declare const SubscribeDripBody: z.ZodObject<{
52
+ sequence: z.ZodEnum<{
53
+ drip_believer: "drip_believer";
54
+ drip_seeker: "drip_seeker";
55
+ }>;
56
+ languageId: z.ZodString;
57
+ firstName: z.ZodOptional<z.ZodString>;
58
+ newsletter: z.ZodOptional<z.ZodBoolean>;
59
+ }, z.core.$strip>;
60
+ export type SubscribeDripBody = z.infer<typeof SubscribeDripBody>;