diff --git a/src/GlobalConfigParser.tsx b/src/GlobalConfigParser.tsx index 7010bb1547..e493b7f702 100644 --- a/src/GlobalConfigParser.tsx +++ b/src/GlobalConfigParser.tsx @@ -28,37 +28,72 @@ async function fetchGlobalConfigArray() { return parseGlobalConfig(configs); } -export function GlobalConfigParser() { - const [globalConfig, setGlobalConfig] = useState>(null); +function HomeRoute({ globalConfig }: { globalConfig: GlobalConfig }) { const [studyConfigs, setStudyConfigs] = useState | null>>({}); useEffect(() => { - async function fetchData() { - if (globalConfig) { - setStudyConfigs(await fetchStudyConfigs(globalConfig)); + let cancelled = false; + + async function fetchData(currentGlobalConfig: GlobalConfig) { + const configs = await fetchStudyConfigs(currentGlobalConfig); + if (!cancelled) { + setStudyConfigs(configs); } } - fetchData(); + + fetchData(globalConfig); + + return () => { + cancelled = true; + }; }, [globalConfig]); + return ( + <> + + + + + + + ); +} + +export function GlobalConfigParser() { + const [globalConfig, setGlobalConfig] = useState>(null); + useEffect(() => { - if (globalConfig) return; + if (globalConfig) { + return undefined; + } fetchGlobalConfigArray().then((gc) => { setGlobalConfig(gc); }); + + return undefined; }, [globalConfig]); // Initialize storage engine const { storageEngine, setStorageEngine } = useStorageEngine(); useEffect(() => { - if (storageEngine !== undefined) return; + if (storageEngine !== undefined) { + return undefined; + } async function fn() { const _storageEngine = await initializeStorageEngine(); setStorageEngine(_storageEngine); } fn(); + + return undefined; }, [setStorageEngine, storageEngine]); const analysisProtectedCallback = async (studyId: string) => { @@ -76,21 +111,7 @@ export function GlobalConfigParser() { - - - - - - - )} + element={} /> { if (routeStudyId !== '__revisit-widget') { - getStudyConfig(routeStudyId, globalConfig).then((config) => { + const loadStudyConfig = async () => { + const config = await getStudyConfig(routeStudyId, globalConfig); setActiveConfig(config); - }); - return () => { }; + }; + + loadStudyConfig(); + return undefined; } + if (globalConfig && routeStudyId) { const messageListener = (event: MessageEvent) => { if (event.data.type === 'revisitWidget/CONFIG') { - parseStudyConfig(event.data.payload).then(async (config) => { + const loadWidgetConfig = async () => { + const config = await parseStudyConfig(event.data.payload); setActiveConfig(config); + const sequenceArray = await generateSequenceArray(config); window.parent.postMessage({ type: 'revisitWidget/SEQUENCE_ARRAY', payload: sequenceArray }, '*'); - }); + }; + + loadWidgetConfig(); } }; @@ -77,11 +121,14 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) { window.removeEventListener('message', messageListener); }; } - return () => { }; + + return undefined; }, [globalConfig, routeStudyId]); const [routes, setRoutes] = useState([]); const [store, setStore] = useState>(null); + const [isCompletionCheckResolved, setIsCompletionCheckResolved] = useState(false); + const [completionCheckError, setCompletionCheckError] = useState(null); const { storageEngine } = useStorageEngine(); const [searchParams] = useSearchParams(); @@ -89,23 +136,45 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) { const studyCondition = useMemo(() => parseConditionParam(searchParams.get('condition')), [searchParams]); useEffect(() => { + let isCancelled = false; + + async function fetchParticipantIp() { + const ipTimeoutController = new AbortController(); + const ipTimeoutId = window.setTimeout(() => ipTimeoutController.abort(), 1200); + + try { + const ipRes = await fetch('https://api.ipify.org?format=json', { + signal: ipTimeoutController.signal, + }).catch(() => ''); + + return ipRes instanceof Response ? await ipRes.json() as { ip: string } : { ip: '' }; + } finally { + window.clearTimeout(ipTimeoutId); + } + } + async function initializeUserStoreRouting() { // Check that we have a storage engine and active config (studyId is set for config, but typescript complains) if (!storageEngine || !activeConfig || !canonicalStudyId) return; + setIsCompletionCheckResolved(false); + setCompletionCheckError(null); try { // Make sure that we have a study database and that the study database has a sequence array await storageEngine.initializeStudyDb(canonicalStudyId); + + const modesPromise = storageEngine.getModes(canonicalStudyId); + const activeHashPromise = hash(JSON.stringify(activeConfig)); + await storageEngine.saveConfig(activeConfig); const sequenceArray = await storageEngine.getSequenceArray(); + if (!sequenceArray) { - await storageEngine.setSequenceArray( - await generateSequenceArray(activeConfig), - ); - } + const generatedSequenceArray = await generateSequenceArray(activeConfig); - const modes = await storageEngine.getModes(canonicalStudyId); + await storageEngine.setSequenceArray(generatedSequenceArray); + } // Get or generate participant session const urlParticipantId = activeConfig.uiConfig.urlParticipantIdParam @@ -114,33 +183,17 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) { : undefined; const searchParamsObject = Object.fromEntries(searchParams.entries()); - const ipTimeoutController = new AbortController(); - const ipTimeoutId = window.setTimeout(() => ipTimeoutController.abort(), 1200); - const ipRes = await fetch('https://api.ipify.org?format=json', { - signal: ipTimeoutController.signal, - }).catch(() => ''); - window.clearTimeout(ipTimeoutId); - const ip: { ip: string } = ipRes instanceof Response ? await ipRes.json() : { ip: '' }; - - const metadata: ParticipantMetadata = { - language: navigator.language, - userAgent: navigator.userAgent, - resolution: { - width: window.screen.width, - height: window.screen.height, - availHeight: window.screen.availHeight, - availWidth: window.screen.availWidth, - colorDepth: window.screen.colorDepth, - orientation: window.screen.orientation.type, - pixelDepth: window.screen.pixelDepth, - }, - ip: ip.ip, - }; + const [modes, activeHash] = await Promise.all([ + modesPromise, + activeHashPromise, + ]); + + const initialMetadata = createParticipantMetadata(); let participantSession = await storageEngine.initializeParticipantSession( searchParamsObject, activeConfig, - metadata, + initialMetadata, participantId || urlParticipantId, ); @@ -157,7 +210,6 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) { conditions: studyCondition, }; } - const activeHash = await hash(JSON.stringify(activeConfig)); let participantConfig = activeConfig; @@ -165,32 +217,78 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) { participantConfig = (await storageEngine.getAllConfigsFromHash([participantSession.participantConfigHash], canonicalStudyId))[participantSession.participantConfigHash] as ParsedConfig; } - const effectiveStudyCondition = resolveParticipantConditions({ + const resolvedCondition = resolveParticipantConditions({ urlCondition: studyCondition, participantConditions: participantSession.conditions, participantSearchParamCondition: participantSession.searchParams?.condition, allowUrlOverride: modes.developmentModeEnabled, }); - const filteredParticipantSequence = filterSequenceByCondition(participantSession.sequence, effectiveStudyCondition); - const participantCompleted = await storageEngine.getParticipantCompletionStatus(participantSession.participantId); + const filteredParticipantSequence = filterSequenceByCondition(participantSession.sequence, resolvedCondition); + // Initialize the redux stores const newStore = await studyStoreCreator( canonicalStudyId, participantConfig, filteredParticipantSequence, - metadata, + participantSession.metadata, participantSession.answers, modes, participantSession.participantId, - participantCompleted, + false, false, participantSession.participantConfigHash !== activeHash, ); + + if (isCancelled) { + return; + } + setStore(newStore); + + if (modes.dataCollectionEnabled) { + fetchParticipantIp().then(async (ip) => { + if (isCancelled || !ip.ip || participantSession.metadata.ip === ip.ip) { + return; + } + + const metadataWithIp = createParticipantMetadata(ip.ip); + participantSession = { + ...participantSession, + metadata: metadataWithIp, + }; + + await storageEngine.updateParticipantMetadata(metadataWithIp); + + if (!isCancelled) { + newStore.store.dispatch(newStore.actions.setMetadata(metadataWithIp)); + } + }).catch((error) => { + console.error('Error fetching participant IP:', error); + }); + } + + if (!modes.dataCollectionEnabled) { + setIsCompletionCheckResolved(true); + } else { + storageEngine.getParticipantCompletionStatus(participantSession.participantId).then((participantCompleted) => { + if (!isCancelled) { + newStore.store.dispatch(newStore.actions.setParticipantCompleted(participantCompleted)); + setIsCompletionCheckResolved(true); + } + }).catch((error) => { + console.error('Error fetching participant completion status:', error); + if (!isCancelled) { + setCompletionCheckError('We could not verify whether this study session was already completed. Please reload this page and try again.'); + // A transient completion-status lookup failure should not block study entry. + setIsCompletionCheckResolved(true); + } + }); + } } catch (error) { console.error('Error initializing user store routing:', error); // Fallback: initialize the store with empty data - const generatedSequences = generateSequenceArray(activeConfig); + const generatedSequences = await generateSequenceArray(activeConfig); + const matchingSequence = generatedSequences[0]; const fallbackSequence = filterSequenceByCondition( matchingSequence, @@ -201,27 +299,24 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) { canonicalStudyId, activeConfig, fallbackSequence, - { - language: '', - userAgent: '', - resolution: { - width: 0, - height: 0, - availHeight: 0, - availWidth: 0, - colorDepth: 0, - orientation: '', - pixelDepth: 0, - }, - ip: '', - }, + createEmptyParticipantMetadata(), {}, { developmentModeEnabled: true, dataSharingEnabled: true, dataCollectionEnabled: false }, '', false, true, ); + + if (isCancelled) { + return; + } + setStore(emptyStore); + setIsCompletionCheckResolved(true); + } + + if (isCancelled) { + return; } // Initialize the routing @@ -255,26 +350,52 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) { ]); } initializeUserStoreRouting(); + return () => { + isCancelled = true; + }; }, [storageEngine, activeConfig, canonicalStudyId, searchParams, participantId, studyCondition]); const routing = useRoutes(routes); + const isLoading = isValidStudyId && (routes.length === 0 || store === null || !isCompletionCheckResolved); - let toRender: ReactNode = null; + let content: ReactNode = null; - // Definitely a 404 if (!isValidStudyId) { - toRender = ; - } else if (routes.length === 0) { - toRender = ; - } else { - // If routing is null, we didn't match any routes - toRender = routing && store ? ( + content = ; + } else if (routing && store) { + content = ( {routing} - ) : ( - ); + } else if (!isLoading) { + content = ; } - return toRender; + + return ( + <> + + {isLoading && completionCheckError && ( + + {completionCheckError} + + + )} + {content} + + ); } diff --git a/src/storage/engines/FirebaseStorageEngine.ts b/src/storage/engines/FirebaseStorageEngine.ts index ea54372160..389edf1f50 100644 --- a/src/storage/engines/FirebaseStorageEngine.ts +++ b/src/storage/engines/FirebaseStorageEngine.ts @@ -3,6 +3,7 @@ import localforage from 'localforage'; import { initializeApp } from 'firebase/app'; import { deleteObject, + getBlob, getDownloadURL, getStorage, ref, @@ -94,9 +95,8 @@ export class FirebaseStorageEngine extends CloudStorageEngine { let storageObj: StorageObject = {} as StorageObject; try { - const url = await getDownloadURL(storageRef); - const response = await fetch(url); - const fullProvStr = await response.text(); + const blob = await getBlob(storageRef); + const fullProvStr = await blob.text(); storageObj = JSON.parse(fullProvStr); } catch { console.warn( diff --git a/src/storage/engines/types.ts b/src/storage/engines/types.ts index 866512429e..9a223f7bc3 100644 --- a/src/storage/engines/types.ts +++ b/src/storage/engines/types.ts @@ -66,6 +66,11 @@ interface StageData { allStages: StageInfo[]; } +type ModesAndStageData = { + modes: Record; + stageData: StageData; +}; + export interface ConditionData { allConditions: string[]; conditionCounts: Record; @@ -188,6 +193,10 @@ export abstract class StorageEngine { return this.cloudEngine; } + protected shouldDeferInitialParticipantDataPersistence() { + return false; + } + /* * PRIMITIVE METHODS * These methods are provided by the storage engine implementation and are used by the higher-level methods. @@ -371,6 +380,33 @@ export abstract class StorageEngine { } } + private async getModesAndStageData(studyId: string): Promise { + const modesDoc = await this.getModes(studyId); + const { stage, ...modeValues } = modesDoc; + + if (stage) { + return { + modes: modeValues as Record, + stageData: stage, + }; + } + + const defaultStageData: StageData = { + currentStage: { stageName: 'DEFAULT', color: defaultStageColor }, + allStages: [{ stageName: 'DEFAULT', color: defaultStageColor }], + }; + + await this._setModesDocument(studyId, { + ...(modeValues as Record), + stage: defaultStageData, + }); + + return { + modes: modeValues as Record, + stageData: defaultStageData, + }; + } + private clearPendingParticipantDataWriteTimer() { if (this.pendingParticipantDataWriteTimer) { clearTimeout(this.pendingParticipantDataWriteTimer); @@ -566,19 +602,8 @@ export abstract class StorageEngine { } async getStageData(studyId: string): Promise { - const modesDoc = await this.getModes(studyId); - - if (modesDoc && modesDoc.stage) { - return modesDoc.stage as StageData; - } - - // Set default stage data if it doesn't exist - const defaultStageData: StageData = { - currentStage: { stageName: 'DEFAULT', color: defaultStageColor }, - allStages: [{ stageName: 'DEFAULT', color: defaultStageColor }], - }; - await this.setCurrentStage(studyId, 'DEFAULT', defaultStageColor); - return defaultStageData; + const { stageData } = await this.getModesAndStageData(studyId); + return stageData; } async getConditionData(studyId: string): Promise { @@ -665,6 +690,10 @@ export abstract class StorageEngine { // Hash the provided config const configHash = await hash(JSON.stringify(config)); + if (currentConfigHash === configHash) { + return; + } + // Push the config to storage and cache it, since it won't change await this._pushToStorage( `configs/${configHash}`, @@ -741,7 +770,7 @@ export abstract class StorageEngine { // This function is one of the most critical functions in the storage engine. // It uses the notion of sequence intents and assignments to determine the current sequence for the participant. // It handles rejected participants and allows for reusing a rejected participant's sequence. - protected async _getSequence(conditions?: string[]) { + protected async _getSequence(conditions?: string[], bootstrapData?: ModesAndStageData) { if (!this.currentParticipantId) { throw new Error('Participant not initialized'); } @@ -750,8 +779,7 @@ export abstract class StorageEngine { } let sequenceAssignments = await this.getAllSequenceAssignments(this.studyId); - const modes = await this.getModes(this.studyId); - const stageData = await this.getStageData(this.studyId); + const { modes, stageData } = bootstrapData ?? await this.getModesAndStageData(this.studyId); const currentStage = stageData.currentStage.stageName; // Find all rejected documents @@ -849,30 +877,25 @@ export abstract class StorageEngine { throw new Error('Participant not initialized'); } + const cachedParticipant = await this.getCachedParticipantDataSnapshot(this.currentParticipantId); + if (cachedParticipant) { + this.participantData = cachedParticipant; + return cachedParticipant; + } + // Check if the participant has already been initialized const participant = await this._getFromStorage( `participants/${this.currentParticipantId}`, 'participantData', ); - const cachedParticipant = await this.getCachedParticipantDataSnapshot(this.currentParticipantId); if (this.studyId === undefined) { throw new Error('Study ID is not set'); } - // Get modes - const modes = await this.getModes(this.studyId); - const stageData = await this.getStageData(this.studyId); + const { modes, stageData } = await this.getModesAndStageData(this.studyId); const currentStage = stageData.currentStage.stageName; - if ( - cachedParticipant - && (!isParticipantData(participant) || shouldPreferCachedParticipantData(cachedParticipant, participant)) - ) { - this.participantData = cachedParticipant; - return cachedParticipant; - } - if (isParticipantData(participant)) { // Participant already initialized this.participantData = participant; @@ -883,7 +906,7 @@ export abstract class StorageEngine { const participantConfigHash = await hash(JSON.stringify(config)); const parsedConditions = parseConditionParam(searchParams.condition); const conditions = parsedConditions.length > 0 ? parsedConditions : undefined; - const { currentRow, creationIndex } = await this._getSequence(conditions); + const { currentRow, creationIndex } = await this._getSequence(conditions, { modes, stageData }); this.participantData = { participantId: this.currentParticipantId, participantConfigHash, @@ -900,7 +923,11 @@ export abstract class StorageEngine { }; if (modes.dataCollectionEnabled) { - await this.persistCurrentParticipantData({ immediate: true }); + if (this.shouldDeferInitialParticipantDataPersistence()) { + this.persistCurrentParticipantData({ immediate: true }).catch(() => undefined); + } else { + await this.persistCurrentParticipantData({ immediate: true }); + } } else { await this.cacheParticipantDataSnapshot(this.participantData, this.currentParticipantId); } @@ -1037,6 +1064,28 @@ export abstract class StorageEngine { await this.persistCurrentParticipantData({ immediate: true }); } + async updateParticipantMetadata(metadata: ParticipantMetadata) { + await this.verifyStudyDatabase(); + + if (!this.participantData) { + throw new Error('Participant data not initialized'); + } + + this.participantData.metadata = metadata; + + if (!this.studyId) { + throw new Error('Study ID is not set'); + } + + const modes = await this.getModes(this.studyId); + if (!modes.dataCollectionEnabled) { + await this.cacheParticipantDataSnapshot(this.participantData, this.currentParticipantId); + return; + } + + await this.persistCurrentParticipantData({ immediate: false }); + } + async updateStudyCondition(condition: string | string[]) { await this.verifyStudyDatabase(); @@ -1696,6 +1745,10 @@ export abstract class CloudStorageEngine extends StorageEngine { protected userManagementData: UserManagementData = {}; + protected shouldDeferInitialParticipantDataPersistence() { + return true; + } + /* * PRIMITIVE METHODS * These methods are provided by the storage engine implementation and are used by the higher-level methods. diff --git a/src/storage/tests/highLevel.spec.ts b/src/storage/tests/highLevel.spec.ts index d77988d0ab..d5ae1f4966 100644 --- a/src/storage/tests/highLevel.spec.ts +++ b/src/storage/tests/highLevel.spec.ts @@ -234,6 +234,12 @@ class DelayedLocalStorageEngine extends LocalStorageEngine { } } +class DeferredInitialWriteLocalStorageEngine extends DelayedLocalStorageEngine { + protected override shouldDeferInitialParticipantDataPersistence() { + return true; + } +} + describe.each([ { TestEngine: LocalStorageEngine }, // { TestEngine: SupabaseStorageEngine }, // Uncomment to test with Supabase @@ -278,6 +284,35 @@ describe.each([ expect(storedHashes[configComplexHash]).toEqual(configSimple2); }); + test('saveConfig skips storage writes when the config hash is unchanged', async () => { + await storageEngine.saveConfig(configSimple); + + const pushSpy = vi.spyOn( + storageEngine as unknown as { + _pushToStorage: StorageEngine['_pushToStorage']; + }, + '_pushToStorage', + ); + const deleteSpy = vi.spyOn( + storageEngine as unknown as { + _deleteFromStorage: StorageEngine['_deleteFromStorage']; + }, + '_deleteFromStorage', + ); + const setHashSpy = vi.spyOn( + storageEngine as unknown as { + _setCurrentConfigHash: StorageEngine['_setCurrentConfigHash']; + }, + '_setCurrentConfigHash', + ); + + await storageEngine.saveConfig(configSimple); + + expect(pushSpy).not.toHaveBeenCalled(); + expect(deleteSpy).not.toHaveBeenCalled(); + expect(setHashSpy).not.toHaveBeenCalled(); + }); + test('getCurrentParticipantId returns the current participant ID', async () => { const participantSession = await storageEngine.initializeParticipantSession({}, configSimple, participantMetadata); const { participantId } = participantSession; @@ -318,6 +353,46 @@ describe.each([ expect(participantData!.participantTags).toEqual([]); }); + test('initializeParticipantSession reads modes only once for a new participant', async () => { + const getModesSpy = vi.spyOn(storageEngine, 'getModes'); + + await storageEngine.initializeParticipantSession({}, configSimple, participantMetadata); + + expect(getModesSpy).toHaveBeenCalledTimes(1); + }); + + test('initializeParticipantSession starts deferred initial participant writes immediately in the background', async () => { + const deferredStorageEngine = new DeferredInitialWriteLocalStorageEngine(true); + await deferredStorageEngine.connect(); + await deferredStorageEngine.initializeStudyDb(studyId); + await deferredStorageEngine.setSequenceArray(sequenceArray); + deferredStorageEngine.holdNextParticipantDataWrite(); + + const participantSessionPromise = deferredStorageEngine.initializeParticipantSession({}, configSimple, participantMetadata); + + await deferredStorageEngine.waitForHeldParticipantDataWrite(); + + const participantSession = await participantSessionPromise; + + const observerEngine = new LocalStorageEngine(true); + await observerEngine.connect(); + await observerEngine.initializeStudyDb(studyId); + + expect(await observerEngine.getParticipantData(participantSession.participantId)).toBeNull(); + + deferredStorageEngine.releaseHeldParticipantDataWrite(); + await deferredStorageEngine.flushPendingParticipantData(); + + const persistedParticipantData = await observerEngine.getParticipantData(participantSession.participantId); + expect(persistedParticipantData).toBeDefined(); + expect(persistedParticipantData?.participantId).toBe(participantSession.participantId); + + // @ts-expect-error using protected method for testing + await deferredStorageEngine._testingReset(studyId); + // @ts-expect-error using protected method for testing + await observerEngine._testingReset(studyId); + }); + test('initializeParticipantSession sets conditions from searchParams condition', async () => { const participantSession = await storageEngine.initializeParticipantSession({ condition: 'color' }, configSimple, participantMetadata); @@ -445,6 +520,51 @@ describe.each([ expect(Object.hasOwn(sequenceAssignment!, 'conditions')).toBe(false); }); + test('updateParticipantMetadata updates participant metadata', async () => { + const participantSession = await storageEngine.initializeParticipantSession({}, configSimple, participantMetadata); + const updatedMetadata = { + ...participantMetadata, + ip: '8.8.8.8', + }; + + await storageEngine.updateParticipantMetadata(updatedMetadata); + + expect(storageEngine.getCurrentParticipantDataSnapshot()!.metadata).toEqual(updatedMetadata); + + await storageEngine.flushPendingParticipantData(); + + const participantData = await storageEngine.getParticipantData(participantSession.participantId); + expect(participantData).toBeDefined(); + expect(participantData!.metadata).toEqual(updatedMetadata); + }); + + test('updateParticipantMetadata stays local when data collection is disabled', async () => { + await storageEngine.getModes(studyId); + await storageEngine.setMode(studyId, 'dataCollectionEnabled', false); + + const participantSession = await storageEngine.initializeParticipantSession({}, configSimple, participantMetadata); + const updatedMetadata = { + ...participantMetadata, + ip: '8.8.8.8', + }; + + const pushSpy = vi.spyOn( + storageEngine as unknown as { + _pushToStorage: StorageEngine['_pushToStorage']; + }, + '_pushToStorage', + ); + + await storageEngine.updateParticipantMetadata(updatedMetadata); + + expect(pushSpy).not.toHaveBeenCalled(); + expect(storageEngine.getCurrentParticipantDataSnapshot()!.metadata).toEqual(updatedMetadata); + + const participantData = await storageEngine.getParticipantData(participantSession.participantId); + expect(participantData).toBeDefined(); + expect(participantData!.metadata).toEqual(updatedMetadata); + }); + test('getConditionData includes default when participants have no explicit condition', async () => { await storageEngine.initializeParticipantSession({}, configSimple, participantMetadata); await storageEngine.clearCurrentParticipantId(); diff --git a/src/store/hooks/useAuth.tsx b/src/store/hooks/useAuth.tsx index 8d9370d751..f8daca8d45 100644 --- a/src/store/hooks/useAuth.tsx +++ b/src/store/hooks/useAuth.tsx @@ -4,6 +4,7 @@ import { useCallback, } from 'react'; import { LoadingOverlay } from '@mantine/core'; +import { useLocation, useMatch } from 'react-router'; import { useStorageEngine } from '../../storage/storageEngineHooks'; import { StoredUser, UserWrapped } from '../../storage/engines/types'; import { isCloudStorageEngine } from '../../storage/engines/utils/storageEngineHelpers'; @@ -65,6 +66,8 @@ export function AuthProvider({ children } : { children: ReactNode }) { const [user, setUser] = useState(loadingNullUser); const [enableAuthTrigger, setEnableAuthTrigger] = useState(false); const { storageEngine } = useStorageEngine(); + const location = useLocation(); + const studyRouteMatch = useMatch('/:studyId/*'); // Logs the user out by removing the user and navigating to '/login' const logout = async () => { @@ -166,9 +169,11 @@ export function AuthProvider({ children } : { children: ReactNode }) { // eslint-disable-next-line react-hooks/exhaustive-deps }), [user]); + const allowChildrenWhileDeterminingStatus = Boolean(studyRouteMatch) && !location.pathname.startsWith('/analysis'); + return ( - {user.determiningStatus ? : children } + {user.determiningStatus && !allowChildrenWhileDeterminingStatus ? : children } ); } diff --git a/src/store/store.tsx b/src/store/store.tsx index 1a4072dfd4..97992313b9 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -146,6 +146,9 @@ export async function studyStoreCreator( setConfig(state, { payload }: PayloadAction) { state.config = payload; }, + setMetadata(state, { payload }: PayloadAction) { + state.metadata = payload; + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any pushToFuncSequence(state, { payload }: PayloadAction<{ component: string, funcName: string, index: number, funcIndex: number, parameters: Record | undefined, correctAnswer: Answer[] | undefined }>) { if (!state.funcSequence[payload.funcName]) {