Skip to content

Commit 7490639

Browse files
add "just in time" dialog (#2530)
* update currentuser to use userme, add country listing and user me mutation hooks, and add the first draft of the just in time modal * use our select components * fix the tests * use "dialog" instead of "modal" * add tests and fix some existing ones by mocking the countries api * fix CI testing issue * fix select placeholder color * update API package and remove first name and last name from updates * use yup schema, errortext * convert mitxonline user queries to queryOptions * fix ts error * fix country display * fix enrollment * remove unncessary check * add tests --------- Co-authored-by: Chris Chudzicki <christopher.chudzicki@gmail.com>
1 parent f824eec commit 7490639

File tree

22 files changed

+615
-59
lines changed

22 files changed

+615
-59
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.9.26",
33+
"@mitodl/mitxonline-api-axios": "^2025.9.30",
3434
"@tanstack/react-query": "^5.66.0",
3535
"axios": "^1.12.2"
3636
}

frontends/api/src/mitxonline/clients.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
UsersApi,
1010
ProgramEnrollmentsApi,
1111
PagesApi,
12+
CountriesApi,
1213
} from "@mitodl/mitxonline-api-axios/v2"
1314
import axios from "axios"
1415

@@ -25,6 +26,7 @@ const BASE_PATH =
2526
process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL?.replace(/\/+$/, "") ?? ""
2627

2728
const usersApi = new UsersApi(undefined, BASE_PATH, axiosInstance)
29+
const countriesApi = new CountriesApi(undefined, BASE_PATH, axiosInstance)
2830
const b2bApi = new B2bApi(undefined, BASE_PATH, axiosInstance)
2931
const programsApi = new ProgramsApi(undefined, BASE_PATH, axiosInstance)
3032
const programCollectionsApi = new ProgramCollectionsApi(
@@ -63,6 +65,7 @@ const pagesApi = new PagesApi(undefined, BASE_PATH, axiosInstance)
6365

6466
export {
6567
usersApi,
68+
countriesApi,
6669
b2bApi,
6770
courseRunEnrollmentsApi,
6871
programEnrollmentsApi,
Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,58 @@
1-
import { useQuery } from "@tanstack/react-query"
2-
import { usersApi } from "../../clients"
1+
import {
2+
queryOptions,
3+
useMutation,
4+
useQuery,
5+
useQueryClient,
6+
} from "@tanstack/react-query"
7+
import { countriesApi, usersApi } from "../../clients"
38
import type { User } from "@mitodl/mitxonline-api-axios/v2"
9+
import { UsersApiUsersMePartialUpdateRequest } from "@mitodl/mitxonline-api-axios/v2"
410

5-
const useMitxOnlineCurrentUser = (opts: { enabled?: boolean } = {}) =>
6-
useQuery({
7-
queryKey: ["mitxonline", "currentUser"],
8-
queryFn: async (): Promise<User> => {
9-
const response = await usersApi.usersCurrentUserRetrieve()
10-
return {
11-
...response.data,
12-
}
11+
const userKeys = {
12+
root: ["mitxonline", "users"] as const,
13+
me: () => [...userKeys.root, "me"] as const,
14+
countries: () => ["mitxonline", "countries"] as const,
15+
}
16+
17+
const queries = {
18+
me: () =>
19+
queryOptions({
20+
queryKey: userKeys.me(),
21+
queryFn: async () => {
22+
const response = await usersApi.usersMeRetrieve()
23+
return response.data
24+
},
25+
}),
26+
countries: () =>
27+
queryOptions({
28+
queryKey: userKeys.countries(),
29+
queryFn: async () => {
30+
const response = await countriesApi.countriesList()
31+
return response.data
32+
},
33+
}),
34+
}
35+
36+
/**
37+
* @deprecated Prefer direct use of queries.me()
38+
*/
39+
const useMitxOnlineUserMe = (opts: { enabled?: boolean } = {}) =>
40+
useQuery({ ...queries.me(), ...opts })
41+
42+
const useUpdateUserMutation = () => {
43+
const queryClient = useQueryClient()
44+
return useMutation({
45+
mutationFn: (opts: UsersApiUsersMePartialUpdateRequest) =>
46+
usersApi.usersMePartialUpdate(opts),
47+
onSuccess: () => {
48+
queryClient.invalidateQueries({ queryKey: userKeys.me() })
1349
},
14-
...opts,
1550
})
51+
}
1652

17-
export { useMitxOnlineCurrentUser }
53+
export {
54+
queries as mitxUserQueries,
55+
useMitxOnlineUserMe,
56+
useUpdateUserMutation,
57+
}
1858
export type { User as MitxOnlineUser }

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ const programPageItem: PartialFactory<ProgramPageItem> = (override) => {
244244
name: faker.person.fullName(),
245245
title_1: faker.person.jobTitle(),
246246
title_2: faker.person.jobTitle(),
247+
title_3: "",
247248
organization: "Massachusetts Institute of Technology",
248249
signature_image: faker.image.urlLoremFlickr({
249250
width: 200,

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import type {
55
ProgramCollectionsApiProgramCollectionsListRequest,
66
ProgramsApiProgramsListV2Request,
77
} from "@mitodl/mitxonline-api-axios/v2"
8-
import { RawAxiosRequestConfig } from "axios"
98
import { queryify } from "ol-test-utilities"
109

1110
const API_BASE_URL = process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL
1211

13-
const currentUser = {
14-
get: (opts?: RawAxiosRequestConfig) =>
15-
`${API_BASE_URL}/api/v0/users/current_user/${queryify(opts)}`,
12+
const userMe = {
13+
get: () => `${API_BASE_URL}/api/v0/users/me`,
14+
}
15+
16+
const countries = {
17+
list: () => `${API_BASE_URL}/api/v0/countries/`,
1618
}
1719

1820
const enrollment = {
@@ -83,7 +85,8 @@ const certificates = {
8385
export {
8486
b2b,
8587
b2bAttach,
86-
currentUser,
88+
userMe,
89+
countries,
8790
enrollment,
8891
programs,
8992
programCollections,

frontends/api/src/test-utils/mockAxios.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ type RequestMaker = (
1414
body?: unknown,
1515
) => Promise<PartialAxiosResponse>
1616

17-
const alwaysError: RequestMaker = (method, url, _body) => {
17+
const alwaysError: RequestMaker = (method, url, body) => {
1818
const msg = `No response specified for ${method} ${url}`
1919
console.error(msg)
20+
if (body) {
21+
console.error("and body:")
22+
console.error(body)
23+
}
2024
throw new Error(msg)
2125
}
2226

frontends/main/package.json

Lines changed: 2 additions & 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.9.26",
17+
"@mitodl/mitxonline-api-axios": "^2025.9.30",
1818
"@mitodl/smoot-design": "^6.17.1",
1919
"@next/bundle-analyzer": "^14.2.15",
2020
"@react-pdf/renderer": "^4.3.0",
@@ -62,6 +62,7 @@
6262
"next-router-mock": "^1.0.2",
6363
"ol-test-utilities": "0.0.0",
6464
"ts-jest": "^29.2.4",
65+
"type-fest": "^5.0.1",
6566
"typescript": "^5"
6667
}
6768
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe("B2BAttachPage", () => {
3030
[Permission.Authenticated]: false,
3131
})
3232

33-
setMockResponse.get(mitxOnlineUrls.currentUser.get(), null)
33+
setMockResponse.get(mitxOnlineUrls.userMe.get(), null)
3434
setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), [])
3535

3636
renderWithProviders(<B2BAttachPage code="test-code" />, {
@@ -58,7 +58,7 @@ describe("B2BAttachPage", () => {
5858
})
5959

6060
setMockResponse.get(
61-
mitxOnlineUrls.currentUser.get(),
61+
mitxOnlineUrls.userMe.get(),
6262
mitxOnlineFactories.user.user(),
6363
)
6464

@@ -88,7 +88,7 @@ describe("B2BAttachPage", () => {
8888
[Permission.Authenticated]: true,
8989
})
9090

91-
setMockResponse.get(mitxOnlineUrls.currentUser.get(), mitxOnlineUser)
91+
setMockResponse.get(mitxOnlineUrls.userMe.get(), mitxOnlineUser)
9292

9393
setMockResponse.post(b2bUrls.b2bAttach.b2bAttachView("test-code"), [])
9494

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from "react"
33
import { styled, Breadcrumbs, Container, Typography } from "ol-components"
44
import * as urls from "@/common/urls"
55
import { useB2BAttachMutation } from "api/mitxonline-hooks/organizations"
6-
import { useMitxOnlineCurrentUser } from "api/mitxonline-hooks/user"
6+
import { useMitxOnlineUserMe } from "api/mitxonline-hooks/user"
77
import { userQueries } from "api/hooks/user"
88
import { useQuery } from "@tanstack/react-query"
99
import { useRouter } from "next-nprogress-bar"
@@ -31,7 +31,7 @@ const B2BAttachPage: React.FC<B2BAttachPageProps> = ({ code }) => {
3131
...userQueries.me(),
3232
staleTime: 0,
3333
})
34-
const { data: mitxOnlineUser } = useMitxOnlineCurrentUser()
34+
const { data: mitxOnlineUser } = useMitxOnlineUserMe()
3535

3636
React.useEffect(() => {
3737
attach?.()

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import {
55
setMockResponse,
66
user,
77
within,
8-
expectWindowNavigation,
98
} from "@/test-utils"
109
import * as mitxonline from "api/mitxonline-test-utils"
10+
import {
11+
urls as testUrls,
12+
factories as testFactories,
13+
mockAxiosInstance,
14+
} from "api/test-utils"
1115
import { DashboardCard, getDefaultContextMenuItems } from "./DashboardCard"
1216
import { dashboardCourse } from "./test-utils"
1317
import { faker } from "@faker-js/faker/locale/en"
1418
import moment from "moment"
1519
import { EnrollmentMode, EnrollmentStatus } from "./types"
16-
import { mockAxiosInstance } from "api/test-utils"
1720

1821
const pastDashboardCourse: typeof dashboardCourse = (...overrides) => {
1922
return dashboardCourse(
@@ -49,6 +52,14 @@ const futureDashboardCourse: typeof dashboardCourse = (...overrides) => {
4952
)
5053
}
5154

55+
beforeEach(() => {
56+
// Mock user API call
57+
const user = testFactories.user.user()
58+
const mitxUser = mitxonline.factories.user.user()
59+
setMockResponse.get(testUrls.userMe.get(), user)
60+
setMockResponse.get(mitxonline.urls.userMe.get(), mitxUser)
61+
})
62+
5263
describe.each([
5364
{ display: "desktop", testId: "enrollment-card-desktop" },
5465
{ display: "mobile", testId: "enrollment-card-mobile" },
@@ -476,19 +487,44 @@ describe.each([
476487
status: EnrollmentStatus.NotEnrolled,
477488
},
478489
})
490+
491+
// Mock user without country and year_of_birth to trigger JustInTimeDialog
492+
const baseUser = mitxonline.factories.user.user()
493+
const mitxUserWithoutRequiredFields = {
494+
...baseUser,
495+
legal_address: { ...baseUser.legal_address, country: undefined },
496+
user_profile: { ...baseUser.user_profile, year_of_birth: undefined },
497+
}
498+
setMockResponse.get(
499+
mitxonline.urls.userMe.get(),
500+
mitxUserWithoutRequiredFields,
501+
)
502+
479503
setMockResponse.post(
480504
mitxonline.urls.b2b.courseEnrollment(course.coursewareId ?? undefined),
481505
{ result: "b2b-enroll-success", order: 1 },
482506
)
507+
// Mock countries data needed by JustInTimeDialog
508+
setMockResponse.get(mitxonline.urls.countries.list(), [
509+
{ code: "US", name: "United States" },
510+
{ code: "CA", name: "Canada" },
511+
])
512+
483513
renderWithProviders(<DashboardCard dashboardResource={course} />)
484514
const card = getCard()
485515
const coursewareButton = within(card).getByTestId("courseware-button")
486516

487-
await expectWindowNavigation(async () => {
488-
await user.click(coursewareButton)
517+
// Now the button should show the JustInTimeDialog instead of directly enrolling
518+
await user.click(coursewareButton)
519+
520+
// Verify the JustInTimeDialog appeared
521+
const dialog = await screen.findByRole("dialog", {
522+
name: "Just a Few More Details",
489523
})
524+
expect(dialog).toBeInTheDocument()
490525

491-
expect(mockAxiosInstance.request).toHaveBeenCalledWith(
526+
// The enrollment API should NOT be called yet (until dialog is completed)
527+
expect(mockAxiosInstance.request).not.toHaveBeenCalledWith(
492528
expect.objectContaining({
493529
method: "POST",
494530
url: mitxonline.urls.b2b.courseEnrollment(

0 commit comments

Comments
 (0)