feat: add customizable contact double opt-in flow (#350)
* 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>
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ContactBook"
|
||||
ADD COLUMN "doubleOptInEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "doubleOptInSubject" TEXT,
|
||||
ADD COLUMN "doubleOptInContent" TEXT;
|
||||
|
||||
-- Backfill legacy unsubscribed contacts so pending state can rely on NULL
|
||||
UPDATE "Contact"
|
||||
SET "unsubscribeReason" = 'UNSUBSCRIBED'
|
||||
WHERE "subscribed" = false
|
||||
AND "unsubscribeReason" IS NULL;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ContactBook"
|
||||
ADD COLUMN "doubleOptInFrom" TEXT;
|
||||
@@ -293,15 +293,19 @@ model EmailEvent {
|
||||
}
|
||||
|
||||
model ContactBook {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
teamId Int
|
||||
properties Json
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
emoji String @default("📙")
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
contacts Contact[]
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
teamId Int
|
||||
properties Json
|
||||
doubleOptInEnabled Boolean @default(false)
|
||||
doubleOptInFrom String?
|
||||
doubleOptInSubject String?
|
||||
doubleOptInContent String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
emoji String @default("📙")
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
contacts Contact[]
|
||||
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user