Skip to main content

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 tierId query 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 updates tierId while 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 shared Enable this email toggle; new rows start enabled and omit it. Product/event email sidesheets never render this toggle.
  • Welcome email subject, body, and guidance share {name} as Member name and {community_name} as Business name. Checkout-abandonment email subject, body, and guidance use the same variables plus {product_title} as Product 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.ts and the existing single-mail functions in src/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 sends mailCourseOffer. 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 Lexical content. Send version = (detail.version ?? 0) + 1; do not send mailCourseOffer, 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 root isEnabled from the open draft; this field does not introduce sequence-level enablement UI.
  • PUT sends root version as the latest response version plus one. When a response omits version, the next update starts at version: 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=true single-mail response for the relevant mail type and only replaces the open message template fields. Welcome reset uses COMMUNITY_ENROLMENT_APPLICATION_APPROVED when the selected tier either requires approval or has at least one non-deleted application field; otherwise it uses COMMUNITY_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

  • title in product discussion maps to the sequence row field mailTitle.
  • Normalize missing runtime mailTitle and mailSubject values to empty strings before opening the editor; validation must reject malformed drafts without throwing.
  • Normalize missing runtime content to 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 mailSubject string expected by mail APIs. Typing &#123; 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 16px text-body-md typography.
  • lockedSection preview data is backend-defined and rendered by the immutable lockSection decorator directly inside the editor. The node is draggable but neither editable nor deletable. Its serialized schema omits buttonText while 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 lockSection by 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.buttonText is 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 adjacent button node may be normalized into buttonText while 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.
  • mailContentObjectId is 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 focused email-status-off key rendered through a neutral small NPLBadge; the existing off key 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 NPLIconButton with plain_secondary, size sm, and the edit icon.
  • Sequence Add another actions reuse the Membership Add another email control: small yellow plain_accent NPLButton with plus-circle, including its shared hover state. Keep 8px above the action and 20px below it.
  • Keep CUSTOM_PRODUCT_EMAILS permission checks around editing controls.
  • Fetch Welcome and Sales Recovery in parallel and guard stale tier responses.
  • .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.