Account portal — DES-54 handoff notes
Desktop prototype. Pairs with the mobile prototype tracked separately.
Vocabulary alignment
The portal renders the strings below directly from orderCycle, orderPrice, and nextCharge. PDP and checkout should use the same vocabulary so the patient sees one consistent way of describing cadence and renewal across the journey.
| Concept | Portal string | Surface |
|---|---|---|
| Cadence summary | every {count} weeks | List card meta, detail table |
| Cycle price | $478.00 per cycle · every 8 weeks | List card |
| Cycle price (detail) | Cycle Price: $478.00 | Subscription Details table |
| Imminent renewal (date hero) | MAY 9 / in 3 days | List card right column |
| Distant renewal | in 23 days (muted) | Date hero countdown |
| Reassessment due | Reassessment / MAY 12 / in 6 days (coral) | List card right column |
| Coming Up hero | Renews May 9 · in 3 days (Lora) | Detail hero band |
| Cycle Price hero | $478.00 / CYCLE PRICE | Detail hero right column |
| Order ID format | Order #ELL-2845 | History rows, order detail |
Renewal and Refill are kept as distinct concepts and defined explicitly in the explainer card on both list and detail screens:
- Renewal: Your subscription cycle is billed (the recurring charge to your card).
- Refill: A new bottle ships from the pharmacy after your provider reviews the renewal, usually 2–3 days after the charge clears.
PDP educational copy should preserve this distinction rather than collapsing the two into "your monthly order."
No "monthly" assumption anywhere. All cadence text flows through orderCycle.count and pluralizes correctly across any week count. The only unit in the engineering contract is 'week' (per Source of Truth); composite cycles like MOTS-C carry their on/off rhythm in the optional description field rather than as a different unit.
Deprecated terms. monthSupply and "monthly price" are deprecated everywhere in the patient portal. Replace with orderCycle + orderPrice and the Cycle Price / Cadence labels above.
Page hierarchy
Where each piece of subscription information sits on the page, top to bottom. PDP and checkout teams should mirror this where applicable so the patient sees the same vocabulary in the same relative position across surfaces.
List page (/account)
- Title row — "Your treatments" + treatment count, no Lora display type
- Reassessment callout (conditional) —
bg-primary-100, surfaces only when at least one subscription is inpending_reassessment. Above the cards by design so action-needed beats the upcoming-renewal signal. - Treatment cards — one card per subscription, sorted by
nextCharge.dateascending. Each card uses a 3-column grid: leadingMedicationIcon(size="lg"), info column with name/category/price-cycle line, and a centered date hero on the right (RENEWS · MAY · 9 · in 3 days, or coral REASSESSMENT variant for pending). - About explainer card — defines Renewal vs Refill. The canonical definition lives here.
Detail page (/account/subscription/[id])
- Back link — text-only "‹ My Ellie Journey".
- Header row — leading
MedicationIcon(size="lg") + product name + status pill + meta line (category · formulation). No Cancel link at the top. - Reassessment callout (conditional) — same atom as the list page.
- Coming Up hero band — Lora "Renews May 9 · in 3 days", body line about charge + ship dates, big Lora
$478.00numeral on the right withCYCLE PRICEcaps label. Solid warm cream background#fdf8f1with 4px secondary teal left accent. - Subscription Details table — 4-column desktop grid (Cadence / Cycle Price / Next Charge / Next Shipment) with
border-b-2 border-blackdouble-rule header. Cycle progress demoted into a warm-cream footer band inside the same card. - Provider card — clinician name + specialty + reassessment status + Message link. No avatar (kept minimal).
- Coming Up table (conditional, when active) — scheduled future events only (renewal + refill), 4 columns (Date / Type pill / Description / Amount).
- History table — past events, 5 columns (Date / Type / Description / Amount / Status). Refill rows link to Order detail at
/account/orders/[orderId]. - Payment Method — VISA chip + ending in 4242 + Update payment link.
- About Refills & Renewals — same explainer atom as the list page.
- Manage — 3-card grid (Adjust cadence / Skip next shipment / Pause subscription). Inline icon variant: small icon next to label, description below.
- Cancel zone — quiet footer below Manage, hairline-separated, muted text: "If you need to stop your treatment entirely, you can cancel this subscription." Demoted from the top so the page does not feel like it is offering cancellation as a primary action.
Order detail page (/account/orders/[orderId])
Per-shipment receipt. Linked from History rows on the subscription detail. Mobile equivalent is a future ticket; this surface is desktop-only for DES-54.
- Back link — "‹ My Ellie Journey".
- Header — Order ID + status pill + Reorder / Download receipt actions.
- Subtitle — charge date + link back to parent subscription.
- Fulfillment progress — Charged → Reviewed → Shipped → Delivered with tracking number and View tracking link.
- Items table — Product / Qty / Cycle Price (with "per 8-week cycle" sub-label) / Total. Cycle Price column is the deliverable: cycle price appears next to line items so patients see the per-cycle context for the charge.
- Totals — Subtotal / Shipping / Total charged.
- Three info cards — Payment / Shipping address / From your subscription (with Manage subscription link back).
Cross-surface alignment
- PDP — Cadence and price formatting matches the portal exactly. Pre-purchase copy ("you'll be charged $X every Y weeks") uses the same
every {count} {unit}shape that the portal renders post-purchase. PDP does not own the canonical Renewal vs Refill definition; if it educates, it should paraphrase or link. - Checkout — The order-line cadence summary uses the same
$X every N {unit}format the portal uses on the list card's price/cycle line. The "Subscription order details" panel mirrors the portal'sSUBSCRIPTION DETAILSsection labels (Cadence, Cycle Price, Next Charge, Next Shipment). - Hierarchy parity — Across all three surfaces, cadence and price appear together (never as separate disconnected facts), and they appear before renewal-timing information. The patient learns "what does this cost and how often" before "when's the next charge."
Mobile / desktop divergence
Mobile and desktop are intentionally not pixel-equivalent. Each viewport adopts the design register that fits it:
| Concern | Mobile | Desktop |
|---|---|---|
| Detail hero | Centered Lora date, calm, bg-secondary-50 mint | Two-column horizontal: text left, $478 hero numeral right, warm cream bg |
| Subscription block | Flat divide-y rows (iOS Settings register) | 4-col columnar table with double-rule header |
| History | Two expandable disclosures: Renewal history + Shipment tracking (split by data type) | Two visible tables: Coming Up + History (split by tense) with order detail links |
| Manage | Stacked iOS-style action rows, Cancel as last destructive in same list | 3-card grid (icon + label + description), Cancel demoted to quiet footer |
| Order detail | Not present (future ticket) | Full surface at /account/orders/[orderId] |
What stays consistent across both viewports:
- Identical engineering contract (
Subscription,OrderCycle, etc.) - Identical vocabulary (Cycle / Cycle price / Cadence / Renewal / Refill / Supply / Order)
- Identical Refill vs Renewal explainer copy
- Identical category-tinted
MedicationIconglyph (size scales by viewport) - Identical canonical names (patient: Emma, clinician: Hollie Hall, FNP-C)
- Identical engineering touchpoint annotations (
upcoming_renewals,activity_log,order_details)
When PDP / checkout adopt this vocabulary, they should treat both viewport patterns as valid expressions of the same underlying contract. Don't flatten one into the other.
Prototype to production component map
The prototype uses descriptive names that don't match the production filenames the ticket points engineering at. Mapping below.
| Prototype file | Production touchpoint(s) |
|---|---|
care_subscription_list.tsx | upcoming_renewals.tsx + order_list_item.tsx |
care_subscription_detail.tsx | order_details.tsx plus related modals: cancel_subscription_widget, change_payment_method_modal, delay_shipment_modal, edit_nickname_modal |
order_detail.tsx | New surface — production order_receipt.tsx or equivalent. Mobile equivalent is a future ticket. |
no_subscriptions.tsx | account_client.tsx (empty branch) |
medication-icon.tsx | New shared atom. Lucide-based, parameterized with size variants. Place in src/components/ for reuse across list, detail, order detail. See file header for caveat about Lucide vs branded icons. |
status-pill.tsx | New shared atom. Place in src/components/ for reuse by reassessment_alert and failed_payment_alert. |
lib/subscriptions.ts | Shared util location. OrderCycle, displayInterval, priceCycleLine, getCycleProgress, Order, getOrderById are utility/type exports; the data fetch lives on the production hooks layer. |
Category-specific template notes
The default account template applies to all five ProductCategory values: Longevity, Weight Loss, Skincare, Sexual Health, HRT. Variation today is data-only:
- Category and
formulationrender as the detail header subtitle (e.g.Weight Loss · GLP-1 injection). MedicationIconbackground tint: Longevity / HRT use the secondary teal family; Skincare / Weight Loss / Sexual Health use the primary coral family.- Tier surfaces inline with product name (e.g.
Semaglutide (Tier 1)). No category-specific layout. - Provider clinician differs by program but uses the same atom.
- Reassessment cadence is driven by
prescriptionValidThrough, not category-specific layout.
No category currently warrants a layout deviation. If one emerges later (e.g. HRT lab ordering, Longevity supplement tracking), document the exception here.
Cadence handling rules
- All cadence text is driven from
OrderCycle = { count, unit: 'week', description? }. Per the Source of Truth engineering contract,unitis always'week'. - Always pluralize via
${count === 1 ? 'week' : 'weeks'}. Centralized indisplayIntervalandpriceCycleLine. - The same row template renders 8-week, 10-week, and 12-week cadences equivalently. No layout branches by count.
- Date hero countdown text uses thresholds:
today/tomorrow/in N days. Coral when state ispending_reassessment; teal when within 14 days; muted black-300 when further out. - Cycle progress reports in weeks:
Week 3 of 8,Week 6 of 10,Week 9 of 12. Helper field isgetCycleProgress().unitInCycle(named generically to allow non-week units in the future without renaming the type).
Engineering touchpoints
Each surface in the prototype maps to a named engineering touchpoint, called out in JSX comments adjacent to the relevant section:
| Touchpoint | Where it lives | Notes |
|---|---|---|
upcoming_renewals | "Coming Up" Section in care_subscription_detail.tsx | Scheduled future events table |
activity_log | "History" Section in care_subscription_detail.tsx | Past events table with order detail cross-links |
order_details | order_detail.tsx + /account/orders/[orderId] route | Per-shipment receipt with cycle price next to line items |
Shared utilities exposed by @/lib/subscriptions:
displayInterval(orderCycle)→ "Every 8 weeks" / "Every 12 weeks"priceCycleLine(price, orderCycle)→ "$478 every 8 weeks"formatDate(iso, opts?)→ "May 9" / "May 9, 2026"daysUntil(iso)→ 3 / -2getCycleProgress(s)→ { unitInCycle, dayInCycle, totalDays, percent }monthsActiveSince(iso)→ 8sortByNextCharge(subs)→ Subscription[] sorted by nextCharge.date ascgetOrderById(id)→ Order | undefinedgetOrdersForSubscription(subscriptionId)→ Order[] sorted newest first