Skip to content

Commit f4d8163

Browse files
committed
feat(davinci-client): support single checkbox component
1 parent 44f9be3 commit f4d8163

12 files changed

Lines changed: 190 additions & 61 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ GEMINI.md
9494

9595
.claude/worktrees
9696
.claude/settings.local.json
97+
.claude/skills
9798
.opensource
9899

99100
# Polaris
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import type { ValidatedBooleanCollector, Updater } from '@forgerock/davinci-client/types';
8+
9+
/**
10+
* Creates a single checkbox and attaches it to the form
11+
* @param {HTMLFormElement} formEl - The form element to attach the checkboxes to
12+
* @param {ValidatedBooleanCollector} collector - Contains the configuration
13+
* @param {Updater} updater - Function to call when selection changes
14+
*/
15+
export default function booleanComponent(
16+
formEl: HTMLFormElement,
17+
collector: ValidatedBooleanCollector,
18+
updater: Updater<ValidatedBooleanCollector>,
19+
) {
20+
// Create a container for the checkboxes
21+
const containerDiv = document.createElement('div');
22+
containerDiv.className = 'single-checkbox-container';
23+
24+
// Create a heading/label for the checkbox group
25+
const groupLabel = document.createElement('div');
26+
groupLabel.textContent = collector.output.label || 'Single Checkbox';
27+
groupLabel.className = 'single-checkbox-label';
28+
containerDiv.appendChild(groupLabel);
29+
30+
// Create checkboxes for each option
31+
const wrapper = document.createElement('div');
32+
wrapper.className = 'checkbox-wrapper';
33+
34+
const checkbox = document.createElement('input');
35+
checkbox.type = 'checkbox';
36+
checkbox.id = collector.output.key;
37+
checkbox.name = collector.output.key || 'single-checkbox-field';
38+
checkbox.checked = collector.output.value as boolean;
39+
checkbox.value = 'checked';
40+
41+
const label = document.createElement('label');
42+
label.htmlFor = checkbox.id;
43+
label.textContent = collector.output.label;
44+
45+
// Add event listener to handle single-select behavior
46+
checkbox.addEventListener('change', (event) => {
47+
const target = event.target as HTMLInputElement;
48+
updater(target.value === 'checked');
49+
});
50+
51+
wrapper.appendChild(checkbox);
52+
wrapper.appendChild(label);
53+
containerDiv.appendChild(wrapper);
54+
55+
// Append the container to the form
56+
formEl.appendChild(containerDiv);
57+
}

e2e/davinci-app/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import fidoComponent from './components/fido.js';
3535
import qrCodeComponent from './components/qr-code.js';
3636
import agreementComponent from './components/agreement.js';
3737
import pollingComponent from './components/polling.js';
38+
import booleanComponent from './components/boolean.js';
3839

3940
const loggerFn = {
4041
error: () => {
@@ -294,6 +295,8 @@ const urlParams = new URLSearchParams(window.location.search);
294295
singleValueComponent(formEl, collector, davinciClient.update(collector));
295296
} else if (collector.type === 'MultiSelectCollector') {
296297
multiValueComponent(formEl, collector, davinciClient.update(collector));
298+
} else if (collector.type === 'ValidatedBooleanCollector') {
299+
booleanComponent(formEl, collector, davinciClient.update(collector));
297300
}
298301
});
299302

packages/davinci-client/src/lib/client.store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
338338
value:
339339
| string
340340
| string[]
341+
| boolean
341342
| PhoneNumberInputValue
342343
| PhoneNumberExtensionInputValue
343344
| FidoRegistrationInputValue

packages/davinci-client/src/lib/client.types.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,28 +44,30 @@ export type CollectorValueType<T> = T extends { type: 'PasswordCollector' }
4444
? string
4545
: T extends { type: 'MultiSelectCollector' }
4646
? string[]
47-
: T extends { type: 'DeviceRegistrationCollector' }
48-
? string
49-
: T extends { type: 'DeviceAuthenticationCollector' }
47+
: T extends { type: 'ValidatedBooleanCollector' }
48+
? boolean
49+
: T extends { type: 'DeviceRegistrationCollector' }
5050
? string
51-
: T extends { type: 'PhoneNumberCollector' }
52-
? PhoneNumberInputValue
53-
: T extends { type: 'FidoRegistrationCollector' }
54-
? FidoRegistrationInputValue
55-
: T extends { type: 'FidoAuthenticationCollector' }
56-
? FidoAuthenticationInputValue
57-
: T extends { category: 'SingleValueCollector' }
58-
? string
59-
: T extends { category: 'ValidatedSingleValueCollector' }
51+
: T extends { type: 'DeviceAuthenticationCollector' }
52+
? string
53+
: T extends { type: 'PhoneNumberCollector' }
54+
? PhoneNumberInputValue
55+
: T extends { type: 'FidoRegistrationCollector' }
56+
? FidoRegistrationInputValue
57+
: T extends { type: 'FidoAuthenticationCollector' }
58+
? FidoAuthenticationInputValue
59+
: T extends { category: 'SingleValueCollector' }
6060
? string
61-
: T extends { category: 'MultiValueCollector' }
62-
? string[]
63-
:
64-
| string
65-
| string[]
66-
| PhoneNumberInputValue
67-
| FidoRegistrationInputValue
68-
| FidoAuthenticationInputValue;
61+
: T extends { category: 'ValidatedSingleValueCollector' }
62+
? string
63+
: T extends { category: 'MultiValueCollector' }
64+
? string[]
65+
:
66+
| string
67+
| string[]
68+
| PhoneNumberInputValue
69+
| FidoRegistrationInputValue
70+
| FidoAuthenticationInputValue;
6971

7072
/**
7173
* Generic updater function that accepts values appropriate for the collector type.

packages/davinci-client/src/lib/collector.types.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { FidoAuthenticationOptions, FidoRegistrationOptions } from './davin
1616
*/
1717
export type SingleValueCollectorTypes =
1818
| 'PasswordCollector'
19+
| 'ValidatedBooleanCollector'
1920
| 'SingleValueCollector'
2021
| 'SingleSelectCollector'
2122
| 'SingleSelectObjectCollector'
@@ -157,14 +158,16 @@ export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> =
157158
? ValidatedTextCollector
158159
: T extends 'PasswordCollector'
159160
? PasswordCollector
160-
: /**
161+
: T extends 'ValidatedBooleanCollector'
162+
? ValidatedBooleanCollector
163+
: /**
161164
* At this point, we have not passed in a collector type
162165
* or we have explicitly passed in 'SingleValueCollector'
163166
* So we can return either a SingleValueCollector with value
164167
* or without a value.
165168
**/
166169
| SingleValueCollectorWithValue<'SingleValueCollector'>
167-
| SingleValueCollectorNoValue<'SingleValueCollector'>;
170+
| SingleValueCollectorNoValue<'SingleValueCollector'>;
168171

169172
/**
170173
* SINGLE-VALUE COLLECTOR TYPES
@@ -178,12 +181,15 @@ export type SingleValueCollectors =
178181
| SingleSelectCollectorWithValue<'SingleSelectCollector'>
179182
| SingleValueCollectorWithValue<'SingleValueCollector'>
180183
| SingleValueCollectorWithValue<'TextCollector'>
181-
| ValidatedSingleValueCollectorWithValue<'TextCollector'>;
184+
| ValidatedSingleValueCollectorWithValue<'TextCollector'>
185+
| ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector'>;
182186

183187
export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>;
184188
export type TextCollector = SingleValueCollectorWithValue<'TextCollector'>;
185189
export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>;
186190
export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>;
191+
export type ValidatedBooleanCollector =
192+
ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector'>;
187193

188194
/** *********************************************************************
189195
* MULTI-VALUE COLLECTORS
@@ -625,10 +631,8 @@ export interface ProtectOutputValue {
625631
universalDeviceIdentification: boolean;
626632
}
627633

628-
export interface AttestationValue extends Omit<
629-
PublicKeyCredential,
630-
'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON'
631-
> {
634+
export interface AttestationValue
635+
extends Omit<PublicKeyCredential, 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON'> {
632636
rawId: string;
633637
response: {
634638
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse
@@ -646,10 +650,8 @@ export interface FidoRegistrationOutputValue {
646650
trigger: string;
647651
}
648652

649-
export interface AssertionValue extends Omit<
650-
PublicKeyCredential,
651-
'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON'
652-
> {
653+
export interface AssertionValue
654+
extends Omit<PublicKeyCredential, 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON'> {
653655
rawId: string;
654656
response: {
655657
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse

packages/davinci-client/src/lib/collector.utils.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
InferActionCollectorType,
1818
NoValueCollectorTypes,
1919
InferNoValueCollectorType,
20+
ValidatedBooleanCollector,
2021
ValidatedSingleValueCollectorWithValue,
2122
ValidatedTextCollector,
2223
InferValueObjectCollectorType,
@@ -45,6 +46,7 @@ import type {
4546
PollingField,
4647
ReadOnlyField,
4748
RedirectField,
49+
SingleCheckboxField,
4850
SingleSelectField,
4951
StandardField,
5052
ValidatedField,
@@ -151,7 +153,7 @@ export function returnSubmitCollector(field: StandardField, idx: number) {
151153
* @returns {SingleValueCollector} The constructed SingleValueCollector object.
152154
*/
153155
export function returnSingleValueCollector<
154-
Field extends StandardField | SingleSelectField | ValidatedField,
156+
Field extends StandardField | SingleSelectField | ValidatedField | SingleCheckboxField,
155157
CollectorType extends SingleValueCollectorTypes = 'SingleValueCollector',
156158
>(field: Field, idx: number, collectorType: CollectorType, data?: string) {
157159
let error = '';
@@ -212,10 +214,40 @@ export function returnSingleValueCollector<
212214
options: options,
213215
},
214216
} as InferSingleValueCollectorType<'SingleSelectCollector'>;
217+
} else if (collectorType === 'ValidatedBooleanCollector') {
218+
const validationArray = [];
219+
if ('required' in field && field.required === true) {
220+
validationArray.push({
221+
type: 'required',
222+
message:
223+
('validation' in field && field.validation?.errorMessage) || 'Value cannot be empty',
224+
rule: true,
225+
});
226+
}
227+
228+
return {
229+
category: 'ValidatedSingleValueCollector',
230+
error: error || null,
231+
type: collectorType,
232+
id: `${field.key}-${idx}`,
233+
name: field.key,
234+
input: {
235+
key: field.key,
236+
value: false,
237+
type: field.type,
238+
validation: validationArray,
239+
},
240+
output: {
241+
key: field.key,
242+
label: field.label,
243+
type: field.type,
244+
value: false,
245+
},
246+
} as ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector'>;
215247
} else if ('validation' in field || 'required' in field) {
216248
const validationArray = [];
217249

218-
if ('validation' in field) {
250+
if ('validation' in field && field.validation && 'regex' in field.validation) {
219251
validationArray.push({
220252
type: 'regex',
221253
message: field.validation?.errorMessage || '',
@@ -464,6 +496,16 @@ export function returnSingleSelectCollector(field: SingleSelectField, idx: numbe
464496
return returnSingleValueCollector(field, idx, 'SingleSelectCollector', data);
465497
}
466498

499+
/**
500+
* @function returnValidatedBooleanCollector - Creates a ValidatedBooleanCollector object based on the provided field and index.
501+
* @param {SingleCheckboxField} field - The field object containing key, label, type, required, and validation.
502+
* @param {number} idx - The index to be used in the id of the ValidatedBooleanCollector.
503+
* @returns {ValidatedBooleanCollector} The constructed ValidatedBooleanCollector object.
504+
*/
505+
export function returnValidatedBooleanCollector(field: SingleCheckboxField, idx: number) {
506+
return returnSingleValueCollector(field, idx, 'ValidatedBooleanCollector');
507+
}
508+
467509
/**
468510
* @function returnProtectCollector - Creates a ProtectCollector object based on the provided field and index.
469511
* @param {DaVinciField} field - The field object containing key, label, type, and links.
@@ -846,7 +888,12 @@ export function returnAgreementCollector(field: AgreementField, idx: number): Ag
846888
* @returns {function} - A "validator" function that validates the input value
847889
*/
848890
export function returnValidator(
849-
collector: ValidatedTextCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors,
891+
collector:
892+
| ValidatedTextCollector
893+
| ValidatedBooleanCollector
894+
| ObjectValueCollectors
895+
| MultiValueCollectors
896+
| AutoCollectors,
850897
) {
851898
const rules = collector.input.validation;
852899
return (value: string | string[] | Record<string, unknown>) => {

packages/davinci-client/src/lib/davinci.types.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ export type ValidatedField = {
112112
};
113113
};
114114

115+
export type SingleCheckboxField = {
116+
type: 'SINGLE_CHECKBOX';
117+
inputType: 'BOOLEAN';
118+
key: string;
119+
label: string;
120+
required: boolean;
121+
validation?: {
122+
errorMessage: string;
123+
};
124+
};
125+
115126
export type SingleSelectField = {
116127
inputType: 'SINGLE_SELECT';
117128
key: string;
@@ -185,10 +196,11 @@ export type ProtectField = {
185196
universalDeviceIdentification: boolean;
186197
};
187198

188-
export interface FidoRegistrationOptions extends Omit<
189-
PublicKeyCredentialCreationOptions,
190-
'challenge' | 'user' | 'pubKeyCredParams' | 'excludeCredentials'
191-
> {
199+
export interface FidoRegistrationOptions
200+
extends Omit<
201+
PublicKeyCredentialCreationOptions,
202+
'challenge' | 'user' | 'pubKeyCredParams' | 'excludeCredentials'
203+
> {
192204
challenge: number[];
193205
user: {
194206
id: number[];
@@ -216,10 +228,8 @@ export type FidoRegistrationField = {
216228
required: boolean;
217229
};
218230

219-
export interface FidoAuthenticationOptions extends Omit<
220-
PublicKeyCredentialRequestOptions,
221-
'challenge' | 'allowCredentials'
222-
> {
231+
export interface FidoAuthenticationOptions
232+
extends Omit<PublicKeyCredentialRequestOptions, 'challenge' | 'allowCredentials'> {
223233
challenge: number[];
224234
allowCredentials?: {
225235
id: number[];
@@ -260,7 +270,12 @@ export type ComplexValueFields =
260270
export type MultiValueFields = MultiSelectField;
261271
export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField;
262272
export type RedirectFields = RedirectField;
263-
export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField;
273+
export type SingleValueFields =
274+
| StandardField
275+
| ValidatedField
276+
| SingleCheckboxField
277+
| SingleSelectField
278+
| ProtectField;
264279

265280
export type DaVinciField =
266281
| ComplexValueFields

0 commit comments

Comments
 (0)