e3e9635a5f
* feat: add customizable contact double opt-in flow * test: add double opt-in service coverage * fix: address review comments for double opt-in PR - Make pending status conditional on doubleOptInEnabled flag - Backfill legacy unsubscribeReason for reliable pending detection - Add doubleOptInContent to contact book listing select - Fix duplicate toast on DOI editor subject save failure - Harden searchParams parsing against string[] values - Make default DOI template use link mark for clickable URL - Make public API create+update atomic via transaction - Prevent contact upsert failure when DOI email send fails - Fix empty string template variable replacement Co-authored-by: opencode <opencode@anthropic.com> * fix: harden double opt-in confirmation safeguards Preserve explicit unsubscribe intent in DOI flows and prevent confirmation links from re-subscribing opted-out contacts. Also sanitize subscribe-page error messaging and use timing-safe hash comparison for link verification. * ui stuff * fix: require doubleOptInUrl in double opt-in templates * feat: add configurable from address for double opt-in emails * feat: add resend confirmation flow for pending contacts * fix: move subscribe confirmation to explicit POST flow * test: add contact book public API endpoint coverage * docs: add double opt-in documentation and update OpenAPI spec Add a user guide for the double opt-in feature covering setup, contact statuses, email customization, template variables, and best practices. Update the OpenAPI spec to include doubleOptIn fields in all contactBook request/response schemas. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: opencode <opencode@anthropic.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const ContactBookSchema = z.object({
|
|
id: z.string().openapi({
|
|
description: "The ID of the contact book",
|
|
example: "clx1234567890",
|
|
}),
|
|
name: z.string().openapi({
|
|
description: "The name of the contact book",
|
|
example: "Newsletter Subscribers",
|
|
}),
|
|
teamId: z.number().openapi({ description: "The ID of the team", example: 1 }),
|
|
properties: z.record(z.string()).openapi({
|
|
description: "Custom properties for the contact book",
|
|
example: { customField1: "value1" },
|
|
}),
|
|
emoji: z.string().openapi({
|
|
description: "The emoji associated with the contact book",
|
|
example: "📙",
|
|
}),
|
|
doubleOptInEnabled: z.boolean().optional().openapi({
|
|
description: "Whether double opt-in is enabled for new contacts",
|
|
example: true,
|
|
}),
|
|
doubleOptInFrom: z.string().nullable().optional().openapi({
|
|
description:
|
|
"From address used for double opt-in emails (must use a verified domain)",
|
|
example: "Newsletter <hello@example.com>",
|
|
}),
|
|
doubleOptInSubject: z.string().nullable().optional().openapi({
|
|
description: "Subject line used for double opt-in confirmation email",
|
|
example: "Please confirm your subscription",
|
|
}),
|
|
doubleOptInContent: z.string().nullable().optional().openapi({
|
|
description:
|
|
"Email editor JSON content used for double opt-in confirmation",
|
|
}),
|
|
createdAt: z.string().openapi({ description: "The creation timestamp" }),
|
|
updatedAt: z.string().openapi({ description: "The last update timestamp" }),
|
|
_count: z
|
|
.object({
|
|
contacts: z
|
|
.number()
|
|
.openapi({ description: "The number of contacts in the contact book" }),
|
|
})
|
|
.optional(),
|
|
});
|