diff --git a/app/src/App.tsx b/app/src/App.tsx index 7e3264bc7..d84e74bce 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -3,17 +3,17 @@ import './styles/App.scss'; import PatientDetailsProvider from './providers/patientProvider/PatientProvider'; import SessionProvider from './providers/sessionProvider/SessionProvider'; import AppRouter from './router/AppRouter'; -import FeatureFlagsProvider from './providers/featureFlagsProvider/FeatureFlagsProvider'; +import ConfigProvider from './providers/configProvider/ConfigProvider'; function App() { return ( - + - + ); } diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx index 675240dcd..f2201c269 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx @@ -12,7 +12,7 @@ import { routes } from '../../../types/generic/routes'; import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; import usePatient from '../../../helpers/hooks/usePatient'; -jest.mock('../../../helpers/hooks/useFeatureFlags'); +jest.mock('../../../helpers/hooks/useConfig'); jest.mock('../deletionConfirmationStage/DeletionConfirmationStage', () => () => (
Deletion complete
)); diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx index 3726968e0..8bbef0eb7 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx @@ -21,7 +21,7 @@ import useBaseAPIUrl from '../../../helpers/hooks/useBaseAPIUrl'; import usePatient from '../../../helpers/hooks/usePatient'; import { errorToParams } from '../../../helpers/utils/errorToParams'; import { isMock } from '../../../helpers/utils/isLocal'; -import useFeatureFlags from '../../../helpers/hooks/useFeatureFlags'; +import useConfig from '../../../helpers/hooks/useConfig'; export type Props = { docType: DOCUMENT_TYPE; @@ -51,7 +51,7 @@ function DeleteDocumentsStage({ const baseUrl = useBaseAPIUrl(); const baseHeaders = useBaseAPIHeaders(); const navigate = useNavigate(); - const featureFlags = useFeatureFlags(); + const config = useConfig(); const nhsNumber: string = patientDetails?.nhsNumber ?? ''; const formattedNhsNumber = formatNhsNumber(nhsNumber); @@ -91,7 +91,7 @@ function DeleteDocumentsStage({ } } catch (e) { const error = e as AxiosError; - if (isMock(error) && !!featureFlags.mockLocal.recordUploaded) { + if (isMock(error) && !!config.mockLocal.recordUploaded) { onSuccess(); } else { if (error.response?.status === 403) { diff --git a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx index 5327f1190..883ced258 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx @@ -7,11 +7,14 @@ import userEvent from '@testing-library/user-event'; import usePatient from '../../../helpers/hooks/usePatient'; import { LinkProps } from 'react-router-dom'; import { routes } from '../../../types/generic/routes'; +import useConfig from '../../../helpers/hooks/useConfig'; +import { defaultFeatureFlags } from '../../../helpers/requests/getFeatureFlags'; -jest.mock('../../../helpers/hooks/useFeatureFlags'); +jest.mock('../../../helpers/hooks/useConfig'); const mockedUseNavigate = jest.fn(); const mockedAxios = axios as jest.Mocked; const mockedUsePatient = usePatient as jest.Mock; +const mockUseConfig = useConfig as jest.Mock; const mockPdf = buildLgSearchResult(); const mockPatient = buildPatientDetails(); const mockSetStage = jest.fn(); @@ -32,6 +35,7 @@ describe('LloydGeorgeDownloadAllStage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; mockedUsePatient.mockReturnValue(mockPatient); + mockUseConfig.mockReturnValue({ featureFlags: defaultFeatureFlags, mockLocal: {} }); }); afterEach(() => { jest.clearAllMocks(); diff --git a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx index 41927bb7e..3e7706baf 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx @@ -22,7 +22,7 @@ import { useNavigate, Link } from 'react-router-dom'; import { errorToParams } from '../../../helpers/utils/errorToParams'; import { AxiosError } from 'axios/index'; import { isMock } from '../../../helpers/utils/isLocal'; -import useFeatureFlags from '../../../helpers/hooks/useFeatureFlags'; +import useConfig from '../../../helpers/hooks/useConfig'; const FakeProgress = require('fake-progress'); @@ -56,7 +56,7 @@ function LloydGeorgeDownloadAllStage({ const linkRef = useRef(null); const mounted = useRef(false); const navigate = useNavigate(); - const featureFlags = useFeatureFlags(); + const { mockLocal } = useConfig(); const patientDetails = usePatient(); const nhsNumber = patientDetails?.nhsNumber ?? ''; const [delayTimer, setDelayTimer] = useState(); @@ -89,7 +89,7 @@ function LloydGeorgeDownloadAllStage({ useEffect(() => { const onFail = (error: AxiosError) => { - if (isMock(error) && !!featureFlags.mockLocal.recordUploaded) { + if (isMock(error) && !!mockLocal.recordUploaded) { if (typeof window !== 'undefined') { const { protocol, host } = window.location; setLinkAttributes({ @@ -149,7 +149,7 @@ function LloydGeorgeDownloadAllStage({ progressTimer, deleteAfterDownload, navigate, - featureFlags, + mockLocal, ]); return inProgress ? ( diff --git a/app/src/components/blocks/testPanel/TestPanel.tsx b/app/src/components/blocks/testPanel/TestPanel.tsx index 1dfb318d0..42383a0ea 100644 --- a/app/src/components/blocks/testPanel/TestPanel.tsx +++ b/app/src/components/blocks/testPanel/TestPanel.tsx @@ -1,24 +1,21 @@ import React from 'react'; import 'react-toggle/style.css'; import { isLocal } from '../../../helpers/utils/isLocal'; -import { - LocalFlags, - useFeatureFlagsContext, -} from '../../../providers/featureFlagsProvider/FeatureFlagsProvider'; +import { LocalFlags, useConfigContext } from '../../../providers/configProvider/ConfigProvider'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import TestToggle, { ToggleProps } from './TestToggle'; function TestPanel() { - const [featureFlags, setFeatureFlags] = useFeatureFlagsContext(); - const { isBsol, recordUploaded, userRole } = featureFlags.mockLocal; + const [config, setConfig] = useConfigContext(); + const { isBsol, recordUploaded, userRole } = config.mockLocal; const updateLocalFlag = (key: keyof LocalFlags, value: boolean | REPOSITORY_ROLE) => { - setFeatureFlags({ - ...featureFlags, + setConfig({ mockLocal: { - ...featureFlags.mockLocal, + ...config.mockLocal, [key]: value, }, + featureFlags: config.featureFlags, }); }; diff --git a/app/src/helpers/hooks/useConfig.test.tsx b/app/src/helpers/hooks/useConfig.test.tsx new file mode 100644 index 000000000..6b4e544f0 --- /dev/null +++ b/app/src/helpers/hooks/useConfig.test.tsx @@ -0,0 +1,45 @@ +import { render, screen } from '@testing-library/react'; +import useConfig from './useConfig'; +import ConfigProvider, { GlobalConfig } from '../../providers/configProvider/ConfigProvider'; +import { defaultFeatureFlags } from '../requests/getFeatureFlags'; + +describe('useConfig', () => { + beforeEach(() => { + sessionStorage.setItem('FeatureFlags', ''); + process.env.REACT_APP_ENVIRONMENT = 'jest'; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns true when feature flag in context', () => { + const config: GlobalConfig = { + featureFlags: { ...defaultFeatureFlags, testFeature1: true }, + mockLocal: {}, + }; + renderHook(config); + expect(screen.getByText(`FLAG: true`)).toBeInTheDocument(); + }); + + it('returns false when there is no feature flag in context', () => { + const config: GlobalConfig = { + featureFlags: { ...defaultFeatureFlags, testFeature1: false }, + mockLocal: {}, + }; + renderHook(config); + expect(screen.getByText(`FLAG: false`)).toBeInTheDocument(); + }); +}); + +const TestApp = () => { + const config = useConfig(); + return
{`FLAG: ${!!config.featureFlags.testFeature1}`.normalize()}
; +}; + +const renderHook = (config?: GlobalConfig) => { + return render( + + + , + ); +}; diff --git a/app/src/helpers/hooks/useConfig.tsx b/app/src/helpers/hooks/useConfig.tsx new file mode 100644 index 000000000..4025f917b --- /dev/null +++ b/app/src/helpers/hooks/useConfig.tsx @@ -0,0 +1,8 @@ +import { useConfigContext } from '../../providers/configProvider/ConfigProvider'; + +function useConfig() { + const [config] = useConfigContext(); + return config; +} + +export default useConfig; diff --git a/app/src/helpers/hooks/useFeatureFlags.test.tsx b/app/src/helpers/hooks/useFeatureFlags.test.tsx deleted file mode 100644 index d1931c940..000000000 --- a/app/src/helpers/hooks/useFeatureFlags.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import useFeatureFlags from './useFeatureFlags'; -import FeatureFlagsProvider, { - FeatureFlags, -} from '../../providers/featureFlagsProvider/FeatureFlagsProvider'; - -describe('useFeatureFlags', () => { - beforeEach(() => { - sessionStorage.setItem('FeatureFlags', ''); - process.env.REACT_APP_ENVIRONMENT = 'jest'; - }); - afterEach(() => { - jest.clearAllMocks(); - }); - - it('returns true when feature flag in context', () => { - const appConfig: Partial = { appConfig: { testFeature: true } }; - renderHook(appConfig); - expect(screen.getByText(`FLAG: true`)).toBeInTheDocument(); - }); - - it('returns false when there is no feature flag in context', () => { - const appConfig: Partial = { appConfig: {} }; - renderHook(appConfig); - expect(screen.getByText(`FLAG: false`)).toBeInTheDocument(); - }); -}); - -const TestApp = () => { - const featureFlags = useFeatureFlags(); - return
{`FLAG: ${!!featureFlags.appConfig.testFeature}`.normalize()}
; -}; - -const renderHook = (featureFlags?: Partial) => { - return render( - - - , - ); -}; diff --git a/app/src/helpers/hooks/useFeatureFlags.tsx b/app/src/helpers/hooks/useFeatureFlags.tsx deleted file mode 100644 index 115a65085..000000000 --- a/app/src/helpers/hooks/useFeatureFlags.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useFeatureFlagsContext } from '../../providers/featureFlagsProvider/FeatureFlagsProvider'; - -function useFeatureFlags() { - const [featureFlags] = useFeatureFlagsContext(); - return featureFlags; -} - -export default useFeatureFlags; diff --git a/app/src/helpers/requests/getFeatureFlags.ts b/app/src/helpers/requests/getFeatureFlags.ts new file mode 100644 index 000000000..3a45ffd78 --- /dev/null +++ b/app/src/helpers/requests/getFeatureFlags.ts @@ -0,0 +1,36 @@ +import { AuthHeaders } from '../../types/blocks/authHeaders'; +import { endpoints } from '../../types/generic/endpoints'; + +import axios from 'axios'; +import { FeatureFlags } from '../../types/generic/featureFlags'; + +type Args = { + baseUrl: string; + baseHeaders: AuthHeaders; +}; + +type GetFeatureFlagsResponse = { + data: FeatureFlags; +}; + +export const defaultFeatureFlags = { + testFeature1: false, + testFeature2: false, + testFeature3: false, +}; + +const getFeatureFlags = async ({ baseUrl, baseHeaders }: Args) => { + const gatewayUrl = baseUrl + endpoints.FEATURE_FLAGS; + try { + const { data }: GetFeatureFlagsResponse = await axios.get(gatewayUrl, { + headers: { + ...baseHeaders, + }, + }); + return data; + } catch (e) { + return defaultFeatureFlags; + } +}; + +export default getFeatureFlags; diff --git a/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx b/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx index 023e25a41..315adb5ee 100644 --- a/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx +++ b/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx @@ -1,11 +1,17 @@ import { render, screen, waitFor } from '@testing-library/react'; import AuthCallbackPage from './AuthCallbackPage'; -import SessionProvider from '../../providers/sessionProvider/SessionProvider'; +import SessionProvider, { + useSessionContext, +} from '../../providers/sessionProvider/SessionProvider'; import axios from 'axios'; import { buildUserAuth } from '../../helpers/test/testBuilders'; import { routes } from '../../types/generic/routes'; +import ConfigProvider, { useConfigContext } from '../../providers/configProvider/ConfigProvider'; +import { act } from 'react-dom/test-utils'; +import { endpoints } from '../../types/generic/endpoints'; +import { defaultFeatureFlags } from '../../helpers/requests/getFeatureFlags'; -jest.mock('../../helpers/hooks/useFeatureFlags'); +jest.mock('../../helpers/hooks/useConfig'); const mockedUseNavigate = jest.fn(); jest.mock('axios'); jest.mock('react-router', () => ({ @@ -26,7 +32,7 @@ const originalWindowLocation = window.location; describe('AuthCallbackPage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; - + sessionStorage.setItem('FeatureFlags', ''); Object.defineProperty(window, 'location', { configurable: true, enumerable: true, @@ -42,71 +48,118 @@ describe('AuthCallbackPage', () => { }); }); - it('returns a loading state until redirection to token request handler', async () => { - mockedAxios.get.mockImplementation(() => Promise.resolve({ data: buildUserAuth() })); - renderCallbackPage(); - expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('Logging in...')).toBeInTheDocument(); - - await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + describe('Rendering', () => { + it('returns a loading state until redirection to token request handler', async () => { + mockedAxios.get.mockImplementation(() => Promise.resolve({ data: buildUserAuth() })); + act(() => { + renderCallbackPage(); + }); + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('Logging in...')).toBeInTheDocument(); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + }); }); }); - it('navigates to the select role page when callback token request is successful', async () => { - mockedAxios.get.mockImplementation(() => Promise.resolve({ data: buildUserAuth() })); - renderCallbackPage(); + describe('Navigation', () => { + it('navigates to the select role page when callback token request is successful', async () => { + mockedAxios.get.mockImplementation(() => Promise.resolve({ data: buildUserAuth() })); + renderCallbackPage(); - expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('Logging in...')).toBeInTheDocument(); + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('Logging in...')).toBeInTheDocument(); - await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + }); }); - }); - it('navigates to auth error page when callback token request is unsuccessful', async () => { - const errorResponse = { - response: { - status: 400, - message: '400 Bad Request', - }, - }; + it('navigates to auth error page when callback token request is unsuccessful', async () => { + const errorResponse = { + response: { + status: 400, + message: '400 Bad Request', + }, + }; - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - renderCallbackPage(); + mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); + renderCallbackPage(); - expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('Logging in...')).toBeInTheDocument(); + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('Logging in...')).toBeInTheDocument(); - await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.AUTH_ERROR); + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.AUTH_ERROR); + }); + }); + it('navigates to unauthorised login page when callback token request is 401', async () => { + const errorResponse = { + response: { + status: 401, + message: '401 Unauthorised', + }, + }; + + mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); + renderCallbackPage(); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('Logging in...')).toBeInTheDocument(); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.UNAUTHORISED_LOGIN); + }); }); }); - it('navigates to unauthorised login page when callback token request is 401', async () => { - const errorResponse = { - response: { - status: 401, - message: '401 Unauthorised', - }, - }; - - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - renderCallbackPage(); - - expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('Logging in...')).toBeInTheDocument(); - - await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.UNAUTHORISED_LOGIN); + + describe('Config', () => { + it('sets session context to user is has a role', async () => { + mockedAxios.get.mockImplementation(() => Promise.resolve({ data: buildUserAuth() })); + renderCallbackPage(); + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + }); + expect(screen.getByText('LOGGEDIN: true')).toBeInTheDocument(); + }); + it('sets config context to user is has feature flags', async () => { + mockedAxios.get.mockImplementation((url) => { + if (url.includes(endpoints.AUTH)) { + return Promise.resolve({ data: buildUserAuth() }); + } else { + return Promise.resolve({ + data: { ...defaultFeatureFlags, testFeature1: true }, + }); + } + }); + renderCallbackPage(); + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + }); + expect(screen.getByText('FLAG: true')).toBeInTheDocument(); }); }); }); +const TestApp = () => { + const [config] = useConfigContext(); + const [session] = useSessionContext(); + return ( +
+ +
{`FLAG: ${JSON.stringify(config.featureFlags.testFeature1)}`.normalize()}
; +
{`LOGGEDIN: ${!!session.auth?.role}`.normalize()}
; +
+ ); +}; + const renderCallbackPage = () => { render( - + + + , ); }; diff --git a/app/src/pages/authCallbackPage/AuthCallbackPage.tsx b/app/src/pages/authCallbackPage/AuthCallbackPage.tsx index 734030a44..59807b65f 100644 --- a/app/src/pages/authCallbackPage/AuthCallbackPage.tsx +++ b/app/src/pages/authCallbackPage/AuthCallbackPage.tsx @@ -10,15 +10,16 @@ import { buildUserAuth } from '../../helpers/test/testBuilders'; import { UserAuth } from '../../types/blocks/userAuth'; import useBaseAPIUrl from '../../helpers/hooks/useBaseAPIUrl'; import { REPOSITORY_ROLE } from '../../types/generic/authRole'; -import useFeatureFlags from '../../helpers/hooks/useFeatureFlags'; +import { useConfigContext } from '../../providers/configProvider/ConfigProvider'; +import getFeatureFlags from '../../helpers/requests/getFeatureFlags'; type Props = {}; const AuthCallbackPage = (props: Props) => { const baseUrl = useBaseAPIUrl(); const [, setSession] = useSessionContext(); + const [{ mockLocal }, setConfig] = useConfigContext(); const navigate = useNavigate(); - const featureFlags = useFeatureFlags(); useEffect(() => { const handleError = (error: AxiosError) => { @@ -32,12 +33,24 @@ const AuthCallbackPage = (props: Props) => { navigate(routes.AUTH_ERROR); } }; - const handleSuccess = (auth: UserAuth) => { + const handleSuccess = async (auth: UserAuth) => { const { GP_ADMIN, GP_CLINICAL, PCSE } = REPOSITORY_ROLE; setSession({ auth: auth, isLoggedIn: true, }); + const jwtToken = auth.authorisation_token ?? ''; + const featureFlagsData = await getFeatureFlags({ + baseUrl, + baseHeaders: { + 'Content-Type': 'application/json', + Authorization: jwtToken, + }, + }); + setConfig({ + mockLocal: mockLocal, + featureFlags: featureFlagsData, + }); if ([GP_ADMIN, GP_CLINICAL, PCSE].includes(auth.role)) { navigate(routes.HOME); @@ -49,12 +62,12 @@ const AuthCallbackPage = (props: Props) => { const handleCallback = async (args: AuthTokenArgs) => { try { const authData = await getAuthToken(args); - handleSuccess(authData); + await handleSuccess(authData); } catch (e) { const error = e as AxiosError; if (isMock(error)) { - const { isBsol, userRole } = featureFlags.mockLocal; - handleSuccess(buildUserAuth({ isBSOL: !!isBsol, role: userRole })); + const { isBsol, userRole } = mockLocal; + await handleSuccess(buildUserAuth({ isBSOL: !!isBsol, role: userRole })); } else { handleError(error); } @@ -65,7 +78,7 @@ const AuthCallbackPage = (props: Props) => { const code = urlSearchParams.get('code') ?? ''; const state = urlSearchParams.get('state') ?? ''; void handleCallback({ baseUrl, code, state }); - }, [baseUrl, setSession, navigate, featureFlags]); + }, [baseUrl, setSession, navigate, mockLocal, setConfig]); return ; }; diff --git a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx index 279c33fbd..9eb97acd9 100644 --- a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx +++ b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx @@ -12,7 +12,7 @@ import usePatient from '../../helpers/hooks/usePatient'; import { act } from 'react-dom/test-utils'; import { routes } from '../../types/generic/routes'; -jest.mock('../../helpers/hooks/useFeatureFlags'); +jest.mock('../../helpers/hooks/useConfig'); jest.mock('axios'); jest.mock('../../helpers/hooks/usePatient'); jest.mock('../../helpers/hooks/useBaseAPIHeaders'); diff --git a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx index ec73e3b76..32b89a850 100644 --- a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx +++ b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx @@ -19,7 +19,7 @@ import { useNavigate } from 'react-router'; import { errorToParams } from '../../helpers/utils/errorToParams'; import { isMock } from '../../helpers/utils/isLocal'; import moment from 'moment'; -import useFeatureFlags from '../../helpers/hooks/useFeatureFlags'; +import useConfig from '../../helpers/hooks/useConfig'; function LloydGeorgeRecordPage() { const patientDetails = usePatient(); @@ -33,7 +33,7 @@ function LloydGeorgeRecordPage() { const mounted = useRef(false); const [stage, setStage] = useState(LG_RECORD_STAGE.RECORD); const navigate = useNavigate(); - const featureFlags = useFeatureFlags(); + const config = useConfig(); const role = useRole(); const isBSOL = useIsBSOL(); const deleteAfterDownload = role === REPOSITORY_ROLE.GP_ADMIN && isBSOL === false; @@ -68,7 +68,7 @@ function LloydGeorgeRecordPage() { } catch (e) { const error = e as AxiosError; if (isMock(error)) { - if (!!featureFlags.mockLocal.recordUploaded) { + if (!!config.mockLocal.recordUploaded) { onSuccess(1, moment().format(), '/dev/testFile.pdf', 59000); } else { setDownloadStage(DOWNLOAD_STAGE.NO_RECORDS); @@ -103,7 +103,7 @@ function LloydGeorgeRecordPage() { setNumberOfFiles, setTotalFileSizeInByte, navigate, - featureFlags, + config, ]); switch (stage) { diff --git a/app/src/providers/featureFlagsProvider/FeatureFlagsProvider.test.tsx b/app/src/providers/configProvider/ConfigProvider.test.tsx similarity index 63% rename from app/src/providers/featureFlagsProvider/FeatureFlagsProvider.test.tsx rename to app/src/providers/configProvider/ConfigProvider.test.tsx index 9ad2e920a..375114bc0 100644 --- a/app/src/providers/featureFlagsProvider/FeatureFlagsProvider.test.tsx +++ b/app/src/providers/configProvider/ConfigProvider.test.tsx @@ -1,6 +1,7 @@ import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import FeatureFlagsProvider, { FeatureFlags, useFeatureFlagsContext } from './FeatureFlagsProvider'; +import ConfigProvider, { GlobalConfig, useConfigContext } from './ConfigProvider'; +import { defaultFeatureFlags } from '../../helpers/requests/getFeatureFlags'; describe('SessionProvider', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; @@ -31,27 +32,31 @@ describe('SessionProvider', () => { }); const TestApp = () => { - const [featureFlags, setFeatureFlags] = useFeatureFlagsContext(); - const flagOn: FeatureFlags = { - ...featureFlags, - appConfig: { - testFeature: true, + const [config, setConfig] = useConfigContext(); + const flagOn: GlobalConfig = { + ...config, + featureFlags: { + ...defaultFeatureFlags, + testFeature1: true, }, }; - const flagOff: FeatureFlags = { - ...featureFlags, - appConfig: {}, + const flagOff: GlobalConfig = { + ...config, + featureFlags: { + ...defaultFeatureFlags, + testFeature1: false, + }, }; return ( <>

Actions

-
setFeatureFlags(flagOn)}>Flag On
-
setFeatureFlags(flagOff)}>Flag Off
+
setConfig(flagOn)}>Flag On
+
setConfig(flagOff)}>Flag Off

Flags

- testFeature - {`${!!featureFlags.appConfig.testFeature}`} + testFeature - {`${!!config.featureFlags.testFeature1}`}
); @@ -59,8 +64,8 @@ const TestApp = () => { const renderFeatureFlagsProvider = () => { render( - + - , + , ); }; diff --git a/app/src/providers/configProvider/ConfigProvider.tsx b/app/src/providers/configProvider/ConfigProvider.tsx new file mode 100644 index 000000000..aeb1d43e4 --- /dev/null +++ b/app/src/providers/configProvider/ConfigProvider.tsx @@ -0,0 +1,77 @@ +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import type { Dispatch, ReactNode, SetStateAction } from 'react'; +import { REPOSITORY_ROLE } from '../../types/generic/authRole'; +import { FeatureFlags } from '../../types/generic/featureFlags'; +import { defaultFeatureFlags } from '../../helpers/requests/getFeatureFlags'; +import { isLocal } from '../../helpers/utils/isLocal'; + +type SetConfigOverride = (config: GlobalConfig) => void; + +type Props = { + children: ReactNode; + configOverride?: Partial; + setConfigOverride?: SetConfigOverride; +}; + +export type LocalFlags = { + isBsol?: boolean; + recordUploaded?: boolean; + userRole?: REPOSITORY_ROLE; +}; + +export type GlobalConfig = { + featureFlags: FeatureFlags; + mockLocal: LocalFlags; +}; + +export type TConfigContext = [ + GlobalConfig, + Dispatch> | SetConfigOverride, +]; + +const ConfigContext = createContext(null); +const ConfigProvider = ({ children, configOverride, setConfigOverride }: Props) => { + const emptyConfig = useMemo( + () => ({ + featureFlags: { ...defaultFeatureFlags, ...configOverride?.featureFlags }, + mockLocal: { + ...configOverride?.mockLocal, + }, + }), + [configOverride], + ); + const defaultMockLocals = isLocal + ? { + isBsol: true, + recordUploaded: true, + userRole: REPOSITORY_ROLE.GP_ADMIN, + } + : null; + const storedConfig = sessionStorage.getItem('AppConfig'); + const currentConfig: GlobalConfig = storedConfig ? JSON.parse(storedConfig) : emptyConfig; + const [config, setConfig] = useState({ + mockLocal: { + ...defaultMockLocals, + ...currentConfig.mockLocal, + ...configOverride?.mockLocal, + }, + featureFlags: { + ...defaultFeatureFlags, + ...currentConfig.featureFlags, + ...configOverride?.featureFlags, + }, + }); + + useEffect(() => { + sessionStorage.setItem('AppConfig', JSON.stringify(config) ?? emptyConfig); + }, [config, emptyConfig]); + + return ( + + {children} + + ); +}; + +export default ConfigProvider; +export const useConfigContext = () => useContext(ConfigContext) as TConfigContext; diff --git a/app/src/providers/featureFlagsProvider/FeatureFlagsProvider.tsx b/app/src/providers/featureFlagsProvider/FeatureFlagsProvider.tsx deleted file mode 100644 index 9f19de5d3..000000000 --- a/app/src/providers/featureFlagsProvider/FeatureFlagsProvider.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import type { Dispatch, ReactNode, SetStateAction } from 'react'; -import { REPOSITORY_ROLE } from '../../types/generic/authRole'; -import { isLocal } from '../../helpers/utils/isLocal'; - -type SetFeatureFlagsOverride = (featureFlags: FeatureFlags) => void; - -type Props = { - children: ReactNode; - featureFlagsOverride?: Partial; - setFeatureFlagsOverride?: SetFeatureFlagsOverride; -}; - -export type LocalFlags = { - isBsol?: boolean; - recordUploaded?: boolean; - userRole?: REPOSITORY_ROLE; -}; - -export type FeatureFlags = { - appConfig: { - testFeature?: true; - testRoute?: true; - }; - mockLocal: LocalFlags; -}; - -export type TFeatureFlagsContext = [ - FeatureFlags, - Dispatch> | SetFeatureFlagsOverride, -]; - -const FeatureFlagsContext = createContext(null); -const FeatureFlagsProvider = ({ - children, - featureFlagsOverride, - setFeatureFlagsOverride, -}: Props) => { - const emptyFlags = useMemo( - () => ({ - appConfig: { ...featureFlagsOverride?.appConfig }, - mockLocal: { - ...featureFlagsOverride?.mockLocal, - }, - }), - [featureFlagsOverride], - ); - const localDefaults = isLocal - ? { - isBsol: true, - recordUploaded: true, - userRole: REPOSITORY_ROLE.GP_ADMIN, - ...featureFlagsOverride?.mockLocal, - } - : null; - const storedFlags = sessionStorage.getItem('FeatureFlags'); - const flags: FeatureFlags = storedFlags ? JSON.parse(storedFlags) : emptyFlags; - const [featureFlags, setFeatureFlags] = useState({ - mockLocal: { - ...localDefaults, - ...flags.mockLocal, - ...featureFlagsOverride?.mockLocal, - }, - appConfig: { - ...flags.appConfig, - ...featureFlagsOverride?.appConfig, - }, - }); - - useEffect(() => { - sessionStorage.setItem('FeatureFlags', JSON.stringify(featureFlags) ?? emptyFlags); - }, [featureFlags, emptyFlags]); - - return ( - - {children} - - ); -}; - -export default FeatureFlagsProvider; -export const useFeatureFlagsContext = () => useContext(FeatureFlagsContext) as TFeatureFlagsContext; diff --git a/app/src/types/generic/endpoints.ts b/app/src/types/generic/endpoints.ts index f3f386db0..a12644172 100644 --- a/app/src/types/generic/endpoints.ts +++ b/app/src/types/generic/endpoints.ts @@ -11,4 +11,6 @@ export enum endpoints { LLOYDGEORGE_STITCH = '/LloydGeorgeStitch', FEEDBACK = '/Feedback', + + FEATURE_FLAGS = '/FeatureFlags', } diff --git a/app/src/types/generic/featureFlags.ts b/app/src/types/generic/featureFlags.ts new file mode 100644 index 000000000..275ba9eb7 --- /dev/null +++ b/app/src/types/generic/featureFlags.ts @@ -0,0 +1,5 @@ +export type FeatureFlags = { + testFeature1: boolean; + testFeature2: boolean; + testFeature3: boolean; +};