Skip to content

Commit 73275fc

Browse files
gureckisclaude
andcommitted
feat: add SONA recruitment support with unpaid study config option
Add SONA (credit) and SONA (paid) as recruitment services with auto-credit granting via redirect URLs. Add `unpaidStudy` config option to switch informed consent text between payment and course credit language. Include dev tools toggle for unpaidStudy and document SONA setup in recruitment, configuration, and lab config docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 57a027b commit 73275fc

12 files changed

Lines changed: 353 additions & 4 deletions

File tree

docs/coding/configuration.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ api.setRuntimeConfig('windowsizerRequest', { width: 800, height: 600 })
7171
api.setRuntimeConfig('windowsizerAggressive', true)
7272

7373
api.setRuntimeConfig('anonymousMode', false)
74+
api.setRuntimeConfig('unpaidStudy', true) // for SONA credit or other unpaid studies
7475
api.setRuntimeConfig('labURL', 'https://gureckislab.org')
7576
api.setRuntimeConfig('brandLogoFn', 'universitylogo.png')
7677

@@ -228,6 +229,11 @@ means they are made available to the web application/experiment.
228229
deployed in anonymous mode. This mode is useful for submitting a link to the
229230
study with a paper submission. In this mode, all <SmileText/>-default
230231
references to the organization conducting the study are removed.
232+
- `unpaidStudy` is a runtime-only config option (set via
233+
`api.setRuntimeConfig('unpaidStudy', true)` in `design.js`) that switches the
234+
default informed consent text from payment language to course credit language.
235+
Defaults to `false`. Useful for SONA credit studies or any unpaid recruitment
236+
service. Can also be toggled in the developer tools sidebar.
231237
- `VITE_LAB_URL` is the URL of the lab website. This can be used to link to the
232238
lab website or university homepage so participants can learn more about the
233239
organization conducting the study.
@@ -267,12 +273,33 @@ VITE_FIREBASE_APPID = appid
267273
268274
# enter google analytics id
269275
VITE_GOOGLE_ANALYTICS = xxxx
276+
277+
# sona (credit)
278+
VITE_SONA_URL = ''
279+
VITE_SONA_EXPERIMENT_ID = ''
280+
VITE_SONA_CREDIT_TOKEN = ''
281+
282+
# sona (paid)
283+
VITE_SONA_PAID_URL = ''
284+
VITE_SONA_PAID_EXPERIMENT_ID = ''
285+
VITE_SONA_PAID_CREDIT_TOKEN = ''
270286
```
271287

272288
- There several `VITE_FIREBASE_` options for configuring Google's Firestore
273289
backend (see [data storage](/coding/datastorage) for more info).
274290
- `VITE_GOOGLE_ANALYTICS` is the Google Analytics ID for your experiment
275291
(optional)
292+
- `VITE_SONA_URL` is the base URL of your institution's SONA instance for
293+
credit-based studies (see [SONA recruitment](/recruit/recruitment#sona) for
294+
details)
295+
- `VITE_SONA_EXPERIMENT_ID` is the SONA experiment ID for credit-based studies
296+
- `VITE_SONA_CREDIT_TOKEN` is the SONA credit token for automatic credit
297+
granting
298+
- `VITE_SONA_PAID_URL` is the base URL of your institution's SONA instance for
299+
paid studies
300+
- `VITE_SONA_PAID_EXPERIMENT_ID` is the SONA experiment ID for paid studies
301+
- `VITE_SONA_PAID_CREDIT_TOKEN` is the SONA payment token for automatic payment
302+
granting
276303

277304
#### Code Version Options (`.env.github.local`)
278305

docs/labconfig.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,15 @@ VITE_FIREBASE_APPID = xxxx
365365
# enter google analytics id
366366
VITE_GOOGLE_ANALYTICS = xxxx
367367
368+
# sona (credit)
369+
VITE_SONA_URL = ''
370+
VITE_SONA_EXPERIMENT_ID = ''
371+
VITE_SONA_CREDIT_TOKEN = ''
372+
373+
# sona (paid)
374+
VITE_SONA_PAID_URL = ''
375+
VITE_SONA_PAID_EXPERIMENT_ID = ''
376+
VITE_SONA_PAID_CREDIT_TOKEN = ''
368377
```
369378

370379
You will want to replace the values for the entries that contain the word

docs/recruit/recruitment.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,111 @@ htt?ps://exps.gureckislab.org/e/note-useless-uncle#/welcome/mturk/assignmentId=1
233233
which loads slightly different content. You can customize aspects of this via
234234
the `recruitment/MTurkRecruitPage.vue` component.
235235

236+
## SONA
237+
238+
[SONA Systems](https://www.sona-systems.com) is a participant management
239+
platform commonly used at universities for recruiting research participants from
240+
subject pools. Participants typically receive course credit for their
241+
participation, though SONA also supports paid studies.
242+
243+
<SmileText /> supports both **SONA (credit)** and **SONA (paid)** as recruitment
244+
services.
245+
246+
### Configuration
247+
248+
SONA requires several environment variables to be set in your `env/.env.local`
249+
file. You will need to obtain these values from your SONA administrator or from
250+
your SONA experiment settings page.
251+
252+
```
253+
# sona (credit)
254+
VITE_SONA_URL = 'https://yourschool.sona-systems.com'
255+
VITE_SONA_EXPERIMENT_ID = 'your_experiment_id'
256+
VITE_SONA_CREDIT_TOKEN = 'your_credit_token'
257+
258+
# sona (paid)
259+
VITE_SONA_PAID_URL = 'https://yourschool.sona-systems.com'
260+
VITE_SONA_PAID_EXPERIMENT_ID = 'your_experiment_id'
261+
VITE_SONA_PAID_CREDIT_TOKEN = 'your_credit_token'
262+
```
263+
264+
You only need to fill in the set that matches your study type (credit or paid).
265+
After updating these values, run `npm run upload_config` to sync them to GitHub
266+
for deployment.
267+
268+
- `VITE_SONA_URL` / `VITE_SONA_PAID_URL` is the base URL of your institution's
269+
SONA instance (e.g., `https://yourschool.sona-systems.com`)
270+
- `VITE_SONA_EXPERIMENT_ID` / `VITE_SONA_PAID_EXPERIMENT_ID` is the experiment
271+
ID assigned by SONA when you create the study
272+
- `VITE_SONA_CREDIT_TOKEN` / `VITE_SONA_PAID_CREDIT_TOKEN` is the
273+
authentication token SONA provides for automatic credit/payment granting via
274+
web studies
275+
276+
### Setting up your study URL in SONA
277+
278+
When creating a web study in SONA, you need to provide the study URL. SONA will
279+
append a `survey_code` parameter that identifies each participant. Your study URL
280+
should follow this pattern:
281+
282+
For credit-based studies:
283+
```
284+
https://exps.gureckislab.org/e/note-useless-uncle/#/welcome/sona/?survey_code=%SURVEY_CODE%
285+
```
286+
287+
For paid studies:
288+
```
289+
https://exps.gureckislab.org/e/note-useless-uncle/#/welcome/sona_paid/?survey_code=%SURVEY_CODE%
290+
```
291+
292+
Replace `note-useless-uncle` with your project's code name. The `%SURVEY_CODE%`
293+
placeholder is automatically replaced by SONA with the participant's unique
294+
survey code.
295+
296+
### Informed consent for unpaid studies
297+
298+
SONA credit-based studies are unpaid, so the default informed consent language
299+
about monetary payment is inappropriate. To show course-credit-appropriate
300+
consent language, set the `unpaidStudy` runtime config option in your
301+
`design.js`:
302+
303+
```js
304+
api.setRuntimeConfig('unpaidStudy', true)
305+
```
306+
307+
This changes the compensation bullet in the default `InformedConsentText.vue`
308+
from payment language to course credit language. See the
309+
[configuration docs](/coding/configuration) for more on runtime config. You can
310+
also toggle this setting in the developer tools sidebar using the "Unpaid"
311+
switch.
312+
313+
::: tip
314+
The `unpaidStudy` option is not specific to SONA — it can be used for any
315+
recruitment service where participants are not paid (e.g., citizen science
316+
studies).
317+
:::
318+
319+
### How completion works
320+
321+
When a participant finishes the study, the thanks page automatically provides a
322+
button that redirects them back to SONA. This redirect URL includes the
323+
participant's survey code and your credit/payment token, so SONA can
324+
automatically grant credit or payment without any manual intervention.
325+
326+
For credit studies, the redirect goes to:
327+
```
328+
{SONA_URL}/webstudy_credit.aspx?experiment_id={ID}&credit_token={TOKEN}&survey_code={CODE}
329+
```
330+
331+
This means participants are credited immediately upon clicking the button — no
332+
completion codes to copy and paste.
333+
334+
### Testing in developer mode
335+
336+
In the developer tools sidebar, you can select "sona" or "sona_paid" from the
337+
**Service** dropdown to simulate SONA recruitment during development. This lets
338+
you test the full flow including the thanks/credit page without needing an
339+
actual SONA participant.
340+
236341
## Crowd-sourcing
237342

238343
In the future the lab might make a citizen science recruitment portal. To

env/.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ VITE_MAX_WRITES = 1000
3232
VITE_MIN_WRITE_INTERVAL = 2000
3333

3434
# stepper limit
35-
VITE_MAX_STEPS = 5000
35+
VITE_MAX_STEPS = 5000

src/builtins/thanks/ThanksView.vue

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,76 @@ onMounted(() => {
231231
</TitleTwoCol>
232232
</div>
233233

234+
<!-- SONA (credit) recruitment service completion -->
235+
<div v-if="api.getRecruitmentService() == 'sona'">
236+
<TitleTwoCol leftFirst leftWidth="w-1/3" :responsiveUI="api.config.responsiveUI">
237+
<template #title>
238+
<h1 class="text-3xl font-bold mb-4">
239+
<i-fa6-solid-square-check class="inline mr-2" />&nbsp;Thanks, let's record your credit!
240+
</h1>
241+
<p class="text-lg mb-8">
242+
Please click the button below to return to SONA and receive credit for completing this study. We really
243+
appreciate your time.
244+
</p>
245+
</template>
246+
<template #left>
247+
<div class="text-left text-muted-foreground">
248+
<h3 class="text-lg font-bold mb-2">Credit Process</h3>
249+
<p class="text-sm text-muted-foreground">
250+
Click the button to return to SONA and automatically receive credit for this study.
251+
</p>
252+
</div>
253+
</template>
254+
<template #right>
255+
<div class="border border-border text-left bg-muted p-6 rounded-lg">
256+
<Button
257+
variant="default"
258+
as="a"
259+
:href="`${appconfig.sona.url}/webstudy_credit.aspx?experiment_id=${appconfig.sona.experimentId}&credit_token=${appconfig.sona.creditToken}&survey_code=${api.private.recruitmentInfo.survey_code}`"
260+
>
261+
Return to SONA for credit
262+
<i-fa6-solid-arrow-right />
263+
</Button>
264+
</div>
265+
</template>
266+
</TitleTwoCol>
267+
</div>
268+
269+
<!-- SONA (paid) recruitment service completion -->
270+
<div v-if="api.getRecruitmentService() == 'sona_paid'">
271+
<TitleTwoCol leftFirst leftWidth="w-1/3" :responsiveUI="api.config.responsiveUI">
272+
<template #title>
273+
<h1 class="text-3xl font-bold mb-4">
274+
<i-fa6-solid-square-check class="inline mr-2" />&nbsp;Thanks, let's begin the payment process!
275+
</h1>
276+
<p class="text-lg mb-8">
277+
Please click the button below to return to SONA and receive payment for completing this study. We really
278+
appreciate your time.
279+
</p>
280+
</template>
281+
<template #left>
282+
<div class="text-left text-muted-foreground">
283+
<h3 class="text-lg font-bold mb-2">Payment Process</h3>
284+
<p class="text-sm text-muted-foreground">
285+
Click the button to return to SONA and automatically receive payment for this study.
286+
</p>
287+
</div>
288+
</template>
289+
<template #right>
290+
<div class="border border-border text-left bg-muted p-6 rounded-lg">
291+
<Button
292+
variant="default"
293+
as="a"
294+
:href="`${appconfig.sonaPaid.url}/webstudy_credit.aspx?experiment_id=${appconfig.sonaPaid.experimentId}&credit_token=${appconfig.sonaPaid.creditToken}&survey_code=${api.private.recruitmentInfo.survey_code}`"
295+
>
296+
Return to SONA for payment
297+
<i-fa6-solid-arrow-right />
298+
</Button>
299+
</div>
300+
</template>
301+
</TitleTwoCol>
302+
</div>
303+
234304
<!-- Web recruitment service completion -->
235305
<div v-if="api.getRecruitmentService() == 'web'">
236306
<TitleTwoCol leftFirst leftWidth="w-1/3" :responsiveUI="api.config.responsiveUI">

src/core/config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ export default {
9292
windowsizerRequest: parseWidthHeight(import.meta.env.VITE_WINDOWSIZER_REQUEST),
9393
windowsizerAggressive: import.meta.env.VITE_WINDOWSIZER_AGGRESSIVE === 'true',
9494
anonymousMode: import.meta.env.VITE_ANONYMOUS_MODE === 'true',
95+
unpaidStudy: false,
96+
sona: {
97+
url: import.meta.env.VITE_SONA_URL,
98+
experimentId: import.meta.env.VITE_SONA_EXPERIMENT_ID,
99+
creditToken: import.meta.env.VITE_SONA_CREDIT_TOKEN,
100+
},
101+
sonaPaid: {
102+
url: import.meta.env.VITE_SONA_PAID_URL,
103+
experimentId: import.meta.env.VITE_SONA_PAID_EXPERIMENT_ID,
104+
creditToken: import.meta.env.VITE_SONA_PAID_CREDIT_TOKEN,
105+
},
95106
firebaseConfig: {
96107
apiKey: import.meta.env.VITE_FIREBASE_APIKEY,
97108
authDomain: import.meta.env.VITE_FIREBASE_AUTHDOMAIN,

src/core/stores/smilestore.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ const initBrowserEphemeral = {
202202
'#/mturk/?assignmentId=123RVWYBAZW00EXAMPLE456RVWYBAZW00EXAMPLE&hitId=123RVWYBAZW00EXAMPLE&turkSubmitTo=https://www.mturk.com/&workerId=AZ3456EXAMPLE',
203203
citizensci:
204204
'#/welcome/citizensci/?CITIZEN_ID=XXXXX&CITIZEN_STUDY_ID=123RVWYBAZW00EXAMPLE&CITIZEN_SESSION_ID=AZ3456EXAMPLE',
205+
sona: '#/welcome/sona/?survey_code=SONA_TEST_12345',
206+
sona_paid: '#/welcome/sona_paid/?survey_code=SONA_PAID_TEST_67890',
205207
web: '#/welcome',
206208
},
207209
}

src/core/utils/utils.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ export function processQuery(query, service) {
6464
assignment_id: urlParams.assignmentId,
6565
})
6666
}
67+
} else if (service === 'sona' && urlParams.survey_code) {
68+
log.log('Sona mode')
69+
smilestore.setRecruitmentService(service, {
70+
survey_code: urlParams.survey_code,
71+
})
72+
} else if (service === 'sona_paid' && urlParams.survey_code) {
73+
log.log('Sona paid mode')
74+
smilestore.setRecruitmentService(service, {
75+
survey_code: urlParams.survey_code,
76+
})
6777
} else if (
6878
service === 'citizensci' &&
6979
urlParams.CITIZEN_ID &&

src/dev/developer_mode/RecruitmentChooserView.vue

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,43 @@ const api = useViewAPI()
103103
</CardContent>
104104
</Card>
105105

106+
<!-- SONA Platform Card -->
107+
<Card class="flex flex-col">
108+
<CardHeader>
109+
<CardTitle>SONA</CardTitle>
110+
<CardDescription
111+
>SONA is a platform for running experiments from the university "for credit" pool of
112+
students.</CardDescription
113+
>
114+
</CardHeader>
115+
<CardContent class="flex justify-end flex-1 items-end">
116+
<Button asChild variant="default" size="xs" class="is-orange" id="sona_tester">
117+
<a :href="api.urls['sona']"
118+
><i-lucide-dices /> SONA User
119+
<ArrowRight class="inline-block ml-1 w-4 h-4" />
120+
</a>
121+
</Button>
122+
</CardContent>
123+
</Card>
124+
125+
<!-- SONA Paid Platform Card -->
126+
<Card class="flex flex-col">
127+
<CardHeader>
128+
<CardTitle>SONA Paid</CardTitle>
129+
<CardDescription
130+
>SONA Paid is a platform for running paid experiments from the university pool.</CardDescription
131+
>
132+
</CardHeader>
133+
<CardContent class="flex justify-end flex-1 items-end">
134+
<Button asChild variant="default" size="xs" class="is-teal" id="sona_paid_tester">
135+
<a :href="api.urls['sona_paid']"
136+
><i-lucide-dices /> SONA Paid User
137+
<ArrowRight class="inline-block ml-1 w-4 h-4" />
138+
</a>
139+
</Button>
140+
</CardContent>
141+
</Card>
142+
106143
<!-- Citizen Science Platform Card -->
107144
<Card class="flex flex-col">
108145
<CardHeader>
@@ -112,7 +149,7 @@ const api = useViewAPI()
112149
>
113150
</CardHeader>
114151
<CardContent class="flex justify-end flex-1 items-end">
115-
<Button asChild variant="default" size="xs" class="is-green" id="citizen_tester">
152+
<Button asChild variant="default" size="xs" class="is-purple" id="citizen_tester">
116153
<a :href="api.urls['citizensci']"
117154
><i-lucide-dices /> Citizen Sci User
118155
<ArrowRight class="inline-block ml-1 w-4 h-4" />
@@ -128,7 +165,7 @@ const api = useViewAPI()
128165
<CardDescription>Anonymous web user is not referred by any recruitment service.</CardDescription>
129166
</CardHeader>
130167
<CardContent class="flex justify-end flex-1 items-end">
131-
<Button asChild variant="default" size="xs" class="is-purple" id="web_tester">
168+
<Button asChild variant="default" size="xs" class="is-green" id="web_tester">
132169
<a :href="api.urls['web']"
133170
><i-lucide-dices /> Anon Web User
134171
<ArrowRight class="inline-block ml-1 w-4 h-4" />
@@ -172,6 +209,16 @@ const api = useViewAPI()
172209
color: #393939;
173210
}
174211
212+
.is-teal {
213+
background-color: rgb(102, 223, 214);
214+
color: #393939;
215+
}
216+
217+
.is-orange {
218+
background-color: rgb(255, 179, 102);
219+
color: #393939;
220+
}
221+
175222
.is-purple {
176223
background-color: #ebaeff;
177224
color: #393939;

0 commit comments

Comments
 (0)