From 394b576a0043c898f824f84efa70c73445d6ccbc Mon Sep 17 00:00:00 2001 From: Josh Dowdle Date: Fri, 1 Dec 2023 10:02:07 -0700 Subject: [PATCH] fix: consent screen use mutate over async mutate --- src/hooks/useConsent.test.tsx | 5 +- src/hooks/useConsent.ts | 17 ++++++- src/screens/ConsentScreen.test.tsx | 78 +++++++++++++++--------------- src/screens/ConsentScreen.tsx | 34 +++++++------ 4 files changed, 78 insertions(+), 56 deletions(-) diff --git a/src/hooks/useConsent.test.tsx b/src/hooks/useConsent.test.tsx index 1ef322a7..6b152465 100644 --- a/src/hooks/useConsent.test.tsx +++ b/src/hooks/useConsent.test.tsx @@ -78,9 +78,11 @@ describe('useUpdateProjectConsentDirective', () => { test('updates the accepted consent', async () => { axiosMock.onPatch('/v1/consent/directives/me/directive-id').reply(200, {}); + const onSuccess = jest.fn(); + const useTestHook = () => { const { useUpdateProjectConsentDirective } = useConsent(); - return useUpdateProjectConsentDirective(); + return useUpdateProjectConsentDirective({ onSuccess }); }; const { result } = renderHookInContext(useTestHook); @@ -91,6 +93,7 @@ describe('useUpdateProjectConsentDirective', () => { }); }); + expect(onSuccess).toHaveBeenCalledTimes(1); expect(axiosMock.history.patch[0].url).toBe( `/v1/consent/directives/me/directive-id`, ); diff --git a/src/hooks/useConsent.ts b/src/hooks/useConsent.ts index 37cdc38f..0cd886ba 100644 --- a/src/hooks/useConsent.ts +++ b/src/hooks/useConsent.ts @@ -2,6 +2,10 @@ import { useActiveProject } from './useActiveProject'; import { Consent, Questionnaire } from 'fhir/r3'; import { useRestQuery, useRestMutation } from './rest-api'; import { RestAPIEndpoints } from '../types/rest-types'; +import { UseMutationOptions } from '@tanstack/react-query'; + +type PatchConsentDirectives = + RestAPIEndpoints['PATCH /v1/consent/directives/me/:directiveId']; export const useConsent = () => { const { activeProject } = useActiveProject(); @@ -13,9 +17,18 @@ export const useConsent = () => { }); }; - const useUpdateProjectConsentDirective = () => { + const useUpdateProjectConsentDirective = ( + options: UseMutationOptions< + PatchConsentDirectives['Response'], + unknown, + PatchConsentDirectives['Request'] & { + directiveId: string; + } + > = {}, + ) => { const mutation = useRestMutation( 'PATCH /v1/consent/directives/me/:directiveId', + options, ); const getInput = ({ directiveId, accept }: ConsentPatch) => { @@ -38,7 +51,7 @@ export const useConsent = () => { }, ], }, - } as RestAPIEndpoints['PATCH /v1/consent/directives/me/:directiveId']['Request'] & { + } as PatchConsentDirectives['Request'] & { directiveId: string; }; }; diff --git a/src/screens/ConsentScreen.test.tsx b/src/screens/ConsentScreen.test.tsx index b0de68a5..f156f9d1 100644 --- a/src/screens/ConsentScreen.test.tsx +++ b/src/screens/ConsentScreen.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, render, waitFor } from '@testing-library/react-native'; +import { fireEvent, render } from '@testing-library/react-native'; import { Alert } from 'react-native'; import { useOAuthFlow, useConsent, useOnboardingCourse } from '../hooks'; import { ConsentScreen } from './ConsentScreen'; @@ -28,7 +28,7 @@ const useUpdateProjectConsentDirectiveMock = jest.fn(); const useOnboardingCourseMock = useOnboardingCourse as jest.Mock; const useDeveloperConfigMock = useDeveloperConfig as jest.Mock; const updateConsentDirectiveMutationMock = { - mutateAsync: jest.fn().mockResolvedValue({}), + mutate: jest.fn().mockResolvedValue({}), }; const navigateMock = { replace: jest.fn(), @@ -101,7 +101,7 @@ test('render the activity indicator when loading', () => { expect(getByTestId('activity-indicator-view')).toBeDefined(); }); -test('renders custom consent screen if present in developer config', async () => { +test('renders custom consent screen if present in developer config', () => { const CustomConsentScreen = jest.fn(); useUpdateProjectConsentDirectiveMock.mockReturnValue({ @@ -130,26 +130,28 @@ test('renders custom consent screen if present in developer config', async () => CustomConsentScreen.mock.calls[0][0]; acceptConsent(); - await waitFor(() => expect(navigateMock.replace).toHaveBeenCalledWith('app')); - expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledTimes( - 1, - ); - expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledWith({ + useUpdateProjectConsentDirectiveMock.mock.calls[0][0].onSuccess(undefined, { + status: 'active', + }); + expect(navigateMock.replace).toHaveBeenCalledWith('app'); + expect(updateConsentDirectiveMutationMock.mutate).toHaveBeenCalledTimes(1); + expect(updateConsentDirectiveMutationMock.mutate).toHaveBeenCalledWith({ directiveId: defaultConsentDirective.id, accept: true, }); // reset for later assertions - updateConsentDirectiveMutationMock.mutateAsync.mockClear(); + updateConsentDirectiveMutationMock.mutate.mockClear(); declineConsent(); expect(alertSpy).toHaveBeenCalled(); alertSpy.mock.calls[0]?.[2]?.[1].onPress!(); - await waitFor(() => expect(logoutMock).toHaveBeenCalledTimes(1)); - expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledTimes( - 1, - ); - expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledWith({ + useUpdateProjectConsentDirectiveMock.mock.calls[0][0].onSuccess(undefined, { + status: 'rejected', + }); + () => expect(logoutMock).toHaveBeenCalledTimes(1); + expect(updateConsentDirectiveMutationMock.mutate).toHaveBeenCalledTimes(1); + expect(updateConsentDirectiveMutationMock.mutate).toHaveBeenCalledWith({ directiveId: defaultConsentDirective.id, accept: false, }); @@ -163,9 +165,7 @@ test('renders custom consent screen if present in developer config', async () => CustomConsentScreen.mockClear(); render(consentScreen); - await waitFor(() => { - expect(CustomConsentScreen).toHaveBeenCalled(); - }); + expect(CustomConsentScreen).toHaveBeenCalled(); expect(CustomConsentScreen).toHaveBeenCalledWith( { consentForm: defaultConsentDirective, @@ -184,37 +184,35 @@ test('renders the consent body and acceptance verbiage', () => { expect(getByText(defaultConsentDirective.form.item[2].text)).toBeDefined(); }); -test('should accept the consent and navigate to the home screen', async () => { +test('should accept the consent and navigate to the home screen', () => { const { getByText } = render(consentScreen); fireEvent.press(getByText('Agree')); - await waitFor(() => { - expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledWith( - { - directiveId: defaultConsentDirective.id, - accept: true, - }, - ); - expect(navigateMock.replace).toHaveBeenCalledWith('app'); + expect(updateConsentDirectiveMutationMock.mutate).toHaveBeenCalledWith({ + directiveId: defaultConsentDirective.id, + accept: true, + }); + useUpdateProjectConsentDirectiveMock.mock.calls[0][0].onSuccess(undefined, { + status: 'active', }); + expect(navigateMock.replace).toHaveBeenCalledWith('app'); }); -test('should accept the consent and navigate to the onboarding course screen', async () => { +test('should accept the consent and navigate to the onboarding course screen', () => { useOnboardingCourseMock.mockReturnValue({ shouldLaunchOnboardingCourse: true, }); const { getByText } = render(consentScreen); fireEvent.press(getByText('Agree')); - await waitFor(() => { - expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledWith( - { - directiveId: defaultConsentDirective.id, - accept: true, - }, - ); - expect(navigateMock.replace).toHaveBeenCalledWith( - 'screens/OnboardingCourseScreen', - ); + expect(updateConsentDirectiveMutationMock.mutate).toHaveBeenCalledWith({ + directiveId: defaultConsentDirective.id, + accept: true, + }); + useUpdateProjectConsentDirectiveMock.mock.calls[0][0].onSuccess(undefined, { + status: 'active', }); + expect(navigateMock.replace).toHaveBeenCalledWith( + 'screens/OnboardingCourseScreen', + ); }); test('it should open an alert if consent is declined', async () => { @@ -227,10 +225,12 @@ test('Pressing logout declines the consent and logs the the user out', async () const { getByText } = render(consentScreen); fireEvent.press(getByText('Decline')); alertSpy.mock.calls[0]?.[2]?.[1].onPress!(); - await waitFor(() => {}); - expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledWith({ + expect(updateConsentDirectiveMutationMock.mutate).toHaveBeenCalledWith({ directiveId: defaultConsentDirective.id, accept: false, }); + useUpdateProjectConsentDirectiveMock.mock.calls[0][0].onSuccess(undefined, { + status: 'rejected', + }); expect(logoutMock).toHaveBeenCalled(); }); diff --git a/src/screens/ConsentScreen.tsx b/src/screens/ConsentScreen.tsx index 377ec8d0..2e5897a7 100644 --- a/src/screens/ConsentScreen.tsx +++ b/src/screens/ConsentScreen.tsx @@ -23,7 +23,18 @@ export const ConsentScreen = ({ useConsent(); const { consentDirectives, isLoading: loadingDirectives } = useShouldRenderConsentScreen(); - const updateConsentDirectiveMutation = useUpdateProjectConsentDirective(); + const updateConsentDirectiveMutation = useUpdateProjectConsentDirective({ + onSuccess: (_, { status }) => { + if (status === 'rejected') { + return logout({}); + } + + const route = shouldLaunchOnboardingCourse + ? 'screens/OnboardingCourseScreen' + : 'app'; + navigation.replace(route); + }, + }); const { logout } = useOAuthFlow(); const { shouldLaunchOnboardingCourse } = useOnboardingCourse(); @@ -38,7 +49,7 @@ export const ConsentScreen = ({ if (!consentToPresent?.id) { return; } - updateConsentDirectiveMutation.mutateAsync({ + updateConsentDirectiveMutation.mutate({ directiveId: consentToPresent.id, accept, }); @@ -46,13 +57,9 @@ export const ConsentScreen = ({ [updateConsentDirectiveMutation, consentToPresent], ); - const acceptConsent = useCallback(async () => { - await updateConsentDirective(true); - const route = shouldLaunchOnboardingCourse - ? 'screens/OnboardingCourseScreen' - : 'app'; - navigation.replace(route); - }, [updateConsentDirective, navigation, shouldLaunchOnboardingCourse]); + const acceptConsent = useCallback(() => { + updateConsentDirective(true); + }, [updateConsentDirective]); const declineConsent = useCallback(() => { Alert.alert( @@ -70,14 +77,13 @@ export const ConsentScreen = ({ { text: t('logout', 'Logout'), style: 'destructive', - onPress: async () => { - await updateConsentDirective(false); - await logout({}); + onPress: () => { + updateConsentDirective(false); }, }, ], ); - }, [logout, updateConsentDirective]); + }, [updateConsentDirective]); const consentText = consentToPresent?.form?.item?.find( (f) => f.linkId === 'terms', @@ -181,7 +187,7 @@ export type CustomConsentScreenProps = { /** Mutation to accept consent is in flight */ isLoadingUpdateConsent: boolean; /** Mutation to accept consent */ - acceptConsent: () => Promise; + acceptConsent: () => void; /** * Warns user with system alert that app can not be used without * accepting consent and continuing will log them out