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:
@@ -1095,6 +1095,28 @@
|
||||
"description": "The emoji associated with the contact book",
|
||||
"example": "📙"
|
||||
},
|
||||
"doubleOptInEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether double opt-in is enabled for new contacts",
|
||||
"example": true
|
||||
},
|
||||
"doubleOptInFrom": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "From address used for double opt-in emails (must use a verified domain)",
|
||||
"example": "Newsletter <hello@example.com>"
|
||||
},
|
||||
"doubleOptInSubject": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Subject line used for double opt-in confirmation email",
|
||||
"example": "Please confirm your subscription"
|
||||
},
|
||||
"doubleOptInContent": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Email editor JSON content used for double opt-in confirmation"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"description": "The creation timestamp"
|
||||
@@ -1142,6 +1164,23 @@
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"doubleOptInEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether double opt-in is enabled for new contacts"
|
||||
},
|
||||
"doubleOptInFrom": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "From address used for double opt-in emails (must use a verified domain)"
|
||||
},
|
||||
"doubleOptInSubject": {
|
||||
"type": "string",
|
||||
"description": "Subject line used for double opt-in confirmation email"
|
||||
},
|
||||
"doubleOptInContent": {
|
||||
"type": "string",
|
||||
"description": "Email editor JSON content used for double opt-in confirmation"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
@@ -1165,6 +1204,10 @@
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"emoji": { "type": "string" },
|
||||
"doubleOptInEnabled": { "type": "boolean", "description": "Whether double opt-in is enabled for new contacts" },
|
||||
"doubleOptInFrom": { "type": "string", "nullable": true, "description": "From address used for double opt-in emails" },
|
||||
"doubleOptInSubject": { "type": "string", "nullable": true, "description": "Subject line used for double opt-in confirmation email" },
|
||||
"doubleOptInContent": { "type": "string", "nullable": true, "description": "Email editor JSON content used for double opt-in confirmation" },
|
||||
"createdAt": { "type": "string" },
|
||||
"updatedAt": { "type": "string" }
|
||||
},
|
||||
@@ -1210,6 +1253,10 @@
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"emoji": { "type": "string" },
|
||||
"doubleOptInEnabled": { "type": "boolean", "description": "Whether double opt-in is enabled for new contacts" },
|
||||
"doubleOptInFrom": { "type": "string", "nullable": true, "description": "From address used for double opt-in emails" },
|
||||
"doubleOptInSubject": { "type": "string", "nullable": true, "description": "Subject line used for double opt-in confirmation email" },
|
||||
"doubleOptInContent": { "type": "string", "nullable": true, "description": "Email editor JSON content used for double opt-in confirmation" },
|
||||
"createdAt": { "type": "string" },
|
||||
"updatedAt": { "type": "string" },
|
||||
"_count": {
|
||||
@@ -1279,6 +1326,23 @@
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"doubleOptInEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether double opt-in is enabled for new contacts"
|
||||
},
|
||||
"doubleOptInFrom": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "From address used for double opt-in emails (must use a verified domain)"
|
||||
},
|
||||
"doubleOptInSubject": {
|
||||
"type": "string",
|
||||
"description": "Subject line used for double opt-in confirmation email"
|
||||
},
|
||||
"doubleOptInContent": {
|
||||
"type": "string",
|
||||
"description": "Email editor JSON content used for double opt-in confirmation"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1301,6 +1365,10 @@
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"emoji": { "type": "string" },
|
||||
"doubleOptInEnabled": { "type": "boolean", "description": "Whether double opt-in is enabled for new contacts" },
|
||||
"doubleOptInFrom": { "type": "string", "nullable": true, "description": "From address used for double opt-in emails" },
|
||||
"doubleOptInSubject": { "type": "string", "nullable": true, "description": "Subject line used for double opt-in confirmation email" },
|
||||
"doubleOptInContent": { "type": "string", "nullable": true, "description": "Email editor JSON content used for double opt-in confirmation" },
|
||||
"createdAt": { "type": "string" },
|
||||
"updatedAt": { "type": "string" }
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user