Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions src/GlobalConfigParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,69 @@ async function fetchGlobalConfigArray() {
return parseGlobalConfig(configs);
}

function isHomeRoute() {
const pathname = window.location.pathname.replace(/\/+$/, '') || '/';
const basePath = PREFIX.replace(/\/+$/, '') || '/';

return pathname === basePath || pathname === `${basePath}/`;
}

export function GlobalConfigParser() {
const [globalConfig, setGlobalConfig] = useState<Nullable<GlobalConfig>>(null);
const [studyConfigs, setStudyConfigs] = useState<Record<string, ParsedConfig<StudyConfig> | null>>({});

useEffect(() => {
async function fetchData() {
if (globalConfig) {
setStudyConfigs(await fetchStudyConfigs(globalConfig));
if (!globalConfig) {
return undefined;
}

if (!isHomeRoute()) {
setStudyConfigs({});
return undefined;
}

let cancelled = false;

async function fetchData(currentGlobalConfig: GlobalConfig) {
const configs = await fetchStudyConfigs(currentGlobalConfig);
if (!cancelled) {
setStudyConfigs(configs);
}
}
fetchData();

fetchData(globalConfig);

return () => {
cancelled = true;
};
}, [globalConfig]);

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) => {
Expand Down
191 changes: 135 additions & 56 deletions src/components/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,40 @@ import {
resolveParticipantConditions,
} from '../utils/handleConditionLogic';

function createParticipantMetadata(ip: string = ''): ParticipantMetadata {
return {
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,
};
}

function createEmptyParticipantMetadata(): ParticipantMetadata {
return {
language: '',
userAgent: '',
resolution: {
width: 0,
height: 0,
availHeight: 0,
availWidth: 0,
colorDepth: 0,
orientation: '',
pixelDepth: 0,
},
ip: '',
};
}

export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) {
// Pull study config
const routeStudyId = useStudyId();
Expand All @@ -53,19 +87,27 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) {

useEffect(() => {
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();
}
};

Expand All @@ -77,7 +119,8 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) {
window.removeEventListener('message', messageListener);
};
}
return () => { };

return undefined;
}, [globalConfig, routeStudyId]);

const [routes, setRoutes] = useState<RouteObject[]>([]);
Expand All @@ -89,23 +132,43 @@ 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;

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
Expand All @@ -114,33 +177,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,
);

Expand All @@ -157,42 +204,75 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) {
conditions: studyCondition,
};
}
const activeHash = await hash(JSON.stringify(activeConfig));

let participantConfig = activeConfig;

if (participantSession.participantConfigHash !== activeHash) {
participantConfig = (await storageEngine.getAllConfigsFromHash([participantSession.participantConfigHash], canonicalStudyId))[participantSession.participantConfigHash] as ParsedConfig<StudyConfig>;
}

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 = await 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);

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);
});

storageEngine.getParticipantCompletionStatus(participantSession.participantId).then((participantCompleted) => {
if (!isCancelled) {
newStore.store.dispatch(newStore.actions.setParticipantCompleted(participantCompleted));
}
}).catch((error) => {
console.error('Error fetching participant completion status:', error);
});
} 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(
const fallbackSequence = await filterSequenceByCondition(
matchingSequence,
studyCondition,
);
Expand All @@ -201,29 +281,25 @@ 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);
}

if (isCancelled) {
return;
}

// Initialize the routing
setRoutes([
{
Expand Down Expand Up @@ -255,6 +331,9 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) {
]);
}
initializeUserStoreRouting();
return () => {
isCancelled = true;
};
}, [storageEngine, activeConfig, canonicalStudyId, searchParams, participantId, studyCondition]);

const routing = useRoutes(routes);
Expand Down
Loading
Loading