openedx / openedx/frontend-app-learning
Source the access-expiration masquerade banner from the tab query, not useModel(tab)
@brian-smith-tcril is already working on this.
Since Aug 15, 2026.
- Dominant language
- JavaScript
- Stars
- 70
- Forks
- 335
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 35
Description
Part of the Redux → React Query migration, epic #1946. Related to the model-store dissolution (#1977) — it lands as a piece of that, not as a standalone prerequisite for any single tab. See Sequencing below.
Summary
useAccessExpirationMasqueradeBanner(courseId, tab) — rendered by the shared InstructorToolbar — reads accessExpiration.masqueradingExpiredCourse via useModel(tab, courseId) to show the "this learner's access expired" masquerade banner. It's the only dynamic useModel(<var>) reader that resolves to a course-home tab's data model (a literal useModel('progress' grep misses it). Because it reads the tab model store, any converted tab has to keep its data on the transitional bridge (meta tag) just to feed this one banner.
Sequencing: with #1977 (after the tabs convert), not as a per-tab prerequisite
We tried building this as a prerequisite below the progress-tab conversion (#1998), so progress could be fully bridge-free. Attempting it surfaced that the migration is awkward at any position in the current transition — it's a data-flow problem, not a stack-ordering one. accessExpiration lives in per-tab data, its consumer is the shared, tab-generic InstructorToolbar, and only some tabs are query-backed yet, so every mid-transition shape is bad:
- Prop-thread it (
TabPage→LoadedTabPage→InstructorToolbar→ hook):useAccessExpirationMasqueradeBannerends up takingaccessExpirationin, inverting the responsibility of a hook named for the data it ought to source. - Hook reads the React Query cache itself: a generic
src/alerts/hook has to importcourse-home/data/queryKeysand special-case progress'stargetUserId— worse coupling.
Both also still need a transitional useModel(activeTabSlug) fallback for the not-yet-converted tabs (courseware/live/discussion).
It's only clean at the north star — every tab query-backed and the model store gone — when the banner sources accessExpiration from a uniform query-backed source with no fallback. So this lands with the model-store dissolution (#1977), once the tabs are converted; it's a piece of that effort, not a blocker for progress.
In the meantime: the progress tab (#1998) keeps a scoped one-reader meta bridge. The model store persists until #1977 regardless, so that one line is just one more consumer of a bridge that's staying anyway — and it lets progress ship without waiting on this.
Target (at the north star)
Source accessExpiration from a query-backed source instead of useModel(tab), with no useModel fallback. TabPage already holds the tab's query via courseStatus, so threading it from there is one option; a uniform tab-data read, or relocating access_expiration onto the courseHomeMeta query (a backend change — the metadata endpoint currently carries only access_expiration_date), are others. The exact mechanism is deliberately left to #1977, when all tabs are query-backed and the tradeoffs are clean — the mid-transition options above are all worse than waiting. (useCourseStartMasqueradeBanner is untouched by any of this — it reads courseHomeMeta and uses tab only as a === 'progress' string check, not useModel(tab).)
Scope (when it lands)
Four shared files + tests: alerts/access-expiration-alert/hooks.js, instructor-toolbar/InstructorToolbar.jsx, tab-page/LoadedTabPage.tsx, tab-page/TabPage.tsx.
[!NOTE]
This issue was authored and updated by Claude (Claude Code) and reviewed before posting.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.