Skip to content

Added sender details customization for welcome emails#27192

Merged
cmraible merged 7 commits intomainfrom
NY-1164_welcome-email-sender-info
Apr 7, 2026
Merged

Added sender details customization for welcome emails#27192
cmraible merged 7 commits intomainfrom
NY-1164_welcome-email-sender-info

Conversation

@troyciesco
Copy link
Copy Markdown
Contributor

@troyciesco troyciesco commented Apr 6, 2026

closes https://linear.app/ghost/issue/NY-1164/add-ability-to-edit-sender-info-using-existing-verification-flow

Summary

Adds the ability for admins to customize sender name, email, and reply-to address for welcome emails, shared across both free and paid welcome email types.

Changes

Backend

  • New PUT /automated_emails/senders/ endpoint to update sender_name, sender_email, and sender_reply_to across all welcome emails in a single call
  • New PUT /automated_emails/verifications/ endpoint to verify pending email changes via single-use token (magic link)
  • MemberWelcomeEmailService now uses per-automated-email sender overrides when sending, falling back to newsletter/default settings
  • Email verification flow for sender_reply_to changes (uses existing SingleUseTokenProvider)
  • Test sends now also respect per-email sender overrides

Frontend

  • New useEditAutomatedEmailSenders and useVerifyAutomatedEmailSender API hooks
  • EmailPreview component updated to show From/Reply-to in envelope header, with optional recipient/subject line visibility props
  • Member emails settings page handles verifyEmail query param on load, shows confirmation modal after successful verification

Tests

  • E2E API tests for shared sender editing and token-based verification
  • Integration tests confirming sender overrides are applied when sending real and test welcome emails

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds end-to-end support for editing and verifying shared sender fields for member welcome automated emails. Backend: new admin API endpoints (PUT /automated_emails/senders, PUT /automated_emails/verifications), input validators, service methods (editSharedSenderOptions, verifySenderPropertyUpdate), single-use token provider integration, and controller/route wiring. Frontend: new API response types and mutation hooks, edit/verify mutations, sender-resolution utilities, default welcome-email helpers, updated welcome-email modals/previews, and hook changes. Tests: new unit, integration, acceptance, and e2e tests covering resolver logic, UI flows, API interactions, and token verification.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Added sender details customization for welcome emails' accurately summarizes the main change in the changeset—adding the ability to customize sender name, email, and reply-to address for welcome emails.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing backend endpoints, frontend hooks, UI updates, and test additions that align with the file-level summaries.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch NY-1164_welcome-email-sender-info

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cmraible cmraible changed the base branch from main to chris-ny-1197-rebased April 7, 2026 18:31
Base automatically changed from chris-ny-1197-rebased to main April 7, 2026 18:43
@cmraible cmraible force-pushed the NY-1164_welcome-email-sender-info branch from 8237b8c to b6c9434 Compare April 7, 2026 18:51
Widened newsletter-emails function parameter types to accept Pick<Newsletter>
and removed redundant require redeclaration in welcome-emails test.
@cmraible
Copy link
Copy Markdown
Collaborator

cmraible commented Apr 7, 2026

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (6)
ghost/core/test/integration/services/member-welcome-emails.test.js (1)

488-526: Consider resetting memberWelcomeEmailService.api before init() for consistency.

The test at line 452 resets memberWelcomeEmailService.api = null before calling init(), but this test (line 489) calls init() directly. While this may work due to test isolation, being explicit about the reset would be more consistent with the pattern used elsewhere in this file (e.g., lines 471-472, 539-540, 554-556).

🔧 Suggested consistency improvement
 it('uses automated sender overrides for test welcome emails', async function () {
+    memberWelcomeEmailService.api = null;
     memberWelcomeEmailService.init();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/integration/services/member-welcome-emails.test.js` around
lines 488 - 526, Reset memberWelcomeEmailService.api to null before calling
memberWelcomeEmailService.init() in this test to match the pattern used
elsewhere; locate the test function that currently calls
memberWelcomeEmailService.init() (the spec using
memberWelcomeEmailService.api.sendTestEmail) and add a line that sets
memberWelcomeEmailService.api = null immediately before invoking
memberWelcomeEmailService.init() so the service is initialized consistently.
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)

80-88: Consider validating email format for sender email fields.

The editSenders validator checks that sender_email and sender_reply_to are strings when provided, but doesn't validate they are valid email addresses. The sendTestEmail validator (lines 101-105) uses validator.isEmail() for this purpose. Consider adding similar validation here to catch invalid emails early.

💡 Suggested enhancement
 editSenders(apiConfig, frame) {
     const senderName = frame.data.sender_name;
     const senderEmail = frame.data.sender_email;
     const senderReplyTo = frame.data.sender_reply_to;

     validateOptionalStringField(senderName, 'Sender name must be a string');
     validateOptionalStringField(senderEmail, 'Sender email must be a string');
     validateOptionalStringField(senderReplyTo, 'Reply-to email must be a string');
+
+    if (senderEmail && !validator.isEmail(senderEmail)) {
+        throw new ValidationError({
+            message: 'Sender email must be a valid email address'
+        });
+    }
+
+    if (senderReplyTo && !validator.isEmail(senderReplyTo)) {
+        throw new ValidationError({
+            message: 'Reply-to email must be a valid email address'
+        });
+    }
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js`
around lines 80 - 88, The editSenders validator currently ensures sender_name,
sender_email, and sender_reply_to are strings but does not validate email
format; update editSenders to, after calling validateOptionalStringField for
sender_email and sender_reply_to, also check their format using the same
validator.isEmail() calls/pattern used in sendTestEmail so invalid addresses are
rejected early (use the same error messages or similar descriptive errors) and
ensure you reference editSenders, sender_email, sender_reply_to,
validateOptionalStringField and validator.isEmail when locating and implementing
the change.
apps/admin-x-settings/src/components/settings/membership/member-emails.tsx (1)

163-206: Consider clearing the verification token from URL after processing.

The verification effect runs whenever the component mounts with a verifyEmail token in the URL. If the user navigates away and returns to the same URL (e.g., via browser history), the verification will be attempted again, likely failing with an expired/used token error.

♻️ Suggested approach
 useEffect(() => {
     if (!verifyEmailToken || !window.location.href.includes('memberemails')) {
         return;
     }

     const verify = async () => {
         try {
             const {meta: {email_verified: emailVerified} = {}} = await verifySenderUpdate({token: verifyEmailToken});
+
+            // Clear token from URL to prevent re-verification attempts
+            const url = new URL(window.location.href);
+            url.searchParams.delete('verifyEmail');
+            window.history.replaceState({}, '', url.toString());

             let title = 'Sender email verified';
             // ... rest of success handling
         } catch (e) {
+            // Clear token from URL even on error
+            const url = new URL(window.location.href);
+            url.searchParams.delete('verifyEmail');
+            window.history.replaceState({}, '', url.toString());
+
             let prompt = 'There was an error verifying your email address. Try again later.';
             // ... rest of error handling
         }
     };

     verify();
 }, [handleError, verifyEmailToken, verifySenderUpdate]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-settings/src/components/settings/membership/member-emails.tsx`
around lines 163 - 206, After successful or failed verification in the
useEffect's verify() (which calls verifySenderUpdate with verifyEmailToken),
remove the verifyEmailToken from the URL so the effect won't re-run on
navigation; update the browser URL using history.replaceState (or
URLSearchParams on window.location.href) to strip the token/query parameter (or
fragment) after handling the response (both in the try and catch paths),
referencing verifyEmailToken, verifySenderUpdate and the verify() function so
the token is cleared immediately after processing.
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx (1)

466-481: Sender hydration effect may cause stale closures with validation.

The hydration effect runs when automatedEmailsData becomes available and sets sender values. However, if the user has already started typing in the inputs before the data loads, their changes will be overwritten.

Consider checking if any sender field has been modified before hydrating.

♻️ Suggested approach
 useEffect(() => {
     if (senderInputsHydrated || automatedEmailsData === undefined) {
         return;
     }

+    // Don't overwrite user input if they've already started typing
+    const currentState = formState.generalSettings;
+    const hasUserInput = currentState.senderName || currentState.senderEmail || currentState.replyToEmail;
+    if (hasUserInput) {
+        setSenderInputsHydrated(true);
+        return;
+    }
+
     setFormState(state => ({
         ...state,
         generalSettings: {
             ...state.generalSettings,
             senderName: senderNameInput,
             senderEmail: senderEmailInput,
             replyToEmail: replyToEmailInput
         }
     }));
     setSenderInputsHydrated(true);
 }, [automatedEmailsData, replyToEmailInput, senderEmailInput, senderInputsHydrated, senderNameInput, setFormState]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`
around lines 466 - 481, The useEffect that hydrates sender fields (useEffect,
senderInputsHydrated, automatedEmailsData) can overwrite user-typed values;
update it to only write senderNameInput, senderEmailInput and replyToEmailInput
into formState.generalSettings via setFormState if the current
formState.generalSettings sender fields are empty/unchanged (i.e., not modified
by the user) or match an initial default, otherwise skip hydration; read the
current values from formState.generalSettings inside the effect (or use a
functional setter/read) to avoid stale closures, then call
setSenderInputsHydrated(true) only after performing the conditional merge.
ghost/core/core/server/services/member-welcome-emails/service.js (2)

47-78: Consider documenting the transporter override pattern.

The MagicLink is initialized with a no-op sendMail (lines 49-51) that gets replaced in #sendEmailVerificationMagicLink. While functional, this pattern could confuse future maintainers. A brief comment explaining why the transporter is overridden per-call would improve clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/core/server/services/member-welcome-emails/service.js` around
lines 47 - 78, Add a brief explanatory comment above the MagicLink
transporter.sendMail no-op to clarify that the noop is intentionally provided
only as a placeholder because the transporter is overridden on a per-call basis
in the `#sendEmailVerificationMagicLink` method; mention that this avoids creating
a real transporter during construction and that the actual sendMail
implementation is injected at send-time to allow dynamic transporter
configuration and testing.

297-322: Transporter mutation is a minor code smell.

Reassigning this.#magicLinkService.transporter on each call (lines 300-313) works because the sequential await in #sendSharedSenderVerifications prevents internal race conditions. However, if this method were called concurrently from elsewhere, it could cause unexpected behavior. Consider passing the mailer callback to MagicLink at construction or using a factory pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/core/server/services/member-welcome-emails/service.js` around
lines 297 - 322, The code mutates this.#magicLinkService.transporter inside
`#sendEmailVerificationMagicLink` which is a race-prone code smell; instead,
modify MagicLink instantiation or its API so the mail-sending callback is
injected immutably (e.g. pass a sendMail/mailFactory into the MagicLink
constructor or expose a sendMagicLink option), and then call
this.#magicLinkService.sendMagicLink({email, tokenData, sendMail: message =>
this.#mailer.send({...})}) rather than reassigning
this.#magicLinkService.transporter; update callers such as
`#sendSharedSenderVerifications` to use the new injection/factory API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/admin-x-settings/src/components/settings/membership/member-emails.tsx`:
- Around line 163-206: After successful or failed verification in the
useEffect's verify() (which calls verifySenderUpdate with verifyEmailToken),
remove the verifyEmailToken from the URL so the effect won't re-run on
navigation; update the browser URL using history.replaceState (or
URLSearchParams on window.location.href) to strip the token/query parameter (or
fragment) after handling the response (both in the try and catch paths),
referencing verifyEmailToken, verifySenderUpdate and the verify() function so
the token is cleared immediately after processing.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`:
- Around line 466-481: The useEffect that hydrates sender fields (useEffect,
senderInputsHydrated, automatedEmailsData) can overwrite user-typed values;
update it to only write senderNameInput, senderEmailInput and replyToEmailInput
into formState.generalSettings via setFormState if the current
formState.generalSettings sender fields are empty/unchanged (i.e., not modified
by the user) or match an initial default, otherwise skip hydration; read the
current values from formState.generalSettings inside the effect (or use a
functional setter/read) to avoid stale closures, then call
setSenderInputsHydrated(true) only after performing the conditional merge.

In
`@ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js`:
- Around line 80-88: The editSenders validator currently ensures sender_name,
sender_email, and sender_reply_to are strings but does not validate email
format; update editSenders to, after calling validateOptionalStringField for
sender_email and sender_reply_to, also check their format using the same
validator.isEmail() calls/pattern used in sendTestEmail so invalid addresses are
rejected early (use the same error messages or similar descriptive errors) and
ensure you reference editSenders, sender_email, sender_reply_to,
validateOptionalStringField and validator.isEmail when locating and implementing
the change.

In `@ghost/core/core/server/services/member-welcome-emails/service.js`:
- Around line 47-78: Add a brief explanatory comment above the MagicLink
transporter.sendMail no-op to clarify that the noop is intentionally provided
only as a placeholder because the transporter is overridden on a per-call basis
in the `#sendEmailVerificationMagicLink` method; mention that this avoids creating
a real transporter during construction and that the actual sendMail
implementation is injected at send-time to allow dynamic transporter
configuration and testing.
- Around line 297-322: The code mutates this.#magicLinkService.transporter
inside `#sendEmailVerificationMagicLink` which is a race-prone code smell;
instead, modify MagicLink instantiation or its API so the mail-sending callback
is injected immutably (e.g. pass a sendMail/mailFactory into the MagicLink
constructor or expose a sendMagicLink option), and then call
this.#magicLinkService.sendMagicLink({email, tokenData, sendMail: message =>
this.#mailer.send({...})}) rather than reassigning
this.#magicLinkService.transporter; update callers such as
`#sendSharedSenderVerifications` to use the new injection/factory API.

In `@ghost/core/test/integration/services/member-welcome-emails.test.js`:
- Around line 488-526: Reset memberWelcomeEmailService.api to null before
calling memberWelcomeEmailService.init() in this test to match the pattern used
elsewhere; locate the test function that currently calls
memberWelcomeEmailService.init() (the spec using
memberWelcomeEmailService.api.sendTestEmail) and add a line that sets
memberWelcomeEmailService.api = null immediately before invoking
memberWelcomeEmailService.init() so the service is initialized consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d99e2d82-188b-4593-bd57-220ebd544c70

📥 Commits

Reviewing files that changed from the base of the PR and between 05c81f8 and 13257d2.

📒 Files selected for processing (17)
  • apps/admin-x-framework/src/api/automated-emails.ts
  • apps/admin-x-settings/src/components/settings/email-design/email-preview.tsx
  • apps/admin-x-settings/src/components/settings/membership/member-emails.tsx
  • apps/admin-x-settings/src/components/settings/membership/member-emails/default-welcome-email-values.ts
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
  • apps/admin-x-settings/src/hooks/use-welcome-email-sender-details.ts
  • apps/admin-x-settings/src/utils/newsletter-emails.ts
  • apps/admin-x-settings/src/utils/welcome-email-sender-details.ts
  • apps/admin-x-settings/test/acceptance/membership/member-welcome-emails.test.ts
  • apps/admin-x-settings/test/unit/utils/welcome-email-sender-details.test.ts
  • ghost/core/core/server/api/endpoints/automated-emails.js
  • ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js
  • ghost/core/core/server/services/member-welcome-emails/service.js
  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • ghost/core/test/e2e-api/admin/automated-emails.test.js
  • ghost/core/test/integration/services/member-welcome-emails.test.js

@cmraible cmraible changed the title SPIKE: add sender details Added sender details customization for welcome emails Apr 7, 2026
@cmraible cmraible marked this pull request as ready for review April 7, 2026 21:12
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/admin-x-settings/test/acceptance/membership/member-welcome-emails.test.ts (1)

831-856: Assert that the query token reaches the verification endpoint.

This test still passes if the component calls PUT /automated_emails/verifications/ without the verifyEmail value, because the mock always returns success. Adding a body assertion would cover the actual query-param plumbing.

🧪 Suggested assertion
-        await mockApi({page, requests: {
+        const {lastApiRequests} = await mockApi({page, requests: {
             ...globalDataRequests,
             ...newslettersRequest,
             browseConfig: {method: 'GET', path: '/config/', response: responseFixtures.config},
@@
         const confirmation = page.getByTestId('confirmation-modal');
         await expect(confirmation).toBeVisible();
         await expect(confirmation).toContainText('Reply-to address verified');
         await expect(page).toHaveURL(/#\/memberemails$/);
+        await expect.poll(() => lastApiRequests.verifyAutomatedEmailSenders?.body).toEqual({
+            token: 'test-verification-token'
+        });
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/test/acceptance/membership/member-welcome-emails.test.ts`
around lines 831 - 856, The test currently mocks the PUT handler
verifyAutomatedEmailSenders but never asserts the request body, so the
verifyEmail query param might not be sent; update the test for the 'shows
verification confirmation for memberemails verifyEmail token' case to
capture/validate the PUT request body sent to verifyAutomatedEmailSenders and
assert it contains the verifyEmail token 'test-verification-token' (e.g., ensure
the handler or mockApi validation checks request.body.verifyEmail ===
'test-verification-token'), while keeping the existing response behavior and
other assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin-x-settings/src/components/settings/membership/member-emails.tsx`:
- Around line 139-145: The effect that reads verifyEmailToken and calls
verifySenderUpdate should be made idempotent: add a ref (e.g.,
submittedTokenRef) to track the token already submitted and check that
verifyEmailToken !== undefined and !== submittedTokenRef.current before calling
verifySenderUpdate; mark submittedTokenRef.current = verifyEmailToken
immediately on submit to prevent duplicate calls across remounts/StrictMode, and
add an isMounted ref or an abort/cleanup inside the useEffect to prevent showing
the modal after unmount (guard any modal/show logic by isMounted.current).
Update the effect that uses verifyEmailToken, verifySenderUpdate, handleError
and any modal state in member-emails.tsx to use these refs so the token is only
consumed once and no state updates occur after unmount.

---

Nitpick comments:
In
`@apps/admin-x-settings/test/acceptance/membership/member-welcome-emails.test.ts`:
- Around line 831-856: The test currently mocks the PUT handler
verifyAutomatedEmailSenders but never asserts the request body, so the
verifyEmail query param might not be sent; update the test for the 'shows
verification confirmation for memberemails verifyEmail token' case to
capture/validate the PUT request body sent to verifyAutomatedEmailSenders and
assert it contains the verifyEmail token 'test-verification-token' (e.g., ensure
the handler or mockApi validation checks request.body.verifyEmail ===
'test-verification-token'), while keeping the existing response behavior and
other assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 30090423-100e-4a5d-89e7-660fb06c493b

📥 Commits

Reviewing files that changed from the base of the PR and between 13257d2 and da60cbb.

📒 Files selected for processing (2)
  • apps/admin-x-settings/src/components/settings/membership/member-emails.tsx
  • apps/admin-x-settings/test/acceptance/membership/member-welcome-emails.test.ts

Copy link
Copy Markdown
Collaborator

@cmraible cmraible left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

@troyciesco troyciesco added the ok to merge for me You can merge this on my behalf if you want. label Apr 7, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`:
- Around line 343-349: The form hydrations and save paths are running before
useBrowseAutomatedEmails() finishes (automatedEmailsData undefined), which
causes overwrites; fix by gating all initial hydration and any calls that rely
on existing automated emails on the browse query completing (i.e. only run
senderInputs hydration, ensureWelcomeEmailRows, and any code that uses
automatedEmails/existingBySlug when automatedEmailsData is not undefined), or
alternatively track form pristine state and only perform the
automatedEmails-derived initialization while the form is pristine; specifically
update logic around automatedEmails/automatedEmailsData, senderInputsHydrated,
ensureWelcomeEmailRows, addAutomatedEmail, editDesign and
editAutomatedEmailSenders so they early-return or defer until
automatedEmailsData is loaded (or until a isPristine flag indicates no user
edits) to avoid clobbering user changes.
- Around line 435-439: The domain comparison in the validation branch using
showSenderEmailInput and senderEmail currently does a case-sensitive check
(senderEmail.split('@')[1] !== senderEmailDomain); normalize both sides to
lower-case before comparing to avoid false negatives (e.g., User@Example.com).
Update the check in the validation block that sets validationErrors.senderEmail
so it compares senderEmail.split('@')[1].toLowerCase() against
senderEmailDomain.toLowerCase() (guarding for existence), leaving the rest of
the validator.isEmail flow unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 63995cdd-0456-4bfb-b0ca-dc6ae646e54e

📥 Commits

Reviewing files that changed from the base of the PR and between da60cbb and 8574dc9.

📒 Files selected for processing (1)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/admin-x-settings/src/components/settings/membership/member-emails.tsx (1)

165-225: ⚠️ Potential issue | 🟠 Major

Add unmount cleanup for async verification side effects.

The token de-dup guard is good, but the async verify() branch can still execute modal/error side effects after unmount (for example, user navigates away before promise settles). Please add a cancellation cleanup and guard post-await UI/history actions.

Suggested hardening
     useEffect(() => {
         if (!verifyEmailToken || !window.location.href.includes('memberemails')) {
             return;
         }
@@
         submittedTokenRef.current = verifyEmailToken;
+        let cancelled = false;
@@
         const verify = async () => {
             try {
                 const {meta: {email_verified: emailVerified} = {}} = await verifySenderUpdate({token: verifyEmailToken});
+                if (cancelled) {
+                    return;
+                }
                 clearVerifyEmailFromRoute();
@@
                 NiceModal.show(ConfirmationModal, {
@@
                 });
             } catch (e) {
+                if (cancelled) {
+                    return;
+                }
                 let prompt = 'There was an error verifying your email address. Try again later.';
@@
                 handleError(e, {withToast: false});
             }
         };
 
-        verify();
+        void verify();
+
+        return () => {
+            cancelled = true;
+        };
     }, [handleError, verifyEmailToken, verifySenderUpdate]);
#!/bin/bash
set -euo pipefail

FILE="$(fd -i 'member-emails.tsx' apps/admin-x-settings | head -n1)"
echo "Inspecting effect body around verification in: $FILE"
nl -ba "$FILE" | sed -n '160,235p'

echo
echo "Verifying whether the effect currently includes cleanup:"
python - <<'PY'
from pathlib import Path
p = next(Path("apps/admin-x-settings").rglob("member-emails.tsx"))
text = p.read_text()
anchor = "useEffect(() => {"
end = "}, [handleError, verifyEmailToken, verifySenderUpdate]);"
s = text.find(anchor)
e = text.find(end, s)
segment = text[s:e]
print("contains 'return () =>':", "return () =>" in segment)
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-settings/src/components/settings/membership/member-emails.tsx`
around lines 165 - 225, The effect can perform UI/history side effects after the
component unmounts; inside the useEffect wrap the async work in a cancellation
guard: add a local let cancelled = false and return a cleanup () => { cancelled
= true }; in verify() check if (cancelled) return immediately after any await
(both success and catch paths) before calling clearVerifyEmailFromRoute,
NiceModal.show, or handleError; keep the token de-dup via submittedTokenRef but
ensure any post-await side effects in verify() are skipped when cancelled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/admin-x-settings/src/components/settings/membership/member-emails.tsx`:
- Around line 165-225: The effect can perform UI/history side effects after the
component unmounts; inside the useEffect wrap the async work in a cancellation
guard: add a local let cancelled = false and return a cleanup () => { cancelled
= true }; in verify() check if (cancelled) return immediately after any await
(both success and catch paths) before calling clearVerifyEmailFromRoute,
NiceModal.show, or handleError; keep the token de-dup via submittedTokenRef but
ensure any post-await side effects in verify() are skipped when cancelled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8968c36c-058f-4d36-8eae-6b672364c0f4

📥 Commits

Reviewing files that changed from the base of the PR and between 8574dc9 and cb25b95.

📒 Files selected for processing (1)
  • apps/admin-x-settings/src/components/settings/membership/member-emails.tsx

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 7, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
4.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx (1)

343-360: ⚠️ Potential issue | 🟠 Major

Wait for sender data to settle before hydrating or saving.

automatedEmailsData === undefined is still being treated like “no welcome emails exist”. If the design query wins the race, the modal becomes editable with fallback sender values; when the browse finishes, Line 471 overwrites those fields, and the same empty-array fallback can drive ensureWelcomeEmailRows() into trying to recreate slugs that already exist. useWelcomeEmailSenderDetails() is also backed by another browse (apps/admin-x-settings/src/hooks/use-welcome-email-sender-details.ts:1-20), so these inputs are not necessarily stable just because automatedEmailsData is defined. Please gate hydration/save on a settled sender-details load state, or only hydrate while the form is still pristine.

Also applies to: 372-387, 404-405, 466-481

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`
around lines 343 - 360, The modal hydrates and allows saving before sender data
is fully settled, causing fallback values and duplicate-slug creation via
ensureWelcomeEmailRows; change hydration and save gating so senderInputsHydrated
is only set true when the welcome-sender details are fully loaded/stable (not
merely automatedEmailsData !== undefined). Update the
useWelcomeEmailSenderDetails call to return (or expose) a settled/isLoaded flag
(or derive one from its internal browse hook), then only run the existing
hydration logic that sets senderNameInput/senderEmailInput/replyToEmailInput and
call ensureWelcomeEmailRows when that settled flag is true and the form is still
pristine; likewise block the save paths
(addAutomatedEmail/editDesign/editAutomatedEmailSenders) until settled to avoid
racing writes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`:
- Around line 405-419: The two-step save (editAutomatedEmailSenders followed by
editDesign using buildAutomatedEmailDesignPayload) can partially succeed and
currently treats any failure as a full save error; change the flow to handle
each mutation's errors separately: call
editAutomatedEmailSenders(senderPayload), catch errors from that call and set a
partial-success state (or call onSaveError with a specific partial-failure flag)
before proceeding to editDesign, then call editDesign(...) and catch its errors
independently so you can surface either "sender saved, design failed" or "design
saved, sender failed" messages (and only re-run the mutation that actually
failed on retry); also preserve and display sent_email_verification handling and
toast.info for the sender success path so verification emails are still
indicated when editAutomatedEmailSenders succeeded.

---

Duplicate comments:
In
`@apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx`:
- Around line 343-360: The modal hydrates and allows saving before sender data
is fully settled, causing fallback values and duplicate-slug creation via
ensureWelcomeEmailRows; change hydration and save gating so senderInputsHydrated
is only set true when the welcome-sender details are fully loaded/stable (not
merely automatedEmailsData !== undefined). Update the
useWelcomeEmailSenderDetails call to return (or expose) a settled/isLoaded flag
(or derive one from its internal browse hook), then only run the existing
hydration logic that sets senderNameInput/senderEmailInput/replyToEmailInput and
call ensureWelcomeEmailRows when that settled flag is true and the form is still
pristine; likewise block the save paths
(addAutomatedEmail/editDesign/editAutomatedEmailSenders) until settled to avoid
racing writes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e56e2a3-a3c9-4fe9-8c6c-975b969823bf

📥 Commits

Reviewing files that changed from the base of the PR and between cb25b95 and 6d02496.

📒 Files selected for processing (1)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx

@cmraible cmraible merged commit 0d5af31 into main Apr 7, 2026
38 of 39 checks passed
@cmraible cmraible deleted the NY-1164_welcome-email-sender-info branch April 7, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok to merge for me You can merge this on my behalf if you want.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants