Skip to content

Commit d94e819

Browse files
committed
feat: added audience email added
1 parent 70b535f commit d94e819

5 files changed

Lines changed: 170 additions & 3 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { beforeEach, describe, expect, it, mock } from "bun:test";
2+
3+
const addUserToDefaultAudienceMock = mock(
4+
(async () => true) as (...args: unknown[]) => Promise<boolean>
5+
);
6+
7+
mock.module("@cossistant/transactional", () => ({
8+
addUserToDefaultAudience: addUserToDefaultAudienceMock,
9+
}));
10+
11+
const authUserAudienceModulePromise = import("./auth-user-audience");
12+
13+
function spyConsole() {
14+
const warnSpy = mock(() => {});
15+
const errorSpy = mock(() => {});
16+
const originalWarn = console.warn;
17+
const originalError = console.error;
18+
19+
console.warn = warnSpy as unknown as typeof console.warn;
20+
console.error = errorSpy as unknown as typeof console.error;
21+
22+
return {
23+
warnSpy,
24+
errorSpy,
25+
restore: () => {
26+
console.warn = originalWarn;
27+
console.error = originalError;
28+
},
29+
};
30+
}
31+
32+
describe("syncUserToDefaultResendAudience", () => {
33+
beforeEach(() => {
34+
addUserToDefaultAudienceMock.mockReset();
35+
addUserToDefaultAudienceMock.mockResolvedValue(true);
36+
});
37+
38+
it("calls addUserToDefaultAudience when user email exists", async () => {
39+
const { syncUserToDefaultResendAudience } =
40+
await authUserAudienceModulePromise;
41+
42+
await syncUserToDefaultResendAudience({
43+
id: "user-1",
44+
email: "person@example.com",
45+
name: "Person Example",
46+
});
47+
48+
expect(addUserToDefaultAudienceMock).toHaveBeenCalledTimes(1);
49+
expect(addUserToDefaultAudienceMock).toHaveBeenCalledWith(
50+
"person@example.com",
51+
"Person Example"
52+
);
53+
});
54+
55+
it("skips and logs a warning when email is missing", async () => {
56+
const { syncUserToDefaultResendAudience } =
57+
await authUserAudienceModulePromise;
58+
const { warnSpy, restore } = spyConsole();
59+
60+
try {
61+
await syncUserToDefaultResendAudience({
62+
id: "user-2",
63+
});
64+
} finally {
65+
restore();
66+
}
67+
68+
expect(addUserToDefaultAudienceMock).not.toHaveBeenCalled();
69+
expect(warnSpy).toHaveBeenCalledTimes(1);
70+
});
71+
72+
it("does not throw and logs an error when enrollment returns false", async () => {
73+
const { syncUserToDefaultResendAudience } =
74+
await authUserAudienceModulePromise;
75+
const { errorSpy, restore } = spyConsole();
76+
addUserToDefaultAudienceMock.mockResolvedValue(false);
77+
78+
try {
79+
await syncUserToDefaultResendAudience({
80+
id: "user-3",
81+
email: "person@example.com",
82+
name: "Person Example",
83+
});
84+
} finally {
85+
restore();
86+
}
87+
88+
expect(addUserToDefaultAudienceMock).toHaveBeenCalledTimes(1);
89+
expect(errorSpy).toHaveBeenCalledTimes(1);
90+
});
91+
92+
it("does not throw and logs an error when enrollment throws", async () => {
93+
const { syncUserToDefaultResendAudience } =
94+
await authUserAudienceModulePromise;
95+
const { errorSpy, restore } = spyConsole();
96+
addUserToDefaultAudienceMock.mockRejectedValue(new Error("resend failed"));
97+
98+
try {
99+
await syncUserToDefaultResendAudience({
100+
id: "user-4",
101+
email: "person@example.com",
102+
name: "Person Example",
103+
});
104+
} finally {
105+
restore();
106+
}
107+
108+
expect(addUserToDefaultAudienceMock).toHaveBeenCalledTimes(1);
109+
expect(errorSpy).toHaveBeenCalledTimes(1);
110+
});
111+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { addUserToDefaultAudience } from "@cossistant/transactional";
2+
3+
type AuthUserLike = {
4+
id?: unknown;
5+
email?: unknown;
6+
name?: unknown;
7+
};
8+
9+
function toNonEmptyString(value: unknown): string | undefined {
10+
if (typeof value !== "string") {
11+
return;
12+
}
13+
14+
const trimmed = value.trim();
15+
return trimmed.length > 0 ? trimmed : undefined;
16+
}
17+
18+
export async function syncUserToDefaultResendAudience(
19+
user: AuthUserLike
20+
): Promise<void> {
21+
const userId = toNonEmptyString(user.id);
22+
const email = toNonEmptyString(user.email);
23+
const name = toNonEmptyString(user.name);
24+
25+
if (!email) {
26+
console.warn(
27+
`[auth] Skipping Resend audience sync: missing email${userId ? ` (userId: ${userId})` : ""}`
28+
);
29+
return;
30+
}
31+
32+
try {
33+
const didAddUser = await addUserToDefaultAudience(email, name);
34+
if (!didAddUser) {
35+
console.error(
36+
`[auth] Failed to add user to default Resend audience: ${email}${userId ? ` (userId: ${userId})` : ""}`
37+
);
38+
}
39+
} catch (error) {
40+
console.error(
41+
`[auth] Error adding user to default Resend audience: ${email}${userId ? ` (userId: ${userId})` : ""}`,
42+
error
43+
);
44+
}
45+
}

apps/api/src/lib/auth.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "better-auth/plugins";
1414
import type { BetterAuthPlugin } from "better-auth/types";
1515
import React from "react";
16+
import { syncUserToDefaultResendAudience } from "./auth-user-audience";
1617
import polarClient from "./polar";
1718

1819
// Needed for email templates
@@ -29,6 +30,15 @@ export const auth = betterAuth({
2930
...schema,
3031
},
3132
}),
33+
databaseHooks: {
34+
user: {
35+
create: {
36+
after: async (user) => {
37+
await syncUserToDefaultResendAudience(user);
38+
},
39+
},
40+
},
41+
},
3242
emailAndPassword: {
3343
enabled: true,
3444
autoSignIn: true,

apps/api/src/workflows/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { Hono } from "hono";
2-
import aiAgentWorkflow from "./ai-agent";
32
import messageWorkflow from "./message";
43

54
const workflowsRouters = new Hono();
65

76
// Include all workflows below
87
workflowsRouters.route("/message", messageWorkflow);
9-
workflowsRouters.route("/ai-agent", aiAgentWorkflow);
108

119
export { workflowsRouters };

packages/transactional/resend-utils/constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Email system constants
22
export const ANTHONY_EMAIL = "anthony@cossistant.com";
33
export const TRANSACTIONAL_EMAIL_DOMAIN = "updates.cossistant.com";
4-
export const RESEND_AUDIENCE_ID = "668cc440-8027-4a31-9f8f-2633efbf12a4";
4+
export const DEFAULT_RESEND_AUDIENCE_ID =
5+
"668cc440-8027-4a31-9f8f-2633efbf12a4";
6+
export const RESEND_AUDIENCE_ID =
7+
process.env.RESEND_AUDIENCE_ID?.trim() || DEFAULT_RESEND_AUDIENCE_ID;
58

69
// Email variant to sender mapping (only notifications and marketing)
710
export const VARIANT_TO_FROM_MAP = {

0 commit comments

Comments
 (0)