Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ GEMINI.md

.claude/worktrees
.claude/settings.local.json
.claude/skills
.opensource

# Polaris
Expand Down
57 changes: 57 additions & 0 deletions e2e/davinci-app/components/boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import type { ValidatedBooleanCollector, Updater } from '@forgerock/davinci-client/types';

/**
* Creates a single checkbox and attaches it to the form
* @param {HTMLFormElement} formEl - The form element to attach the checkboxes to
* @param {ValidatedBooleanCollector} collector - Contains the configuration
* @param {Updater} updater - Function to call when selection changes
*/
export default function booleanComponent(
formEl: HTMLFormElement,
collector: ValidatedBooleanCollector,
updater: Updater<ValidatedBooleanCollector>,
) {
// Create a container for the checkboxes
const containerDiv = document.createElement('div');
containerDiv.className = 'single-checkbox-container';

// Create a heading/label for the checkbox group
const groupLabel = document.createElement('div');
groupLabel.textContent = collector.output.label || 'Single Checkbox';
groupLabel.className = 'single-checkbox-label';
containerDiv.appendChild(groupLabel);

// Create checkboxes for each option
const wrapper = document.createElement('div');
wrapper.className = 'checkbox-wrapper';

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = collector.output.key;
checkbox.name = collector.output.key || 'single-checkbox-field';
checkbox.checked = collector.output.value;
checkbox.value = 'checked';
Comment on lines +34 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Minor: Consider adding required attribute support.

The SingleCheckboxField type includes a required property, but the component doesn't set the HTML required attribute on the checkbox element. This means required validation won't work as expected.

🛡️ Proposed fix
   const checkbox = document.createElement('input');
   checkbox.type = 'checkbox';
   checkbox.id = collector.output.key;
   checkbox.name = collector.output.key || 'single-checkbox-field';
   checkbox.checked = collector.output.value as boolean;
   checkbox.value = 'checked';
+  if (collector.output.required) {
+    checkbox.required = true;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = collector.output.key;
checkbox.name = collector.output.key || 'single-checkbox-field';
checkbox.checked = collector.output.value as boolean;
checkbox.value = 'checked';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = collector.output.key;
checkbox.name = collector.output.key || 'single-checkbox-field';
checkbox.checked = collector.output.value as boolean;
checkbox.value = 'checked';
if (collector.output.required) {
checkbox.required = true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/davinci-app/components/boolean.ts` around lines 34 - 39, The
SingleCheckboxField's required flag isn't applied to the created checkbox
element; update the element creation in the component that builds the checkbox
(where checkbox is created from collector.output) to set checkbox.required =
!!collector.output.required (and optionally
checkbox.setAttribute('aria-required','true') for accessibility) so the HTML
required validation is honored; ensure you reference collector.output.required
and the checkbox element in the SingleCheckboxField handling logic.


const label = document.createElement('label');
label.htmlFor = checkbox.id;
label.textContent = collector.output.label;

// Add event listener to handle single-select behavior
checkbox.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;
updater(target.value === 'checked');
});
Comment on lines +46 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify checkbox event handling in similar components

# Search for other checkbox change event handlers in the codebase
rg -n -A3 -B3 "addEventListener\s*\(\s*['\"]change['\"]" --glob "*.ts" --glob "*.js" -g '!node_modules'

Repository: ForgeRock/ping-javascript-sdk

Length of output: 7149


🏁 Script executed:

cat -n e2e/davinci-app/components/boolean.ts

Repository: ForgeRock/ping-javascript-sdk

Length of output: 2558


Use target.checked instead of target.value === 'checked'.

The checkbox value attribute (line 39) is a static string that never changes based on checkbox state. The expression target.value === 'checked' will always evaluate to true, regardless of whether the checkbox is checked or unchecked. This breaks the boolean field functionality. Use target.checked to get the actual checkbox state, which is also the pattern used in other checkbox handlers throughout the codebase (e.g., terms-and-conditions.ts, confirmation.ts).

Proposed fix
   checkbox.addEventListener('change', (event) => {
     const target = event.target as HTMLInputElement;
-    updater(target.value === 'checked');
+    updater(target.checked);
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
checkbox.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;
updater(target.value === 'checked');
});
checkbox.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;
updater(target.checked);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/davinci-app/components/boolean.ts` around lines 46 - 49, The change
handler for the checkbox uses the static value attribute and compares
target.value === 'checked', which is always true; update the event listener on
checkbox (the callback passed to checkbox.addEventListener('change', ...)) to
call updater with the actual boolean state using target.checked instead of
comparing target.value, i.e., replace the expression target.value === 'checked'
with target.checked so the updater receives the real checked state.


wrapper.appendChild(checkbox);
wrapper.appendChild(label);
containerDiv.appendChild(wrapper);

// Append the container to the form
formEl.appendChild(containerDiv);
}
3 changes: 3 additions & 0 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import fidoComponent from './components/fido.js';
import qrCodeComponent from './components/qr-code.js';
import agreementComponent from './components/agreement.js';
import pollingComponent from './components/polling.js';
import booleanComponent from './components/boolean.js';

const loggerFn = {
error: () => {
Expand Down Expand Up @@ -294,6 +295,8 @@ const urlParams = new URLSearchParams(window.location.search);
singleValueComponent(formEl, collector, davinciClient.update(collector));
} else if (collector.type === 'MultiSelectCollector') {
multiValueComponent(formEl, collector, davinciClient.update(collector));
} else if (collector.type === 'ValidatedBooleanCollector') {
booleanComponent(formEl, collector, davinciClient.update(collector));
}
});

Expand Down
112 changes: 92 additions & 20 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export interface CollectorErrors {
}

// @public (undocumented)
export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector;
export type Collectors = FlowCollector | PasswordCollector | TextCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector;

// @public
export type CollectorValueType<T> = T extends {
Expand All @@ -194,6 +194,8 @@ export type CollectorValueType<T> = T extends {
} ? string : T extends {
type: 'MultiSelectCollector';
} ? string[] : T extends {
type: 'ValidatedBooleanCollector';
} ? boolean : T extends {
type: 'DeviceRegistrationCollector';
} ? string : T extends {
type: 'DeviceAuthenticationCollector';
Expand All @@ -212,7 +214,7 @@ export type CollectorValueType<T> = T extends {
} ? string[] : string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue;

// @public (undocumented)
export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | FidoRegistrationField | FidoAuthenticationField | PollingField;
export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField;

// @public (undocumented)
export interface ContinueNode {
Expand Down Expand Up @@ -270,7 +272,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
poll: (collector: PollingCollector) => Poller;
pollStatus: (collector: PollingCollector) => Poller;
getClient: () => {
status: "start";
} | {
Expand Down Expand Up @@ -1032,10 +1034,10 @@ export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : T extends 'AgreementCollector' ? AgreementCollector : NoValueCollectorBase<'NoValueCollector'>;

// @public
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;

// @public (undocumented)
export type InferValueObjectCollectorType<T extends ObjectValueCollectorTypes> = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;
export type InferValueObjectCollectorType<T extends ObjectValueCollectorTypes> = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;

// @public (undocumented)
export type InitFlow = () => Promise<FlowNode | InternalErrorResponse>;
Expand Down Expand Up @@ -1170,8 +1172,8 @@ value: Record<string, unknown>;
}, string>;

// @public
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | ValidatedBooleanCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | ValidatedBooleanCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
};

// @public (undocumented)
Expand Down Expand Up @@ -1283,10 +1285,10 @@ export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRe
export type ObjectValueCollector<T extends ObjectValueCollectorTypes> = ObjectOptionsCollectorWithObjectValue<T> | ObjectOptionsCollectorWithStringValue<T> | ObjectValueCollectorWithObjectValue<T>;

// @public (undocumented)
export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>;
export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>;

// @public
export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector';
export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'PhoneNumberExtensionCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector';

// @public (undocumented)
export interface ObjectValueCollectorWithObjectValue<T extends ObjectValueCollectorTypes, IV = Record<string, string>, OV = Record<string, string>> {
Expand Down Expand Up @@ -1328,13 +1330,68 @@ export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>
// @public (undocumented)
export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>;

// @public (undocumented)
export interface PhoneNumberExtensionCollector {
// (undocumented)
category: 'ObjectValueCollector';
// (undocumented)
error: string | null;
// (undocumented)
id: string;
// (undocumented)
input: {
key: string;
value: PhoneNumberExtensionInputValue;
type: string;
validation: (ValidationRequired | ValidationPhoneNumber)[] | null;
};
// (undocumented)
name: string;
// (undocumented)
output: {
key: string;
label: string;
type: string;
extensionLabel: string;
value: PhoneNumberExtensionOutputValue;
};
// (undocumented)
type: 'PhoneNumberExtensionCollector';
}

// @public (undocumented)
export type PhoneNumberExtensionField = PhoneNumberField & {
showExtension: boolean;
extensionLabel: string;
};

// @public (undocumented)
export interface PhoneNumberExtensionInputValue {
// (undocumented)
countryCode: string;
// (undocumented)
extension: string;
// (undocumented)
phoneNumber: string;
}

// @public (undocumented)
export interface PhoneNumberExtensionOutputValue {
// (undocumented)
countryCode?: string;
// (undocumented)
extension?: string;
// (undocumented)
phoneNumber?: string;
}

// @public (undocumented)
export type PhoneNumberField = {
type: 'PHONE_NUMBER';
key: string;
label: string;
defaultCountryCode: string | null;
required: boolean;
defaultCountryCode: string | null;
validatePhoneNumber: boolean;
};

Expand Down Expand Up @@ -1481,6 +1538,18 @@ export interface SelectorOption {
value: string;
}

// @public (undocumented)
export type SingleCheckboxField = {
type: 'SINGLE_CHECKBOX';
inputType: 'BOOLEAN';
key: string;
label: string;
required: boolean;
validation?: {
errorMessage: string;
};
};

// @public (undocumented)
export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>;

Expand Down Expand Up @@ -1588,13 +1657,13 @@ export interface SingleValueCollectorNoValue<T extends SingleValueCollectorTypes
}

// @public (undocumented)
export type SingleValueCollectors = SingleValueCollectorNoValue<'PasswordCollector'> | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>;
export type SingleValueCollectors = PasswordCollector | SingleSelectCollector | TextCollector | ValidatedTextCollector | ValidatedBooleanCollector | SingleValueCollectorWithValue<'SingleValueCollector'>;

// @public
export type SingleValueCollectorTypes = 'PasswordCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector';
export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector';

// @public (undocumented)
export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTypes> {
export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTypes, V = string> {
// (undocumented)
category: 'SingleValueCollector';
// (undocumented)
Expand All @@ -1604,7 +1673,7 @@ export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTyp
// (undocumented)
input: {
key: string;
value: string | number | boolean;
value: V;
type: string;
};
// (undocumented)
Expand All @@ -1614,14 +1683,14 @@ export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTyp
key: string;
label: string;
type: string;
value: string | number | boolean;
value: V;
};
// (undocumented)
type: T;
}

// @public (undocumented)
export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField;
export type SingleValueFields = StandardField | ValidatedField | SingleCheckboxField | SingleSelectField | ProtectField;

// @public (undocumented)
export type StandardField = {
Expand Down Expand Up @@ -1724,13 +1793,16 @@ export type UnknownField = Record<string, unknown>;
// @public (undocumented)
export const updateCollectorValues: ActionCreatorWithPayload< {
id: string;
value: string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue;
value: string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue;
index?: number;
}, string>;

// @public
export type Updater<T = unknown> = (value: CollectorValueType<T>, index?: number) => InternalErrorResponse | null;

// @public (undocumented)
export type ValidatedBooleanCollector = ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean>;

// @public (undocumented)
export type ValidatedField = {
type: 'TEXT';
Expand All @@ -1744,7 +1816,7 @@ export type ValidatedField = {
};

// @public (undocumented)
export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCollectorTypes> {
export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCollectorTypes, V = string> {
// (undocumented)
category: 'ValidatedSingleValueCollector';
// (undocumented)
Expand All @@ -1754,9 +1826,9 @@ export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCol
// (undocumented)
input: {
key: string;
value: string | number | boolean;
type: string;
validation: (ValidationRequired | ValidationRegex)[];
value: V;
};
// (undocumented)
name: string;
Expand All @@ -1765,7 +1837,7 @@ export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCol
key: string;
label: string;
type: string;
value: string | number | boolean;
value: V;
};
// (undocumented)
type: T;
Expand Down
Loading
Loading