Products and Commerce Feature Context
Source: .agents/references/features/products-and-commerce.md
Content
Products and Commerce Feature Context
Purpose
Products and commerce cover the catalog and detail experiences for courses, events, challenges, memberships, digital files, forms, sessions, physical products, unified product cards, and affiliate commerce surfaces. The domain connects public discovery, product detail pages, availability, enrollment, pricing, and checkout entry.
User-Facing Workflows
- Visitors browse product cards on community pages and open product detail pages.
- Buyers choose product variants or tiers, inspect pricing and metadata, then enter checkout.
- Members see enrolled/access states, resource view pages, event attendance, course/file access, and challenge participation.
- Creators/admin users may configure product behavior through related admin/pro surfaces, but public product rendering should stay decoupled from admin-only assumptions.
- Physical products include image carousels, variant pages, shipping/location availability, and inquiry analytics.
- Product and business-setting media forms can open Magic Content V2 in place from AI Everywhere media actions, including challenge, event, course, 1:1 session, digital file/folder, physical product, and community/business settings cover uploads.
Key Entrypoints
- Routes:
src/pages/[communityLink]/products/[productSlug]/src/pages/[communityLink]/products.page.jssrc/pages/[communityLink]/courses/[courseSlug].page.tsxsrc/pages/[communityLink]/courses/[courseSlug]/view.page.tsxsrc/pages/[communityLink]/events/[eventSlug]/index.page.jssrc/pages/[communityLink]/challenges/[challengeSlug]/src/pages/[communityLink]/digital-files/[digitalFileSlug].page.tsxsrc/pages/[communityLink]/digital-files/[digitalFileSlug]/view.page.tsxsrc/pages/[communityLink]/forms/[formSlug].page.tsxsrc/pages/[communityLink]/sessions/src/pages/[communityLink]/physical-products/
- Components/features:
src/components/common/EntityMarketingSection/src/components/features/CommunityPage/components/allProducts/src/features/CommunityProductPage/src/features/EventPublicPage/src/features/ResourcePage/src/features/1on1Sessions/src/features/Affiliates/src/pages/portal/components/MultimediaUpload/src/pages/[communityLink]/physical-products/components/src/pages/portal/physical-products/create/components/steps/RearrangeImagesStep/
- Hooks:
src/hooks/useEntityPrice.tssrc/hooks/useMembershipTiers.tsxsrc/hooks/useManageCommunityEvents.jssrc/hooks/member/useCommunityEventsData.jssrc/pages/[communityLink]/physical-products/usePhysicalProductLandingPage.ts
- Services:
src/services/unifiedProduct/unifiedProductServices.tssrc/services/courseService.jssrc/services/calendarService.jssrc/services/leadFormsService/src/services/referral/referralService.ts
- Utilities:
src/utility/productUtils.tssrc/utility/eventUtils.jssrc/utility/checkoutUtils.jssrc/pages/[communityLink]/physical-products/utils.ts
Data Flow and Service Boundaries
- Unified product data powers product cards and many discovery views. Check
src/services/unifiedProduct/and product card helpers before changing labels, CTA state, or pricing. - Lead forms are a unified product type for public discovery and creator display management, but they are not priced purchase cards: public lead form cards route to the form public page with a view CTA and should not show price UI; reorder/display-management rows can omit product details for lead forms.
- Detail pages may use route-level static props, client hooks, and product-specific services. Inspect the page file before adding fetch logic to a nested component.
- Product access state is not the same as public visibility. A product can be visible but unavailable, sold out, enrolled, pending, inactive, location-restricted, or gated by membership.
- For public
TICKET_TYPESevents, default selection uses the first non-archived ticket that is open and not sold out. Registration availability follows the selected ticket rather than legacy event-level sold-out state. - Event attendee message drafts retain the active ticket-type filter. Ticket-scoped registration email fetch, update, reset, and test requests all carry the same ticket type ID.
- Legacy event and ticket discounts with no
isActivefield remain visible; only explicitisActive: falseentries are hidden. - Event creation at
/portal/event/addrenders every start/end time option with the same zero-paddedhh:mm AM/PMlabel while preserving the existing minute values, disabled-option rules, and submitted scheduling data. - Event management Pending approval, Approved, and Rejected email editors present an existing serialized
lockSectionas Event summary with an editable CTA label, defaulting missing button text toView event. Content without alockSectionnode must not render or synthesize the protected block. Existing custom CTA text and backend preview metadata remain intact; other event mail types keep their current behavior. - Unsaved membership tiers have no canonical tier ID, so Welcome message editing reuses
EmailEditorSideSheetas a single local deferred draft. Editor Save validates and updates the tier's local subject/content, whilesaveSelectedTierperforms the community-mail update after pricing creates the tier. - Every successful membership-tier pricing mutation (save, enable/disable, reorder, or delete) refreshes both the shared tier query and the active-community snapshot. These independent refreshes run in parallel, and the active-community refresh stays silent so membership updates do not trigger a portal-wide loading state.
- Persisted Edit Tier sheets load every tier-scoped
COMMUNITY_ENROLMENTrow in backend response order. Each row uses the shared Magic Reach detail/update/reset/test flow with title, timing, subject, and body; the Membership caller omits enablement and deletion UI, preserves rowisEnabled, and retains Member name / Business name variables plus theCUSTOM_PRODUCT_EMAILSrestriction UI. A failed empty sequence response falls back to the legacy single-message entry so editing is not blocked. Both unsaved and persisted Welcome rows use the established labeled neutral-primary small Edit button; Magic Reach rows remain icon-only. Both Membership editor paths use{name}as Member name and{community_name}as Business name for subject/body suggestions and the legend, refreshing serialized variable labels on initial load and reset. - The membership tier “Add another email” handoff targets
/portal/magic-reach?activeTab=automatedEmails&tierId=<tierId>. New and existing tiers both show Save tier to continue and must pass the tier validation/save pipeline first, so current form and welcome-message edits persist before navigation. Redirect only after the shared membership-tier list refreshes and the save returns the canonical tier ID. - Membership Add/Edit Tier uses the same compact add-action geometry for Add another plan and both new-tier
and persisted-tier Add another email controls: small NPL button with plus-circle lead icon and yellow
plain_accenthierarchy. Copy and click behavior remain action-specific. - New-event Tickets preserve the existing membership-tier access gate: show
All members / By tiersonly when more than one tier satisfiesisActive || hasActiveSubscription; derive the selected mode fromtieredPrices.isActive, retain both ticket drafts and tier-price drafts when switching modes, and keep one shared capacity, bulk-registration, questions, and approval block beneath the tier rows inBy tiers. - New-event All members Flexible pricing seeds Minimum and Suggested with real controlled values of
5and10only on first enable, synchronizing legacy amounts as500and1000cents. After a user clears either field, preserve the empty controlled value andundefinedlegacy amount across off/on toggles, showing0as the placeholder. Existing typed values remain intact across toggles. - Edit-event All members Tickets keep ticket names editable, always expose Add ticket, and show Delete on every card when more than one ticket draft exists; the final remaining ticket cannot be deleted.
- Animated event-ticket cards keep their rounded 1px visual edge on the height-clipping motion wrapper, while measured inner content remains borderless so add/reflow animations cannot clip the card edge.
- Edit-event pricing always uses the new Tickets experience, including membership-tier and legacy events
without
pricingMode. The All members / By tiers segment is create-only; tier edits render a static By Tiers editor. - Edit-event dirty state shows the 112px sticky gradient footer used by create-event: 24px horizontal and bottom padding, 40px top fade, 12px gap, equal-width Cancel then Save buttons.
- Edit-event ticket deletion uses the admin-event
goingAttendeescount for the persisted legacy ticket: tickets with sales show the non-destructive blocked notice, while zero-sale and newly added ticket drafts show a confirmation before removal. Replace this legacy count with per-ticket sales data when the backend exposes persisted multiple ticket types; do not infer per-ticket sales by splitting the event total. - Manage-event Tickets has separate closure scopes:
Close ticket registrationstays inside the ticket card and updates the event ticket contract with the complete ticket-type list; it must not mutate the event-levelisRegistrationClosedfield. Serialize rapid ticket toggles from cumulative desired state so no concurrent stale full-list PUT can revert another ticket, then authoritatively revalidate after the final intent. Closing or reopening it immediately shows a localized toast. All-members tickets include the displayed ticket name;By Tiersuses generic ticket-registration copy without a ticket name. A ticket-toggle action atomically replaces the prior toast with its latest state, so rapid toggles never stack or suppress messages. Ticket Edit remains enabled regardless of ticket-level or event-level registration closure. Flexible-price summaries show the localized Suggested label andpricingConfig.suggestedAmount; they must not interpret the absent fixedpricingConfig.amountas Free.Close event registrationremains under More and owns the existingisRegistrationClosedmutation. - Manage-event attendee tables show the
Ticketcolumn only when multiple non-archived ticket types exist. The attendee API must provide both the event-level multiple-ticket indicator and each attendee's purchased ticket name; keep any pre-contract UI preview explicitly mocked and isolated for later replacement. - Manage-event Add attendees sends root
ticketTypeObjectIdfor everypricingMode=TICKET_TYPESevent. Show the Figma ticket selector only when more than one non-archived ticket accepts registrations; with one eligible ticket, hide the selector and submit that sole ticket ID. Single-ticket and membership-tier events retain the legacy attendee-only payload. - Manage-event attendee removal offers a refund only when the selected attendee is not manually added and
the attendee response's authoritative
paidAmount.amountis finite and greater than zero. Zero, negative, missing, or non-finite paid amounts keep the remove-only flow; legacy flat/local amount fields do not establish refund eligibility. The backend resolves the exact transaction from the attendee ID. Removal requests send onlyrequestRefundand the optional trimmednote; the note is included in the removal email for both refund and remove-only flows. For refunds, the backend also uses a non-empty note as the internal refund reason and defaults an omitted note toAttendee removed by community manager; new frontend code must not send the backward-compatiblerefundReasonfield. Removal confirmation copy uses the attendee email when the attendee name is null, missing, or blank. - Public event ticket-viewer badges prefer each incoming ticket's purchased
ticketNamein array order, then the purchase-levelticketType.name. When both are absent, render the localized genericTicketlabel; never fabricate a specific ticket-type name. - Public event attendee copy follows the authoritative sold-out state, not event-level remaining capacity.
Unlimited-capacity events use
Join <count> other people; sold-out events use<count> people are going. - Public multi-ticket registration rows render flexible ticket pricing as localized
Pay what you want; suggested and minimum amounts remain checkout guidance and must not be formatted as a fixed ticket price. - Public multi-ticket quantity controls cap each selection at the lower of that ticket's
maxQuantityPerPurchaseand known remaining capacity. Disable increment at that effective limit and retain the state-level clamp; when remaining capacity is unknown, use the per-purchase limit alone. - Event checkout carries the selected ticket type ID to pricing/signup APIs without duplicating the ticket
name in URL metadata. Checkout resolves the selected ticket name from the public event API by that ID;
the quantity row falls back to localized
Ticketsonly when the API ticket name is unavailable. - Public
TICKET_TYPESregistration derives checkout routing from the selected ticket rather than the legacy event-level price. Paid fixed, flexible, approval-required, and event-application selections use checkout; a selected zero-amount fixed ticket can register directly. A free sibling ticket must never bypass payment for a paid selection. - Public
TICKET_TYPESevents with exactly one non-archived ticket use the legacy-style single-ticket CTA instead of a redundant selector, while Register still submits that ticket's ID and selected quantity. Events with zero or multiple non-archived tickets retain the multi-ticket CTA. - Purchase-history event cards append the purchased
ticketType.namefrom the member purchase-history response to the event date/time metadata with a•separator. Render the API's requiredquantityas<ticket type name> x<quantity>, includingx1; preserve the legacy date/time-only label when ticket type is absent. - Registered public-event attendees always see the registered card instead of the inline ticket selector,
including
pricingMode=TICKET_TYPESevents. When another purchase is available,Buy more ticketsopens a separate selector and preserves the selected ticket type ID and quantity through checkout. Free-ticket registration keeps this modal open with Register loading while the request is pending, closes it after a successful signup response, and leaves it open after failure so the attendee can retry. Upcoming registered attendees seeView my ticket(s)as the first action only for in-person events; online events omit the ticket-view action. Its count sums every active attendee purchase quantity, and opening the viewer loads each purchase's private attendee record in parallel before displaying every returned ticket reference in purchase order. Legacy single-attendee responses remain a fallback. - Public legacy single-ticket and membership-tier CTA summaries display the first non-archived
ticketTypes[].name, then the trimmed legacyticketName, falling back to localizedGeneral Admissiononly when both are missing or blank. Production ticket UI must not accept development mock-ticket overrides. - Public event attendance cancellation is transaction-scoped: keep duplicate ticket types from separate
checkouts as distinct rows, target actions by
transactionId, and cancel all represented tickets in that transaction together without changing other transactions. Until the authoritative transaction API exists, keep permutation fixtures and their switcher development-only and preserve the production attendee flow. - Manage-event
Close ticket registrationuses the exact tooltip copyThis ticket will no longer accept new registrations; keep both its info trigger and toggle programmatically named while preserving optimistic ticket-local state during the serialized backend update. - Manage-event headers for
pricingMode=TICKET_TYPESreplace the legacy event access row with one 40px ticket-icon row per non-archived ticket. Preserve the creator's exact ticket-name casing; show fixed price, flexible suggested price plus the localized Suggested badge, or Free. Join localized approval and capacity subtitles with・; preserve the legacy row for single-ticket and membership-tier events. - Event-ticket discount creation keeps the disabled scope select's nested event-and-ticket pill border fully visible while preserving truncation for its text and for unrelated shared selects.
- Event-ticket discount creation persists immediately through the existing community discount API while editing an event; new-event discount creation remains a local ticket draft until the event is created.
- New-event submit excludes event-level and ticket-local discount drafts from the event request, waits for the successful event response, then creates discounts through the existing community discount API using the returned event ID and matching returned ticket-type ID. Event creation failure must not issue discount requests; a later discount failure must not cause the already-created event to be submitted again.
- Persisted event-ticket discounts include the selected ticket's
_idasticketTypeObjectIdalongside the event linked entity so the discount does not apply to sibling ticket types. - In edit-event ticket cards, removing any persisted discount opens the shared disable confirmation;
discounts without a persisted
_idremain locally removable drafts. - Confirming persisted ticket-discount removal disables the discount immediately through the shared
discount mutation and removes its
_idfrom ticket cards only after success; failures preserve the UI. - Event edit ticket cards hide fetched discounts with
isActive: falsefor both legacy and ticket-type responses; missing flags remain visible for backward compatibility. - Discount applies-to event rows render the event name and each selected active ticket name separated by
・; comma-separated ticket lists and generatedticket/ticketssuffixes are not part of the label. - Event-form accordions use CSS transitions with coordinated panel/grid opacity timing: 220ms open, 170ms
close, a 180ms transform-only chevron, and instant state changes under
prefers-reduced-motion. - Physical product pages have separate layout, carousel, checkout, and variant concerns under
src/pages/[communityLink]/physical-products/. - Physical-product email sidesheets use mail-type-specific variables through the shared email editor:
MEMBER_PHYSICAL_PRODUCT_ORDER_CREATED:{name}(Member name),{product_title}(Product title),{product_variant}(Product variant),{community_name}(Business name).MEMBER_PHYSICAL_PRODUCT_ORDER_SHIPPED: the same ordered variables with{order_number}(Order number) before{community_name}. The resolved list must drive subject and body suggestions/highlights plus the ordered guidance legend; saves keep the raw placeholder identifiers.
- Persisted digital products under
/portal/digital-files/manageand courses under/portal/courses/manageexpose an Emails tab immediately after Marketing; shared-folder variants retain their existing tabs. The tab contains the product-scoped Order Confirmation email forCOMMUNITY_FOLDER_PURCHASE_MEMBERand reuses the existing community-mail permission, load, reset, test, save, discard, and responsive sidesheet behavior. Its ordered variable list drives subject/body highlighting and suggestions plus the guidance legend:{name}(Member name),{product_title}(Product title), and{community_name}(Business name). Saves preserve these raw placeholder identifiers and pass the digital-product or course ID through the existingmailCourseOffermapping. The email list uses the same responsive 960px horizontal grid as Content. SerializedlockSectionnodes are permanent protected email structure: editing their CTA label remains allowed, but editor deletion must not remove the node. - One-on-one session email sidesheets use one ordered variable list for subject/body tokens and the
single-bullet localized guidance rows:
{name}(Member name),{community_name}(Business name), and{session_title}(Session title). Display-label changes must not change the raw placeholders sent to the backend. The localized row template already supplies•; do not add CSS list markers. Its locked Session summary uses the NPL1-1-sessionicon. - Challenge Confirmation email uses an explicit ordered legend and variable list:
{challenge_title}(Challenge title),{participant_name}(Participant name),{checkpoint_count}(Number of checkpoints), and{community_name}(Business name). Do not show the generic{name}legend. Scheduled Challenge start/end date and time variables remain available after these four confirmation variables. - Challenge Checkpoint start email uses the ordered base legend
{challenge_title}(Challenge title),{checkpoint_title}(Checkpoint title), and{participant_name}(Participant name). The same labels drive subject/body tokens. Fixed-date challenges insert{checkpoint_start_date}(Checkpoint start date),{checkpoint_end_date}(Checkpoint end date),{checkpoint_start_time}(Checkpoint start time), and{checkpoint_end_time}(Checkpoint end time) before Participant name in both editor options and the legend. Challenge email lists must resolve guidance by selected mail type and challenge schedule instead of falling back to generic member/business guidance. Variable suggestions must expose every supplied option and scope cached results to the active variable set so switching email types cannot reuse stale choices. - Challenge Declared winners email uses one ordered variable list for editor tokens and its legend:
{challenge_title}(Challenge title),{participant_name}(Participant name), and{completed_checkpoint_count}(Completed checkpoint count). Do not show generic{name}or{community_name}guidance for this mail type. Leaderboard-specific editor variables remain governed by the existing leaderboard behavior and do not alter this three-row legend. Its serializedlockSectionuses the button-only locked wrapper: keep the editable CTA and omit the Challenge summary card. Other Challenge email variants retain their summary cards. - Challenge “Marked participants as failed” email uses the failed-participant button-only locked variant.
Its subject/body tokens and legend share the ordered variables
{challenge_title}(Challenge title),{participant_name}(Participant name), and{failed_reason}(Failed reason); do not show generic member/business guidance or change the raw placeholders. Its serializedlockSection.buttonTextrenders and edits through the shared button-label modal even when the response omitslockedSectionpreview metadata; other locked summary variants still require preview data. If the node label is absent on initial load or reset, initialize it to localizedOpen Challenge; preserve existing custom labels and keep the node as the editable/saveable source. Preserve the full-width CTA, dotted underline,edit-04tail icon, and backend save, reset, and test behavior. - Persisted physical-product Details exposes
Emailsbetween Shipping & Return policy and Collect additional info fromInventoryShippingCard; pre-create forms without_iddo not show the entry. Details action rows are semantic full-width buttons and must retain their established 48px geometry, text alignment, hover treatment, and keyboard activation. The 480px page-local list sidesheet reusesCustomizeEmailMessagesfor Order Confirmation (MEMBER_PHYSICAL_PRODUCT_ORDER_CREATED) and Shipping Confirmation (MEMBER_PHYSICAL_PRODUCT_ORDER_SHIPPED), passing the physical-product ID asmailCourseOfferthrough the existing community-mail service flow. Per-email saves happen in the shared 560px editor; the parent list footer only closes navigation and must not introduce an aggregate draft/save API. KeepCUSTOM_PRODUCT_EMAILSgating, backend reset/test behavior, locked content, and responsive full-width narrow sheets intact. Product email editors never render theEnable this emailtoggle, even when a backend response exposesallowDisableToggle; preserve the responseisEnabledvalue in update payloads. Physical-product Figma variants stay caller-scoped: the list uses a black-25 backdrop, Order Confirmation and Shipping Confirmation both use a 500px editable area, and any other physical mail type retains the 278px caller fallback. Variable-guidance rows indent 8px, and skeleton title and description lines are 20px and 22px. Shared SideSheet and email-editor defaults remain black-50 and 420px for every other caller. On initial load and reset, normalize legacy physical-product{product_name}subject placeholders, body variable nodes, and body plain placeholders to{product_title}with theProduct titlelabel before opening the editor; saves must not preserve the legacy placeholder identity. - Product/media upload flows use shared Magic Content V2 handoffs.
EntityMarketingRunAdsAssetModalowns the AI Everywhere asset modal; itsNew Productoption is local UI state only and must not be inserted into product-list API data orproductRefs. Selecting that option and clicking Create with AI opensMagicContentCreationStepsin start-from-scratch mode. Selecting an existing product preserves the product-linked Magic Content flow. - Magic Content prompt generation must identify
EXTERNAL_PRODUCTitems withexternalProductObjectIdand omitentityObjectId, including external products returned through the NAS product list and external-product reuse throughGenerate Another. Internal NAS products continue to useentityObjectId. - Magic Content generated media is the only product/media cover media eligible for AI edit. Use the
shared
MultimediaUploadhelpers before showing edit affordances; non-Magic Content uploaded or saved media should not expose AI edit. - Affiliate behavior can affect product CTAs and referral codes. Search
Affiliatesandreferralbefore changing join/purchase links.
Conventions and Gotchas
- Product type labels and CTA labels should use existing helpers where available. Avoid introducing parallel string switches.
- Keep pricing and availability checks close to the helper/hook that already owns product state.
- Do not assume all product types share the same slug or ID shape.
- After client resource and community-role data confirm access, public digital-product and folder landing
pages replace their URL with
/{communityLink}/products/{resourceSlug}/view. Use the canonical products member-view route for both automatic redirects and Access CTAs; do not redirect from cached static data. - Preserve access-control redirects and view pages for member-only product content.
- Use exact Figma spacing and NPL typography tokens for design-driven product cards.
- Avoid deriving product display state in
useEffect; derive from product, member, and enrollment data during render unless synchronizing with an external system. - Processing Magic Content image/video cover media should render the shared Magic Content processing visual. Product and business-setting forms should warn before closing when unsaved processing Magic Content media would be discarded, while ordinary uploaded media keeps the existing generic unsaved-change warning.
Related Standards
.agents/rules/clean-react.mdc.agents/rules/clean-typescript.mdc.agents/rules/no-try-catch-service-api.mdc.agents/rules/direct-translation-and-component-files.mdc.agents/references/coding-standard/figma-guidance.md.agents/references/coding-standard/useeffect-guidance.md
Useful Graph Queries and Fallback Searches
Use the terms below with search_graph/search_code, then run trace_path on the central product or
commerce symbols. Run these shell searches only as documented fallbacks.
rtk rg "UnifiedProduct|productSlug|entityPrice|membershipTier|physicalProduct|leadForms|variant|MagicContentCreationSteps|isMagicContentGeneratedMedia" src/pages/[communityLink] src/components/features src/features src/hooks src/services src/pages/portal src/components/common
rtk rg "ProductCardCTA|ProductCardPriceDisplay|ProductCardSubtitle|ProductFilter|LEAD_FORM" src/components/features/CommunityPage src/pages/portal/products/unified-product
rtk find src/pages/[communityLink]/physical-products -maxdepth 5 -type f
Update Triggers
Update this reference when product type behavior, unified product contracts, product card CTAs, pricing, access checks, creator display ordering/visibility, physical product flows, product/media Magic Content handoffs, or cover-media edit and discard-warning rules change.