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.
- package/dist/data/languages/index.d.ts +0 -1
- package/dist/data/languages/languages.json +0 -2
- package/dist/data/languages/languages.schema.json +0 -5
- package/dist/data/languages/languages.zod.d.ts +0 -12
- package/dist/data/languages/languages.zod.js +0 -1
- package/dist/functions/utils.d.ts +0 -1
- package/dist/functions/utils.js +0 -4
- package/dist/types/dripSequence.d.ts +5 -1
- package/dist/types/dripSequence.js +1 -1
- package/dist/types/languages.d.ts +0 -6
- package/dist/types/users.d.ts +5 -1
- package/dist/types/users.js +1 -0
- package/package.json +8 -5
- package/dist/functions/resend.d.ts +0 -96
- package/dist/functions/resend.js +0 -210
- package/dist/types/resend.d.ts +0 -58
- package/dist/types/resend.js +0 -62
|
@@ -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({
|
package/dist/functions/utils.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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";
|
package/dist/types/users.d.ts
CHANGED
|
@@ -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";
|
package/dist/types/users.js
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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.
|
|
29
|
-
"
|
|
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>;
|
package/dist/functions/resend.js
DELETED
|
@@ -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
|
-
}
|
package/dist/types/resend.d.ts
DELETED
|
@@ -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>;
|
package/dist/types/resend.js
DELETED
|
@@ -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
|
-
});
|