Skip to content

Commit

Permalink
Merge pull request #478 from lifeomic/FLME-398/custom-consent-screen
Browse files Browse the repository at this point in the history
feat: support custom consent screen
  • Loading branch information
jkdowdle committed Dec 1, 2023
2 parents c52bb2c + 8233600 commit 8fd889f
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/common/DeveloperConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Route } from '../navigators/types';
import { LogoHeaderOptions } from '../hooks/useLogoHeaderOptions';
import { EducationContent } from '../components/TrackTile/services/TrackTileService';
import { Project } from '../hooks/useSubjectProjects';
import type { CustomConsentScreenProps } from '../screens/ConsentScreen';

/**
* DeveloperConfig provides a single interface to configure the app at build-time.
Expand Down Expand Up @@ -122,6 +123,7 @@ export type DeveloperConfig = {
}>;
CustomStacks?: Record<string, () => JSX.Element>;
renderCustomLoginScreen?: () => JSX.Element;
CustomConsentScreen?: (props: CustomConsentScreenProps) => JSX.Element;
sharingRenderers?: {
pointBreakdown: (props: PointBreakdownProps) => React.JSX.Element;
};
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useConsent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@ type ConsentPatch = {
accept: boolean;
};

type ConsentAndForm = Consent & {
export type ConsentAndForm = Consent & {
form: Questionnaire;
};
84 changes: 84 additions & 0 deletions src/screens/ConsentScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react-native';
import { Alert } from 'react-native';
import { useOAuthFlow, useConsent, useOnboardingCourse } from '../hooks';
import { ConsentScreen } from './ConsentScreen';
import { useDeveloperConfig } from '../hooks';

jest.unmock('i18next');
jest.unmock('@react-navigation/native');
Expand All @@ -15,13 +16,17 @@ jest.mock('../hooks/useConsent', () => ({
jest.mock('../hooks/useOnboardingCourse', () => ({
useOnboardingCourse: jest.fn(),
}));
jest.mock('../hooks/useDeveloperConfig', () => ({
useDeveloperConfig: jest.fn(),
}));

const useOAuthFlowMock = useOAuthFlow as jest.Mock;
const logoutMock = jest.fn();
const useConsentMock = useConsent as jest.Mock;
const useShouldRenderConsentScreenMock = jest.fn();
const useUpdateProjectConsentDirectiveMock = jest.fn();
const useOnboardingCourseMock = useOnboardingCourse as jest.Mock;
const useDeveloperConfigMock = useDeveloperConfig as jest.Mock;
const updateConsentDirectiveMutationMock = {
mutateAsync: jest.fn().mockResolvedValue({}),
};
Expand Down Expand Up @@ -71,6 +76,9 @@ const consentScreen = (
beforeEach(() => {
useOAuthFlowMock.mockReturnValue({ logout: logoutMock });

useDeveloperConfigMock.mockReturnValue({
CustomConsentScreen: null,
});
useUpdateProjectConsentDirectiveMock.mockReturnValue(
updateConsentDirectiveMutationMock,
);
Expand All @@ -93,6 +101,82 @@ test('render the activity indicator when loading', () => {
expect(getByTestId('activity-indicator-view')).toBeDefined();
});

test('renders custom consent screen if present in developer config', async () => {
const CustomConsentScreen = jest.fn();

useUpdateProjectConsentDirectiveMock.mockReturnValue({
...updateConsentDirectiveMutationMock,
isLoading: false,
});
useDeveloperConfigMock.mockReturnValue({
CustomConsentScreen,
});

render(consentScreen);

expect(CustomConsentScreen).toHaveBeenCalled();
expect(CustomConsentScreen).toHaveBeenCalledWith(
{
consentForm: defaultConsentDirective,
acceptConsent: expect.any(Function),
declineConsent: expect.any(Function),
isLoadingUpdateConsent: false,
},
{},
);

// check passed methods are correct
const { acceptConsent, declineConsent } =
CustomConsentScreen.mock.calls[0][0];

acceptConsent();
await waitFor(() => expect(navigateMock.replace).toHaveBeenCalledWith('app'));
expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledTimes(
1,
);
expect(updateConsentDirectiveMutationMock.mutateAsync).toHaveBeenCalledWith({
directiveId: defaultConsentDirective.id,
accept: true,
});

// reset for later assertions
updateConsentDirectiveMutationMock.mutateAsync.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({
directiveId: defaultConsentDirective.id,
accept: false,
});

// check passed loading state
useUpdateProjectConsentDirectiveMock.mockReturnValue({
...updateConsentDirectiveMutationMock,
isLoading: true,
});

CustomConsentScreen.mockClear();
render(consentScreen);

await waitFor(() => {
expect(CustomConsentScreen).toHaveBeenCalled();
});
expect(CustomConsentScreen).toHaveBeenCalledWith(
{
consentForm: defaultConsentDirective,
acceptConsent: expect.any(Function),
declineConsent: expect.any(Function),
isLoadingUpdateConsent: true,
},
{},
);
});

test('renders the consent body and acceptance verbiage', () => {
const { getByText } = render(consentScreen);
expect(getByText(defaultConsentDirective.form.item[0].text)).toBeDefined();
Expand Down
31 changes: 31 additions & 0 deletions src/screens/ConsentScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import {
useOAuthFlow,
useOnboardingCourse,
} from '../hooks';
import type { ConsentAndForm } from '../hooks/useConsent';
import { useDeveloperConfig } from '../hooks/useDeveloperConfig';
import { LoggedInRootScreenProps } from '../navigators/types';

export const ConsentScreen = ({
navigation,
}: LoggedInRootScreenProps<'screens/ConsentScreen'>) => {
const { styles } = useStyles(defaultStyles);
const { CustomConsentScreen } = useDeveloperConfig();
const { useShouldRenderConsentScreen, useUpdateProjectConsentDirective } =
useConsent();
const { consentDirectives, isLoading: loadingDirectives } =
Expand Down Expand Up @@ -97,6 +100,17 @@ export const ConsentScreen = ({
);
}

if (CustomConsentScreen) {
return (
<CustomConsentScreen
consentForm={consentToPresent}
acceptConsent={acceptConsent}
declineConsent={declineConsent}
isLoadingUpdateConsent={updateConsentDirectiveMutation.isLoading}
/>
);
}

return (
<View style={styles.view}>
<ScrollView style={styles.scrollView}>
Expand Down Expand Up @@ -157,3 +171,20 @@ declare module '@styles' {
}

export type ConsentScreenStyles = NamedStylesProp<typeof defaultStyles>;

export type CustomConsentScreenProps = {
/**
* The full Consent and Questionnaire FHIR Resources pertaining to the
* consent form. Terms and acceptance text can be derived from here
*/
consentForm: ConsentAndForm | undefined;
/** Mutation to accept consent is in flight */
isLoadingUpdateConsent: boolean;
/** Mutation to accept consent */
acceptConsent: () => Promise<void>;
/**
* Warns user with system alert that app can not be used without
* accepting consent and continuing will log them out
*/
declineConsent: () => void;
};

0 comments on commit 8fd889f

Please sign in to comment.