Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ module.exports = {
pattern: '@(~app|~shared|~features|~pages|~entities)/**',
group: 'internal',
},
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use absolute import aliases instead

pattern: '~/lib/**',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merger with rule above

group: 'internal',
},
],
pathGroupsExcludedImportTypes: ['internal', 'react'],
'newlines-between': 'always',
Expand Down
2 changes: 1 addition & 1 deletion src/entities/activity/ui/ActivityCardItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useMemo } from 'react';

import { AdditionalTextResponse } from './AdditionalTextResponse';
import { Answer, hasAdditionalResponse, requiresAdditionalResponse } from '../lib';
import { ItemPicker } from './items/ItemPicker';
import { Answer, hasAdditionalResponse, requiresAdditionalResponse } from '../lib';

import { appletModel } from '~/entities/applet';
import { SliderAnimation } from '~/shared/animations';
Expand Down
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.isProlificIntergrationEnabled;
type Options<TData> = QueryOptions<FetchFn, TData>;

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

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

export const useProlificIntegrationStateQuery = <TData = ReturnAwaited<FetchFn>>(
params: (PublicParams | PrivateParams) & { prolificStudyId: null | string },
options?: Options<TData>,
) => {
return useBaseQuery(
['prolificIntegrationEnabled', params],
() => {
Comment thread
elierotenberg marked this conversation as resolved.
if (params.isPublic) {
return ProlificService.isProlificIntergrationEnabled(
params.publicAppletKey,
params.prolificStudyId ?? undefined,
);
}
return ProlificService.isProlificIntergrationEnabled(params.appletId);
},
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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move up for of

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?.();
Comment thread
elierotenberg marked this conversation as resolved.
return;
}
Comment thread
elierotenberg marked this conversation as resolved.

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 @@ -330,6 +330,10 @@
"privacy": "Απόρρητο",
"termsOfService": "Όροι"
},
"noActivities": "Δεν υπάρχουν διαθέσιμες δραστηριότητες για να ολοκληρώσετε αυτήν τη στιγμή"
"noActivities": "Δεν υπάρχουν διαθέσιμες δραστηριότητες για να ολοκληρώσετε αυτήν τη στιγμή",
"prolific": {
"nocode": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση του κωδικού ολοκλήρωσης. Παρακαλώ χρησιμοποιήστε την πολιτική NOCODE στο Prolific.",
"alreadyAnswered": "Έχετε ήδη απαντήσει σε αυτήν τη δραστηριότητα. Παρακαλώ επιστρέψτε στο Prolific και υποβάλετε τον κωδικό ολοκλήρωσής σας."
}
}
}
6 changes: 5 additions & 1 deletion src/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@
"timeRemaining": "{{time}} remaining",
"forItem": "for this item",
"noApplets": "No applets",
"noActivities": "No activities are available for you to complete right now"
"noActivities": "No activities are available for you to complete right now",
Comment thread
elierotenberg marked this conversation as resolved.
"prolific": {
"nocode": "An error occurred while retrieving the completion code. Please use the NOCODE policy in Prolific.",
"alreadyAnswered": "You have already answered to this activity. Please return to Prolific and submit your completion code."
}
}
}
Loading