Program Infobox Updates#3589
Conversation
OpenAPI ChangesNo changes detected Unexpected changes? Ensure your branch is up-to-date with |
There was a problem hiding this comment.
Pull request overview
Updates MITxOnline Program and Program-as-Course product pages to use the redesigned InfoBox building blocks introduced for courses, adding program-specific offering logic and a new “savings” price framing while keeping course/program variants aligned through shared presentational components and hooks.
Changes:
- Replaces legacy program enrollment dialog/button flows with shared, hook-driven enrollment state + shared header/infobox CTA components (mirroring behavior across page header and InfoBox).
- Adds program pricing/savings + financial-aid text handling via a dedicated data hook and a new full-width savings price block.
- Refactors InfoBox composition into a shared layout/grid + shared offering-box renderer used by both course and program variants.
Reviewed changes
Copilot reviewed 54 out of 55 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| frontends/main/src/page-components/EnrollmentDialogs/ProgramEnrollmentDialog.tsx | Removes legacy program enrollment modal UI (superseded by new infobox/header enrollment system). |
| frontends/main/src/page-components/EnrollmentDialogs/ProgramEnrollmentDialog.test.tsx | Removes tests for the deleted legacy program enrollment modal. |
| frontends/main/src/app-pages/ProductPages/util.ts | Adds getTotalRequiredCourses helper for program requirements-derived counts. |
| frontends/main/src/app-pages/ProductPages/useProgramIsEnrolled.ts | New hook to determine whether the authenticated user is enrolled in a specific program (auth-gated). |
| frontends/main/src/app-pages/ProductPages/useProgramEnrollment.ts | New hook implementing the program enrollment “state machine” + CTA actions + analytics. |
| frontends/main/src/app-pages/ProductPages/useProgramEnrollment.test.tsx | Tests for program enrollment hook (offering logic, auth gating, analytics, actions). |
| frontends/main/src/app-pages/ProductPages/useProgramCertificatePrice.ts | New hook providing full program price, savings framing inputs, and financial-aid link/applied state. |
| frontends/main/src/app-pages/ProductPages/useProgramCertificatePrice.test.tsx | Tests for program certificate price hook, including savings and financial-aid behavior. |
| frontends/main/src/app-pages/ProductPages/useCourseEnrollment.ts | Refactors course enrollment hook to use shared enroll analytics/types helpers. |
| frontends/main/src/app-pages/ProductPages/useCourseCertificatePrice.ts | Renames/refines course certificate pricing hook and auth-gates flexible-price lookup. |
| frontends/main/src/app-pages/ProductPages/TrackCard.tsx | Adds priceBlock support and introduces AccessFeatureRow to keep shared bullet copy in sync. |
| frontends/main/src/app-pages/ProductPages/ProgramSavingsBlock.tsx | New component rendering “savings” price framing (full program vs purchased separately + save line). |
| frontends/main/src/app-pages/ProductPages/ProgramSavingsBlock.test.tsx | Tests for savings block rendering + pluralization semantics. |
| frontends/main/src/app-pages/ProductPages/ProgramPage.tsx | Switches program page header CTA to the new shared program header enroll button. |
| frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx | Stabilizes program page tests by pinning offering modes to avoid CTA/heading flakiness. |
| frontends/main/src/app-pages/ProductPages/programOffering.ts | New pure function mapping program enrollment modes + product purchasability to actionable offering. |
| frontends/main/src/app-pages/ProductPages/programOffering.test.ts | Tests for actionable program offering/demotion logic. |
| frontends/main/src/app-pages/ProductPages/ProgramHeaderEnrollButton.tsx | New program-specific wrapper around shared HeaderEnrollButton using useProgramEnrollment. |
| frontends/main/src/app-pages/ProductPages/ProgramHeaderEnrollButton.test.tsx | Tests header CTA labels/disabled/enrolled/signup/error behaviors for programs. |
| frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx | Removes legacy program enrollment CTA component. |
| frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx | Removes tests for deleted legacy program enrollment CTA. |
| frontends/main/src/app-pages/ProductPages/ProgramEnrollArea.tsx | New program infobox enroll-area container wiring enrollment + pricing + savings into offering boxes. |
| frontends/main/src/app-pages/ProductPages/ProgramEnrollArea.test.tsx | Tests infobox program enroll-area structure (both/paid/free/enrolled/none, savings, financial-aid). |
| frontends/main/src/app-pages/ProductPages/ProgramBundleUpsell.tsx | Adjusts upsell styling to sit flush within the InfoBox card across widths. |
| frontends/main/src/app-pages/ProductPages/ProgramAsCourseSummary.test.tsx | Updates program-as-course summary tests for new required props and new dates-row behavior. |
| frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.tsx | Switches program-as-course page header CTA to the new shared program header enroll button. |
| frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.test.tsx | Updates assertions for new CTA labels and card rendering rules under the redesigned composition. |
| frontends/main/src/app-pages/ProductPages/ProgramAsCourseCertificateTrackCard.tsx | Removes bespoke program-as-course certificate track card (replaced by shared TrackCard-based components). |
| frontends/main/src/app-pages/ProductPages/ProductSummary.tsx | Removes legacy program price row; adds certificate row; adds program dates row for program-as-course; makes tabletColumns required. |
| frontends/main/src/app-pages/ProductPages/ProductSummary.test.tsx | Updates product summary tests for required tabletColumns and new program summary row ordering/content. |
| frontends/main/src/app-pages/ProductPages/LearnForFreeCard.tsx | Uses shared AccessFeatureRow for consistent access bullet copy. |
| frontends/main/src/app-pages/ProductPages/LearnForFreeCard.test.tsx | Updates expected access bullet copy for program noun. |
| frontends/main/src/app-pages/ProductPages/InfoBoxProgramAsCourse.tsx | Converts program-as-course infobox into a thin wrapper around InfoBoxProgram (displayAsCourse). |
| frontends/main/src/app-pages/ProductPages/InfoBoxProgramAsCourse.test.tsx | Adds coverage for program-as-course infobox behaviors (labels, finaid link, no savings, upsell, a11y heading). |
| frontends/main/src/app-pages/ProductPages/InfoBoxProgram.tsx | Rebuilds program infobox using shared InfoBox layout + program enroll area + optional program-as-course upsell. |
| frontends/main/src/app-pages/ProductPages/InfoBoxProgram.test.tsx | Adds coverage for program infobox layouts (box counts, structure, savings, enrolled, none). |
| frontends/main/src/app-pages/ProductPages/InfoBoxParts.tsx | Removes now-obsolete infobox padding/action stack wrappers (handled by shared grid/layout). |
| frontends/main/src/app-pages/ProductPages/InfoBoxLayout.tsx | New shared InfoBox shell coordinating heading, grid, tablet column logic, upsell, and Ask TIM section. |
| frontends/main/src/app-pages/ProductPages/InfoBoxGrid.tsx | New shared responsive grid + divider used by course and program infoboxes. |
| frontends/main/src/app-pages/ProductPages/InfoBoxCourse.tsx | Migrates course infobox to InfoBoxLayout and shared grid/count logic. |
| frontends/main/src/app-pages/ProductPages/HeaderEnrollButton.tsx | New shared page-header enroll CTA component (enrolled/options/none + signup + error alert). |
| frontends/main/src/app-pages/ProductPages/enrollTypes.ts | New shared enrollment types + offeringBoxCount helper for consistent grid counts. |
| frontends/main/src/app-pages/ProductPages/enrollTypes.test.ts | Tests for shared offering-box count logic. |
| frontends/main/src/app-pages/ProductPages/EnrollOfferingBoxes.tsx | New shared infobox enroll-area presentational component (cards, buttons, error alert, signup popover). |
| frontends/main/src/app-pages/ProductPages/EnrollAreaParts.tsx | New shared low-level UI primitives for offering cell layout + enroll button behavior/spinner a11y. |
| frontends/main/src/app-pages/ProductPages/enrollAnalytics.ts | New shared PostHog analytics helper for enroll CTA clicks (course + program). |
| frontends/main/src/app-pages/ProductPages/courseRun.ts | Reuses shared Offering type and removes duplicated offering box count logic. |
| frontends/main/src/app-pages/ProductPages/courseRun.test.ts | Removes tests for the deleted duplicated offeringBoxCount in courseRun.ts. |
| frontends/main/src/app-pages/ProductPages/CoursePage.tsx | Swaps course page header CTA implementation to shared HeaderEnrollButton and adds header error surfacing. |
| frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx | Adds test ensuring header enrollment failures surface an error alert beneath the header CTA. |
| frontends/main/src/app-pages/ProductPages/CourseEnrollArea.tsx | Refactors course infobox enroll area to use shared EnrollOfferingBoxes + renamed pricing hook. |
| frontends/main/src/app-pages/ProductPages/CourseEnrollArea.test.tsx | Updates inline comment to reflect renamed pricing hook. |
| frontends/main/src/app-pages/ProductPages/CertificateTrackCard.tsx | Adds priceBlock passthrough and uses shared AccessFeatureRow for consistent access bullet copy. |
| frontends/main/src/app-pages/ProductPages/CertificateTrackCard.test.tsx | Updates expected access bullet copy and adds a test for priceBlock suppressing top-right price. |
| const ProgramVerticalDivider = styled.div(() => ({ | ||
| width: "1px", | ||
| height: "48px", | ||
| backgroundColor: theme.custom.colors.lightGray2, | ||
| flexShrink: 0, |
d08bcae to
0d2f395
Compare
There was a problem hiding this comment.
I was able to verify the screenshot and the manual testing path but I did find an issue that seems like it’s a bug. When logged out and viewing a program with no enrollment modes or no purchasable product, the InfoBox probably should not display “Certificate: Program certificate on completion” since there's no certificate available to earn.
3aef496 to
dc0f77e
Compare
…ollButton, BoxGrid)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…card Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Composes useProgramEnrollment + useProgramCertificatePrice with the shared EnrollAreaParts/CertificateTrackCard/LearnForFreeCard/EnrolledLink building blocks, mirroring CourseEnrollArea's paid/free box composition for programs (no degraded run states, no certificateDeadlineNote).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a program-level dates row (Start/End) to ProgramAsCourseSummary, using program.start_date/end_date with the shared LocalDate/InfoLabelValue treatment. Reorder rows to Format, Estimated, Dates per spec; the row is omitted entirely when both dates are unset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nrollArea Rewrite InfoBoxProgram to mirror InfoBoxCourse's structure: BoxGrid with a data-boxes count derived from getProgramOffering/programOfferingBoxCount and useProgramIsEnrolled (the same sources useProgramEnrollment uses, so the grid can't disagree with what ProgramEnrollArea renders), a conditional SectionDivider, and ProgramEnrollArea in place of the old fixed ProgramEnrollmentButton. Adds InfoBoxProgram.test.tsx pinning the both/paid- only/savings behaviors, and fixes two ProgramPage.test.tsx assertions that implicitly relied on the old button's fixed "Enroll in Program" label, which is now offering-dependent.
…drop bespoke card Rewrites InfoBoxProgramAsCourse onto the same BoxGrid/ProgramEnrollArea composition as InfoBoxProgram (Task 10), passing displayAsCourse for course-labeled buttons and ProgramAsCourseSummary for meta. Deletes the bespoke ProgramAsCourseCertificateTrackCard in favor of the shared CertificateTrackCard rendered via ProgramEnrollArea, dropping the old certificate_available-gated logic in favor of enrollment-mode-driven offering (two deliberate behavior changes, per spec). Retains the "Part of a Program" upsell for programs with parent programs. Updates ProgramAsCoursePage.test.tsx assertions that drove the old "Enroll" label to the new "Start Learning"/"Enroll" labels, and pins an explicit paid enrollment mode where the headings test relied on certificate_available alone. Adds InfoBoxProgramAsCourse.test.tsx covering paid-only, both, financial aid, no-savings, parent-program upsell, and the a11y heading.
Tasks 1-11 recomposed program InfoBoxes and page headers onto shared blocks, leaving ProgramEnrollmentButton, ProductSummary's ProgramPriceRow, the empty CourseSignupButton stub, and three consumer-free InfoBox primitives dead. Verified via grep that nothing outside these files still imports them before deleting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e, finaid query gate) Deletes the now-orphaned ProgramEnrollmentDialog, drops redundant/dead tests, restores the enrollment-type gate on the flexible-price query dropped during refactoring, and tightens/trims a couple of test overrides.
…acing) - Free program enrollment fires GTM program-enrolled (was course-enrolled; trackProgramEnrolled had lost its last caller with the dialog's removal) - Header enroll button surfaces mutation failures again with an inline alert below the button, matching the pre-redesign header CTA - Enrollment error alerts span the full BoxGrid row on tablet instead of auto-flowing into a half-width column slot (shared FullRowCell, applied on both course and program enroll areas) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Save-$X and comparison text were separate flex items, so each wrapped inside its own box at card width; inline spans in one block wrap as text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rt-price hooks Consolidate duplicated enrollment logic into shared modules: - enrollTypes.ts owns Offering + canonical offeringBoxCount - enrollAnalytics.ts owns EnrollCtaPlacement + fireEnrollCta - getTotalRequiredCourses moved to util.ts - rename useCertificatePrice -> useCourseCertificatePrice (.tsx -> .ts), gate the course price query on useUserIsAuthenticated so anon course visitors no longer fire the user-scoped flexible-price query - rename useProgramCertificatePrice .tsx -> .ts, data-only shape Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce reusable presentation blocks shared by course and program: - InfoBoxLayout.tsx: card shell (labelled section, BoxGrid, meta grid, divider gating, AskTim) with boxCount/tabletColumns derivations - EnrollOfferingBoxes.tsx: offering boxes, enrolled collapse, error alert - HeaderEnrollButton.tsx: HeaderButtonSlot + HeaderAlertSizer (width fix) + three-state render; program header adapts onto it - ProgramSavingsBlock.tsx (+ test): styled savings JSX, price row wraps - TrackCard.tsx: shared AccessFeatureRow used by cert + free cards - ProgramBundleUpsell.tsx: flush top-border separator on tablet+desktop (was doubling the card border on tablet) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… blocks Wire the course and program InfoBoxes onto the shared shell + primitives: - InfoBoxProgram.tsx is now the single program component with optional displayAsCourse; InfoBoxProgramAsCourse is a thin wrapper - InfoBoxCourse.tsx adopts InfoBoxLayout (keeps session/enrolled logic) - CourseEnrollArea/ProgramEnrollArea are thin adapters over the shared EnrollOfferingBoxes presenter; course header now surfaces enroll errors - tabletColumns is now required on the summary components + SummaryRows - summary direct-render tests updated for required tabletColumns; cloned course structural + enrolled data-boxes tests into InfoBoxProgram.test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…able The program InfoBox rendered "Certificate: Program certificate on completion" unconditionally, claiming a certificate could be earned even for a program with no paid enrollment mode (free-only, or no modes). Gate the row on `program.certificate_available` — the API's own signal, computed server-side as `any(mode.requires_payment)` — the same flag MitxOnlineResourceCard already trusts. Visibility stays independent of product data: a paid mode with no purchasable product still shows its (odd) certificate card. That's a bad-data combination we deliberately don't police in the frontend. Pin the existing cert-row tests to certificate_available (the factory default is a random boolean). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dc0f77e to
b18c9d6
Compare
Thanks for catching this. The exact mode you mentioned—no enrollment types / no product—is kind of bad data, but the cert row was definitely showing even if enrollment_mode was free-only. That's fixed. (So is the one your mentioned.) Ready for another look. |






What are the relevant tickets?
For
Part 2 of the InfoBox redesign: the Program and Program-as-Course infoboxes. PR1 (#3523) shipped the Course infobox and its building blocks.
Description (What does it do?)
Brings Program and Program-as-Course pages onto the redesigned building blocks from PR1 — it's mostly composition. The one genuinely new piece is the savings price framing.
/dashboard/program/[id]).Changes at a glance
The diff is large, but the net change (about +700) is program-related tests. Production code shrank slightly. Changes basically fall in two buckets:
programOffering.tsenrollTypes.tsofferingTracks/buildEnrollState/offeringBoxCountenrollAnalytics.tsfireEnrollCta(PostHogenroll_cta_clicked)useProgramEnrollment.tsuseProgramCertificatePrice.tsProductSummaryprice/savings/finaid, reshapeduseProgramIsEnrolled.tsProgramSavingsBlock.tsxEnrollOfferingBoxes.tsxCourseEnrollAreaEnrollAreaParts.tsxHeaderEnrollButton.tsxInfoBoxLayout.tsx/InfoBoxGrid.tsxProgramEnrollArea.tsx/ProgramHeaderEnrollButton.tsxScreenshots (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 (tablet shots at the end). Based on the QA programs in the setup script linked under testing.
Savings price framing (anon)
S1 · Program price vs. purchasing courses separately
The new savings block: program price ("full program") beside the struck bundle price ("purchased separately"), with a green "Save $X …" line. On main this is the old single price row + "Audit for free or upgrade" box.
Program enrollment paths (anon)
P1 · Both paths — free audit + paid certificate
P2 · Paid certificate only
P3 · Free only — audit, no certificate
Program-as-Course (anon)
C2 · Both paths, course vocabulary + "Part of a Program" upsell
Same blocks as a program, but with course copy ("Access to this course & course materials") and no savings block; the "Part of a Program" upsell renders as a flush separator inside the card.
Logged-in — enrolled & financial aid
P1 · Enrolled — collapses to a single link
P2 · Financial assistance approved — full price, applied at checkout
Tablet (anon)
Tablet — P1 · Both paths
Tablet — P2 · Paid only
How can this be tested?
Prerequisites: MITxOnline and Learn integrated locally; the
MitxOnlineProductPagesfeature flag ON.Test Data: setup script at https://gist.github.com/ChristopherChudzicki/78e77f73139b8fdd59e91f522b0bb8b2 — it provisions the QA programs / program-as-course pages (the P/S/C scenarios above) plus the
student_infobox@odl.localuser (enrolled in some, financial-aid-approved on others).Manual Testing (screenshots cover appearance; the flows below are the important part):
program-enrolledGTM event)./dashboard/program/[id]— no cards or buttons.Additional Context
Alongside the composition, the course and program variants were pulled onto shared internals (the presentational components above +
enrollAnalytics.ts, the requiredtabletColumnsprop, an auth-gate on the course price hook) rather than parallel copies, so the two can't drift. Course test suites are unchanged and green as the regression oracle. Full ProductPages suite: 30 suites / 359 tests green; typecheck + lint clean.Follow-ups / out of scope: visual QA of the savings block against Figma (100%-width block, green "Save $X" line without a check icon — confirm intent); splitting the large
ProductSummary.tsx; program discounts (deferred backend work); a few a11y refinements; and the smoot-design Alert contrast on the dark header banner (tracked separately in smoot-design).