Skip to content

Commit 622545c

Browse files
committed
fix: refresh profile context after enrolment mutations
Ensure profile UI state is updated after enrol/unenrol. Refs: KK-1510
1 parent 929109b commit 622545c

9 files changed

Lines changed: 322 additions & 42 deletions

File tree

src/domain/event/enrol/EnrolPage.tsx

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ import {
1717
} from '../../api/generatedTypes/graphql';
1818
import LoadingSpinner from '../../../common/components/spinner/LoadingSpinner';
1919
import enrolOccurrenceMutation from '../mutations/enrolOccurrenceMutation';
20-
import { saveChildEvents } from '../state/EventActions';
2120
import ErrorMessage from '../../../common/components/error/Error';
2221
import getEventOrEventGroupOccurrenceRefetchQueries from '../getEventOrEventGroupOccurrenceRefetchQueries';
2322
import { GQLErrors } from './EnrolConstants';
2423
import Enrol from './Enrol';
2524
import useGetPathname from '../../../common/route/utils/useGetPathname';
26-
import { publicSvgIconPaths } from '../../../public_files';
27-
import Icon from '../../../common/components/icon/Icon';
2825
import graphqlClient from '../../api/client';
26+
import { useProfileContext } from '../../profile/hooks/useProfileContext';
27+
import { handleEnrolCompleted } from './handleEnrolCompleted';
2928

3029
function containsAlreadyJoinedError(
3130
errors: ReadonlyArray<GraphQLFormattedError>
@@ -52,6 +51,7 @@ const EnrolPage = () => {
5251
i18n: { language },
5352
} = useTranslation();
5453
const dispatch = useDispatch();
54+
const { refetchProfile } = useProfileContext();
5555
const params = useParams<{
5656
childId: string;
5757
eventId: string;
@@ -91,28 +91,15 @@ const EnrolPage = () => {
9191
childId,
9292
eventGroupId: data?.occurrence?.event?.eventGroup?.id,
9393
}),
94-
onCompleted: (data) => {
95-
if (data?.enrolOccurrence?.enrolment?.child?.occurrences?.edges) {
96-
dispatch(
97-
saveChildEvents({
98-
childId,
99-
occurrences: data.enrolOccurrence.enrolment.child.occurrences,
100-
})
101-
);
102-
toast.success(
103-
<div>
104-
<Icon
105-
src={publicSvgIconPaths['tada']}
106-
className={styles.tadaIcon}
107-
/>
108-
<h1>{t('enrollment.successToast.heading')}</h1>
109-
<p>{t('enrollment.successToast.paragraph')}</p>
110-
</div>
111-
);
112-
}
113-
114-
goToOccurrence();
115-
},
94+
awaitRefetchQueries: true,
95+
onCompleted: async (data) =>
96+
handleEnrolCompleted(data, {
97+
childId,
98+
dispatch,
99+
refetchProfile,
100+
goToOccurrence,
101+
t,
102+
}),
116103
onError: (error) => {
117104
if (containsAlreadyJoinedError(error.graphQLErrors)) {
118105
goToOccurrence();
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { toast } from 'react-toastify';
2+
3+
import { handleEnrolCompleted } from '../handleEnrolCompleted';
4+
5+
vi.mock('react-toastify', () => ({
6+
toast: {
7+
success: vi.fn(),
8+
},
9+
}));
10+
11+
describe('handleEnrolCompleted', () => {
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
});
15+
16+
it('dispatches and shows success toast when occurrences exist', async () => {
17+
const dispatch = vi.fn();
18+
const refetchProfile = vi.fn().mockResolvedValue(undefined);
19+
const goToOccurrence = vi.fn();
20+
21+
await handleEnrolCompleted(
22+
{
23+
enrolOccurrence: {
24+
enrolment: {
25+
child: {
26+
occurrences: {
27+
edges: [{ node: { id: 'o1' } }],
28+
},
29+
},
30+
},
31+
},
32+
} as any,
33+
{
34+
childId: 'child-1',
35+
dispatch,
36+
refetchProfile,
37+
goToOccurrence,
38+
t: (key) => key,
39+
}
40+
);
41+
42+
expect(dispatch).toHaveBeenCalledTimes(1);
43+
expect(toast.success).toHaveBeenCalledTimes(1);
44+
expect(refetchProfile).toHaveBeenCalledTimes(1);
45+
expect(goToOccurrence).toHaveBeenCalledTimes(1);
46+
});
47+
48+
it('refetches and navigates even without occurrences', async () => {
49+
const dispatch = vi.fn();
50+
const refetchProfile = vi.fn().mockResolvedValue(undefined);
51+
const goToOccurrence = vi.fn();
52+
53+
await handleEnrolCompleted(
54+
{
55+
enrolOccurrence: {
56+
enrolment: {
57+
child: {
58+
occurrences: null,
59+
},
60+
},
61+
},
62+
} as any,
63+
{
64+
childId: 'child-1',
65+
dispatch,
66+
refetchProfile,
67+
goToOccurrence,
68+
t: (key) => key,
69+
}
70+
);
71+
72+
expect(dispatch).not.toHaveBeenCalled();
73+
expect(toast.success).not.toHaveBeenCalled();
74+
expect(refetchProfile).toHaveBeenCalledTimes(1);
75+
expect(goToOccurrence).toHaveBeenCalledTimes(1);
76+
});
77+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { toast } from 'react-toastify';
2+
import type { UnknownAction } from 'redux';
3+
4+
import Icon from '../../../common/components/icon/Icon';
5+
import { publicSvgIconPaths } from '../../../public_files';
6+
import styles from './enrol.module.scss';
7+
import { EnrolOccurrenceMutation } from '../../api/generatedTypes/graphql';
8+
import { saveChildEvents } from '../state/EventActions';
9+
10+
type EnrolCompletedDeps = {
11+
childId: string;
12+
dispatch: (action: UnknownAction) => void;
13+
refetchProfile: () => Promise<void>;
14+
goToOccurrence: () => void;
15+
t: (key: string) => string;
16+
};
17+
18+
export async function handleEnrolCompleted(
19+
data: EnrolOccurrenceMutation | null | undefined,
20+
{ childId, dispatch, refetchProfile, goToOccurrence, t }: EnrolCompletedDeps
21+
) {
22+
if (data?.enrolOccurrence?.enrolment?.child?.occurrences?.edges) {
23+
dispatch(
24+
saveChildEvents({
25+
childId,
26+
occurrences: data.enrolOccurrence.enrolment.child.occurrences,
27+
})
28+
);
29+
toast.success(
30+
<div>
31+
<Icon src={publicSvgIconPaths['tada']} className={styles.tadaIcon} />
32+
<h1>{t('enrollment.successToast.heading')}</h1>
33+
<p>{t('enrollment.successToast.paragraph')}</p>
34+
</div>
35+
);
36+
}
37+
38+
await refetchProfile();
39+
goToOccurrence();
40+
}

src/domain/event/modal/UnenrolModal.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import {
1313
UnenrolOccurrenceMutationVariables,
1414
} from '../../api/generatedTypes/graphql';
1515
import ConfirmModal from '../../../common/components/confirm/ConfirmModal';
16-
import { saveChildEvents } from '../state/EventActions';
1716
import getEventOrEventGroupOccurrenceRefetchQueries from '../getEventOrEventGroupOccurrenceRefetchQueries';
1817
import AppConfig from '../../app/AppConfig';
1918
import graphqlClient from '../../api/client';
19+
import { useProfileContext } from '../../profile/hooks/useProfileContext';
20+
import { handleUnenrolCompleted } from './handleUnenrolCompleted';
2021

2122
interface UnenrolModalProps {
2223
isOpen: boolean;
@@ -37,6 +38,7 @@ const UnenrolModal = ({
3738
const { t } = useTranslation();
3839
const dispatch = useDispatch();
3940
const getPathname = useGetPathname();
41+
const { refetchProfile } = useProfileContext();
4042

4143
const [unenrolOccurrence] = useMutation<
4244
UnenrolOccurrenceMutation,
@@ -48,17 +50,14 @@ const UnenrolModal = ({
4850
eventGroupId,
4951
}),
5052
awaitRefetchQueries: true,
51-
onCompleted: (data) => {
52-
if (data.unenrolOccurrence?.child?.occurrences.edges) {
53-
dispatch(
54-
saveChildEvents({
55-
childId: data.unenrolOccurrence.child.id,
56-
occurrences: data.unenrolOccurrence.child.occurrences,
57-
})
58-
);
59-
}
60-
navigate(getPathname(`/profile/child/${childId}`), { replace: true });
61-
},
53+
onCompleted: async (data) =>
54+
handleUnenrolCompleted(data, {
55+
dispatch,
56+
refetchProfile,
57+
navigateToProfile: () => {
58+
navigate(getPathname(`/profile/child/${childId}`), { replace: true });
59+
},
60+
}),
6261
});
6362

6463
const unenrol = async () => {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { handleUnenrolCompleted } from '../handleUnenrolCompleted';
2+
3+
describe('handleUnenrolCompleted', () => {
4+
beforeEach(() => {
5+
vi.clearAllMocks();
6+
});
7+
8+
it('dispatches when occurrences exist', async () => {
9+
const dispatch = vi.fn();
10+
const refetchProfile = vi.fn().mockResolvedValue(undefined);
11+
const navigateToProfile = vi.fn();
12+
13+
await handleUnenrolCompleted(
14+
{
15+
unenrolOccurrence: {
16+
child: {
17+
id: 'child-1',
18+
occurrences: {
19+
edges: [{ node: { id: 'o1' } }],
20+
},
21+
},
22+
},
23+
} as any,
24+
{
25+
dispatch,
26+
refetchProfile,
27+
navigateToProfile,
28+
}
29+
);
30+
31+
expect(dispatch).toHaveBeenCalledTimes(1);
32+
expect(refetchProfile).toHaveBeenCalledTimes(1);
33+
expect(navigateToProfile).toHaveBeenCalledTimes(1);
34+
});
35+
36+
it('still refetches and navigates when occurrences are empty', async () => {
37+
const dispatch = vi.fn();
38+
const refetchProfile = vi.fn().mockResolvedValue(undefined);
39+
const navigateToProfile = vi.fn();
40+
41+
await handleUnenrolCompleted(
42+
{
43+
unenrolOccurrence: {
44+
child: {
45+
id: 'child-1',
46+
occurrences: {
47+
edges: null,
48+
},
49+
},
50+
},
51+
} as any,
52+
{
53+
dispatch,
54+
refetchProfile,
55+
navigateToProfile,
56+
}
57+
);
58+
59+
expect(dispatch).not.toHaveBeenCalled();
60+
expect(refetchProfile).toHaveBeenCalledTimes(1);
61+
expect(navigateToProfile).toHaveBeenCalledTimes(1);
62+
});
63+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { UnknownAction } from 'redux';
2+
3+
import { UnenrolOccurrenceMutation } from '../../api/generatedTypes/graphql';
4+
import { saveChildEvents } from '../state/EventActions';
5+
6+
type UnenrolCompletedDeps = {
7+
dispatch: (action: UnknownAction) => void;
8+
refetchProfile: () => Promise<void>;
9+
navigateToProfile: () => void;
10+
};
11+
12+
export async function handleUnenrolCompleted(
13+
data: UnenrolOccurrenceMutation | null | undefined,
14+
{ dispatch, refetchProfile, navigateToProfile }: UnenrolCompletedDeps
15+
) {
16+
if (data?.unenrolOccurrence?.child?.occurrences?.edges) {
17+
dispatch(
18+
saveChildEvents({
19+
childId: data.unenrolOccurrence.child.id,
20+
occurrences: data.unenrolOccurrence.child.occurrences,
21+
})
22+
);
23+
}
24+
25+
await refetchProfile();
26+
navigateToProfile();
27+
}

src/domain/profile/ProfileContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type ProfileContextType = {
1111
profile: ProfileType;
1212
clearProfile: () => void;
1313
updateProfile: UpdateProfileType;
14-
refetchProfile: () => void;
14+
refetchProfile: () => Promise<void>;
1515
isLoading: boolean;
1616
isFetchCalled: boolean;
1717
};
@@ -26,7 +26,7 @@ const defaultContext: ProfileContextType = {
2626
updateProfile: () => {
2727
throw new Error('Not implemented');
2828
},
29-
refetchProfile: () => {
29+
refetchProfile: async () => {
3030
throw new Error('Not implemented');
3131
},
3232
};

src/domain/profile/ProfileProvider.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import * as Sentry from '@sentry/browser';
23

34
import { ProfileContext, ProfileType } from './ProfileContext';
45
import useProfileFetcher from './hooks/useProfileFetcher';
@@ -18,12 +19,17 @@ export default function ProfileProvider({
1819
setProfileToContext,
1920
});
2021

21-
const refetchProfile = React.useCallback(() => {
22-
refetch().then(({ data: refreshedData }) => {
22+
const refetchProfile = React.useCallback(async () => {
23+
try {
24+
const { data: refreshedData } = await refetch();
2325
setProfileToContext(refreshedData?.myProfile || null);
2426
// eslint-disable-next-line no-console
2527
console.info('ProfileContext', 'profile refetched');
26-
});
28+
} catch (error) {
29+
// eslint-disable-next-line no-console
30+
console.error(error);
31+
Sentry.captureException(error);
32+
}
2733
}, [refetch]);
2834

2935
const clearProfile = React.useCallback(() => {

0 commit comments

Comments
 (0)