waha-shared 1.0.513-resend-migration.0 → 1.0.513

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.
@@ -13,7 +13,6 @@ export declare const languages: {
13
13
  trainingVideos: "notStarted" | "inProgress" | "testing" | "launched";
14
14
  gdmc: "notStarted" | "inProgress" | "testing" | "launched";
15
15
  web: "notStarted" | "inProgress" | "testing" | "launched";
16
- drip?: "notStarted" | "inProgress" | "testing" | "launched" | undefined;
17
16
  };
18
17
  script: "arabic" | "bengali" | "burmese" | "chinese" | "cyrillic" | "devanagari" | "geez" | "georgian" | "greek" | "gujarati" | "gurmukhi" | "hebrew" | "japanese" | "kannada" | "khmer" | "korean" | "latin" | "malayalam" | "tamil" | "telugu" | "thai" | "uyghur";
19
18
  uuid: string;
@@ -483,7 +483,6 @@
483
483
  "script": "arabic",
484
484
  "status": {
485
485
  "dbs": "launched",
486
- "drip": "inProgress",
487
486
  "gdmc": "notStarted",
488
487
  "trainingVideos": "launched",
489
488
  "web": "notStarted"
@@ -2006,7 +2005,6 @@
2006
2005
  "script": "latin",
2007
2006
  "status": {
2008
2007
  "dbs": "launched",
2009
- "drip": "launched",
2010
2008
  "gdmc": "launched",
2011
2009
  "trainingVideos": "launched",
2012
2010
  "web": "launched"
@@ -282,11 +282,6 @@
282
282
  "type": "string",
283
283
  "enum": ["notStarted", "inProgress", "testing", "launched"],
284
284
  "description": "The status of this language's website localization on https://waha.app."
285
- },
286
- "drip": {
287
- "description": "The status of this language's onboarding email drip sequences. Only `launched` languages are provisioned in Resend and enroll new signups; `inProgress` and `testing` languages only render as local previews. Omitted means `notStarted`.",
288
- "type": "string",
289
- "enum": ["notStarted", "inProgress", "testing", "launched"]
290
285
  }
291
286
  },
292
287
  "required": ["dbs", "trainingVideos", "gdmc", "web"],
@@ -74,12 +74,6 @@ export declare const LanguageStatuses: z.ZodObject<{
74
74
  testing: "testing";
75
75
  launched: "launched";
76
76
  }>;
77
- drip: z.ZodOptional<z.ZodEnum<{
78
- notStarted: "notStarted";
79
- inProgress: "inProgress";
80
- testing: "testing";
81
- launched: "launched";
82
- }>>;
83
77
  }, z.core.$strip>;
84
78
  export declare const Languages: z.ZodArray<z.ZodObject<{
85
79
  languageId: z.ZodString;
@@ -274,12 +268,6 @@ export declare const Languages: z.ZodArray<z.ZodObject<{
274
268
  testing: "testing";
275
269
  launched: "launched";
276
270
  }>;
277
- drip: z.ZodOptional<z.ZodEnum<{
278
- notStarted: "notStarted";
279
- inProgress: "inProgress";
280
- testing: "testing";
281
- launched: "launched";
282
- }>>;
283
271
  }, z.core.$strip>;
284
272
  endonym: z.ZodOptional<z.ZodString>;
285
273
  givingId: z.ZodOptional<z.ZodString>;
@@ -54,7 +54,6 @@ exports.LanguageStatuses = zod_1.z.object({
54
54
  trainingVideos: exports.Status.describe("The status of this language's disciple making course training videos."),
55
55
  gdmc: exports.Status.describe("The status of this language's growing as a dmc sets."),
56
56
  web: exports.Status.describe("The status of this language's website localization on https://waha.app."),
57
- drip: exports.Status.optional().describe("The status of this language's onboarding email drip sequences. Only `launched` languages are provisioned in Resend and enroll new signups; `inProgress` and `testing` languages only render as local previews. Omitted means `notStarted`."),
58
57
  });
59
58
  exports.Languages = zod_1.z.array(zod_1.z
60
59
  .object({
@@ -7,4 +7,3 @@ 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,7 +2,6 @@
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;
6
5
  const join = (...parts) => parts.join('/').replace(/\/+/g, '/');
7
6
  exports.join = join;
8
7
  const basename = (p) => p.split('/').pop();
@@ -22,6 +21,3 @@ function getErrorInfo(e) {
22
21
  const message = e instanceof Error ? e.message : undefined;
23
22
  return { code, message };
24
23
  }
25
- function lower(string) {
26
- return string.trim().toLowerCase();
27
- }
@@ -18,7 +18,11 @@ export declare const SubscribeDripBody: z.ZodObject<{
18
18
  drip_believer: "drip_believer";
19
19
  drip_seeker: "drip_seeker";
20
20
  }>;
21
- languageId: z.ZodString;
21
+ languageId: z.ZodEnum<{
22
+ eng: "eng";
23
+ ara: "ara";
24
+ swe: "swe";
25
+ }>;
22
26
  firstName: z.ZodOptional<z.ZodString>;
23
27
  }, z.core.$strip>;
24
28
  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: zod_1.default.string(),
15
+ languageId: exports.DripLanguageId,
16
16
  firstName: zod_1.default.string().optional(),
17
17
  });
@@ -57,12 +57,6 @@ export declare const LanguageInfoStatuses: z.ZodObject<{
57
57
  testing: "testing";
58
58
  launched: "launched";
59
59
  }>;
60
- drip: z.ZodOptional<z.ZodEnum<{
61
- notStarted: "notStarted";
62
- inProgress: "inProgress";
63
- testing: "testing";
64
- launched: "launched";
65
- }>>;
66
60
  dmCourse: z.ZodEnum<{
67
61
  notStarted: "notStarted";
68
62
  inProgress: "inProgress";
@@ -17,6 +17,7 @@ export declare const SharedUserReduxState: z.ZodObject<{
17
17
  }, z.core.$strip>>;
18
18
  phoneNumber: z.ZodOptional<z.ZodString>;
19
19
  isInAGroup: z.ZodOptional<z.ZodBoolean>;
20
+ isSubscribedToEmailUpdates: z.ZodOptional<z.ZodBoolean>;
20
21
  hasDonated: z.ZodOptional<z.ZodBoolean>;
21
22
  }, z.core.$strip>;
22
23
  /**
@@ -51,6 +52,7 @@ export declare const UserReduxState: z.ZodObject<{
51
52
  }, z.core.$strip>>;
52
53
  phoneNumber: z.ZodOptional<z.ZodString>;
53
54
  isInAGroup: z.ZodOptional<z.ZodBoolean>;
55
+ isSubscribedToEmailUpdates: z.ZodOptional<z.ZodBoolean>;
54
56
  hasDonated: z.ZodOptional<z.ZodBoolean>;
55
57
  }, z.core.$strip>;
56
58
  export type UserReduxState = z.infer<typeof UserReduxState>;
@@ -126,6 +128,7 @@ export declare const WahaUser: z.ZodObject<{
126
128
  }, z.core.$strip>>;
127
129
  phoneNumber: z.ZodOptional<z.ZodString>;
128
130
  isInAGroup: z.ZodOptional<z.ZodBoolean>;
131
+ isSubscribedToEmailUpdates: z.ZodOptional<z.ZodBoolean>;
129
132
  hasDonated: z.ZodOptional<z.ZodBoolean>;
130
133
  wahaUserId: z.ZodString;
131
134
  name: z.ZodOptional<z.ZodString>;
@@ -148,6 +151,7 @@ export declare const WahaUserFields: {
148
151
  location: "location";
149
152
  phoneNumber: "phoneNumber";
150
153
  isInAGroup: "isInAGroup";
154
+ isSubscribedToEmailUpdates: "isSubscribedToEmailUpdates";
151
155
  hasDonated: "hasDonated";
152
156
  lastUpdate: "lastUpdate";
153
157
  hasAccount: "hasAccount";
@@ -314,6 +318,7 @@ export declare const LegacyWahaUserFields: {
314
318
  location: "location";
315
319
  phoneNumber: "phoneNumber";
316
320
  isInAGroup: "isInAGroup";
321
+ isSubscribedToEmailUpdates: "isSubscribedToEmailUpdates";
317
322
  hasDonated: "hasDonated";
318
323
  pastLocations: "pastLocations";
319
324
  utm: "utm";
@@ -332,7 +337,6 @@ export declare const LegacyWahaUserFields: {
332
337
  sharingScore: "sharingScore";
333
338
  isAdvocate: "isAdvocate";
334
339
  isCCWorker: "isCCWorker";
335
- isSubscribedToEmailUpdates: "isSubscribedToEmailUpdates";
336
340
  howFamiliarWithBible: "howFamiliarWithBible";
337
341
  followingFor2Years: "followingFor2Years";
338
342
  notificationsClicked: "notificationsClicked";
@@ -22,6 +22,7 @@ exports.SharedUserReduxState = zod_1.default
22
22
  location: location_1.WahaUserLocation,
23
23
  phoneNumber: zod_1.default.string(),
24
24
  isInAGroup: zod_1.default.boolean(),
25
+ isSubscribedToEmailUpdates: zod_1.default.boolean(),
25
26
  hasDonated: zod_1.default.boolean(),
26
27
  })
27
28
  .partial();
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "waha-shared",
3
- "version": "1.0.513-resend-migration.0",
3
+ "version": "1.0.513",
4
4
  "author": "Waha",
5
5
  "dependencies": {
6
6
  "@types/signale": "^1.4.7",
7
- "resend": "^6.27.0",
8
7
  "signale": "^1.4.0"
9
8
  },
10
9
  "peerDependencies": {
@@ -14,8 +13,12 @@
14
13
  },
15
14
  "description": "Shared assets for Waha's various apps",
16
15
  "devDependencies": {
16
+ "@crowdin/crowdin-api-client": "^1.59.1",
17
+ "@types/react": "^19.3.0",
17
18
  "copyfiles": "^2.4.1",
18
- "typescript": "~5.9.2"
19
+ "react": "^19.3.0",
20
+ "typescript": "~5.9.2",
21
+ "zod": "^4.6.5"
19
22
  },
20
23
  "files": [
21
24
  "dist",
@@ -25,7 +28,7 @@
25
28
  "license": "MIT",
26
29
  "scripts": {
27
30
  "build": "npx tsc && npx copyfiles \"data/**/*.json\" dist",
28
- "update": "bash publish.sh",
29
- "update:preview": "bash publish.sh --preview"
31
+ "update": "npm run build && REGISTRY_VERSION=$(npm view waha-shared version) && npm version $REGISTRY_VERSION --no-git-tag-version --allow-same-version && npm version patch --no-git-tag-version && git add . && (git commit -m 'chore: build waha-shared' || true) && bash -c 'npm publish --access public --registry https://registry.npmjs.org --//registry.npmjs.org/:_authToken=$(grep NPM_TOKEN= ../.env | cut -d \"=\" -f2-)'",
32
+ "updateManual": "(npm whoami || npm login) && npm run build && REGISTRY_VERSION=$(npm view waha-shared version) && npm version $REGISTRY_VERSION --no-git-tag-version --allow-same-version && npm version patch --no-git-tag-version && git add dist package.json && (git commit -m 'chore: build waha-shared' || true) && npm publish"
30
33
  }
31
34
  }
@@ -1,96 +0,0 @@
1
- import { type DripEmailId, type DripLanguageId, type DripSequenceId } from '../types/resend';
2
- /** From: https://resend.com/audience/topics */
3
- export declare enum Topics {
4
- Newsletter = "83d566a5-a42d-47e9-85e7-b43fb5b10298"
5
- }
6
- export declare enum Segments {
7
- /**
8
- * App donors who asked for the thank-you sequence (plus donors carried over
9
- * from AC).
10
- */
11
- Donor = "74ee171a-f00b-4a10-94b6-ef8508635ee1",
12
- /** Everyone who created an app account (enrolled in the onboarding drip). */
13
- AppAccounts = "07e0f635-7e85-4d96-9c31-12a7e34f6869"
14
- }
15
- /** From: https://resend.com/automations/events */
16
- export declare enum Events {
17
- AppDonorSequence = "donor_thank_you_sequence",
18
- NewsletterWelcome = "newsletter_welcome"
19
- }
20
- /** From: https://resend.com/audience/properties */
21
- export declare enum Properties {
22
- /** Set by the donor automation when it runs, so it runs once per contact. */
23
- AppDonorSequence = "donor_thank_you_sequence",
24
- /**
25
- * Set by the welcome automation when it runs, so it runs once per contact.
26
- */
27
- NewsletterWelcome = "newsletter_welcome",
28
- /** The onboarding drip a contact was enrolled in, so it runs once each. */
29
- NewAccountSequence = "drip_sequence",
30
- /** The language a contact's onboarding drip was sent in. */
31
- Language = "language"
32
- }
33
- /**
34
- * Adds a language id to a base id to create a unique drip identifier. A
35
- * sequence's id gives its trigger event (and automation) name; an email's id
36
- * gives its template alias.
37
- */
38
- export declare function dripId(baseId: DripSequenceId | DripEmailId, languageId: DripLanguageId): string;
39
- export declare function isDripLanguage(value: string): value is DripLanguageId;
40
- /**
41
- * Returns the existing contact for an email, or creates one. Existing contacts
42
- * are returned untouched, so their newsletter choice and account-wide
43
- * unsubscribe are never overridden here. New contacts start off the
44
- * newsletter.
45
- */
46
- export declare function upsertResendContact({ token, email, name, }: {
47
- token: string;
48
- email: string;
49
- name?: string;
50
- }): Promise<{
51
- id: string;
52
- created: boolean;
53
- }>;
54
- /** Every contact's email, lowercased. Read-only. */
55
- export declare function listContactEmails(token: string): Promise<Set<string>>;
56
- /**
57
- * An explicit newsletter signup. This is fresh consent, so it opts the contact
58
- * back in even if they opted out of the newsletter or unsubscribed from
59
- * everything before. Bounce suppressions still apply. Also triggers the welcome
60
- * email, which the automation sends only once per contact.
61
- */
62
- export declare function subscribeToNewsletter({ token, email, name, }: {
63
- token: string;
64
- email: string;
65
- name?: string;
66
- }): Promise<void>;
67
- /**
68
- * Starts the donor thank-you sequence for a donor who asked for it and adds
69
- * them to the Donor segment. Their newsletter setting is left alone. Donors who
70
- * don't ask aren't added to Resend at all.
71
- */
72
- export declare function enrollInDonorSequence({ token, email, name, }: {
73
- token: string;
74
- email: string;
75
- name?: string;
76
- }): Promise<void>;
77
- export type DripEnrollment = {
78
- status: 'enrolled';
79
- } | {
80
- status: 'already_enrolled';
81
- sequence: string;
82
- };
83
- /**
84
- * Enrolls an app user in a localized drip sequence: adds them to the App
85
- * Accounts segment and fires the event that starts that sequence and
86
- * language's automation (provisioned by waha-website's
87
- * `scripts/emailDrip/push.ts`). A contact already enrolled in any sequence is
88
- * left alone, so each person gets at most one drip.
89
- */
90
- export declare function enrollInDripSequence({ token, email, firstName, sequence, languageId, }: {
91
- token: string;
92
- email: string;
93
- firstName?: string;
94
- sequence: DripSequenceId;
95
- languageId: DripLanguageId;
96
- }): Promise<DripEnrollment>;
@@ -1,210 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Properties = exports.Events = exports.Segments = exports.Topics = void 0;
4
- exports.dripId = dripId;
5
- exports.isDripLanguage = isDripLanguage;
6
- exports.upsertResendContact = upsertResendContact;
7
- exports.listContactEmails = listContactEmails;
8
- exports.subscribeToNewsletter = subscribeToNewsletter;
9
- exports.enrollInDonorSequence = enrollInDonorSequence;
10
- exports.enrollInDripSequence = enrollInDripSequence;
11
- const resend_1 = require("resend");
12
- const resend_2 = require("../types/resend");
13
- const utils_1 = require("./utils");
14
- /** Rate limits, server errors and network failures (no status code). */
15
- function isRetryable(error) {
16
- const status = error.statusCode;
17
- return status === null || status === 429 || status >= 500;
18
- }
19
- /** Makes a Resend call, retrying transient failures twice with backoff. */
20
- async function withRetry(call) {
21
- for (let attempt = 1;; attempt++) {
22
- const result = await call();
23
- if (!result.error || !isRetryable(result.error) || attempt === 3)
24
- return result;
25
- await new Promise((resolve) => setTimeout(resolve, attempt * 500));
26
- }
27
- }
28
- /**
29
- * The Resend SDK resolves to `{ data, error }` instead of throwing, so an
30
- * unchecked call fails silently and the next step gets `undefined`.
31
- */
32
- function verify(result, context) {
33
- if (result.error)
34
- throw new Error(`${context}: ${result.error.message}`);
35
- if (!result.data)
36
- throw new Error(`${context}: Resend returned no data.`);
37
- return result.data;
38
- }
39
- /** From: https://resend.com/audience/topics */
40
- var Topics;
41
- (function (Topics) {
42
- Topics["Newsletter"] = "83d566a5-a42d-47e9-85e7-b43fb5b10298";
43
- })(Topics || (exports.Topics = Topics = {}));
44
- /* From: https://resend.com/audience/segments */
45
- var Segments;
46
- (function (Segments) {
47
- /**
48
- * App donors who asked for the thank-you sequence (plus donors carried over
49
- * from AC).
50
- */
51
- Segments["Donor"] = "74ee171a-f00b-4a10-94b6-ef8508635ee1";
52
- /** Everyone who created an app account (enrolled in the onboarding drip). */
53
- Segments["AppAccounts"] = "07e0f635-7e85-4d96-9c31-12a7e34f6869";
54
- })(Segments || (exports.Segments = Segments = {}));
55
- /** From: https://resend.com/automations/events */
56
- var Events;
57
- (function (Events) {
58
- Events["AppDonorSequence"] = "donor_thank_you_sequence";
59
- Events["NewsletterWelcome"] = "newsletter_welcome";
60
- // Drip sequence events are per language and thus generated dynamically, so
61
- // they aren't in this enum.
62
- })(Events || (exports.Events = Events = {}));
63
- /** From: https://resend.com/audience/properties */
64
- var Properties;
65
- (function (Properties) {
66
- /** Set by the donor automation when it runs, so it runs once per contact. */
67
- Properties["AppDonorSequence"] = "donor_thank_you_sequence";
68
- /**
69
- * Set by the welcome automation when it runs, so it runs once per contact.
70
- */
71
- Properties["NewsletterWelcome"] = "newsletter_welcome";
72
- /** The onboarding drip a contact was enrolled in, so it runs once each. */
73
- Properties["NewAccountSequence"] = "drip_sequence";
74
- /** The language a contact's onboarding drip was sent in. */
75
- Properties["Language"] = "language";
76
- })(Properties || (exports.Properties = Properties = {}));
77
- /**
78
- * Adds a language id to a base id to create a unique drip identifier. A
79
- * sequence's id gives its trigger event (and automation) name; an email's id
80
- * gives its template alias.
81
- */
82
- function dripId(baseId, languageId) {
83
- return `${baseId}_${languageId}`;
84
- }
85
- function isDripLanguage(value) {
86
- return resend_2.DRIP_LANGUAGES.includes(value);
87
- }
88
- /** Splits a single "name" form field into first and last name. */
89
- function splitName(name) {
90
- const [firstName, ...rest] = (name ?? '').trim().split(/\s+/);
91
- return {
92
- firstName: firstName || undefined,
93
- lastName: rest.join(' ') || undefined,
94
- };
95
- }
96
- /**
97
- * Returns the existing contact for an email, or creates one. Existing contacts
98
- * are returned untouched, so their newsletter choice and account-wide
99
- * unsubscribe are never overridden here. New contacts start off the
100
- * newsletter.
101
- */
102
- async function upsertResendContact({ token, email, name, }) {
103
- const resend = new resend_1.Resend(token);
104
- const normalized = (0, utils_1.lower)(email);
105
- const existing = await withRetry(() => resend.contacts.get({ email: normalized }));
106
- if (existing.data)
107
- return { id: existing.data.id, created: false };
108
- if (existing.error?.name !== 'not_found')
109
- verify(existing, `Looking up contact ${normalized}`);
110
- const created = verify(await withRetry(() => resend.contacts.create({ email: normalized, ...splitName(name) })), `Creating contact ${normalized}`);
111
- return { id: created.id, created: true };
112
- }
113
- /** Every contact's email, lowercased. Read-only. */
114
- async function listContactEmails(token) {
115
- const resend = new resend_1.Resend(token);
116
- const emails = new Set();
117
- for (let after;;) {
118
- const page = verify(await withRetry(() => resend.contacts.list({ limit: 100, ...(after && { after }) })), `Listing contacts after ${after ?? 'the start'}`);
119
- for (const contact of page.data)
120
- emails.add((0, utils_1.lower)(contact.email));
121
- const last = page.data[page.data.length - 1];
122
- if (!page.has_more || !last)
123
- return emails;
124
- after = last.id;
125
- }
126
- }
127
- /**
128
- * An explicit newsletter signup. This is fresh consent, so it opts the contact
129
- * back in even if they opted out of the newsletter or unsubscribed from
130
- * everything before. Bounce suppressions still apply. Also triggers the welcome
131
- * email, which the automation sends only once per contact.
132
- */
133
- async function subscribeToNewsletter({ token, email, name, }) {
134
- const resend = new resend_1.Resend(token);
135
- // Upsert the contact.
136
- const contact = await upsertResendContact({ token, email, name });
137
- // If contact exists, resubscribe them in case they previously unsubscribed.
138
- if (!contact.created)
139
- verify(await withRetry(() => resend.contacts.update({ id: contact.id, unsubscribed: false })), `Resubscribing contact ${contact.id}`);
140
- // Add them to the newsletter topic.
141
- verify(await withRetry(() => resend.contacts.topics.update({
142
- id: contact.id,
143
- topics: [{ id: Topics.Newsletter, subscription: 'opt_in' }],
144
- })), `Subscribing contact ${contact.id} to the newsletter`);
145
- // Send them a welcome email.
146
- verify(await withRetry(() => resend.events.send({
147
- event: Events.NewsletterWelcome,
148
- contactId: contact.id,
149
- })), `Sending ${Events.NewsletterWelcome} for contact ${contact.id}`);
150
- }
151
- /** Adds a contact to a segment. Adding someone already in it is harmless. */
152
- async function addToSegment(resend, contactId, segmentId) {
153
- verify(await withRetry(() => resend.contacts.segments.add({ contactId, segmentId })), `Adding contact ${contactId} to segment ${segmentId}`);
154
- }
155
- /**
156
- * Starts the donor thank-you sequence for a donor who asked for it and adds
157
- * them to the Donor segment. Their newsletter setting is left alone. Donors who
158
- * don't ask aren't added to Resend at all.
159
- */
160
- async function enrollInDonorSequence({ token, email, name, }) {
161
- const resend = new resend_1.Resend(token);
162
- const { id } = await upsertResendContact({ token, email, name });
163
- await addToSegment(resend, id, Segments.Donor);
164
- verify(await withRetry(() => resend.events.send({
165
- event: Events.AppDonorSequence,
166
- contactId: id,
167
- })), `Sending ${Events.AppDonorSequence} for contact ${id}`);
168
- }
169
- /**
170
- * Enrolls an app user in a localized drip sequence: adds them to the App
171
- * Accounts segment and fires the event that starts that sequence and
172
- * language's automation (provisioned by waha-website's
173
- * `scripts/emailDrip/push.ts`). A contact already enrolled in any sequence is
174
- * left alone, so each person gets at most one drip.
175
- */
176
- async function enrollInDripSequence({ token, email, firstName, sequence, languageId, }) {
177
- const resend = new resend_1.Resend(token);
178
- const normalized = (0, utils_1.lower)(email);
179
- const existing = await withRetry(() => resend.contacts.get({ email: normalized }));
180
- if (existing.error?.name !== 'not_found' && !existing.data)
181
- verify(existing, `Looking up contact ${normalized}`);
182
- const contact = existing.data;
183
- const enrolledIn = contact?.properties?.[Properties.NewAccountSequence]?.value;
184
- if (enrolledIn)
185
- return { status: 'already_enrolled', sequence: `${enrolledIn}` };
186
- /*
187
- * The sequence property is deliberately not set yet. Marking a contact
188
- * enrolled before the event below fires would strand them if it failed: the
189
- * check above would skip them forever though their drip never started.
190
- */
191
- const properties = { [Properties.Language]: languageId };
192
- const id = contact
193
- ? verify(await withRetry(() => resend.contacts.update({ id: contact.id, firstName, properties })), `Updating contact ${contact.id}`).id
194
- : verify(await withRetry(() => resend.contacts.create({ email: normalized, firstName, properties })), `Creating contact ${normalized}`).id;
195
- await addToSegment(resend, id, Segments.AppAccounts);
196
- const event = dripId(sequence, languageId);
197
- verify(await withRetry(() => resend.events.send({ event, contactId: id })), `Sending ${event} for contact ${id}`);
198
- /*
199
- * The drip has started, so a failure here only weakens the guard against a
200
- * second enrollment. It's logged rather than failing a request that
201
- * otherwise succeeded.
202
- */
203
- const marked = await withRetry(() => resend.contacts.update({
204
- id,
205
- properties: { [Properties.NewAccountSequence]: sequence },
206
- }));
207
- if (marked.error)
208
- console.error(`Marking contact ${id} enrolled failed:`, marked.error);
209
- return { status: 'enrolled' };
210
- }
@@ -1,58 +0,0 @@
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
- }, z.core.$strip>;
44
- export type DonorSequenceBody = z.infer<typeof DonorSequenceBody>;
45
- /**
46
- * `POST /api/subscribe-drip` (JSON, Firebase ID token). Any language is
47
- * accepted so callers don't need to know which ones have a drip; unsupported
48
- * ones are a no-op.
49
- */
50
- export declare const SubscribeDripBody: z.ZodObject<{
51
- sequence: z.ZodEnum<{
52
- drip_believer: "drip_believer";
53
- drip_seeker: "drip_seeker";
54
- }>;
55
- languageId: z.ZodString;
56
- firstName: z.ZodOptional<z.ZodString>;
57
- }, z.core.$strip>;
58
- export type SubscribeDripBody = z.infer<typeof SubscribeDripBody>;
@@ -1,62 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SubscribeDripBody = exports.DonorSequenceBody = exports.ContactFormBody = exports.NewsletterSignupBody = exports.DripLanguageId = exports.DRIP_LANGUAGES = exports.DripSequenceId = void 0;
7
- exports.dripLanguagesWithStatus = dripLanguagesWithStatus;
8
- const zod_1 = __importDefault(require("zod"));
9
- const languages_1 = require("../data/languages");
10
- /** The two onboarding drip audiences a new signup can be enrolled in. */
11
- exports.DripSequenceId = zod_1.default.enum(['drip_believer', 'drip_seeker']);
12
- /** Ids of languages whose `status.drip` in languages.json is one of these. */
13
- function dripLanguagesWithStatus(...statuses) {
14
- return languages_1.languages
15
- .filter((language) => statuses.includes(language.status.drip ?? 'notStarted'))
16
- .map((language) => language.languageId);
17
- }
18
- /**
19
- * Languages the onboarding drip is live in: provisioned in Resend and enrolled
20
- * into on signup. Driven by `status.drip` in languages.json.
21
- */
22
- exports.DRIP_LANGUAGES = dripLanguagesWithStatus('launched');
23
- exports.DripLanguageId = zod_1.default
24
- .string()
25
- .refine((id) => exports.DRIP_LANGUAGES.includes(id), 'Not a launched drip language.');
26
- /*
27
- * Request bodies for the website endpoints that write to Resend. The website
28
- * parses incoming requests with these, and the app types what it sends with
29
- * them. Auth (a reCAPTCHA token or a Firebase ID token) isn't part of the body
30
- * schemas; each endpoint checks it separately.
31
- */
32
- const Email = zod_1.default.string().trim().pipe(zod_1.default.email());
33
- const Name = zod_1.default.string().trim().max(200);
34
- /** `POST /api/submit-subscribe` (form data): an explicit newsletter signup. */
35
- exports.NewsletterSignupBody = zod_1.default.object({
36
- email: Email,
37
- name: Name.optional(),
38
- });
39
- /** `POST /api/submit-contact` (form data): the website's contact form. */
40
- exports.ContactFormBody = zod_1.default.object({
41
- email: Email,
42
- name: Name,
43
- message: zod_1.default.string().trim().min(1).max(5000),
44
- });
45
- /**
46
- * `POST /api/add-to-donation-sequence` (JSON): starts the donor thank-you
47
- * drip.
48
- */
49
- exports.DonorSequenceBody = zod_1.default.object({
50
- email: Email,
51
- name: Name.optional(),
52
- });
53
- /**
54
- * `POST /api/subscribe-drip` (JSON, Firebase ID token). Any language is
55
- * accepted so callers don't need to know which ones have a drip; unsupported
56
- * ones are a no-op.
57
- */
58
- exports.SubscribeDripBody = zod_1.default.object({
59
- sequence: exports.DripSequenceId,
60
- languageId: zod_1.default.string(),
61
- firstName: Name.optional(),
62
- });