Magic Reach Automated Emails
Source: .agents/references/features/magic-reach-automated-emails.md
Content
Magic Reach Automated Emails
Purpose
This creator-facing area manages automated email sequences from
/portal/magic-reach?activeTab=automatedEmails and reuses the same email editor
shell for product-specific email types. Live backend responses and the current
Lark schema are authoritative; inspect them before changing request fields.
User-Facing Workflows
- Automated Emails has two groups: membership-tier-scoped Community Enrolment and community-scoped Abandoned Checkout Reminder.
- Persisted Membership Edit Tier sheets reuse the same Community Enrolment list,
one-mail detail, save, reset, and test state while requesting no Sales Recovery
sequence. They render backend rows in response order and omit the Magic Reach-only
enablement and deletion controls. Their rows use the Membership labeled Edit button,
while Magic Reach rows keep the icon-only edit action. Unsaved tiers remain on the
local deferred single-message editor until a canonical tier ID exists. Membership
Edit email subject/body suggestions and legends share
{name}Member name and{community_name}Business name, including initial/reset label normalization. - A
tierIdquery parameter selects the matching Community Enrolment tier after tier data loads; missing or invalid IDs fall back to the first available tier. Changing the tier dropdown shallowly updatestierIdwhile retaining the Automated Emails tab in the URL. Membership handoffs save both new and existing tiers and await the shared tier-list refresh before redirecting; when the same selected tier ID receives refreshed metadata, replace the selected tier object instead of retaining its stale name or approval state. - Every row has its own enabled state. There is no sequence-level enablement UI and no fallback send when every row is off.
- The side sheet edits
mailTitle, non-negative integer delay, subject, body, and row enablement. Existing Magic Reach rows explicitly opt into the sharedEnable this emailtoggle; new rows start enabled and omit it. Product/event email sidesheets never render this toggle. - Welcome email subject, body, and guidance share
{name}asMember nameand{community_name}asBusiness name. Checkout-abandonment email subject, body, and guidance use the same variables plus{product_title}asProduct title. The configured list must drive initial highlighting and{suggestions in both editors. - Welcome additions default to zero hours (immediately after joining); Sales Recovery additions default to one hour.
- Delay-unit dropdowns omit unsupported backend-provided units and retain the last occurrence of each supported unit, preventing unknown values from falling through to the Months label and preserving the backend's final valid ordering.
- New Sales Recovery drafts reuse the backend protected checkout structure but
normalize the locked summary label to localized
Product summary; do not mutate the source template or override existing-email labels. - Duplicate delays are valid, rows stay in backend response order, and the frontend does not cap the number of rows.
- Default rows can reset the open message from the backend default template. Magic Reach exposes Delete for any existing Welcome row while the sequence contains more than one email, and for any existing Sales Recovery row while the sequence contains more than two emails. New unsaved rows and product/email editors outside Magic Reach do not expose Delete.
- Test mail sends the current unsaved editor values and does not save first. Sales Recovery does not require frontend preview data and must not show an ineligible-data tooltip or disabled state; the backend supplies mocked preview data when needed.
- Save API failures use the existing error toast only; the sidesheet must not add a Content rejected/Revert footer that is absent from the design.
- Save owns a sidesheet-local submitting state so its NPL button shows loading from click until the save promise settles, even when a caller has no loading state of its own.
- Save stays disabled while the current draft equals the initially opened draft; this does not change Send test eligibility for otherwise valid fields.
- The shared sidesheet sanitizes loaded content with the same variable/content rules as the editor before establishing its dirty-state baseline. Editor initialization must not trigger discard confirmation; actual field/content changes still do.
- Automated Emails sequence loading uses its layout-matched skeleton and never shows the page-level messages/drafts spinner. Sent and Drafts keep their existing global loading behavior. On narrow screens the 560px side sheet naturally fills the viewport width.
Key Entrypoints
- Route controller:
src/pages/portal/magic-reach/components/MagicReachContent/index.tsx - Automated list and state:
src/pages/portal/magic-reach/components/AutomatedEmails/ - Shared side sheet:
src/features/EmailEditorSideSheet/ - Shared product-email list:
src/features/CustomizeEmailMessages/CustomizeEmailMessages.js - Existing product-email adapter:
src/pages/portal/events/manage/components/EditEventMessageTypeDrawer/EditEventEmailTypeDrawer.js - Services:
src/services/mailSequencesService.tsand the existing single-mail functions insrc/services/communitiesService.js.
Data Flow and Service Boundaries
- Sequence list GET/aggregate PUT:
/api/v1/communities/:communityId/mail-sequences/:sequenceType. - Community Enrolment passes
mailCourseOffer=<membershipTierId>; Abandoned Checkout Reminder is Magic Reach community-scoped. - Sequence list rows are metadata-only. Opening an existing row fetches its
editor detail from
/api/v1/communities/:communityId/mail-sequences/:sequenceType/mails/:mailContentObjectId; this request never sendsmailCourseOffer. For shared DEFAULT rows, preserve list title, timing, enabled state, and selected tier while merging detail subject, content, locked preview, reset ID, and version. - Existing CUSTOM rows save through the one-mail PUT at that same path with
version,isEnabled,delay,mailTitle,mailSubject, and restored Lexicalcontent. Sendversion = (detail.version ?? 0) + 1; do not sendmailCourseOffer, reset IDs, or response-only preview data. - Existing DEFAULT rows and all new rows save through aggregate PUT so the
backend can create/copy the custom sequence and assign canonical IDs.
Aggregate mail arrays are sparse: unchanged rows contain only
mailContentObjectId, while the edited/new row contains all editable fields. Aggregate PUT includes required rootisEnabledfrom the open draft; this field does not introduce sequence-level enablement UI. - PUT sends root
versionas the latest response version plus one. When a response omitsversion, the next update starts atversion: 1; successful response snapshots remain authoritative for following updates. - Preserve last-write-wins behavior and use successful backend snapshots as authoritative metadata/version sources.
- Reset uses the backend
default=truesingle-mail response for the relevant mail type and only replaces the open message template fields. Welcome reset usesCOMMUNITY_ENROLMENT_APPLICATION_APPROVEDwhen the selected tier either requires approval or has at least one non-deleted application field; otherwise it usesCOMMUNITY_ENROLMENT_WITHOUT_APPLICATION. - Single product emails continue using their existing mail type,
mailCourseOffer, permission gate, locked preview context, and send/reset behavior; only the editor presentation is shared. - Migrated email reset flows keep the legacy 480px NPL confirmation design and warning copy even when their reset API handler changes.
Conventions and Gotchas
titlein product discussion maps to the sequence row fieldmailTitle.- Normalize missing runtime
mailTitleandmailSubjectvalues to empty strings before opening the editor; validation must reject malformed drafts without throwing. - Normalize missing runtime
contentto the canonical empty editor state before splitting protected nodes; opening a mail must never dereference absent content. - Normalize recognized variable placeholders found inside initial plain text nodes into styled variable nodes before Lexical parses editable content. Preserve surrounding text formatting and leave unknown placeholders unchanged.
- Subject uses the shared single-line Tiptap variable input. Configured
placeholders render as highlighted tokens even beside punctuation, while
changes continue to serialize to the plain
mailSubjectstring expected by mail APIs. Typing{opens variable suggestions, and Backspace removes a selected variable atomically without restoring a trigger character. The suggestion popup must render above the NPL sidesheet stacking layer. Subject plain text and variable tokens both use 16pxtext-body-mdtypography. lockedSectionpreview data is backend-defined and rendered by the immutablelockSectiondecorator directly inside the editor. The node is draggable but neither editable nor deletable. Its serialized schema omitsbuttonTextwhile the CTA label is empty and includes the string attribute when the label has a value, wherever the node is moved.- Sales Recovery protected-template validation must locate the serialized
lockSectionby node type rather than assuming it is the first top-level content node; users can reposition the immutable node before saving. - Locked-section presentation uses the mail-type-derived visual variant for abandoned checkout, product order, event, 1:1 session, lead-form submission, challenge, checkpoint, or failed challenge participant. Runtime preview data is injected into the renderer and never added to the serialized node.
lockSection.buttonTextis the single source for the protected CTA label. The CTA opens the NPL Edit Button Label modal, and modal saves, message saves, resets, and test-mail payloads all read and update that same node field. A legacy adjacentbuttonnode may be normalized intobuttonTextwhile reading, but it is not serialized back into update payloads.- The Edit Button Label modal keeps intrinsic-width Cancel and Save actions grouped on the right side of its footer; neither action is stretched.
mailContentObjectIdis the stable row identity and must be preserved for existing rows. Delete eligibility depends only on the sequence type, whether the row is persisted, and the live row count; do not infer row origin from title, position, or delay.- Use
t('meaningful-kebab-case-key'). Individual disabled rows use the focusedemail-status-offkey rendered through a neutral smallNPLBadge; the existingoffkey remains discount copy. - Row titles are single-line ellipses. Keep the status pill and edit action
visible and do not add a title tooltip. Keep the status pill immediately after
the title with the established 8px gap; short titles must not grow to consume
the row, while long titles shrink and truncate before the pill. Row editing
uses an
NPLIconButtonwithplain_secondary, sizesm, and theediticon. - Sequence Add another actions reuse the Membership Add another email control:
small yellow
plain_accentNPLButtonwithplus-circle, including its shared hover state. Keep 8px above the action and 20px below it. - Keep
CUSTOM_PRODUCT_EMAILSpermission checks around editing controls. - Fetch Welcome and Sales Recovery in parallel and guard stale tier responses.
Related Standards
.agents/rules/*.mdc.agents/references/coding-standard/figma-guidance.md.agents/references/coding-standard/useeffect-guidance.md.agents/references/features/products-and-commerce.md.agents/references/features/checkout-and-payments.md
Useful Searches
rtk rg "mail-sequences|COMMUNITY_ENROLMENT|ABANDONED_CHECKOUT_REMINDER" src
rtk rg "CustomizeEmailMessages|EmailEditorSideSheet" src
Update Triggers
Update this reference when the sequence DTO gains row identity, reset metadata,
locked-section placement, concurrency semantics, new sequence types, or when a
product email changes its mail type or mailCourseOffer mapping.