789a818c6b
Extends the checkout pattern (inline red error text, refocus on submit if invalid) to all four newsletter-signup entry points. Two of them (WeeklyImpulsesHero's inline hero form on /newsletter, and /challenge's EmailCapture) turned out to be completely non-functional before this too — same static-markup-with-no-onSubmit issue as Newsletter.tsx/NewsletterModal.tsx had, just missed in the previous pass since they're separate components sharing only the visual pattern, not the code. Consolidated the shared email+consent+submit state (previously duplicated per-component) into useNewsletterSignup.ts, and pulled the plain email-format regex (previously duplicated in CheckoutContent.tsx and the subscribe route) into lib/email.ts as a single source of truth. /challenge's EmailCapture is now its own client component (app/challenge/components/EmailCapture.tsx) since its parent page is an async Server Component and can't hold form state itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
51 lines
2.0 KiB
TypeScript
51 lines
2.0 KiB
TypeScript
// Server-only — syncs newsletter opt-ins to Brevo's Contacts API. Brevo
|
|
// owns everything downstream of that (list membership, unsubscribe links,
|
|
// and whatever Welcome Flow automation is configured on the list in
|
|
// Brevo's own UI — that automation isn't manageable via their public API
|
|
// at all, only contacts/lists are). This app never sends marketing mail
|
|
// itself; it only ever hands Brevo the contact + consent.
|
|
const BREVO_API_URL = "https://api.brevo.com/v3/contacts";
|
|
|
|
export type BrevoSyncResult = { ok: true } | { ok: false; reason: string };
|
|
|
|
export type NewsletterOptInSource = "checkout" | "newsletter-page" | "newsletter-modal" | "newsletter-hero" | "challenge";
|
|
|
|
// `source` becomes a Brevo contact attribute so campaigns/segments can
|
|
// tell a checkout opt-in apart from the standalone signup forms without
|
|
// needing separate lists.
|
|
export async function upsertNewsletterContact(
|
|
email: string,
|
|
source: NewsletterOptInSource,
|
|
): Promise<BrevoSyncResult> {
|
|
const apiKey = process.env.BREVO_API_KEY;
|
|
const listId = process.env.BREVO_LIST_ID;
|
|
if (!apiKey || !listId) {
|
|
return { ok: false, reason: "BREVO_API_KEY/BREVO_LIST_ID nicht konfiguriert." };
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(BREVO_API_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"api-key": apiKey,
|
|
},
|
|
body: JSON.stringify({
|
|
email,
|
|
listIds: [Number(listId)],
|
|
updateEnabled: true,
|
|
attributes: { OPT_IN_SOURCE: source },
|
|
}),
|
|
signal: AbortSignal.timeout(8000),
|
|
});
|
|
// 204 for both a fresh contact and an existing one (updateEnabled
|
|
// above merges the list membership onto the existing contact instead
|
|
// of erroring).
|
|
if (res.ok || res.status === 204) return { ok: true };
|
|
const body = await res.json().catch(() => null);
|
|
return { ok: false, reason: body?.message ?? `Brevo antwortete mit ${res.status}` };
|
|
} catch (err) {
|
|
return { ok: false, reason: err instanceof Error ? err.message : "Brevo ist gerade nicht erreichbar." };
|
|
}
|
|
}
|