Skip to content

Updated InfoBox for Course Product Pages#3523

Merged
ChristopherChudzicki merged 41 commits into
mainfrom
cc/hq-11787-course-infobox
Jul 1, 2026
Merged

Updated InfoBox for Course Product Pages#3523
ChristopherChudzicki merged 41 commits into
mainfrom
cc/hq-11787-course-infobox

Conversation

@ChristopherChudzicki

@ChristopherChudzicki ChristopherChudzicki commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

For

Only implements the updated course info boxes, not the program (or program-as-course) infoboxes.

Description (What does it do?)

Updates the infoboxes:

  1. Choose Your Path: New layout separates free vs paid enrollment into two boxes with separate enrollment buttons.
  2. Session Selector: Courses with multiple enrollable runs now display a dropdown for selecting the run.
    • This means all infobox data is now run-dependent, rahter than based on the next_run_id.
  3. Enrollment Indicated: If user is enrolled in the selected run, enrollment buttons are now replaced by a "Enrolled ✔️" button-link that takes users to dashboard

Changes at a glance

  • Scenario logic (courseRun.ts) — pure functions that map a run to the case it should display (free / paid / both, deadline-passed, archived…).
  • Data hooks (useCourseEnrollment, useCourseEnrolledRunIds, useCertificatePrice) — enrollment state machine, enrolled-run lookup, and certificate pricing/financial-aid.
  • Presentational components (TrackCard + CertificateTrackCard/LearnForFreeCard, SessionSelect, EnrolledLink, summary rows) — stateless, just render what they're handed.
  • Containers (InfoBoxCourse, CourseEnrollArea, CoursePage) — wire the hooks + scenario into the presentational pieces.

Screenshots (if appropriate):

Each pair is main (before) on the left, this PR (after) on the right, at
the desktop sidebar width where the InfoBox renders. Based on a subset of the course from https://gist.github.com/ChristopherChudzicki/712a9ab4a2e065a8fa02ff35126388bb

Single run, various cases

A1 · Both paths — free audit + paid certificate (anon)

Before (main) After (this PR)
A1 both — main A1 both — this PR

A2 · Paid only — certificate, no free audit (anon)

Before (main) After (this PR)
A2 paid only — main A2 paid only — this PR

A3 · Free only — audit, no certificate (anon)

Before (main) After (this PR)
A3 free only — main A3 free only — this PR

A7 · Instructor-Paced course

Before (main) After (this PR)
A7 — main A7 — this PR

A8 · Self-Paced course starting in future

Before (main) After (this PR)
A8 — main A8 — this PR
Warnings (upgrade deadline passed, archived course, no runs)

A4 · Certificate deadline passed — audit still available (anon)

Before (main) After (this PR)
A4 deadline passed — main A4 deadline passed — this PR

A5 · Archived — content access only (anon)

Before (main) After (this PR)
A5 archived — main A5 archived — this PR

A6 · No open sessions — warning, no enrollment (anon)

Before (main) After (this PR)
A6 no sessions — main A6 no sessions — this PR
Multiple runs

B1 · Multiple sessions — session selector (anon)

Before (main) After (this PR)
B1 multi-session — main B1 multi-session — this PR

With the session list expanded — main shows a plain expanded date list, this PR shows the session selector dropdown:

Before (main) After (this PR)
B1 dates expanded — main B1 session selector open — this PR
Financial aid

C1 · Financial assistance available (anon)

Before (main) After (this PR)
C1 finaid available — main C1 finaid available — this PR

C2 · Financial assistance applied — approved user (logged in)

Before (main) After (this PR)
C2 finaid applied — main C2 finaid applied — this PR
Enrolled states

E1 · Enrolled — collapses to a single link (logged in)

Before (main) After (this PR)
E1 enrolled — main E1 enrolled — this PR

E2 · Enrolled in one of multiple sessions (logged in)

Before (main) After (this PR)
E2 enrolled multi-session — main E2 enrolled multi-session — this PR
Tablet

Tablet — A1 · Both paths (anon)

Before (main) After (this PR)
A1 both tablet — main A1 both tablet — this PR

Tablet — A2 · Paid only (anon)

Before (main) After (this PR)
A2 paid only tablet — main A2 paid only tablet — this PR

Tablet — E1 · Enrolled (logged in)

Before (main) After (this PR)
E1 enrolled tablet — main E1 enrolled tablet — this PR

How can this be tested?

Prerequisites: MITxOnline and Learn integrated locally.

Test Data: I tested a lot of scenarios above. I think at least the following is worth setting up:

  • Note: All these scenarios are covered in https://gist.github.com/ChristopherChudzicki/712a9ab4a2e065a8fa02ff35126388bb, if you want to use it.

  • Different enrollment mode combos:

    • A course with both paid and free enrollment modes Script's A1
    • A course with only paid enrollment modes (verified) Script's A2
    • A course with only free enrollment modes (audit) Script's A3
  • A course with multiple enrollable runs Script's B1

    • one run that doesn't offer a cert (either missing the enrollment mode, or more realistically, the run is past its upgrade_deadline)
    • maybe a second run that does offer a cert, at a different price.
    • one run that is upgradable
  • An instructor-paced course Script's A7

  • A self-paced course that starts in future Script's A8

Manual Testing:

  1. Check that the course pages for above look good / match screenshots above.
  2. Most importantly (since it isn't covered by the screenshots) check the enrollment flows:
    • Enrolling via the "Free" button enrolls you in audit mode
    • Enrolling via the "Paid" button enrolls you in the paid mode (note: use fake cybersource credit cards if you actually want to finish the enrollment)
    • When multiple runs are available, enrolling via the dropdown enrolls you in the correct run
    • Enrollment via header button should always match the infobox primary-styled button
  3. Additionally, if you are already enrolled, the infobox should indicate that.
    • When enrolled, there should just be a single button linking to dashboard
    • Product page enrollment indication does not distinguish between audit vs verified. (Dashboard does, though.)
  4. If a course has multiple runs and you are enrolled in only one of them, it should indicate your enrollment only for the run you're enrolled in (selected via dropdown).

Additional Context

Program update portion of https://github.com/mitodl/hq/issues/11787 will be handled separately.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

OpenAPI Changes

24 changes: 0 error, 9 warning, 15 info

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

@ChristopherChudzicki ChristopherChudzicki changed the title Cc/hq 11787 course infobox Updated InfoBox for Course Product Pages Jun 26, 2026
@ChristopherChudzicki ChristopherChudzicki marked this pull request as ready for review June 26, 2026 00:34
Copilot AI review requested due to automatic review settings June 26, 2026 00:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates the MITxOnline course product page InfoBox experience in frontends/main to support the new “Choose Your Path” layout, run-dependent metadata/enrollment behavior, and a session selector for multi-run courses.

Changes:

  • Introduces a run-scenario model (courseRun.ts) plus new hooks (useCourseEnrollment, useCourseEnrolledRunIds, useCertificatePrice) to drive run-dependent enrollment UI and enrolled-state collapse.
  • Replaces the legacy single CourseEnrollmentButton with a new CourseEnrollArea that renders Certificate/Learn-for-free “track” cards and an Enrolled link state.
  • Adds SessionSelect and updates CourseSummary/InfoBoxCourse/CoursePage so the selected run is shared between the InfoBox and the page-header CTA.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
frontends/main/src/app-pages/ProductPages/useCourseEnrollment.ts New hook mapping run + user state into enroll-area UI state and click handlers.
frontends/main/src/app-pages/ProductPages/useCourseEnrollment.test.tsx Unit tests for enrollment state mapping, auth gating, and action behavior.
frontends/main/src/app-pages/ProductPages/useCourseEnrolledRunIds.ts Auth-gated query helper for enrolled run IDs scoped to the current course.
frontends/main/src/app-pages/ProductPages/useCertificatePrice.tsx Hook for certificate price display and financial-aid state for a selected run.
frontends/main/src/app-pages/ProductPages/TrackCard.tsx Shared structural component for track cards (Certificate Track / Learn for Free).
frontends/main/src/app-pages/ProductPages/SessionSelect.tsx Session dropdown UI for multi-run courses with annotations (enrolled/cert unavailable/start anytime).
frontends/main/src/app-pages/ProductPages/SessionSelect.test.tsx Tests for SessionSelect labeling, annotations, and ordering.
frontends/main/src/app-pages/ProductPages/ProductSummary.tsx Refactors course summary to be selected-run driven; adds session-row rendering support and deadline/archived messaging.
frontends/main/src/app-pages/ProductPages/ProductSummary.test.tsx Updates tests for selected-run behavior, archived/deadline-passed UI, and payment deadline rendering.
frontends/main/src/app-pages/ProductPages/LearnForFreeCard.tsx New “Learn for Free” track card component.
frontends/main/src/app-pages/ProductPages/LearnForFreeCard.test.tsx Tests for LearnForFreeCard rendering and optional deadline note.
frontends/main/src/app-pages/ProductPages/InfoBoxCourse.tsx Rebuilds InfoBox layout as a count-aware grid, wires in session selector and new enroll area.
frontends/main/src/app-pages/ProductPages/InfoBoxCourse.test.tsx Integration-style tests for session switching, enrolled collapse, and grid structure/count attributes.
frontends/main/src/app-pages/ProductPages/EnrolledLink.tsx New reusable “Enrolled” button-link component.
frontends/main/src/app-pages/ProductPages/courseRun.ts New helpers/types for selecting runs, ordering runs, and deriving offering/status scenario.
frontends/main/src/app-pages/ProductPages/courseRun.test.ts Tests for run selection and scenario derivation helpers.
frontends/main/src/app-pages/ProductPages/CoursePage.tsx Lifts selected run state to the page so header CTA and InfoBox stay in sync; updates outline query gating.
frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx Updates/extends tests to cover new header CTA behavior and session syncing.
frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx Removes legacy single-button enrollment implementation.
frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx Removes legacy tests tied to CourseEnrollmentButton.
frontends/main/src/app-pages/ProductPages/CourseEnrollArea.tsx New enroll-area renderer with track cards, CTA placement rules, enrolled collapse, and error handling.
frontends/main/src/app-pages/ProductPages/CourseEnrollArea.test.tsx Tests for offering layouts, degraded states, loading state, and financial-aid display behavior.
frontends/main/src/app-pages/ProductPages/CertificateTrackCard.tsx New “Certificate Track” card component with optional financial-aid link.
frontends/main/src/app-pages/ProductPages/CertificateTrackCard.test.tsx Tests for CertificateTrackCard rendering, bullets, and financial-aid link behavior.

Comment thread frontends/main/src/app-pages/ProductPages/courseRun.ts
Comment thread frontends/main/src/app-pages/ProductPages/useCourseEnrollment.ts Outdated
@ChristopherChudzicki ChristopherChudzicki force-pushed the cc/hq-11787-course-infobox branch from f3fe8a5 to ed67b74 Compare June 26, 2026 12:55
@daniellefrappier18 daniellefrappier18 self-assigned this Jun 26, 2026
loading={isStatusLoading}
pending={isPending}
variant="bordered"
announceStatus={false}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it looks like announceStatus={false} bypasses the busyProps check in CourseEnrollArea. I think you want to remove it so the header CTA gets aria-busy={isBusy} during loading/pending states.


const { state, isStatusLoading, isPending } = useCourseEnrollment(
course,
selectedRun,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens if there is an error?

@daniellefrappier18 daniellefrappier18 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good. I was able to verify all the scenarios. Thank you for included the script that was very helpful. I left one question and one a11y issue for you to look at but don't see any blockers.

Scenario Course title Direct URL Screenshot
A1 - logged out [QA] Both paths — free audit + paid certificate (single run) /courses/course-v1:qa-a1 Image
A2 - logged out [QA] Paid only — certificate, no free audit (single run) /courses/course-v1:qa-a2 Image
A3 - logged out [QA] Free only — audit, no certificate (single run) /courses/course-v1:qa-a3 Image
A4 - logged out [QA] Certificate deadline passed — audit still available /courses/course-v1:qa-a4 Image
A5 - logged out [QA] Archived — content access only /courses/course-v1:qa-a5 Image
A6 - logged out [QA] No open sessions — warning, no enrollment /courses/course-v1:qa-a6 Image
A7 - logged out [QA] Instructor-paced — paid only (single run) /courses/course-v1:qa-a7 Image
A8 - logged out [QA] Paid only — enrollment open, starts in future (single run) /courses/course-v1:qa-a8 Image
B1 - logged out [QA] Multiple sessions — Session selector (all Both) /courses/course-v1:qa-b1 Image
C1 - logged out [QA] Financial assistance — available (anon) / applied (approved user) /courses/course-v1:qa-c-finaid Image
Scenario Course title Direct URL Screenshot
E1 - Logged in as student_infobox@odl.local [QA] Both paths — free audit + paid certificate (single run) /courses/course-v1:qa-a1 Image
E2 - Logged in as student_infobox@odl.local [QA] Multiple sessions — Session selector (all Both) /courses/course-v1:qa-b1 Image
C2 - Logged in as student_infobox@odl.local [QA] Financial assistance — available (anon) / applied (approved user) /courses/course-v1:qa-c-finaid Image

ChristopherChudzicki and others added 17 commits July 1, 2026 14:20
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…run ids)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nroll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a purely presentational paid-path card used in the Course InfoBox
redesign (PR1). Accepts an opaque price ReactNode, optional financialAid
link, productNoun for bullet copy, and optional embedded action node.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Presentational dropdown for selecting a course session (enrollable run).
Built on SimpleSelectField with accessible 'Session' label; converts
run ids at the boundary (String on options, Number in onChange). Shows
'Anytime' for self-paced past-start runs; appends '— Enrolled' for
runs the user is already enrolled in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CourseSummary now accepts selectedRun prop (CourseRunV2 | undefined)
  instead of deriving the run from course.next_run_id internally
- Adds optional sessionSelect prop rendered in the dates row slot
- Payment deadline line extracted from CoursePriceRow into CourseSummary,
  rendered beneath dates when selectedRun has upgrade_deadline and !is_archived
- Price/financial-assistance content removed from CourseSummary (now in cards)
- CoursePriceRow, CourseCertificateBox and their helpers deleted (file-local,
  now unused after removing from CourseSummary)
- InfoBoxCourse: stopgap one-liner passes getSelectedRun(course) as selectedRun;
  Task 7b will wire real selection state

Deleted test coverage:
- "Price Row" describe block: behavior moved to CertificateTrackCard (Task 3/4)
- "Financial Assistance" describe block: same — already covered by card tests
These deletions are intentional; coverage lives in CertificateTrackCard.test.tsx

Open item: no-runs Alert "Learn More" link — no destination URL defined in
codebase for the no-runs case; left without link pending design input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ceRow testid

Collapsed filter in CourseDatesRow was keying off course.next_run_id
instead of the passed-in nextRun prop, so selecting a non-default run
showed the wrong dates. Fixed to use nextRun.id.

TestIds.PriceRow is still live (used by ProgramPriceRow), so no removal.
Added divergence test: next_run_id != selectedRun → collapsed row shows
selectedRun's date.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Track card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires InfoBoxCourse with selectedRunId state, useCourseEnrolledRunIds,
SessionSelect (multi-run only), CourseEnrollArea, and a BoxGrid styled
component that implements the count-aware responsive CSS grid (data-boxes
1|2|3 drives meta-span and single-column fallback at tablet breakpoint).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… Choose Your Path heading

C1: paidOnly/freeOnly/deadlinePassed/archived scenarios now wrap their
card + below-button in a single div (data-card="paid"|"free"), making
each offering exactly one direct grid child. The both-case wrappers were
already correct.

I1: Add data-choose-path to the "Choose Your Path" heading in
CourseEnrollArea, then target it with grid-column: 1/-1 in InfoBoxCourse's
BoxGrid so it spans both columns in the 3-box (both) tablet layout.

Test: add structural assertion in InfoBoxCourse.test.tsx confirming
paidOnly grid has exactly 2 direct children and that the Enroll button
shares the same wrapper as the Certificate Track card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…from course page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… run)

The InfoBox now renders enroll-card h3 headings depending on the course
run's enrollability, which makeCourse() randomizes. Pin a non-enrollable
run so the page heading outline is deterministic while the outline
section (which needs a run with courseware_id) still renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… popover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete orphaned CourseEnrollmentButton.tsx and its test file — no live importer remains after Tasks 7b and 8 rewired the Course page and InfoBox to use CourseEnrollArea/EnrollButton directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… label; review cleanups

- access kind in makeOnClick now takes the same audit-enroll + dashboard-redirect path as
  free, fixing the silent no-op on "Access Course Materials" clicks (archived scenario)
- deadlinePassed option changed from kind:"free"/label:"Start Learning" to
  kind:"access"/label:"Access Course Materials" for label consistency with archived
- useCourseEnrolledRunIds: remove dead ?? false on always-boolean isLoading/isError
- InfoBoxCourse: fix comment referencing .choose-path-heading → [data-choose-path]
- CertificateTrackCard: FeatureIcon styled via callback form (({ theme }) => ...) for
  consistency with LearnForFreeCard; remove module-level theme import
- Tests: add archived-enroll regression guard; update deadlinePassed label assertions;
  replace FIX4 no-op smoke tests with real assertions or removal

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChristopherChudzicki and others added 24 commits July 1, 2026 14:23
…st placement tests, polish

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Metadata: reorder Format → Estimated → Session; "Course Format" → "Format"
on the course row; payment deadline recolored to darkGray2/body3 and moved
into a 3-col SessionRow grid beneath the dropdown.

Session control: bare smoot Select + inline bold "Session:" label
(labelId→aria-labelledby), size=medium (40px), full-width.

Enroll area: "Choose Your Path" → subtitle2; full-width buttons; OfferingCell
gives a 16px card↔button gap; secondary variant only in the "both" case,
primary everywhere else.

Cards: financial-assistance moves into the feature list as a 4th bullet;
LearnForFreeCard border silverGrayLight → lightGray2.

Structure: lightGray2 <hr> divider between metadata and Choose-Your-Path
(hidden in tablet 2-col grid); header button text restored to darkGray2.

Tests updated for "Format" label and the new divider element.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… dates, header↔infobox run sync

- SessionSelect: bump label→dropdown gap to 16px (icon→label stays 8px) via
  marginRight on the label; collapse the redundant start-year for same-year
  date ranges ("Sep 8 - Dec 16, 2026"), keep both years across a year boundary.

- ProductSummary: archived single-run courses lead the dates row with
  "Course content available anytime" + the end date instead of a stale start
  date (gated to the single-date view; multi-date lists keep concrete dates).

- Lift selectedRunId to CoursePage so the header CTA tracks the session chosen
  in the InfoBox. CourseInfoBox is now controlled (selectedRunId/onSelectRun);
  the header receives the shared selectedRun.

Tests: same/cross-year range formatting, single-run archived dates row, and a
CoursePage correlation test (header CTA follows the InfoBox session switch).
InfoBoxCourse tests use a ControlledInfoBox wrapper for the lifted state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ement

Visual-QA fixes against the canonical Figma frames (A4/A5/A6):

- Deadline-passed (A4): add the yellow "Certificate deadline has passed."
  alert; lead the date row with "Course content available anytime" + End;
  suppress the payment-deadline line (no upcoming payment once the window
  closed). Matches frame 39861:102467.
- Free card: the "Certificate deadline passed" note is now bold + underlined
  and sits above the access bullet; show it for archived runs too (their
  certificate window has also closed), not only deadline-passed.
- Warning placement: move the degraded-state notices (no-sessions, archived,
  deadline-passed) to the bottom of the metadata block, just above the
  offerings, matching Figma. They were rendered at the top — pre-existing
  placement the spec never pinned.

Tests: update assertions for the new note string/placement; add a
deadline-passed describe block (alert / available-anytime date row /
suppressed payment deadline); pin the enrollment scenario in the date-row and
payment-deadline tests so factory randomness can't land on deadline-passed now
that it renders distinctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rchived cert-note gating, a11y

- deadline-passed keeps its normal date row; "content available anytime" archived-only
- archived "Certificate deadline passed" note gated on the run having offered a cert
- drop the in-card note for deadline-passed (redundant with its alert)
- a11y: accessible name on the loading enroll button; accessible framing for the
  discounted certificate price (mirrors the program path)
- test: pin multi-run payment-deadline placement + no-runs section-divider absence
- reuse the dateLoading skeleton constant

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…art Anytime' annotation, truncate collapsed value

- dropdown options always render the date range; self-paced past-start runs append an
  italic '— Start Anytime' annotation instead of a bare 'Anytime' (multiple such runs
  were previously indistinguishable)
- collapsed value drops the redundant '— Enrolled' marker and ellipsis-truncates,
  fixing overflow in the narrow sidebar column

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… line, sort by start date

Drop the "— Start Anytime" annotation from the dropdown's collapsed value (it
overflowed the narrow sidebar like "— Enrolled" did); the collapsed value is now
dates only. Surface the anytime nature as its own line under the dropdown (above
the payment-deadline line) via SessionAnnotation in CourseSummary, gated on the
selected run starting anytime. Drop the italics on "Start Anytime" everywhere.

Order dropdown options descending by start date (future sessions near the top;
null start last) via a byStartDateDesc comparator, mirroring the "More Dates"
list; the default selection still follows next_run_id.

Spec §4g updated with the ordering, the collapsed dates-only rule, the dedicated
anytime line, and an explicit note that these selector affordances deviate from
Figma (not depicted there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Start Anytime + payment deadline were two separate grid rows under the session
dropdown, each separated by the row's full 8px gap — reading as airy whitespace.
Group them into a single SessionSubText block with a tight 2px inter-line gap;
the dropdown→note spacing (the row's 8px rowGap) is unchanged. They now read as
one unit of secondary text while payment deadline keeps its own line and weight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eadline-passed too

Previously the in-card note was archived-only — dropped for deadline-passed as
redundant with that scenario's own "Certificate deadline has passed." alert. But
that made the two passed-deadline states inconsistent: deadline-passed conveyed
it only via the alert, archived via the in-card line. Show the in-card line in
both (gated on the run having offered a certificate), matching Figma frame
39861:102467, which shows both the alert and the in-card line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rtificate available)"

Mirror the legacy enrollment dialog (CourseProductDetailEnroll.js): a run whose
certificate can no longer be purchased (is_upgradable === false) appends a
"(no certificate available)" note to its dropdown option, so a user choosing a
session knows before enrolling. Open-menu options only; the collapsed value
stays dates-only. Tests asserting exact date labels now pin is_upgradable since
the factory randomizes it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, dedup)

Five Minor findings from the multi-lens review:
- InfoBoxCourse: count an enrolled run as one offering box even when its
  scenario is "none" (enrolled collapse supersedes degraded scenarios, §4h).
  Previously `scenario === "none"` was checked before `isEnrolled`, so an
  enrolled user on a paid-only-expired run rendered the Enrolled link but
  counted 0 boxes (data-boxes=1, divider suppressed). New box-count test.
- CourseEnrollArea: pin the busy button's accessible name ("Loading") in the
  loading-state test — a regression dropping the aria-label would have stayed
  green (WCAG 4.1.2).
- courseRun: hoist runStartsAnytime and byStartDateDesc into the shared module;
  SessionSelect and ProductSummary now import them instead of keeping
  byte-identical copies, and CourseDatesRow uses the shared sort comparator.
- CourseEnrollArea: wrap the busy LoadingSpinner in <span aria-hidden> so the
  spinner is actually hidden from assistive tech — ol-components LoadingSpinner
  drops aria-hidden, leaking a redundant role=progressbar named "Loading".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… union

getCourseScenario now returns a discriminated union with two orthogonal facets —
what's enrollable (offering: none|free|paid|both) and the lifecycle (status:
active|deadlinePassed|archived) — instead of a flat 6-value enum. Typed so the
impossible combinations are unrepresentable (a degraded run can't offer a
purchasable certificate; archived is always audit-only). Consumers simplify where
the old enum forced "archived || deadlinePassed" repetition:
- suppressPaymentDeadline / deadlineNote key off `status !== "active"`
- offeringBoxes keys off `offering`
- useCourseEnrollment builds options from `offering` (paid + free tracks) instead
  of a 6-case switch; the free label is derived from status

Behavior change: a paid-only run past its deadline is now {deadlinePassed, none}
and surfaces the "Certificate deadline has passed." warning (no enroll button)
instead of silently rendering nothing.

Also folds in two earlier-agreed changes that touch the same files:
- collapse EnrollActionKind "access" into "free" (identical free-enrollment path;
  only the button label differed)
- equal-height offering cards in the tablet "both" layout, driven from BoxGrid CSS
  (fills each card's flex chain to its stretched cell, CTA pinned to the bottom)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… refs

Review-round-3 fixes (all behavior-preserving except the new test):
- getCourseScenario now owns `offeredCertificate` on its degraded variants;
  CourseEnrollArea reads the flag instead of re-running getEnrollmentType, so
  enrollment-mode parsing stays in courseRun.ts.
- Add an offeringBoxCount(scenario, isEnrolled) helper as the single owner of
  the box-count mapping; InfoBoxCourse consumes it instead of a hand-mirrored
  ternary.
- Drop the unused `disabled` field from EnrollAction — busy state already lives
  on EnrollButton via the pending/loading props.
- Add a hook test pinning the approved paid-only-past-deadline behavior
  (offering "none" → no enroll button) at the offering→button mapping layer.
- Remove comments that pointed at the gitignored spec (§4g/§4h/§5, "the spec
  pinned…"), inlining the rationale instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…m card DOM

The Certificate Track and Learn for Free cards duplicated ~80 lines of
identical styled-component scaffold, and BoxGrid reached three levels into that
private DOM (`[data-card] > * > * > :last-child`) to equalize card height in the
side-by-side layout.

- Extract a shared TrackCard primitive (variant-driven surface, header, price,
  optional note slot, feature bullets, action) that both cards compose.
- Move the fill-to-stretch + bottom-align-CTA behavior into TrackCard via a
  `fill` prop the cards opt into for the "both" layout; remove the
  DOM-structure-coupled CSS from BoxGrid. Behavior-preserving — `fill` is a
  no-op wherever the grid cell is only content-height.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review-round-4 comment hygiene — strip process/temporal grounding and
redundancy while keeping the durable rationale:
- Drop the dead `CourseProductDetailEnroll` legacy-symbol citation in
  getCourseScenario (the symbol isn't in this repo).
- Replace "matching Figma (…leaves placement open)" with the actual spatial
  reason the degraded notices sit where they do.
- Drop cross-references that couple a comment to a sibling's current impl
  (useCertificatePrice → ProductSummary program-price path).
- De-duplicate the offeringBoxCount contract (kept on the helper's doc, trimmed
  at the call site) and the byStartDateDesc note (kept on the comparator's doc).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rikethrough

On the course Certificate Track card, financial aid no longer discounts the
displayed price (the struck-through original / discounted final is removed).
Strikethrough already means "purchased separately" on program cards, so reusing
it for finaid overloaded the same visual with two meanings.

Now the card always shows the full certificate price and conveys finaid as text:
- approved → "Financial assistance approved (applied at checkout)" (the discount
  applies later, in checkout; an approved-but-$0 discount is an accepted edge)
- not yet approved → "Financial assistance available"
Both states keep linking to the financial-aid form. Course-only; the program
price path is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etch enrolled button

Two tablet-only (sm–md) fixes:
- SummaryRows: the 2-column metadata split only reads well full-width. In the
  2-box course layout the metadata sits in a half-width grid cell, so the split
  crammed the rows. Gate it behind a $tabletColumns prop; CourseInfoBox passes 1
  when boxCount === 2.
- Enrolled state: EnrolledLink was a bare grid child and stretched to the tall
  metadata column's height. Wrap it in OfferingCell so it keeps its natural
  button height pinned to the top, like the other offering boxes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four review-identified consolidations, each a parameterized table with
meaningful per-row labels. No coverage change (still 318 tests) — every row
pins the same distinct branch the standalone test did; this only removes
copy-pasted setup/assertion boilerplate:

- useCourseEnrollment: 7 scenario→label/kind tests → one table (the biggest
  boilerplate source; degraded rows still assert their CourseScenario shape).
- InfoBoxCourse: 3 not-enrolled data-boxes tests → one table (kept the no-runs
  and the two enrolled cases separate — distinct setup/assertions).
- CoursePage: 4 header-scenario tests → one table (kept enrolled + the
  header↔InfoBox run-sync test separate).
- CertificateTrackCard + CourseEnrollArea: the finaid available/approved pairs
  → tables (kept CourseEnrollArea's "full price, not a discount" test separate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The finaid tests spelled out the full product_flexible_price discount object,
but "approved" only keys off product_flexible_price?.id (useCertificatePrice),
and the flexiblePrice factory already defaults that nested discount to a real
id. So:
- approved link case → makeFlexiblePrice() (factory default is "approved")
- available case → makeFlexiblePrice({ product_flexible_price: null })
- full-price test → makeDiscount({ discount_type, amount }); only the $25
  dollars-off is under test (it's the would-be $75 that must NOT render), the
  factory fills the rest. Dropped the unused id/price overrides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A second, broader sweep (not anchored on the product_flexible_price pattern)
found three more:
- ProductSummary: hand-rolled 11-field product_flexible_price discount literal →
  makeDiscount({ amount, discount_type }); only those two fields drive the
  asserted $125/$200/"applied", the factory fills the rest.
- ProductSummary: product({ price: "1499.00" }) → product(); the test asserts
  formatPrice(product.price), so the specific value was never needed.
- CoursePage: product({ price: "500" }) → product(); the price is never asserted
  (the test only checks the program-bundle upsell renders).

Verified no flakiness: scripts/test/jest-repeat.sh 30 over both files (varied
seeds) — all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
formatDate renders in the local timezone, so a run timestamp near a day
boundary formatted to a different calendar date on the (UTC) server than in
the browser — a hydration error on the collapsed dropdown value (and option
labels). Wrap the date-range output in <NoSSR> (the same pattern LocalDate and
the payment-deadline line already use), so the server and first client paint
render nothing and the local date fills in after mount. Local-tz formatting is
kept, so the dropdown stays consistent with the date row's LocalDate output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap the hardcoded `#004D1A` in ProgramStartForFreeBox's gradient for
`theme.custom.colors.darkGreen` (exact match). Wires the previously-unused
theme arg through. No visual change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isError previously OR'd in enrollmentsIsError (failure to load the user's
enrolled-run list). That made the enroll area show "There was a problem
processing your enrollment" when the user hadn't attempted to enroll — only
the read of their enrollment status failed. Limit isError to actual basket /
create-enrollment action failures; a status-load failure now degrades silently
(user treated as not-enrolled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebasing onto main pulled in the trackViewCoursePage/trackCourseProgramView
useEffect (PR #3524). The CoursePage test's partial gtm mock only stubbed
trackCourseEnrolled, so those two became undefined and the effect threw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ChristopherChudzicki ChristopherChudzicki force-pushed the cc/hq-11787-course-infobox branch from ce9d858 to fb428ce Compare July 1, 2026 20:27
@ChristopherChudzicki ChristopherChudzicki merged commit 08c026a into main Jul 1, 2026
13 checks passed
@ChristopherChudzicki ChristopherChudzicki deleted the cc/hq-11787-course-infobox branch July 1, 2026 23:54
@odlbot odlbot mentioned this pull request Jul 2, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants