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

[JN-1486] Referral source + skip pre-enroll + pre-fill pre-enroll #1280

Open
wants to merge 10 commits into
base: development
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ public HubResponse enrollGovernedUser(
EnvironmentName envName,
String studyShortcode,
UUID preEnrollmentId,
UUID governedPpUserId // could be null if a totally new user
) {
UUID governedPpUserId) { // could be null if a totally new user

PortalParticipantUser portalParticipantUser =
authUtilService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ public class PreEnrollmentResponse extends BaseEntity {
private String fullData;
@Builder.Default
private boolean qualified = false; // whether or not the responses meet the criteria for eligibility.
private String referralSource;
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public PreEnrollmentResponse createAnonymousPreEnroll(
.surveyId(survey.getId())
.qualified(parsedResponse.isQualified())
.fullData(objectMapper.writeValueAsString(parsedResponse.getAnswers()))
.referralSource(parsedResponse.getReferralSource())
.studyEnvironmentId(studyEnvironmentId).build();
return preEnrollmentResponseDao.create(response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
databaseChangeLog:
- changeSet:
id: "pre_enroll_referral_source"
author: mbemis
changes:
- addColumn:
tableName: pre_enrollment_response
columns:
- column: { name: referral_source, type: text }
3 changes: 3 additions & 0 deletions core/src/main/resources/db/changelog/db.changelog-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ databaseChangeLog:
- include:
file: changesets/2024_11_22_minimal_navbar.yaml
relativeToChangelogFile: true
- include:
file: changesets/2024_12_02_pre_enroll_referral_source.yaml
relativeToChangelogFile: true


# README: it is a best practice to put each DDL statement in its own change set. DDL statements
Expand Down
19 changes: 16 additions & 3 deletions ui-admin/src/study/participants/survey/PreEnrollmentView.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import React from 'react'
import SurveyFullDataView from './SurveyFullDataView'
import { Answer, PreregistrationResponse, Survey } from 'api/api'
import { Answer, Survey } from 'api/api'
import { StudyEnvContextT } from 'study/StudyEnvironmentRouter'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleNodes } from '@fortawesome/free-solid-svg-icons'
import { PreEnrollmentResponse, ReferralSource } from '@juniper/ui-core'

/** show a preEnrollment response */
export default function PreEnrollmentView({ studyEnvContext, preEnrollResponse, preEnrollSurvey }: {
studyEnvContext: StudyEnvContextT, preEnrollResponse?: PreregistrationResponse, preEnrollSurvey: Survey
studyEnvContext: StudyEnvContextT, preEnrollResponse?: PreEnrollmentResponse, preEnrollSurvey: Survey
}) {
if (!preEnrollResponse) {
return <span className="text-muted fst-italic"> no pre-enrollment data</span>
}
const answers: Answer[] = JSON.parse(preEnrollResponse.fullData)

let parsedReferralSource
if (preEnrollResponse.referralSource) {
parsedReferralSource = JSON.parse(preEnrollResponse.referralSource) as ReferralSource
}

return <div>
<h5>Pre-enrollment response</h5>
{parsedReferralSource && <div className="border p-3 rounded-3 my-3">
<span><FontAwesomeIcon className={'fa-lg'} icon={faCircleNodes}/> This participant was referred by </span>
<code>{parsedReferralSource.referringSite}</code>
</div>}
{preEnrollResponse &&
<SurveyFullDataView answers={answers} survey={preEnrollSurvey} studyEnvContext={studyEnvContext}/>
<SurveyFullDataView answers={answers} survey={preEnrollSurvey} studyEnvContext={studyEnvContext}/>
}
</div>
}
5 changes: 5 additions & 0 deletions ui-core/src/types/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,17 @@ export type PreregistrationResponse = FormResponse & {
qualified: boolean
}

export type ReferralSource = {
referringSite: string
}

export type PreEnrollmentResponse = FormResponse & {
surveyId: string
studyEnvironmentId: string
answers: Answer[]
fullData: string
qualified: boolean
referralSource?: string
}

// Survey configuration
Expand Down
3 changes: 2 additions & 1 deletion ui-core/src/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { KitRequest } from 'src/types/kits'
import { ParticipantTask } from 'src/types/task'
import {
PreEnrollmentResponse,
PreregistrationResponse,
SurveyResponse
} from 'src/types/forms'
Expand Down Expand Up @@ -45,7 +46,7 @@ export type Enrollee = {
relations?: EnrolleeRelation[]
participantUserId: string
preRegResponse?: PreregistrationResponse
preEnrollmentResponse?: PreregistrationResponse
preEnrollmentResponse?: PreEnrollmentResponse
profile: Profile
profileId: string
shortcode: string
Expand Down
1 change: 0 additions & 1 deletion ui-participant/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const ParticipantTermsOfUsePage = lazy(() => import('terms/ParticipantTermsOfUse
const ScrollToTop = () => {
const location = useLocation()
useEffect(() => {
// @ts-expect-error TS thinks "instant" isn't a valid scroll behavior.
Copy link
Member Author

Choose a reason for hiding this comment

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

IDE didn't like this anymore, probably thanks to a dependency upgrade?

window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
}, [location.pathname])
return null
Expand Down
12 changes: 7 additions & 5 deletions ui-participant/src/api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,9 @@ export default {
},

/** creates an enrollee for the signed-in user and study. */
async createEnrollee({ studyShortcode, preEnrollResponseId }:
{ studyShortcode: string, preEnrollResponseId: string | null }):
async createEnrollee({ studyShortcode, preEnrollResponseId }: {
studyShortcode: string, preEnrollResponseId: string | null
}):
Promise<HubResponse> {
const params = queryString.stringify({ preEnrollResponseId })
const url = `${baseStudyEnvUrl(false, studyShortcode)}/enrollee?${params}`
Expand All @@ -285,9 +286,10 @@ export default {
return await this.processJsonResponse(response)
},

async createGovernedEnrollee(
{ studyShortcode, preEnrollResponseId, governedPpUserId }
: { studyShortcode: string, preEnrollResponseId: string | null, governedPpUserId: string | null }
async createGovernedEnrollee({ studyShortcode, preEnrollResponseId, governedPpUserId }: {
studyShortcode: string, preEnrollResponseId: string | null,
governedPpUserId: string | null
}
): Promise<HubResponse> {
const params = queryString.stringify({ preEnrollResponseId, governedPpUserId })
const url = `${baseStudyEnvUrl(false, studyShortcode)}/enrollee?${params}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export default function PortalRegistrationRouter({
setPreRegResponseId(preRegId)
}


useEffect(() => {
// if there's a preRegResponseId on initial load (because it was in local storage) validate it and then redirect
if (preRegResponseId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const registrationSurveyModel: Survey = {
createdAt: now,
lastUpdatedAt: now,
content: JSON.stringify(registrationSurvey),
surveyType: 'RESEARCH'
surveyType: 'RESEARCH',
recurrenceType: 'NONE'
}

/**
Expand Down
1 change: 0 additions & 1 deletion ui-participant/src/login/RedirectFromOAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export const RedirectFromOAuth = () => {

const defaultEnrollStudy = findDefaultEnrollmentStudy(returnToStudy, portal.portalStudies)


useEffect(() => {
const handleRedirectFromOauth = async () => {
// RedirectFromOAuth may be rendered before react-oidc-context's AuthProvider has finished doing its thing. If so,
Expand Down
6 changes: 6 additions & 0 deletions ui-participant/src/providers/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@juniper/ui-core'
import envVars from 'util/envVars'
import mixpanel from 'mixpanel-browser'
import { useEnrollmentParams } from '../studies/enroll/useEnrollmentParams'

/**
* The user provide contains the _raw_ user context, which is more or less directly derived
Expand Down Expand Up @@ -76,6 +77,11 @@ export default function UserProvider({ children }: { children: React.ReactNode }
const [loginState, setLoginState] = useState<LoginResult | null>(null)
const [isLoading, setIsLoading] = useState(true)
const auth = useAuth()
const { captureEnrollmentParams } = useEnrollmentParams()

useEffect(() => {
captureEnrollmentParams()
}, [])

/**
* Sign in to the UI based on the result of signing in to the API.
Expand Down
13 changes: 11 additions & 2 deletions ui-participant/src/studies/enroll/PreEnroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { useNavigate } from 'react-router-dom'
import { StudyEnrollContext } from './StudyEnrollRouter'
import {
getResumeData,
getSurveyJsAnswerList,
getSurveyJsAnswerList, makeSurveyJsData,
SurveyAutoCompleteButton,
SurveyReviewModeButton,
useI18n,
useSurveyJSModel
} from '@juniper/ui-core'
import { useUser } from '../../providers/UserProvider'
import { useActiveUser } from '../../providers/ActiveUserProvider'
import { useEnrollmentParams } from './useEnrollmentParams'

/**
* pre-enrollment surveys are expected to have a calculated value that indicates
Expand All @@ -33,9 +34,13 @@ export default function PreEnrollView({ enrollContext, survey }:
const navigate = useNavigate()
// for now, we assume all pre-screeners are a single page
const pager = { pageNumber: 0, updatePageNumber: () => 0 }

const { preFilledAnswers, referralSource, clearStoredEnrollmentParams } = useEnrollmentParams()
const resumeData = makeSurveyJsData(undefined, preFilledAnswers, user?.id)

const { surveyModel, refreshSurvey, SurveyComponent } = useSurveyJSModel(
survey,
null,
resumeData,
handleComplete,
pager,
studyEnv.environmentName,
Expand All @@ -58,6 +63,7 @@ export default function PreEnrollView({ enrollContext, survey }:
answers: getSurveyJsAnswerList(surveyModel, selectedLanguage),
surveyId: survey.id,
studyEnvironmentId: studyEnv.id,
referralSource: referralSource || undefined,
qualified
}

Expand All @@ -67,6 +73,9 @@ export default function PreEnrollView({ enrollContext, survey }:
surveyVersion: survey.version,
preEnrollResponse: responseDto
}).then(result => {
//clear the stored enrollment params in case someone else uses the same browser
//without closing the tab
clearStoredEnrollmentParams()
if (!qualified) {
navigate('../ineligible')
} else {
Expand Down
11 changes: 6 additions & 5 deletions ui-participant/src/studies/enroll/StudyEnrollRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
Route,
Routes,
useNavigate,
useParams,
useSearchParams
useParams, useSearchParams
} from 'react-router-dom'
import { usePortalEnv } from 'providers/PortalProvider'
import Api, {
Expand Down Expand Up @@ -39,6 +38,7 @@ import {
} from 'util/enrolleeUtils'
import { logError } from 'util/loggingUtils'
import { getNextConsentTask, getTaskPath } from 'hub/task/taskUtils'
import { useEnrollmentParams } from './useEnrollmentParams'

export type StudyEnrollContext = {
user: ParticipantUser | null,
Expand Down Expand Up @@ -83,8 +83,7 @@ function StudyEnrollOutletMatched(props: StudyEnrollOutletMatchedProps) {
const { i18n } = useI18n()

const [searchParams] = useSearchParams()
const isProxyEnrollment = searchParams.get('isProxyEnrollment') === 'true'
const ppUserId = searchParams.get('ppUserId')
const { skipPreEnroll, isProxyEnrollment, ppUserId } = useEnrollmentParams()

const { user, ppUsers, enrollees, refreshLoginState } = useUser()

Expand Down Expand Up @@ -146,7 +145,7 @@ function StudyEnrollOutletMatched(props: StudyEnrollOutletMatchedProps) {
if (mustProvidePassword) {
return
}
if (preEnrollSatisfied) {
if (preEnrollSatisfied || skipPreEnroll) {
if (!user) {
navigate('register', { replace: true })
} else {
Expand Down Expand Up @@ -202,6 +201,8 @@ function StudyEnrollOutletMatched(props: StudyEnrollOutletMatchedProps) {
/>
) : (
<Routes>
{skipPreEnroll &&
<Route path="preEnroll/*" element={<PortalRegistrationRouter portal={portal} returnTo={null}/>}/>}
{hasPreEnroll && <Route path="preEnroll" element={
<PreEnrollView enrollContext={enrollContext} survey={enrollContext.studyEnv.preEnrollSurvey as Survey}/>
}/>}
Expand Down
80 changes: 80 additions & 0 deletions ui-participant/src/studies/enroll/useEnrollmentParams.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Answer } from '@juniper/ui-core'

export function useEnrollmentParams() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is a great use of hooks

const [searchParams] = useSearchParams()
const [skipPreEnroll, setSkipPreEnroll] = useState(() => {
return sessionStorage.getItem('skipPreEnroll') === 'true'
})
const [referralSource, setReferralSource] = useState<string | null>(() => {
return sessionStorage.getItem('referralSource')
})
const [isProxyEnrollment, setIsProxyEnrollment] = useState(false)
const [ppUserId, setPpUserId] = useState<string | null>(null)
const [preFilledAnswers, setPreFilledAnswers] = useState<Answer[]>(
parsePreFilledAnswers(sessionStorage.getItem('preFilledAnswers')))

function clearStoredEnrollmentParams() {
sessionStorage.removeItem('skipPreEnroll')
sessionStorage.removeItem('referralSource')
sessionStorage.removeItem('preFilledAnswers')
}

function captureEnrollmentParams() {
const skipPreEnrollParam = searchParams.get('skipPreEnroll')
const referralSourceParam = searchParams.get('referralSource')
const proxyEnrollmentParam = searchParams.get('isProxyEnrollment')
const ppUserIdParam = searchParams.get('ppUserId')
const preFilledAnswersParam = searchParams.get('preFilledAnswers')

if (proxyEnrollmentParam === 'true') {
setIsProxyEnrollment(true)
}

if (ppUserIdParam) {
setPpUserId(ppUserIdParam)
}

// The following three params are stored in sessionStorage, so they persist across page reloads.
// We want to do our best to avoid losing any information coming from referring sites

if (skipPreEnrollParam === 'true') {
sessionStorage.setItem('skipPreEnroll', 'true')
setSkipPreEnroll(true)
}

if (referralSourceParam) {
sessionStorage.setItem('referralSource', referralSourceParam)
setReferralSource(referralSourceParam)
}

if (preFilledAnswersParam) {
const answers = parsePreFilledAnswers(preFilledAnswersParam)
sessionStorage.setItem('preFilledAnswers', JSON.stringify(answers))
setPreFilledAnswers(answers)
}
}

useEffect(() => {
captureEnrollmentParams()
}, [searchParams])

return {
skipPreEnroll, referralSource, isProxyEnrollment, ppUserId, preFilledAnswers,
captureEnrollmentParams, clearStoredEnrollmentParams
}
}

const parsePreFilledAnswers = (preFilledAnswers: string | null): Answer[] => {
if (!preFilledAnswers) {
return []
}

try {
return JSON.parse(preFilledAnswers) as Answer[]
} catch (error) {
console.error('Failed to parse preFilledAnswers:', error)
return []
}
}
Loading