Create Loops profiles on user signup (#180)

This commit is contained in:
Ben Senescu 2026-05-11 21:24:23 -04:00 committed by GitHub
parent ba9f30fde0
commit 740c899764
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 95 additions and 0 deletions

3
src/env.d.ts vendored
View File

@ -13,6 +13,9 @@ declare namespace Cloudflare {
POSTHOG_HOST?: string;
BETTER_AUTH_SECRET?: string;
BETTER_AUTH_URL?: string;
LOOPS_API_KEY?: string;
LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID?: string;
LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID?: string;
// DataForSEO API Basic auth value (base64 of login:password)
DATAFORSEO_API_KEY: string;

View File

@ -9,6 +9,7 @@ import { getOrCreateDefaultHostedOrganization } from "@/server/auth/default-host
import {
sendHostedPasswordResetEmail,
sendHostedVerificationEmail,
upsertHostedSignupContact,
} from "@/server/email/loops";
const hostedBaseUrlSchema = z
@ -61,6 +62,25 @@ function createAuth() {
}),
plugins: [...baseAuthConfig.plugins, tanstackStartCookies()],
databaseHooks: {
user: {
create: {
after: async (user) => {
try {
await upsertHostedSignupContact({
userId: user.id,
email: user.email,
name: user.name,
});
} catch (error) {
console.error("Failed to create Loops profile for signup:", {
userId: user.id,
email: user.email,
error,
});
}
},
},
},
session: {
create: {
before: async (session) => {

View File

@ -1,6 +1,7 @@
import { env } from "cloudflare:workers";
const LOOPS_TRANSACTIONAL_URL = "https://app.loops.so/api/v1/transactional";
const LOOPS_CONTACT_UPDATE_URL = "https://app.loops.so/api/v1/contacts/update";
function getRequiredEnv(name: string) {
const value: unknown = Reflect.get(env, name);
@ -67,6 +68,77 @@ async function sendLoopsTransactionalEmail({
);
}
function getOptionalEnv(name: string) {
const value: unknown = Reflect.get(env, name);
const trimmed = typeof value === "string" ? value.trim() : "";
return trimmed || null;
}
function getContactNameParts(name: string | null | undefined) {
const trimmedName = name?.trim();
if (!trimmedName) {
return {};
}
const [firstName, ...lastNameParts] = trimmedName.split(/\s+/);
const lastName = lastNameParts.join(" ");
return {
firstName,
...(lastName ? { lastName } : {}),
};
}
export async function upsertHostedSignupContact({
userId,
email,
name,
}: {
userId: string;
email: string;
name?: string | null;
}) {
const apiKey = getOptionalEnv("LOOPS_API_KEY");
if (!apiKey) {
console.warn(
"Skipping Loops signup contact sync: LOOPS_API_KEY is not set",
);
return;
}
const response = await fetch(LOOPS_CONTACT_UPDATE_URL, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
email,
userId,
source: "openseo-signup",
userGroup: "app-user",
...getContactNameParts(name),
}),
});
if (response.ok) {
return;
}
const errorPayload = await response.json().catch(() => null);
console.error("Loops signup contact sync error:", {
status: response.status,
email,
userId,
errorPayload,
});
throw new Error(`Failed to sync Loops signup contact (${response.status})`);
}
export async function sendHostedVerificationEmail({
email,
confirmationUrl,