From 646736b1d4b45a032cc32b1d34d7fd458cf58b6a Mon Sep 17 00:00:00 2001 From: Carey Gumaer Date: Thu, 6 Nov 2025 15:17:07 -0500 Subject: [PATCH 1/6] if the user's b2b orgs haven't changed after redeeming an enrollment code, redirect to the dashboard with an error flag in the query string params --- .../EnrollmentCodePage/EnrollmentCodePage.tsx | 14 +++++++++++++- frontends/main/src/common/urls.ts | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx index ade2e949f4..cca447ecb1 100644 --- a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx +++ b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx @@ -6,6 +6,7 @@ import { useB2BAttachMutation } from "api/mitxonline-hooks/organizations" import { userQueries } from "api/hooks/user" import { useQuery } from "@tanstack/react-query" import { useRouter } from "next-nprogress-bar" +import { mitxUserQueries } from "api/mitxonline-hooks/user" type EnrollmentCodePage = { code: string @@ -17,6 +18,8 @@ const InterstitialMessage = styled(Typography)(({ theme }) => ({ })) const EnrollmentCodePage: React.FC = ({ code }) => { + const mitxOnlineUser = useQuery(mitxUserQueries.me()) + const userOrgs = structuredClone(mitxOnlineUser.data?.b2b_organizations || []) const enrollment = useB2BAttachMutation({ enrollment_code: code, }) @@ -28,9 +31,18 @@ const EnrollmentCodePage: React.FC = ({ code }) => { }) const enrollAsync = enrollment.mutateAsync + React.useEffect(() => { if (user?.is_authenticated) { - enrollAsync().then(() => router.push(urls.DASHBOARD_HOME)) + enrollAsync().then(() => { + if ( + userOrgs.length === mitxOnlineUser.data?.b2b_organizations?.length + ) { + router.push(urls.DASHBOARD_HOME_ENROLLMENT_ERROR) + } else { + router.push(urls.DASHBOARD_HOME) + } + }) } }, [user?.is_authenticated, enrollAsync, router]) diff --git a/frontends/main/src/common/urls.ts b/frontends/main/src/common/urls.ts index 8843084378..27c81577d9 100644 --- a/frontends/main/src/common/urls.ts +++ b/frontends/main/src/common/urls.ts @@ -60,6 +60,7 @@ export const DASHBOARD_VIEW = "/dashboard/[tab]" const dashboardView = (tab: string) => generatePath(DASHBOARD_VIEW, { tab }) export const DASHBOARD_HOME = "/dashboard" +export const DASHBOARD_HOME_ENROLLMENT_ERROR = "/dashboard?enrollment_error=1" export const MY_LISTS = dashboardView("my-lists") export const PROFILE = dashboardView("profile") export const SETTINGS = dashboardView("settings") From 34a403f90836ff046995e6f408a94c67b8eddc6a Mon Sep 17 00:00:00 2001 From: Carey Gumaer Date: Thu, 6 Nov 2025 16:43:01 -0500 Subject: [PATCH 2/6] if enrollment code redemption fails, display an error --- .../DashboardPage/HomeContent.test.tsx | 30 +++++++ .../app-pages/DashboardPage/HomeContent.tsx | 29 ++++++- .../EnrollmentCodePage.test.tsx | 85 ++++++++++++++++++- .../EnrollmentCodePage/EnrollmentCodePage.tsx | 48 ++++++++--- frontends/main/src/common/urls.ts | 3 +- 5 files changed, 174 insertions(+), 21 deletions(-) diff --git a/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx b/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx index a81e290173..6a41e08257 100644 --- a/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx @@ -239,4 +239,34 @@ describe("HomeContent", () => { } }, ) + + test("Does not display enrollment error alert when query param is not present", async () => { + setupAPIs() + renderWithProviders() + + await screen.findByRole("heading", { + name: "Your MIT Learning Journey", + }) + + expect(screen.queryByText("Enrollment Error")).not.toBeInTheDocument() + }) + + test("Displays enrollment error alert when query param is present", async () => { + setupAPIs() + renderWithProviders(, { + url: "/dashboard?enrollment_error=1", + }) + + await screen.findByRole("heading", { + name: "Your MIT Learning Journey", + }) + + expect(screen.getByText("Enrollment Error")).toBeInTheDocument() + expect( + screen.getByText( + /The Enrollment Code is incorrect or no longer available/, + ), + ).toBeInTheDocument() + expect(screen.getByText("Contact Support")).toBeInTheDocument() + }) }) diff --git a/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx b/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx index 377d1eb7e9..a65f0262d4 100644 --- a/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx +++ b/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx @@ -1,9 +1,9 @@ "use client" import React, { Suspense } from "react" -import { ButtonLink } from "@mitodl/smoot-design" +import { Alert, ButtonLink } from "@mitodl/smoot-design" import { ResourceTypeEnum } from "api" -import { styled, Typography } from "ol-components" -import { PROFILE } from "@/common/urls" +import { Link, styled, Typography } from "ol-components" +import { PROFILE, ENROLLMENT_ERROR_QUERY_PARAM } from "@/common/urls" import { TopPicksCarouselConfig, TopicCarouselConfig, @@ -18,6 +18,7 @@ import { useFeatureFlagEnabled } from "posthog-js/react" import { FeatureFlags } from "@/common/feature_flags" import { useUserMe } from "api/hooks/user" import { OrganizationCards } from "./CoursewareDisplay/OrganizationCards" +import { useSearchParams } from "next/navigation" const SubTitleText = styled(Typography)(({ theme }) => ({ color: theme.custom.colors.darkGray2, @@ -66,13 +67,20 @@ const TitleText = styled(Typography)(({ theme }) => ({ }, })) as typeof Typography +const AlertBanner = styled(Alert)({ + marginTop: "32px", +}) + const HomeContent: React.FC = () => { + const searchParams = useSearchParams() + const enrollmentError = searchParams.get(ENROLLMENT_ERROR_QUERY_PARAM) const { isLoading: isLoadingProfile, data: user } = useUserMe() const topics = user?.profile?.preference_search_filters.topic const certification = user?.profile?.preference_search_filters.certification const showEnrollments = useFeatureFlagEnabled( FeatureFlags.EnrollmentDashboard, ) + const supportEmail = process.env.NEXT_PUBLIC_MITOL_SUPPORT_EMAIL || "" return ( <> @@ -88,6 +96,21 @@ const HomeContent: React.FC = () => { + {enrollmentError && ( + + + Enrollment Error + + + {" - "} + The Enrollment Code is incorrect or no longer available.{" "} + + Contact Support + {" "} + for assistance. + + + )} {showEnrollments ? : null} diff --git a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx index af357581de..1b6011719f 100644 --- a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx +++ b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx @@ -1,7 +1,7 @@ import React from "react" import { renderWithProviders, setMockResponse, waitFor } from "@/test-utils" import { makeRequest, urls } from "api/test-utils" -import { urls as b2bUrls } from "api/mitxonline-test-utils" +import { urls as b2bUrls, factories } from "api/mitxonline-test-utils" import * as commonUrls from "@/common/urls" import { Permission } from "api/hooks/user" import EnrollmentCodePage from "./EnrollmentCodePage" @@ -25,6 +25,9 @@ describe("EnrollmentCodePage", () => { [Permission.Authenticated]: false, }) + const mitxUser = factories.user.user() + setMockResponse.get(b2bUrls.userMe.get(), mitxUser) + setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), [], { code: 403, }) @@ -54,6 +57,9 @@ describe("EnrollmentCodePage", () => { [Permission.Authenticated]: true, }) + const mitxUser = factories.user.user() + setMockResponse.get(b2bUrls.userMe.get(), mitxUser) + setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), []) renderWithProviders(, { @@ -66,8 +72,24 @@ describe("EnrollmentCodePage", () => { [Permission.Authenticated]: true, }) + const initialOrg = factories.organizations.organization({}) + const newOrg = factories.organizations.organization({}) + const initialMitxUser = factories.user.user({ + b2b_organizations: [initialOrg], + }) + const updatedMitxUser = factories.user.user({ + b2b_organizations: [initialOrg, newOrg], + }) + + // First call returns initial user, subsequent calls return updated user + let callCount = 0 + setMockResponse.get(b2bUrls.userMe.get(), () => { + callCount++ + return callCount === 1 ? initialMitxUser : updatedMitxUser + }) + const attachUrl = b2bUrls.b2bAttach.b2bAttachView("test-code") - setMockResponse.post(attachUrl, []) + setMockResponse.post(attachUrl, updatedMitxUser) renderWithProviders(, { url: commonUrls.B2B_ATTACH_VIEW, @@ -77,6 +99,63 @@ describe("EnrollmentCodePage", () => { expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined) }) - expect(mockPush).toHaveBeenCalledWith(commonUrls.DASHBOARD_HOME) + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith(commonUrls.DASHBOARD_HOME) + }) + }) + + test("Redirects to dashboard with error when b2b organizations don't change", async () => { + setMockResponse.get(urls.userMe.get(), { + [Permission.Authenticated]: true, + }) + + const organization = factories.organizations.organization({}) + const mitxUser = factories.user.user({ + b2b_organizations: [organization], + }) + + setMockResponse.get(b2bUrls.userMe.get(), mitxUser) + + const attachUrl = b2bUrls.b2bAttach.b2bAttachView("invalid-code") + setMockResponse.post(attachUrl, mitxUser) + + renderWithProviders(, { + url: commonUrls.B2B_ATTACH_VIEW, + }) + + await waitFor(() => { + expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined) + }) + + expect(mockPush).toHaveBeenCalledWith( + commonUrls.DASHBOARD_HOME_ENROLLMENT_ERROR, + ) + }) + + test("Redirects to dashboard with error when user has no organizations initially", async () => { + setMockResponse.get(urls.userMe.get(), { + [Permission.Authenticated]: true, + }) + + const mitxUser = factories.user.user({ + b2b_organizations: [], + }) + + setMockResponse.get(b2bUrls.userMe.get(), mitxUser) + + const attachUrl = b2bUrls.b2bAttach.b2bAttachView("invalid-code") + setMockResponse.post(attachUrl, mitxUser) + + renderWithProviders(, { + url: commonUrls.B2B_ATTACH_VIEW, + }) + + await waitFor(() => { + expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined) + }) + + expect(mockPush).toHaveBeenCalledWith( + commonUrls.DASHBOARD_HOME_ENROLLMENT_ERROR, + ) }) }) diff --git a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx index cca447ecb1..0cd8a99b1c 100644 --- a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx +++ b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx @@ -19,32 +19,52 @@ const InterstitialMessage = styled(Typography)(({ theme }) => ({ const EnrollmentCodePage: React.FC = ({ code }) => { const mitxOnlineUser = useQuery(mitxUserQueries.me()) - const userOrgs = structuredClone(mitxOnlineUser.data?.b2b_organizations || []) + const initialOrgsRef = React.useRef(null) + const [hasEnrolled, setHasEnrolled] = React.useState(false) + + // Capture initial organization count once + if ( + initialOrgsRef.current === null && + mitxOnlineUser.data?.b2b_organizations + ) { + initialOrgsRef.current = mitxOnlineUser.data.b2b_organizations.length + } + + const router = useRouter() + const enrollment = useB2BAttachMutation({ enrollment_code: code, }) - const router = useRouter() const { isLoading: userLoading, data: user } = useQuery({ ...userQueries.me(), staleTime: 0, }) - const enrollAsync = enrollment.mutateAsync + React.useEffect(() => { + if (user?.is_authenticated && !hasEnrolled && !enrollment.isPending) { + setHasEnrolled(true) + enrollment.mutate() + } + }, [user?.is_authenticated, hasEnrolled, enrollment]) + // Handle redirect after mutation succeeds and query is refetched React.useEffect(() => { - if (user?.is_authenticated) { - enrollAsync().then(() => { - if ( - userOrgs.length === mitxOnlineUser.data?.b2b_organizations?.length - ) { - router.push(urls.DASHBOARD_HOME_ENROLLMENT_ERROR) - } else { - router.push(urls.DASHBOARD_HOME) - } - }) + if (enrollment.isSuccess && !enrollment.isPending) { + const currentOrgCount = + mitxOnlineUser.data?.b2b_organizations?.length ?? 0 + if (initialOrgsRef.current === currentOrgCount) { + router.push(urls.DASHBOARD_HOME_ENROLLMENT_ERROR) + } else { + router.push(urls.DASHBOARD_HOME) + } } - }, [user?.is_authenticated, enrollAsync, router]) + }, [ + enrollment.isSuccess, + enrollment.isPending, + mitxOnlineUser.data?.b2b_organizations?.length, + router, + ]) React.useEffect(() => { if (userLoading) { diff --git a/frontends/main/src/common/urls.ts b/frontends/main/src/common/urls.ts index 27c81577d9..b70edf1181 100644 --- a/frontends/main/src/common/urls.ts +++ b/frontends/main/src/common/urls.ts @@ -60,7 +60,8 @@ export const DASHBOARD_VIEW = "/dashboard/[tab]" const dashboardView = (tab: string) => generatePath(DASHBOARD_VIEW, { tab }) export const DASHBOARD_HOME = "/dashboard" -export const DASHBOARD_HOME_ENROLLMENT_ERROR = "/dashboard?enrollment_error=1" +export const ENROLLMENT_ERROR_QUERY_PARAM = "enrollment_error" +export const DASHBOARD_HOME_ENROLLMENT_ERROR = `/dashboard?${ENROLLMENT_ERROR_QUERY_PARAM}=1` export const MY_LISTS = dashboardView("my-lists") export const PROFILE = dashboardView("profile") export const SETTINGS = dashboardView("settings") From 672f17148d8a63de5c5f948db2cada589c9d8eda Mon Sep 17 00:00:00 2001 From: Carey Gumaer Date: Wed, 12 Nov 2025 18:02:17 -0500 Subject: [PATCH 3/6] adjust the logic to use response codes added to the mitxonline attach endpoint --- .../mitxonline/hooks/organizations/queries.ts | 8 ++- .../EnrollmentCodePage.test.tsx | 62 ++++++------------- .../EnrollmentCodePage/EnrollmentCodePage.tsx | 36 +++-------- 3 files changed, 35 insertions(+), 71 deletions(-) diff --git a/frontends/api/src/mitxonline/hooks/organizations/queries.ts b/frontends/api/src/mitxonline/hooks/organizations/queries.ts index b6173bcdbe..01862ed6c2 100644 --- a/frontends/api/src/mitxonline/hooks/organizations/queries.ts +++ b/frontends/api/src/mitxonline/hooks/organizations/queries.ts @@ -32,7 +32,13 @@ const organizationQueries = { const useB2BAttachMutation = (opts: B2bApiB2bAttachCreateRequest) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: () => b2bApi.b2bAttachCreate(opts), + mutationFn: async () => { + const response = await b2bApi.b2bAttachCreate(opts) + // 200 (already attached) indicates user already attached to all contracts + // 201 (successfully attached) is success + // 404 (invalid or expired code) will be thrown as error by axios + return response + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["mitxonline"] }) }, diff --git a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx index 1b6011719f..df03c49885 100644 --- a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx +++ b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx @@ -67,29 +67,14 @@ describe("EnrollmentCodePage", () => { }) }) - test("Redirects to dashboard on successful attachment", async () => { + test("Redirects to dashboard on successful attachment (201 status)", async () => { setMockResponse.get(urls.userMe.get(), { [Permission.Authenticated]: true, }) - const initialOrg = factories.organizations.organization({}) - const newOrg = factories.organizations.organization({}) - const initialMitxUser = factories.user.user({ - b2b_organizations: [initialOrg], - }) - const updatedMitxUser = factories.user.user({ - b2b_organizations: [initialOrg, newOrg], - }) - - // First call returns initial user, subsequent calls return updated user - let callCount = 0 - setMockResponse.get(b2bUrls.userMe.get(), () => { - callCount++ - return callCount === 1 ? initialMitxUser : updatedMitxUser - }) - const attachUrl = b2bUrls.b2bAttach.b2bAttachView("test-code") - setMockResponse.post(attachUrl, updatedMitxUser) + // 201 status indicates successful attachment to new contract(s) + setMockResponse.post(attachUrl, {}, { code: 201 }) renderWithProviders(, { url: commonUrls.B2B_ATTACH_VIEW, @@ -104,22 +89,16 @@ describe("EnrollmentCodePage", () => { }) }) - test("Redirects to dashboard with error when b2b organizations don't change", async () => { + test("Redirects to dashboard when user already attached to all contracts (200 status)", async () => { setMockResponse.get(urls.userMe.get(), { [Permission.Authenticated]: true, }) - const organization = factories.organizations.organization({}) - const mitxUser = factories.user.user({ - b2b_organizations: [organization], - }) - - setMockResponse.get(b2bUrls.userMe.get(), mitxUser) - - const attachUrl = b2bUrls.b2bAttach.b2bAttachView("invalid-code") - setMockResponse.post(attachUrl, mitxUser) + const attachUrl = b2bUrls.b2bAttach.b2bAttachView("already-used-code") + // 200 status indicates user already attached to all contracts - still redirect to dashboard without error + setMockResponse.post(attachUrl, {}, { code: 200 }) - renderWithProviders(, { + renderWithProviders(, { url: commonUrls.B2B_ATTACH_VIEW, }) @@ -127,24 +106,19 @@ describe("EnrollmentCodePage", () => { expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined) }) - expect(mockPush).toHaveBeenCalledWith( - commonUrls.DASHBOARD_HOME_ENROLLMENT_ERROR, - ) + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith(commonUrls.DASHBOARD_HOME) + }) }) - test("Redirects to dashboard with error when user has no organizations initially", async () => { + test("Redirects to dashboard with error for invalid code (404 status)", async () => { setMockResponse.get(urls.userMe.get(), { [Permission.Authenticated]: true, }) - const mitxUser = factories.user.user({ - b2b_organizations: [], - }) - - setMockResponse.get(b2bUrls.userMe.get(), mitxUser) - const attachUrl = b2bUrls.b2bAttach.b2bAttachView("invalid-code") - setMockResponse.post(attachUrl, mitxUser) + // 404 status indicates invalid or expired enrollment code + setMockResponse.post(attachUrl, {}, { code: 404 }) renderWithProviders(, { url: commonUrls.B2B_ATTACH_VIEW, @@ -154,8 +128,10 @@ describe("EnrollmentCodePage", () => { expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined) }) - expect(mockPush).toHaveBeenCalledWith( - commonUrls.DASHBOARD_HOME_ENROLLMENT_ERROR, - ) + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith( + commonUrls.DASHBOARD_HOME_ENROLLMENT_ERROR, + ) + }) }) }) diff --git a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx index 0cd8a99b1c..56f93aae55 100644 --- a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx +++ b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx @@ -6,7 +6,6 @@ import { useB2BAttachMutation } from "api/mitxonline-hooks/organizations" import { userQueries } from "api/hooks/user" import { useQuery } from "@tanstack/react-query" import { useRouter } from "next-nprogress-bar" -import { mitxUserQueries } from "api/mitxonline-hooks/user" type EnrollmentCodePage = { code: string @@ -18,18 +17,7 @@ const InterstitialMessage = styled(Typography)(({ theme }) => ({ })) const EnrollmentCodePage: React.FC = ({ code }) => { - const mitxOnlineUser = useQuery(mitxUserQueries.me()) - const initialOrgsRef = React.useRef(null) const [hasEnrolled, setHasEnrolled] = React.useState(false) - - // Capture initial organization count once - if ( - initialOrgsRef.current === null && - mitxOnlineUser.data?.b2b_organizations - ) { - initialOrgsRef.current = mitxOnlineUser.data.b2b_organizations.length - } - const router = useRouter() const enrollment = useB2BAttachMutation({ @@ -48,23 +36,17 @@ const EnrollmentCodePage: React.FC = ({ code }) => { } }, [user?.is_authenticated, hasEnrolled, enrollment]) - // Handle redirect after mutation succeeds and query is refetched + // Handle redirect based on response status code + // 201: Successfully attached to new contract(s) -> redirect to dashboard + // 200: Already attached to all contracts -> redirect to dashboard + // 404: Invalid or expired code -> show error React.useEffect(() => { - if (enrollment.isSuccess && !enrollment.isPending) { - const currentOrgCount = - mitxOnlineUser.data?.b2b_organizations?.length ?? 0 - if (initialOrgsRef.current === currentOrgCount) { - router.push(urls.DASHBOARD_HOME_ENROLLMENT_ERROR) - } else { - router.push(urls.DASHBOARD_HOME) - } + if (enrollment.isSuccess) { + router.push(urls.DASHBOARD_HOME) + } else if (enrollment.isError) { + router.push(urls.DASHBOARD_HOME_ENROLLMENT_ERROR) } - }, [ - enrollment.isSuccess, - enrollment.isPending, - mitxOnlineUser.data?.b2b_organizations?.length, - router, - ]) + }, [enrollment.isSuccess, enrollment.isError, router]) React.useEffect(() => { if (userLoading) { From 6deb3ab39be33a6a2b9bc7573d79a427a00a434e Mon Sep 17 00:00:00 2001 From: Carey Gumaer Date: Wed, 12 Nov 2025 18:26:12 -0500 Subject: [PATCH 4/6] clear the error message query param after the first time it's seen --- .../DashboardPage/HomeContent.test.tsx | 36 ++++++++++++++++++- .../app-pages/DashboardPage/HomeContent.tsx | 15 ++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx b/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx index 6a41e08257..ec9bf9df29 100644 --- a/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx @@ -18,8 +18,10 @@ import * as mitxonline from "api/mitxonline-test-utils" import { useFeatureFlagEnabled } from "posthog-js/react" import HomeContent from "./HomeContent" import invariant from "tiny-invariant" +import * as NextProgressBar from "next-nprogress-bar" jest.mock("posthog-js/react") +jest.mock("next-nprogress-bar") const mockedUseFeatureFlagEnabled = jest .mocked(useFeatureFlagEnabled) .mockImplementation(() => false) @@ -251,8 +253,15 @@ describe("HomeContent", () => { expect(screen.queryByText("Enrollment Error")).not.toBeInTheDocument() }) - test("Displays enrollment error alert when query param is present", async () => { + test("Displays enrollment error alert when query param is present and then clears it", async () => { setupAPIs() + const mockReplace = jest.fn() + jest.spyOn(NextProgressBar, "useRouter").mockReturnValue({ + replace: mockReplace, + } as Partial> as ReturnType< + typeof NextProgressBar.useRouter + >) + renderWithProviders(, { url: "/dashboard?enrollment_error=1", }) @@ -261,6 +270,7 @@ describe("HomeContent", () => { name: "Your MIT Learning Journey", }) + // Verify the alert was shown expect(screen.getByText("Enrollment Error")).toBeInTheDocument() expect( screen.getByText( @@ -268,5 +278,29 @@ describe("HomeContent", () => { ), ).toBeInTheDocument() expect(screen.getByText("Contact Support")).toBeInTheDocument() + + // Verify the query param is cleared + expect(mockReplace).toHaveBeenCalledWith("/dashboard") + }) + + test("Does not clear query param when it is not present", async () => { + setupAPIs() + const mockReplace = jest.fn() + jest.spyOn(NextProgressBar, "useRouter").mockReturnValue({ + replace: mockReplace, + } as Partial> as ReturnType< + typeof NextProgressBar.useRouter + >) + + renderWithProviders(, { + url: "/dashboard", + }) + + await screen.findByRole("heading", { + name: "Your MIT Learning Journey", + }) + + // Verify router.replace was not called + expect(mockReplace).not.toHaveBeenCalled() }) }) diff --git a/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx b/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx index a65f0262d4..a7903bfd8c 100644 --- a/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx +++ b/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx @@ -19,6 +19,7 @@ import { FeatureFlags } from "@/common/feature_flags" import { useUserMe } from "api/hooks/user" import { OrganizationCards } from "./CoursewareDisplay/OrganizationCards" import { useSearchParams } from "next/navigation" +import { useRouter } from "next-nprogress-bar" const SubTitleText = styled(Typography)(({ theme }) => ({ color: theme.custom.colors.darkGray2, @@ -73,6 +74,7 @@ const AlertBanner = styled(Alert)({ const HomeContent: React.FC = () => { const searchParams = useSearchParams() + const router = useRouter() const enrollmentError = searchParams.get(ENROLLMENT_ERROR_QUERY_PARAM) const { isLoading: isLoadingProfile, data: user } = useUserMe() const topics = user?.profile?.preference_search_filters.topic @@ -81,6 +83,19 @@ const HomeContent: React.FC = () => { FeatureFlags.EnrollmentDashboard, ) const supportEmail = process.env.NEXT_PUBLIC_MITOL_SUPPORT_EMAIL || "" + + // Clear the enrollment error query param on mount so it doesn't persist on reload/back navigation + React.useEffect(() => { + if (enrollmentError) { + const newParams = new URLSearchParams(searchParams.toString()) + newParams.delete(ENROLLMENT_ERROR_QUERY_PARAM) + const newUrl = newParams.toString() + ? `${window.location.pathname}?${newParams.toString()}` + : window.location.pathname + router.replace(newUrl) + } + }, [enrollmentError, searchParams, router]) + return ( <> From 9973b43ade50bc81f26d61c5419ba97ee8a397df Mon Sep 17 00:00:00 2001 From: Carey Gumaer Date: Fri, 14 Nov 2025 15:34:44 -0500 Subject: [PATCH 5/6] prevent immediate hiding of the error when the query string is updated --- frontends/main/src/app-pages/DashboardPage/HomeContent.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx b/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx index a7903bfd8c..8b4e022d08 100644 --- a/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx +++ b/frontends/main/src/app-pages/DashboardPage/HomeContent.tsx @@ -76,6 +76,7 @@ const HomeContent: React.FC = () => { const searchParams = useSearchParams() const router = useRouter() const enrollmentError = searchParams.get(ENROLLMENT_ERROR_QUERY_PARAM) + const [showEnrollmentError, setShowEnrollmentError] = React.useState(false) const { isLoading: isLoadingProfile, data: user } = useUserMe() const topics = user?.profile?.preference_search_filters.topic const certification = user?.profile?.preference_search_filters.certification @@ -84,9 +85,10 @@ const HomeContent: React.FC = () => { ) const supportEmail = process.env.NEXT_PUBLIC_MITOL_SUPPORT_EMAIL || "" - // Clear the enrollment error query param on mount so it doesn't persist on reload/back navigation + // Show error and clear the query param React.useEffect(() => { if (enrollmentError) { + setShowEnrollmentError(true) const newParams = new URLSearchParams(searchParams.toString()) newParams.delete(ENROLLMENT_ERROR_QUERY_PARAM) const newUrl = newParams.toString() @@ -111,7 +113,7 @@ const HomeContent: React.FC = () => { - {enrollmentError && ( + {showEnrollmentError && ( Enrollment Error From fadb2e30067cea9dfbc6dc428dce34872c453ed8 Mon Sep 17 00:00:00 2001 From: Carey Gumaer Date: Fri, 14 Nov 2025 16:32:18 -0500 Subject: [PATCH 6/6] remove unnecessary state variable --- .../EnrollmentCodePage/EnrollmentCodePage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx index 56f93aae55..2d87f9e5c4 100644 --- a/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx +++ b/frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx @@ -17,7 +17,6 @@ const InterstitialMessage = styled(Typography)(({ theme }) => ({ })) const EnrollmentCodePage: React.FC = ({ code }) => { - const [hasEnrolled, setHasEnrolled] = React.useState(false) const router = useRouter() const enrollment = useB2BAttachMutation({ @@ -30,11 +29,14 @@ const EnrollmentCodePage: React.FC = ({ code }) => { }) React.useEffect(() => { - if (user?.is_authenticated && !hasEnrolled && !enrollment.isPending) { - setHasEnrolled(true) + if ( + user?.is_authenticated && + !enrollment.isPending && + !enrollment.isSuccess + ) { enrollment.mutate() } - }, [user?.is_authenticated, hasEnrolled, enrollment]) + }, [user?.is_authenticated, enrollment]) // Handle redirect based on response status code // 201: Successfully attached to new contract(s) -> redirect to dashboard