Skip to content

Commit 2453e72

Browse files
committed
if enrollment code redemption fails, display an error
1 parent ef5d9a6 commit 2453e72

File tree

5 files changed

+174
-21
lines changed

5 files changed

+174
-21
lines changed

frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,34 @@ describe("HomeContent", () => {
239239
}
240240
},
241241
)
242+
243+
test("Does not display enrollment error alert when query param is not present", async () => {
244+
setupAPIs()
245+
renderWithProviders(<HomeContent />)
246+
247+
await screen.findByRole("heading", {
248+
name: "Your MIT Learning Journey",
249+
})
250+
251+
expect(screen.queryByText("Enrollment Error")).not.toBeInTheDocument()
252+
})
253+
254+
test("Displays enrollment error alert when query param is present", async () => {
255+
setupAPIs()
256+
renderWithProviders(<HomeContent />, {
257+
url: "/dashboard?enrollment_error=1",
258+
})
259+
260+
await screen.findByRole("heading", {
261+
name: "Your MIT Learning Journey",
262+
})
263+
264+
expect(screen.getByText("Enrollment Error")).toBeInTheDocument()
265+
expect(
266+
screen.getByText(
267+
/The Enrollment Code is incorrect or no longer available/,
268+
),
269+
).toBeInTheDocument()
270+
expect(screen.getByText("Contact Support")).toBeInTheDocument()
271+
})
242272
})

frontends/main/src/app-pages/DashboardPage/HomeContent.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"use client"
22
import React, { Suspense } from "react"
3-
import { ButtonLink } from "@mitodl/smoot-design"
3+
import { Alert, ButtonLink } from "@mitodl/smoot-design"
44
import { ResourceTypeEnum } from "api"
5-
import { styled, Typography } from "ol-components"
6-
import { PROFILE } from "@/common/urls"
5+
import { Link, styled, Typography } from "ol-components"
6+
import { PROFILE, ENROLLMENT_ERROR_QUERY_PARAM } from "@/common/urls"
77
import {
88
TopPicksCarouselConfig,
99
TopicCarouselConfig,
@@ -18,6 +18,7 @@ import { useFeatureFlagEnabled } from "posthog-js/react"
1818
import { FeatureFlags } from "@/common/feature_flags"
1919
import { useUserMe } from "api/hooks/user"
2020
import { OrganizationCards } from "./CoursewareDisplay/OrganizationCards"
21+
import { useSearchParams } from "next/navigation"
2122

2223
const SubTitleText = styled(Typography)(({ theme }) => ({
2324
color: theme.custom.colors.darkGray2,
@@ -66,13 +67,20 @@ const TitleText = styled(Typography)(({ theme }) => ({
6667
},
6768
})) as typeof Typography
6869

70+
const AlertBanner = styled(Alert)({
71+
marginTop: "32px",
72+
})
73+
6974
const HomeContent: React.FC = () => {
75+
const searchParams = useSearchParams()
76+
const enrollmentError = searchParams.get(ENROLLMENT_ERROR_QUERY_PARAM)
7077
const { isLoading: isLoadingProfile, data: user } = useUserMe()
7178
const topics = user?.profile?.preference_search_filters.topic
7279
const certification = user?.profile?.preference_search_filters.certification
7380
const showEnrollments = useFeatureFlagEnabled(
7481
FeatureFlags.EnrollmentDashboard,
7582
)
83+
const supportEmail = process.env.NEXT_PUBLIC_MITOL_SUPPORT_EMAIL || ""
7684
return (
7785
<>
7886
<HomeHeader>
@@ -88,6 +96,21 @@ const HomeContent: React.FC = () => {
8896
</ButtonLink>
8997
</HomeHeaderRight>
9098
</HomeHeader>
99+
{enrollmentError && (
100+
<AlertBanner severity="error" closable={true}>
101+
<Typography variant="subtitle2" component="span">
102+
Enrollment Error
103+
</Typography>
104+
<Typography variant="body2" component="span">
105+
{" - "}
106+
The Enrollment Code is incorrect or no longer available.{" "}
107+
<Link color="red" href={`mailto:${supportEmail}`}>
108+
Contact Support
109+
</Link>{" "}
110+
for assistance.
111+
</Typography>
112+
</AlertBanner>
113+
)}
91114
<OrganizationCards />
92115
{showEnrollments ? <EnrollmentDisplay /> : null}
93116
<Suspense>

frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.test.tsx

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react"
22
import { renderWithProviders, setMockResponse, waitFor } from "@/test-utils"
33
import { makeRequest, urls } from "api/test-utils"
4-
import { urls as b2bUrls } from "api/mitxonline-test-utils"
4+
import { urls as b2bUrls, factories } from "api/mitxonline-test-utils"
55
import * as commonUrls from "@/common/urls"
66
import { Permission } from "api/hooks/user"
77
import EnrollmentCodePage from "./EnrollmentCodePage"
@@ -25,6 +25,9 @@ describe("EnrollmentCodePage", () => {
2525
[Permission.Authenticated]: false,
2626
})
2727

28+
const mitxUser = factories.user.user()
29+
setMockResponse.get(b2bUrls.userMe.get(), mitxUser)
30+
2831
setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), [], {
2932
code: 403,
3033
})
@@ -54,6 +57,9 @@ describe("EnrollmentCodePage", () => {
5457
[Permission.Authenticated]: true,
5558
})
5659

60+
const mitxUser = factories.user.user()
61+
setMockResponse.get(b2bUrls.userMe.get(), mitxUser)
62+
5763
setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), [])
5864

5965
renderWithProviders(<EnrollmentCodePage code="test-code" />, {
@@ -66,8 +72,24 @@ describe("EnrollmentCodePage", () => {
6672
[Permission.Authenticated]: true,
6773
})
6874

75+
const initialOrg = factories.organizations.organization({})
76+
const newOrg = factories.organizations.organization({})
77+
const initialMitxUser = factories.user.user({
78+
b2b_organizations: [initialOrg],
79+
})
80+
const updatedMitxUser = factories.user.user({
81+
b2b_organizations: [initialOrg, newOrg],
82+
})
83+
84+
// First call returns initial user, subsequent calls return updated user
85+
let callCount = 0
86+
setMockResponse.get(b2bUrls.userMe.get(), () => {
87+
callCount++
88+
return callCount === 1 ? initialMitxUser : updatedMitxUser
89+
})
90+
6991
const attachUrl = b2bUrls.b2bAttach.b2bAttachView("test-code")
70-
setMockResponse.post(attachUrl, [])
92+
setMockResponse.post(attachUrl, updatedMitxUser)
7193

7294
renderWithProviders(<EnrollmentCodePage code="test-code" />, {
7395
url: commonUrls.B2B_ATTACH_VIEW,
@@ -77,6 +99,63 @@ describe("EnrollmentCodePage", () => {
7799
expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined)
78100
})
79101

80-
expect(mockPush).toHaveBeenCalledWith(commonUrls.DASHBOARD_HOME)
102+
await waitFor(() => {
103+
expect(mockPush).toHaveBeenCalledWith(commonUrls.DASHBOARD_HOME)
104+
})
105+
})
106+
107+
test("Redirects to dashboard with error when b2b organizations don't change", async () => {
108+
setMockResponse.get(urls.userMe.get(), {
109+
[Permission.Authenticated]: true,
110+
})
111+
112+
const organization = factories.organizations.organization({})
113+
const mitxUser = factories.user.user({
114+
b2b_organizations: [organization],
115+
})
116+
117+
setMockResponse.get(b2bUrls.userMe.get(), mitxUser)
118+
119+
const attachUrl = b2bUrls.b2bAttach.b2bAttachView("invalid-code")
120+
setMockResponse.post(attachUrl, mitxUser)
121+
122+
renderWithProviders(<EnrollmentCodePage code="invalid-code" />, {
123+
url: commonUrls.B2B_ATTACH_VIEW,
124+
})
125+
126+
await waitFor(() => {
127+
expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined)
128+
})
129+
130+
expect(mockPush).toHaveBeenCalledWith(
131+
commonUrls.DASHBOARD_HOME_ENROLLMENT_ERROR,
132+
)
133+
})
134+
135+
test("Redirects to dashboard with error when user has no organizations initially", async () => {
136+
setMockResponse.get(urls.userMe.get(), {
137+
[Permission.Authenticated]: true,
138+
})
139+
140+
const mitxUser = factories.user.user({
141+
b2b_organizations: [],
142+
})
143+
144+
setMockResponse.get(b2bUrls.userMe.get(), mitxUser)
145+
146+
const attachUrl = b2bUrls.b2bAttach.b2bAttachView("invalid-code")
147+
setMockResponse.post(attachUrl, mitxUser)
148+
149+
renderWithProviders(<EnrollmentCodePage code="invalid-code" />, {
150+
url: commonUrls.B2B_ATTACH_VIEW,
151+
})
152+
153+
await waitFor(() => {
154+
expect(makeRequest).toHaveBeenCalledWith("post", attachUrl, undefined)
155+
})
156+
157+
expect(mockPush).toHaveBeenCalledWith(
158+
commonUrls.DASHBOARD_HOME_ENROLLMENT_ERROR,
159+
)
81160
})
82161
})

frontends/main/src/app-pages/EnrollmentCodePage/EnrollmentCodePage.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,52 @@ const InterstitialMessage = styled(Typography)(({ theme }) => ({
1919

2020
const EnrollmentCodePage: React.FC<EnrollmentCodePage> = ({ code }) => {
2121
const mitxOnlineUser = useQuery(mitxUserQueries.me())
22-
const userOrgs = structuredClone(mitxOnlineUser.data?.b2b_organizations || [])
22+
const initialOrgsRef = React.useRef<number | null>(null)
23+
const [hasEnrolled, setHasEnrolled] = React.useState(false)
24+
25+
// Capture initial organization count once
26+
if (
27+
initialOrgsRef.current === null &&
28+
mitxOnlineUser.data?.b2b_organizations
29+
) {
30+
initialOrgsRef.current = mitxOnlineUser.data.b2b_organizations.length
31+
}
32+
33+
const router = useRouter()
34+
2335
const enrollment = useB2BAttachMutation({
2436
enrollment_code: code,
2537
})
26-
const router = useRouter()
2738

2839
const { isLoading: userLoading, data: user } = useQuery({
2940
...userQueries.me(),
3041
staleTime: 0,
3142
})
3243

33-
const enrollAsync = enrollment.mutateAsync
44+
React.useEffect(() => {
45+
if (user?.is_authenticated && !hasEnrolled && !enrollment.isPending) {
46+
setHasEnrolled(true)
47+
enrollment.mutate()
48+
}
49+
}, [user?.is_authenticated, hasEnrolled, enrollment])
3450

51+
// Handle redirect after mutation succeeds and query is refetched
3552
React.useEffect(() => {
36-
if (user?.is_authenticated) {
37-
enrollAsync().then(() => {
38-
if (
39-
userOrgs.length === mitxOnlineUser.data?.b2b_organizations?.length
40-
) {
41-
router.push(urls.DASHBOARD_HOME_ENROLLMENT_ERROR)
42-
} else {
43-
router.push(urls.DASHBOARD_HOME)
44-
}
45-
})
53+
if (enrollment.isSuccess && !enrollment.isPending) {
54+
const currentOrgCount =
55+
mitxOnlineUser.data?.b2b_organizations?.length ?? 0
56+
if (initialOrgsRef.current === currentOrgCount) {
57+
router.push(urls.DASHBOARD_HOME_ENROLLMENT_ERROR)
58+
} else {
59+
router.push(urls.DASHBOARD_HOME)
60+
}
4661
}
47-
}, [user?.is_authenticated, enrollAsync, router])
62+
}, [
63+
enrollment.isSuccess,
64+
enrollment.isPending,
65+
mitxOnlineUser.data?.b2b_organizations?.length,
66+
router,
67+
])
4868

4969
React.useEffect(() => {
5070
if (userLoading) {

frontends/main/src/common/urls.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export const DASHBOARD_VIEW = "/dashboard/[tab]"
6060
const dashboardView = (tab: string) => generatePath(DASHBOARD_VIEW, { tab })
6161

6262
export const DASHBOARD_HOME = "/dashboard"
63-
export const DASHBOARD_HOME_ENROLLMENT_ERROR = "/dashboard?enrollment_error=1"
63+
export const ENROLLMENT_ERROR_QUERY_PARAM = "enrollment_error"
64+
export const DASHBOARD_HOME_ENROLLMENT_ERROR = `/dashboard?${ENROLLMENT_ERROR_QUERY_PARAM}=1`
6465
export const MY_LISTS = dashboardView("my-lists")
6566
export const PROFILE = dashboardView("profile")
6667
export const SETTINGS = dashboardView("settings")

0 commit comments

Comments
 (0)