Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add access to applet for prolific respondent #572

Merged
merged 6 commits into from
Feb 27, 2025
Merged
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
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,4 @@ export default [
},
},
eslintConfigPrettier,
];
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ProlificService, QueryOptions, ReturnAwaited, useBaseQuery } from '~/shared/api';

type FetchFn = typeof ProlificService.getStudyCompletionCodes;
type Options<TData> = QueryOptions<FetchFn, TData>;

type Params = {
appletId: string;
studyId: string;
};

export const useProlificCompletionCodeQuery = <TData = ReturnAwaited<FetchFn>>(
params: Params,
options?: Options<TData>,
) => {
return useBaseQuery(
['prolificCompletionCode', params],
() => ProlificService.getStudyCompletionCodes(params.appletId, params.studyId),
options,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ProlificService, QueryOptions, ReturnAwaited, useBaseQuery } from '~/shared/api';

type FetchFn = typeof ProlificService.isProlificStudyValidated;
type Options<TData> = QueryOptions<FetchFn, TData>;

type PublicParams = {
isPublic: true;
publicAppletKey: string;
};

type PrivateParams = {
isPublic: false;
appletId: string;
};

export const useProlificStudyStateQuery = <TData = ReturnAwaited<FetchFn>>(
params: (PublicParams | PrivateParams) & { prolificStudyId: null | string },
options?: Options<TData>,
) => {
return useBaseQuery(
['prolificStudyValidated', params],
() => {
if (params.isPublic) {
return ProlificService.isProlificStudyValidated(
params.publicAppletKey,
params.prolificStudyId ?? undefined,
);
}
return ProlificService.isProlificStudyValidated(params.appletId);
},
options,
);
};
20 changes: 20 additions & 0 deletions src/entities/applet/api/integrations/useProlificUserExistsQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ProlificService, QueryOptions, ReturnAwaited, useBaseQuery } from '~/shared/api';

type FetchFn = typeof ProlificService.prolificUserExists;
type Options<TData> = QueryOptions<FetchFn, TData>;

type Params = {
prolificPid: string;
studyId: string;
};

export const useProlificUserExistsQuery = <TData = ReturnAwaited<FetchFn>>(
params: Params,
options?: Options<TData>,
) => {
return useBaseQuery(
['prolificUserExists', params],
() => ProlificService.prolificUserExists(params.prolificPid, params.studyId),
options,
);
};
53 changes: 52 additions & 1 deletion src/entities/applet/model/hooks/useEntityComplete.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { useCallback } from 'react';

import { t } from 'i18next';
import type { NavigateOptions } from 'react-router/dist/lib/context';

import { useUpdateProlificParams } from './useSaveProlificParams';

import { ActivityPipelineType } from '~/abstract/lib';
import { appletModel } from '~/entities/applet';
import { useProlificCompletionCodeQuery } from '~/entities/applet/api/integrations/useProlificCompletionCodeQuery';
import { prolificParamsSelector } from '~/entities/applet/model/selectors';
import { useBanners } from '~/entities/banner/model';
import { ActivityFlowDTO } from '~/shared/api';
import ROUTES from '~/shared/constants/routes';
import { useCustomNavigation } from '~/shared/utils';
import { useAppSelector, useCustomNavigation } from '~/shared/utils';

type CompletionType = 'regular' | 'autoCompletion';

Expand Down Expand Up @@ -36,6 +42,22 @@ export const useEntityComplete = (props: Props) => {
const { entityCompleted, flowUpdated, getGroupProgress } =
appletModel.hooks.useGroupProgressStateManager();

const prolificParams = useAppSelector(prolificParamsSelector);
const { data: completionCodesReponse, isError: isCompletionCodesReponseError } =
useProlificCompletionCodeQuery(
{
appletId: props.appletId,
studyId: prolificParams?.studyId ?? '',
},
{
enabled: !!prolificParams,
},
);

const { addErrorBanner } = useBanners();

const { clearProlificParams } = useUpdateProlificParams();

const completeEntityAndRedirect = useCallback(
(completionType: CompletionType) => {
entityCompleted({
Expand All @@ -50,6 +72,30 @@ export const useEntityComplete = (props: Props) => {
return;
}

if (prolificParams && props.publicAppletKey) {
if (!isCompletionCodesReponseError && completionCodesReponse) {
clearProlificParams(); // Resetting redux state after completion

const { completionCodes } = completionCodesReponse.data;
for (const code of completionCodes) {
if (code.codeType === 'COMPLETED') {
window.location.replace(
`https://app.prolific.com/submissions/complete?cc=${code.code}`,
);
return;
}
}

addErrorBanner({ children: t('prolific.nocode'), duration: null });
return navigator.navigate(
ROUTES.publicJoin.navigateTo(props.publicAppletKey, prolificParams),
{
replace: true,
},
);
}
}

if (props.publicAppletKey) {
return navigator.navigate(ROUTES.publicJoin.navigateTo(props.publicAppletKey), {
replace: true,
Expand All @@ -70,6 +116,11 @@ export const useEntityComplete = (props: Props) => {
entityCompleted,
isInMultiInformantFlow,
navigator,
prolificParams,
completionCodesReponse,
isCompletionCodesReponseError,
clearProlificParams,
addErrorBanner,
props.activityId,
props.appletId,
props.eventId,
Expand Down
23 changes: 23 additions & 0 deletions src/entities/applet/model/hooks/useSaveProlificParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useCallback } from 'react';

import { actions } from '../slice';
import type { ProlificUrlParamsPayload } from '../types';

import { useAppDispatch } from '~/shared/utils';

export const useUpdateProlificParams = () => {
const dispatch = useAppDispatch();

const saveProlificParams = useCallback(
(payload: ProlificUrlParamsPayload) => {
dispatch(actions.saveProlificParams(payload));
},
[dispatch],
);

const clearProlificParams = useCallback(() => {
dispatch(actions.clearProlificParams());
}, [dispatch]);

return { saveProlificParams, clearProlificParams };
};
5 changes: 5 additions & 0 deletions src/entities/applet/model/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ export const multiInformantStateSelector = createSelector(
(state) => state.multiInformantState,
);

export const prolificParamsSelector = createSelector(
appletsSelector,
(state) => state.prolificParams,
);

export const selectConsents = createSelector(appletsSelector, (applets) => applets.consents);
10 changes: 10 additions & 0 deletions src/entities/applet/model/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
UpdateUserEventByIndexPayload,
RemoveGroupProgressPayload,
SaveSummaryDataInContext,
ProlificUrlParamsPayload,
} from './types';

import {
Expand All @@ -41,6 +42,7 @@ type InitialState = {
completions: CompletedEventEntities;
multiInformantState: MultiInformantState;
consents: ActivityConsents;
prolificParams?: ProlificUrlParamsPayload;
};

const initialState: InitialState = {
Expand Down Expand Up @@ -371,6 +373,14 @@ const appletsSlice = createSlice({

consents.shareMediaToPublic = !currentValue;
},

saveProlificParams: (state, action: PayloadAction<ProlificUrlParamsPayload>) => {
state.prolificParams = action.payload;
},

clearProlificParams: (state) => {
state.prolificParams = undefined;
},
},
});

Expand Down
6 changes: 6 additions & 0 deletions src/entities/applet/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ export type UpdateUserEventByIndexPayload = {
userEvent: UserEvent;
};

export type ProlificUrlParamsPayload = {
prolificPid: string;
studyId: string;
sessionId: string;
};

export type SupportableActivities = Record<string, boolean>;

export type CompletedEntitiesState = Record<string, number>;
Expand Down
4 changes: 4 additions & 0 deletions src/features/PassSurvey/hooks/useAnswers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SurveyContext } from '../lib';
import AnswersConstructService from '../model/AnswersConstructService';

import { appletModel } from '~/entities/applet';
import { ProlificUrlParamsPayload as ProlificUrlParams } from '~/entities/applet/model';
import { ActivityFlowDTO, AnswerPayload, EncryptionDTO, ScheduleEventDto } from '~/shared/api';

export type BuildAnswerParams = {
Expand All @@ -19,6 +20,8 @@ export type BuildAnswerParams = {
publicAppletKey?: string | null;
encryption?: EncryptionDTO | null;
flow?: ActivityFlowDTO | null;

prolificParams?: ProlificUrlParams;
};

export interface AnswerBuilder {
Expand Down Expand Up @@ -61,6 +64,7 @@ export const useAnswerBuilder = (): AnswerBuilder => {
encryption,
publicAppletKey: params.publicAppletKey ?? context.publicAppletKey,
isFlowCompleted: params.isFlowCompleted,
prolificParams: params.prolificParams,
});

const answer = answerConstructService.construct();
Expand Down
13 changes: 11 additions & 2 deletions src/features/PassSurvey/hooks/useSubmitAnswersMutation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from 'react';

import { AxiosResponse } from 'axios';
import { AxiosError, AxiosResponse } from 'axios';

import { SurveyContext } from '..';

Expand All @@ -20,9 +20,10 @@ import {
type Props = {
isPublic: boolean;
onSubmitSuccess?: (variables: AnswerPayload) => void;
onSubmitError?: () => void;
};

export const useSubmitAnswersMutations = ({ isPublic, onSubmitSuccess }: Props) => {
export const useSubmitAnswersMutations = ({ isPublic, onSubmitSuccess, onSubmitError }: Props) => {
const { isInMultiInformantFlow, getMultiInformantState, updateMultiInformantState } =
appletModel.hooks.useMultiInformantState();

Expand Down Expand Up @@ -71,6 +72,14 @@ export const useSubmitAnswersMutations = ({ isPublic, onSubmitSuccess }: Props)
mutateAsync: publicSubmitAsync,
} = usePublicSaveAnswerMutation({
onSuccess,
onError: (error) => {
if (error instanceof AxiosError && error.response.status === 400) {
onSubmitError?.();
return;
} else if (error instanceof Error) {
throw error;
}
},
});

return {
Expand Down
7 changes: 7 additions & 0 deletions src/features/PassSurvey/model/AnswersConstructService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ItemAnswer, mapAlerts, mapToAnswers } from '../helpers';
import { ActivityPipelineType, GroupProgress } from '~/abstract/lib';
import { DefaultAnswer } from '~/entities/activity';
import { appletModel } from '~/entities/applet';
import { ProlificUrlParamsPayload } from '~/entities/applet/model';
import { userModel } from '~/entities/user';
import {
ActivityFlowDTO,
Expand Down Expand Up @@ -48,6 +49,8 @@ type Input = {
publicAppletKey: string | null;

isFlowCompleted?: boolean;

prolificParams?: ProlificUrlParamsPayload;
};

export default class AnswersConstructService implements ICompletionConstructService {
Expand All @@ -73,6 +76,8 @@ export default class AnswersConstructService implements ICompletionConstructServ

private isFlowCompleted: boolean | undefined; // We can enforce this to be defined if needed

private prolificParams?: ProlificUrlParamsPayload;

constructor(input: Input) {
this.activityId = input.activityId;
this.event = input.event;
Expand All @@ -85,6 +90,7 @@ export default class AnswersConstructService implements ICompletionConstructServ
this.items = input.items;
this.userEvents = input.userEvents;
this.isFlowCompleted = input.isFlowCompleted;
this.prolificParams = input.prolificParams;
}

public construct(): AnswerPayload {
Expand Down Expand Up @@ -116,6 +122,7 @@ export default class AnswersConstructService implements ICompletionConstructServ
version: this.appletVersion,
createdAt: new Date().getTime(),
isFlowCompleted: isSurveyCompleted,
prolificParams: this.prolificParams,
answer: {
answer: encryptedAnswers,
itemIds: answersDictionary.itemIds,
Expand Down
5 changes: 4 additions & 1 deletion src/features/SaveAssessmentAndExit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import ButtonBase from '@mui/material/ButtonBase';

import { SurveyContext } from '../PassSurvey';

import { prolificParamsSelector } from '~/entities/applet/model/selectors';
import { ROUTES, Theme } from '~/shared/constants';
import Text from '~/shared/ui/Text';
import {
addSurveyPropsToEvent,
Mixpanel,
MixpanelEventType,
useAppSelector,
useCustomNavigation,
useCustomTranslation,
} from '~/shared/utils';
Expand All @@ -24,6 +26,7 @@ export const SaveAndExitButton = ({ appletId, publicAppletKey }: Props) => {
const { applet, activityId, flow } = useContext(SurveyContext);

const navigator = useCustomNavigation();
const prolificParams = useAppSelector(prolificParamsSelector);

const onSaveAndExitClick = () => {
Mixpanel.track(
Expand All @@ -35,7 +38,7 @@ export const SaveAndExitButton = ({ appletId, publicAppletKey }: Props) => {

return navigator.navigate(
publicAppletKey
? ROUTES.publicJoin.navigateTo(publicAppletKey)
? ROUTES.publicJoin.navigateTo(publicAppletKey, prolificParams)
: ROUTES.appletDetails.navigateTo(appletId),
);
};
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/el/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,10 @@
"timeRemaining": "Απομένει(-ουν) {{time}}",
"forItem": "για το στοιχείο αυτό",
"noApplets": "Δεν υπάρχουν βοηθητικές εφαρμογές",
"noActivities": "Δεν υπάρχουν διαθέσιμες δραστηριότητες για να ολοκληρώσετε αυτήν τη στιγμή"
"noActivities": "Δεν υπάρχουν διαθέσιμες δραστηριότητες για να ολοκληρώσετε αυτήν τη στιγμή",
"prolific": {
"nocode": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση του κωδικού ολοκλήρωσης. Παρακαλώ χρησιμοποιήστε την πολιτική NOCODE στο Prolific.",
"alreadyAnswered": "Έχετε ήδη απαντήσει σε αυτήν τη δραστηριότητα. Παρακαλώ επιστρέψτε στο Prolific και υποβάλετε τον κωδικό ολοκλήρωσής σας."
}
}
}
Loading