Skip to content

Commit 6d25db1

Browse files
jkachelpre-commit-ci[bot]gumaerc
authored
B2B Provisioning: Add interstitial page to process redemption code retrieval (#2422)
* Adds (really pretty ugly) interstitial page, routing, and cobbles together a sample API hook The hook does hit the right endpoint but it's a stand-in until the API PR gets merged. * Adding more tests, fixing a few things, making the design a little bit less terrible * Re-enable the redirect (disabled to grab a screenshot) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * formatting * update api client and use the new attach function, also update interstitial page to use it and the nextjs redirect function instead of setting window.location directly * fix upstream issue with course with course runs return type changing * More upstream fixes * use useEffect * put attach in dependencies --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Carey Gumaer <[email protected]>
1 parent 95b9319 commit 6d25db1

File tree

13 files changed

+182
-25
lines changed

13 files changed

+182
-25
lines changed

frontends/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"ol-test-utilities": "0.0.0"
3131
},
3232
"dependencies": {
33-
"@mitodl/mitxonline-api-axios": "2025.8.6",
33+
"@mitodl/mitxonline-api-axios": "2025.8.12",
3434
"@tanstack/react-query": "^5.66.0",
3535
"axios": "^1.6.3"
3636
}

frontends/api/src/mitxonline/hooks/courses/queries.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { queryOptions } from "@tanstack/react-query"
22
import type {
33
CoursesApiApiV2CoursesListRequest,
4-
PaginatedV2CourseWithCourseRunsList,
4+
PaginatedCourseWithCourseRunsSerializerV2List,
55
} from "@mitodl/mitxonline-api-axios/v2"
66
import { coursesApi } from "../../clients"
77

@@ -18,9 +18,10 @@ const coursesQueries = {
1818
coursesList: (opts?: CoursesApiApiV2CoursesListRequest) =>
1919
queryOptions({
2020
queryKey: coursesKeys.coursesList(opts),
21-
queryFn: async (): Promise<PaginatedV2CourseWithCourseRunsList> => {
22-
return coursesApi.apiV2CoursesList(opts).then((res) => res.data)
23-
},
21+
queryFn:
22+
async (): Promise<PaginatedCourseWithCourseRunsSerializerV2List> => {
23+
return coursesApi.apiV2CoursesList(opts).then((res) => res.data)
24+
},
2425
}),
2526
}
2627

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { organizationQueries } from "./queries"
1+
import { organizationQueries, useB2BAttachMutation } from "./queries"
22

3-
export { organizationQueries }
3+
export { organizationQueries, useB2BAttachMutation }

frontends/api/src/mitxonline/hooks/organizations/queries.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import { B2bApiB2bOrganizationsRetrieveRequest } from "@mitodl/mitxonline-api-axios/v0"
2-
import { OrganizationPage } from "@mitodl/mitxonline-api-axios/v2"
3-
import { queryOptions } from "@tanstack/react-query"
1+
import {
2+
queryOptions,
3+
useMutation,
4+
useQueryClient,
5+
} from "@tanstack/react-query"
46
import { b2bApi } from "../../clients"
7+
import {
8+
OrganizationPage,
9+
B2bApiB2bAttachCreateRequest,
10+
B2bApiB2bOrganizationsRetrieveRequest,
11+
} from "@mitodl/mitxonline-api-axios/v2"
512

613
const organizationKeys = {
714
root: ["mitxonline", "organizations"],
@@ -22,4 +29,16 @@ const organizationQueries = {
2229
}),
2330
}
2431

25-
export { organizationQueries, organizationKeys }
32+
const useB2BAttachMutation = (opts: B2bApiB2bAttachCreateRequest) => {
33+
const queryClient = useQueryClient()
34+
return useMutation({
35+
mutationFn: () => b2bApi.b2bAttachCreate(opts),
36+
onSuccess: () => {
37+
queryClient.invalidateQueries({
38+
queryKey: organizationKeys.organizationsRetrieve(),
39+
})
40+
},
41+
})
42+
}
43+
44+
export { organizationQueries, organizationKeys, useB2BAttachMutation }

frontends/api/src/mitxonline/test-utils/factories/courses.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { mergeOverrides, makePaginatedFactory } from "ol-test-utilities"
22
import type { PartialFactory } from "ol-test-utilities"
3-
import type { V2CourseWithCourseRuns } from "@mitodl/mitxonline-api-axios/v2"
3+
import type { CourseWithCourseRunsSerializerV2 } from "@mitodl/mitxonline-api-axios/v2"
44
import { faker } from "@faker-js/faker/locale/en"
55
import { UniqueEnforcer } from "enforce-unique"
66

77
const uniqueCourseId = new UniqueEnforcer()
88
const uniqueCourseRunId = new UniqueEnforcer()
99

10-
const course: PartialFactory<V2CourseWithCourseRuns> = (overrides = {}) => {
11-
const defaults: V2CourseWithCourseRuns = {
10+
const course: PartialFactory<CourseWithCourseRunsSerializerV2> = (
11+
overrides = {},
12+
) => {
13+
const defaults: CourseWithCourseRunsSerializerV2 = {
1214
id: uniqueCourseId.enforce(() => faker.number.int()),
1315
title: faker.lorem.words(3),
1416
readable_id: faker.lorem.slug(),
@@ -86,7 +88,7 @@ const course: PartialFactory<V2CourseWithCourseRuns> = (overrides = {}) => {
8688
ingest_content_files_for_ai: faker.datatype.boolean(),
8789
}
8890

89-
return mergeOverrides<V2CourseWithCourseRuns>(defaults, overrides)
91+
return mergeOverrides<CourseWithCourseRunsSerializerV2>(defaults, overrides)
9092
}
9193

9294
const courses = makePaginatedFactory(course)

frontends/api/src/mitxonline/test-utils/urls.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ const organization = {
4646
`${API_BASE_URL}/api/v0/b2b/organizations/${organizationSlug}/`,
4747
}
4848

49+
const b2bAttach = {
50+
b2bAttachView: (code: string) => `${API_BASE_URL}/api/v0/b2b/attach/${code}/`,
51+
}
52+
4953
export {
5054
b2b,
55+
b2bAttach,
5156
currentUser,
5257
enrollment,
5358
programs,

frontends/main/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@emotion/cache": "^11.13.1",
1515
"@emotion/styled": "^11.11.0",
1616
"@mitodl/course-search-utils": "3.3.2",
17-
"@mitodl/mitxonline-api-axios": "2025.8.6",
17+
"@mitodl/mitxonline-api-axios": "2025.8.12",
1818
"@mitodl/smoot-design": "^6.10.0",
1919
"@next/bundle-analyzer": "^14.2.15",
2020
"@remixicon/react": "^4.2.0",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from "react"
2+
import { renderWithProviders, setMockResponse, waitFor } from "@/test-utils"
3+
import { urls } from "api/test-utils"
4+
import { urls as b2bUrls } from "api/mitxonline-test-utils"
5+
import * as commonUrls from "@/common/urls"
6+
import { Permission } from "api/hooks/user"
7+
import B2BAttachPage from "./B2BAttachPage"
8+
import { redirect } from "next/navigation"
9+
10+
// Mock Next.js redirect function
11+
jest.mock("next/navigation", () => ({
12+
redirect: jest.fn(),
13+
}))
14+
15+
const mockRedirect = jest.mocked(redirect)
16+
17+
describe("B2BAttachPage", () => {
18+
beforeEach(() => {
19+
jest.clearAllMocks()
20+
})
21+
22+
test("Renders when logged in", async () => {
23+
setMockResponse.get(urls.userMe.get(), {
24+
[Permission.Authenticated]: true,
25+
})
26+
27+
setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), [])
28+
29+
renderWithProviders(<B2BAttachPage code="test-code" />, {
30+
url: commonUrls.B2B_ATTACH_VIEW,
31+
})
32+
})
33+
34+
test("Redirects to dashboard on successful attachment", async () => {
35+
setMockResponse.get(urls.userMe.get(), {
36+
[Permission.Authenticated]: true,
37+
})
38+
39+
setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), [])
40+
41+
renderWithProviders(<B2BAttachPage code="test-code" />, {
42+
url: commonUrls.B2B_ATTACH_VIEW,
43+
})
44+
45+
// Wait for the mutation to complete and verify redirect was called
46+
await waitFor(() => {
47+
expect(mockRedirect).toHaveBeenCalledWith(commonUrls.DASHBOARD_HOME)
48+
})
49+
})
50+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client"
2+
import React from "react"
3+
import { redirect } from "next/navigation"
4+
import { styled, Breadcrumbs, Container, Typography } from "ol-components"
5+
import * as urls from "@/common/urls"
6+
import { useB2BAttachMutation } from "api/mitxonline-hooks/organizations"
7+
8+
type B2BAttachPageProps = {
9+
code: string
10+
}
11+
12+
const InterstitialMessage = styled(Typography)(({ theme }) => ({
13+
...theme.typography.body1,
14+
textAlign: "center",
15+
}))
16+
17+
const B2BAttachPage: React.FC<B2BAttachPageProps> = ({ code }) => {
18+
const {
19+
mutate: attach,
20+
isSuccess,
21+
isPending,
22+
} = useB2BAttachMutation({
23+
enrollment_code: code,
24+
})
25+
26+
React.useEffect(() => {
27+
attach?.()
28+
}, [attach])
29+
30+
React.useEffect(() => {
31+
if (isSuccess) {
32+
redirect(urls.DASHBOARD_HOME)
33+
}
34+
}, [isSuccess])
35+
36+
return (
37+
<Container>
38+
<Breadcrumbs
39+
variant="light"
40+
ancestors={[{ href: urls.HOME, label: "Home" }]}
41+
current="Use Enrollment Code"
42+
/>
43+
{isPending && (
44+
<InterstitialMessage>Validating code "{code}"...</InterstitialMessage>
45+
)}
46+
</Container>
47+
)
48+
}
49+
50+
export default B2BAttachPage

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import {
88
CourseRunEnrollment,
9-
V2CourseWithCourseRuns,
9+
CourseWithCourseRunsSerializerV2,
1010
V2Program,
1111
V2ProgramCollection,
1212
} from "@mitodl/mitxonline-api-axios/v2"
@@ -72,7 +72,7 @@ const mitxonlineEnrollments = (data: CourseRunEnrollment[]) =>
7272
data.map((course) => mitxonlineEnrollment(course))
7373

7474
const mitxonlineUnenrolledCourse = (
75-
course: V2CourseWithCourseRuns,
75+
course: CourseWithCourseRunsSerializerV2,
7676
): DashboardCourse => {
7777
const run = course.courseruns.find((run) => run.id === course.next_run_id)
7878
return {
@@ -98,7 +98,7 @@ const mitxonlineUnenrolledCourse = (
9898
}
9999

100100
const mitxonlineCourses = (raw: {
101-
courses: V2CourseWithCourseRuns[]
101+
courses: CourseWithCourseRunsSerializerV2[]
102102
enrollments: CourseRunEnrollment[]
103103
}) => {
104104
const enrollmentsByCourseId = groupBy(

0 commit comments

Comments
 (0)