diff --git a/.changeset/silent-ideas-joke.md b/.changeset/silent-ideas-joke.md index cd2c44f1ca..a80c0a6690 100644 --- a/.changeset/silent-ideas-joke.md +++ b/.changeset/silent-ideas-joke.md @@ -2,4 +2,4 @@ '@forgerock/davinci-client': minor --- -Support form agreements with AgreementCollector +Support form agreements with additional `title` property ReadOnlyCollector diff --git a/.changeset/single-checkbox.md b/.changeset/single-checkbox.md index 819f7671eb..3477120003 100644 --- a/.changeset/single-checkbox.md +++ b/.changeset/single-checkbox.md @@ -2,7 +2,7 @@ '@forgerock/davinci-client': minor --- -Adds support for the SINGLE_CHECKBOX field. A new ValidatedBooleanCollector interface was introduced including validation support for required checkboxes and updater support for booleans. +Adds support for the SINGLE_CHECKBOX field. A new ValidatedBooleanCollector interface was introduced including validation support for required checkboxes and updater support for booleans. A BooleanCollector was also added for parity in the case that no validation is required. **Type improvements** diff --git a/e2e/davinci-app/components/agreement.ts b/e2e/davinci-app/components/agreement.ts deleted file mode 100644 index 002658aa5b..0000000000 --- a/e2e/davinci-app/components/agreement.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { AgreementCollector } from '@forgerock/davinci-client/types'; - -export default function (formEl: HTMLFormElement, collector: AgreementCollector) { - const output = collector.output; - const componentEnabled = output.enabled; - - if (!componentEnabled) { - return; - } - - const content = output.label; - const titleEnabled = output.titleEnabled; - const title = output.title; - - if (titleEnabled) { - const titleEl = document.createElement('h3'); - titleEl.innerText = title; - formEl?.appendChild(titleEl); - } - - const agreement = document.createElement('p'); - agreement.innerText = content; - formEl?.appendChild(agreement); -} diff --git a/e2e/davinci-app/components/boolean.ts b/e2e/davinci-app/components/boolean.ts index d26d792d6b..8627085c9a 100644 --- a/e2e/davinci-app/components/boolean.ts +++ b/e2e/davinci-app/components/boolean.ts @@ -5,6 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import type { + BooleanCollector, ValidatedBooleanCollector, Updater, Validator, @@ -14,15 +15,15 @@ import { dotToCamelCase, richContentInterpolation } from '../helper.js'; /** * Creates a single checkbox and attaches it to the form * @param {HTMLFormElement} formEl - The form element to attach the checkbox to - * @param {ValidatedBooleanCollector} collector - Contains the configuration + * @param {BoooleanCollector | ValidatedBooleanCollector} collector - Contains the configuration * @param {Updater} updater - Function to call when selection changes * @param {Validator} validator - Function to validate the input */ export default function booleanComponent( formEl: HTMLFormElement, - collector: ValidatedBooleanCollector, - updater: Updater, - validator: Validator, + collector: BooleanCollector | ValidatedBooleanCollector, + updater: Updater, + validator?: Validator, ) { const collectorKey = dotToCamelCase(collector.output.key); @@ -57,7 +58,16 @@ export default function booleanComponent( // Add event listener to handle single-select behavior checkbox.addEventListener('change', (event) => { const checked = (event.target as HTMLInputElement).checked; - const result = validator(checked); + + if (collector.type === 'BooleanCollector') { + const updateError = updater(checked); + if (updateError && 'error' in updateError) { + console.error(updateError.error.message); + } + return; + } + + const result = validator && validator(checked); const errorEl = formEl?.querySelector(`.${collectorKey}-error`); // Validate the input diff --git a/e2e/davinci-app/components/label.ts b/e2e/davinci-app/components/label.ts deleted file mode 100644 index ab20b78678..0000000000 --- a/e2e/davinci-app/components/label.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2025 - 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 { ReadOnlyCollector, RichTextCollector } from '@forgerock/davinci-client/types'; -import { richContentInterpolation } from '../helper.js'; - -export default function ( - formEl: HTMLFormElement, - collector: ReadOnlyCollector | RichTextCollector, -) { - const p = document.createElement('p'); - p.style.whiteSpace = 'pre-line'; - - if (collector.type !== 'RichTextCollector') { - p.innerText = collector.output.content; - formEl?.appendChild(p); - return; - } - - const { richContent } = collector.output; - - if (richContent.replacements.length === 0) { - p.innerText = collector.output.content; - formEl?.appendChild(p); - return; - } - - // Interpolate the template by splitting on {{key}} and inserting links - const pRichText = richContentInterpolation(richContent); - - formEl?.appendChild(pRichText); -} diff --git a/e2e/davinci-app/components/read-only.ts b/e2e/davinci-app/components/read-only.ts new file mode 100644 index 0000000000..c2eb1650f9 --- /dev/null +++ b/e2e/davinci-app/components/read-only.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 - 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 { ReadOnlyCollector, RichTextCollector } from '@forgerock/davinci-client/types'; +import { richContentInterpolation } from '../helper.js'; + +export default function ( + formEl: HTMLFormElement, + collector: ReadOnlyCollector | RichTextCollector, +) { + const p = document.createElement('p'); + p.style.whiteSpace = 'pre-line'; + + if (collector.type === 'ReadOnlyCollector') { + // Display agreement title if it exists + if (collector.output.title) { + const titleEl = document.createElement('h3'); + titleEl.innerText = collector.output.title; + formEl?.appendChild(titleEl); + } + + p.innerText = collector.output.content; + formEl?.appendChild(p); + } else if (collector.type === 'RichTextCollector') { + const { richContent } = collector.output; + + if (richContent.replacements.length === 0) { + p.innerText = collector.output.content; + formEl?.appendChild(p); + return; + } + + // Interpolate the template by splitting on {{key}} and inserting links + const pRichText = richContentInterpolation(richContent); + + formEl?.appendChild(pRichText); + } +} diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index d2e72d96e7..f4fa57970e 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -10,6 +10,7 @@ import { davinci } from '@forgerock/davinci-client'; import { oidc } from '@forgerock/oidc-client'; import type { OidcConfig } from '@forgerock/oidc-client/types'; import type { + Collectors, CustomLogger, DaVinciConfig, DavinciClient, @@ -30,11 +31,10 @@ import socialLoginButtonComponent from './components/social-login-button.js'; import { serverConfigs } from './server-configs.js'; import singleValueComponent from './components/single-value.js'; import multiValueComponent from './components/multi-value.js'; -import labelComponent from './components/label.js'; +import readOnlyComponent from './components/read-only.js'; import objectValueComponent from './components/object-value.js'; 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'; @@ -206,7 +206,7 @@ const urlParams = new URLSearchParams(window.location.search); const collectors = davinciClient.getCollectors(); - collectors.forEach((collector) => { + collectors.forEach((collector: Collectors) => { if (collector.type === 'TextCollector' && collector.name === 'protectsdk') { // eslint-disable-next-line @typescript-eslint/no-unused-expressions collector; @@ -226,14 +226,12 @@ const urlParams = new URLSearchParams(window.location.search); submitForm, ); } else if (collector.type === 'ReadOnlyCollector' || collector.type === 'RichTextCollector') { - labelComponent( + readOnlyComponent( formEl, // You can ignore this; it's just for rendering collector, // This is the plain object of the collector ); } else if (collector.type === 'QrCodeCollector') { qrCodeComponent(formEl, collector); - } else if (collector.type === 'AgreementCollector') { - agreementComponent(formEl, collector); } else if (collector.type === 'TextCollector') { textComponent( formEl, // You can ignore this; it's just for rendering @@ -288,6 +286,15 @@ const urlParams = new URLSearchParams(window.location.search); davinciClient.update(collector), // Returns an update function for this collector submitForm, ); + } else if (collector.type === 'BooleanCollector') { + booleanComponent(formEl, collector, davinciClient.update(collector)); + } else if (collector.type === 'ValidatedBooleanCollector') { + booleanComponent( + formEl, + collector, + davinciClient.update(collector), + davinciClient.validate(collector), + ); } else if (collector.type === 'FlowCollector') { flowLinkComponent( formEl, // You can ignore this; it's just for rendering @@ -302,13 +309,6 @@ 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), - davinciClient.validate(collector), - ); } }); diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index 85ef2b1930..f98aa2445c 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -71,23 +71,6 @@ export interface ActionCollectorWithUrl { export { ActionTypes } -// @public (undocumented) -export interface AgreementCollector extends NoValueCollectorBase<'AgreementCollector'> { - // (undocumented) - output: { - key: string; - label: string; - type: string; - titleEnabled: boolean; - title: string; - agreement: { - id: string; - useDynamicAgreement: boolean; - }; - enabled: boolean; - }; -} - // @public (undocumented) export type AgreementField = { type: 'AGREEMENT'; @@ -162,6 +145,15 @@ export type AutoCollectors = ProtectCollector | FidoRegistrationCollector | Fido // @public (undocumented) export type AutoCollectorTypes = SingleValueAutoCollectorTypes | ObjectValueAutoCollectorTypes; +// @public (undocumented) +export interface BooleanCollector extends SingleValueCollectorWithValue<'BooleanCollector', boolean> { + // (undocumented) + output: SingleValueCollectorWithValue<'BooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + // @public (undocumented) export interface CollectorErrors { // (undocumented) @@ -181,7 +173,7 @@ export interface CollectorRichContent { } // @public (undocumented) -export type Collectors = FlowCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector; +export type Collectors = FlowCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector; // @public export type CollectorValueType = T extends { @@ -205,6 +197,8 @@ export type CollectorValueType = T extends { type: 'TextCollector'; category: 'ValidatedSingleValueCollector'; } ? string : T extends { + type: 'BooleanCollector'; +} | { type: 'ValidatedBooleanCollector'; } ? boolean : T extends { type: 'MultiSelectCollector'; @@ -230,7 +224,7 @@ export type CollectorValueType = T extends { category: 'NoValueCollector'; } ? never : CollectorValueTypes; -// @public (undocumented) +// @public export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; // @public (undocumented) @@ -289,7 +283,7 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; pollStatus: (collector: PollingCollector) => Poller; @@ -307,20 +301,22 @@ export function davinci(input: { description?: string; name?: string; status: "error"; - } | { - status: "failure"; } | { authorization?: { code?: string; state?: string; }; status: "success"; + } | { + status: "failure"; } | null; getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; getServer: () => { + status: "start"; + } | { _links?: Links; id?: string; interactionId?: string; @@ -328,8 +324,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -340,20 +334,20 @@ export function davinci(input: { } | { _links?: Links; eventName?: string; - href?: string; id?: string; interactionId?: string; interactionToken?: string; - status: "failure"; + href?: string; + session?: string; + status: "success"; } | { _links?: Links; eventName?: string; + href?: string; id?: string; interactionId?: string; interactionToken?: string; - href?: string; - session?: string; - status: "success"; + status: "failure"; } | null; cache: { getLatestResponse: () => ({ @@ -361,14 +355,14 @@ export function davinci(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "data" | "fulfilledTimeStamp"> & Required(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "error"> & Required(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "data" | "fulfilledTimeStamp"> & Required(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "error"> & Required = T extends 'Pr export type InferMultiValueCollectorType = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>; // @public -export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : T extends 'AgreementCollector' ? AgreementCollector : NoValueCollectorBase<'NoValueCollector'>; +export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : NoValueCollectorBase<'NoValueCollector'>; // @public -export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; +export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : T extends 'BooleanCollector' ? BooleanCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; // @public (undocumented) export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; @@ -1137,10 +1131,10 @@ export interface NoValueCollectorBase { } // @public (undocumented) -export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector; +export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector; // @public -export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector' | 'AgreementCollector'; +export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector'; // @public export interface OAuthDetails { @@ -1510,6 +1504,7 @@ export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollect // (undocumented) output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { content: string; + title?: string; }; } @@ -1701,10 +1696,10 @@ export interface SingleValueCollectorNoValue; +export type SingleValueCollectors = PasswordCollector | ValidatedPasswordCollector | SingleSelectCollector | TextCollector | ValidatedTextCollector | BooleanCollector | ValidatedBooleanCollector | SingleValueCollectorWithValue<'SingleValueCollector'>; // @public -export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedPasswordCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; +export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedPasswordCollector' | 'BooleanCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; // @public (undocumented) export interface SingleValueCollectorWithValue { @@ -1853,6 +1848,9 @@ export interface ValidatedBooleanCollector extends ValidatedSingleValueCollector }; } +// @public +export type ValidatedCollectors = ValidatedTextCollector | ValidatedBooleanCollector | ValidatedPasswordCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors; + // @public (undocumented) export type ValidatedField = { type: 'TEXT'; @@ -1955,7 +1953,7 @@ export interface ValidationRequired { } // @public -export type Validator = (value: CollectorValueType) => string[] | { +export type Validator = (value: CollectorValueType) => string[] | { error: { message: string; type: string; diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index 7f0ca20f1c..66556bfdc7 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -71,23 +71,6 @@ export interface ActionCollectorWithUrl { export { ActionTypes } -// @public (undocumented) -export interface AgreementCollector extends NoValueCollectorBase<'AgreementCollector'> { - // (undocumented) - output: { - key: string; - label: string; - type: string; - titleEnabled: boolean; - title: string; - agreement: { - id: string; - useDynamicAgreement: boolean; - }; - enabled: boolean; - }; -} - // @public (undocumented) export type AgreementField = { type: 'AGREEMENT'; @@ -162,6 +145,15 @@ export type AutoCollectors = ProtectCollector | FidoRegistrationCollector | Fido // @public (undocumented) export type AutoCollectorTypes = SingleValueAutoCollectorTypes | ObjectValueAutoCollectorTypes; +// @public (undocumented) +export interface BooleanCollector extends SingleValueCollectorWithValue<'BooleanCollector', boolean> { + // (undocumented) + output: SingleValueCollectorWithValue<'BooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + // @public (undocumented) export interface CollectorErrors { // (undocumented) @@ -181,7 +173,7 @@ export interface CollectorRichContent { } // @public (undocumented) -export type Collectors = FlowCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector; +export type Collectors = FlowCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector; // @public export type CollectorValueType = T extends { @@ -205,6 +197,8 @@ export type CollectorValueType = T extends { type: 'TextCollector'; category: 'ValidatedSingleValueCollector'; } ? string : T extends { + type: 'BooleanCollector'; +} | { type: 'ValidatedBooleanCollector'; } ? boolean : T extends { type: 'MultiSelectCollector'; @@ -230,7 +224,7 @@ export type CollectorValueType = T extends { category: 'NoValueCollector'; } ? never : CollectorValueTypes; -// @public (undocumented) +// @public export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; // @public (undocumented) @@ -289,7 +283,7 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; pollStatus: (collector: PollingCollector) => Poller; @@ -307,20 +301,22 @@ export function davinci(input: { description?: string; name?: string; status: "error"; - } | { - status: "failure"; } | { authorization?: { code?: string; state?: string; }; status: "success"; + } | { + status: "failure"; } | null; getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; getServer: () => { + status: "start"; + } | { _links?: Links; id?: string; interactionId?: string; @@ -328,8 +324,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -340,20 +334,20 @@ export function davinci(input: { } | { _links?: Links; eventName?: string; - href?: string; id?: string; interactionId?: string; interactionToken?: string; - status: "failure"; + href?: string; + session?: string; + status: "success"; } | { _links?: Links; eventName?: string; + href?: string; id?: string; interactionId?: string; interactionToken?: string; - href?: string; - session?: string; - status: "success"; + status: "failure"; } | null; cache: { getLatestResponse: () => ({ @@ -361,14 +355,14 @@ export function davinci(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "data" | "fulfilledTimeStamp"> & Required(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "error"> & Required(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "data" | "fulfilledTimeStamp"> & Required(input: { } & Omit<{ requestId: string; data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; + error?: SerializedError | FetchBaseQueryError | undefined; endpointName: string; startedTimeStamp: number; fulfilledTimeStamp?: number; }, "error"> & Required = T extends 'Pr export type InferMultiValueCollectorType = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>; // @public -export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : T extends 'AgreementCollector' ? AgreementCollector : NoValueCollectorBase<'NoValueCollector'>; +export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : NoValueCollectorBase<'NoValueCollector'>; // @public -export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; +export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : T extends 'BooleanCollector' ? BooleanCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; // @public (undocumented) export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; @@ -1134,10 +1128,10 @@ export interface NoValueCollectorBase { } // @public (undocumented) -export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector; +export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector; // @public -export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector' | 'AgreementCollector'; +export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector'; // @public export interface OAuthDetails { @@ -1507,6 +1501,7 @@ export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollect // (undocumented) output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { content: string; + title?: string; }; } @@ -1698,10 +1693,10 @@ export interface SingleValueCollectorNoValue; +export type SingleValueCollectors = PasswordCollector | ValidatedPasswordCollector | SingleSelectCollector | TextCollector | ValidatedTextCollector | BooleanCollector | ValidatedBooleanCollector | SingleValueCollectorWithValue<'SingleValueCollector'>; // @public -export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedPasswordCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; +export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedPasswordCollector' | 'BooleanCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; // @public (undocumented) export interface SingleValueCollectorWithValue { @@ -1850,6 +1845,9 @@ export interface ValidatedBooleanCollector extends ValidatedSingleValueCollector }; } +// @public +export type ValidatedCollectors = ValidatedTextCollector | ValidatedBooleanCollector | ValidatedPasswordCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors; + // @public (undocumented) export type ValidatedField = { type: 'TEXT'; @@ -1952,7 +1950,7 @@ export interface ValidationRequired { } // @public -export type Validator = (value: CollectorValueType) => string[] | { +export type Validator = (value: CollectorValueType) => string[] | { error: { message: string; type: string; diff --git a/packages/davinci-client/src/lib/client.types.ts b/packages/davinci-client/src/lib/client.types.ts index 3dd3dbe849..eb8655c05e 100644 --- a/packages/davinci-client/src/lib/client.types.ts +++ b/packages/davinci-client/src/lib/client.types.ts @@ -29,6 +29,9 @@ export interface InternalErrorResponse { export type InitFlow = () => Promise; +/** + * Allowed value types accepted by collector updaters + */ export type CollectorValueTypes = | string | string[] @@ -67,7 +70,7 @@ export type CollectorValueType = : T extends { type: 'TextCollector'; category: 'ValidatedSingleValueCollector' } ? string : // boolean input types - T extends { type: 'ValidatedBooleanCollector' } + T extends { type: 'BooleanCollector' } | { type: 'ValidatedBooleanCollector' } ? boolean : // string[] input types T extends { type: 'MultiSelectCollector' } @@ -110,21 +113,26 @@ export type Updater = ( index?: number, ) => InternalErrorResponse | null; +/** + * Collectors which can be validated + */ +export type ValidatedCollectors = + | ValidatedTextCollector + | ValidatedBooleanCollector + | ValidatedPasswordCollector + | ObjectValueCollectors + | MultiValueCollectors + | AutoCollectors; + /** * Validates a collector's current value and returns any validation errors. * * @param value - The current value of the collector to validate. * @returns An array of error message strings, or an error object. Returns an empty array when validation passes. */ -export type Validator< - T = - | ValidatedTextCollector - | ValidatedBooleanCollector - | ValidatedPasswordCollector - | ObjectValueCollectors - | MultiValueCollectors - | AutoCollectors, -> = (value: CollectorValueType) => +export type Validator = ( + value: CollectorValueType, +) => | string[] | { error: { diff --git a/packages/davinci-client/src/lib/collector.types.test-d.ts b/packages/davinci-client/src/lib/collector.types.test-d.ts index e9576f2b19..6c39def2b3 100644 --- a/packages/davinci-client/src/lib/collector.types.test-d.ts +++ b/packages/davinci-client/src/lib/collector.types.test-d.ts @@ -28,7 +28,6 @@ import type { ReadOnlyCollector, RichTextCollector, QrCodeCollector, - AgreementCollector, PhoneNumberCollector, PhoneNumberExtensionCollector, ObjectValueCollectorWithObjectValue, @@ -630,28 +629,23 @@ describe('Collector Types', () => { expectTypeOf(tCollector).toEqualTypeOf(); }); - it('should correctly infer AgreementCollector Type', () => { - const tCollector: InferNoValueCollectorType<'AgreementCollector'> = { + it('should correctly infer ReadOnlyCollector Type for AGREEMENT fields', () => { + const tCollector: InferNoValueCollectorType<'ReadOnlyCollector'> = { category: 'NoValueCollector', error: null, - type: 'AgreementCollector', + type: 'ReadOnlyCollector', id: 'agreement-0', name: 'agreement-0', output: { key: 'agreement-0', label: 'Please accept the terms and conditions', type: 'AGREEMENT', - titleEnabled: true, + content: 'Please accept the terms and conditions', title: 'Terms and Conditions', - agreement: { - id: 'agreement-123', - useDynamicAgreement: false, - }, - enabled: true, }, }; - expectTypeOf(tCollector).toEqualTypeOf(); + expectTypeOf(tCollector).toEqualTypeOf(); }); }); diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index 62b3a52b44..58360c2d8b 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -21,6 +21,7 @@ import type { export type SingleValueCollectorTypes = | 'PasswordCollector' | 'ValidatedPasswordCollector' + | 'BooleanCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' @@ -93,16 +94,6 @@ export interface ValidatedSingleValueCollectorWithValue< }; } -export interface ValidatedBooleanCollector extends ValidatedSingleValueCollectorWithValue< - 'ValidatedBooleanCollector', - boolean -> { - output: ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean>['output'] & { - appearance: string; - richContent?: CollectorRichContent; - }; -} - export interface SingleSelectCollectorWithValue { category: 'SingleValueCollector'; error: string | null; @@ -178,16 +169,18 @@ export type InferSingleValueCollectorType = ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector - : T extends 'ValidatedBooleanCollector' - ? ValidatedBooleanCollector - : /** - * At this point, we have not passed in a collector type - * or we have explicitly passed in 'SingleValueCollector' - * So we can return either a SingleValueCollector with value - * or without a value. - **/ - | SingleValueCollectorWithValue<'SingleValueCollector'> - | SingleValueCollectorNoValue<'SingleValueCollector'>; + : T extends 'BooleanCollector' + ? BooleanCollector + : T extends 'ValidatedBooleanCollector' + ? ValidatedBooleanCollector + : /** + * At this point, we have not passed in a collector type + * or we have explicitly passed in 'SingleValueCollector' + * So we can return either a SingleValueCollector with value + * or without a value. + **/ + | SingleValueCollectorWithValue<'SingleValueCollector'> + | SingleValueCollectorNoValue<'SingleValueCollector'>; /** * SINGLE-VALUE COLLECTOR TYPES @@ -234,16 +227,38 @@ export interface ValidatedPasswordCollector { verify: boolean; }; } + +export interface BooleanCollector extends SingleValueCollectorWithValue< + 'BooleanCollector', + boolean +> { + output: SingleValueCollectorWithValue<'BooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + +export interface ValidatedBooleanCollector extends ValidatedSingleValueCollectorWithValue< + 'ValidatedBooleanCollector', + boolean +> { + output: ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + export type TextCollector = SingleValueCollectorWithValue<'TextCollector'>; export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>; export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>; export type SingleValueCollectors = - | ValidatedPasswordCollector | PasswordCollector + | ValidatedPasswordCollector | SingleSelectCollector | TextCollector | ValidatedTextCollector + | BooleanCollector | ValidatedBooleanCollector | SingleValueCollectorWithValue<'SingleValueCollector'>; @@ -585,8 +600,7 @@ export type NoValueCollectorTypes = | 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' - | 'QrCodeCollector' - | 'AgreementCollector'; + | 'QrCodeCollector'; export interface NoValueCollectorBase { category: 'NoValueCollector'; @@ -643,6 +657,7 @@ export interface QrCodeCollector extends NoValueCollectorBase<'QrCodeCollector'> export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollector'> { output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { content: string; + title?: string; }; } @@ -660,21 +675,6 @@ export interface RichTextCollector extends NoValueCollectorBase<'RichTextCollect }; } -export interface AgreementCollector extends NoValueCollectorBase<'AgreementCollector'> { - output: { - key: string; - label: string; - type: string; - titleEnabled: boolean; - title: string; - agreement: { - id: string; - useDynamicAgreement: boolean; - }; - enabled: boolean; - }; -} - /** * Type to help infer the collector based on the collector type * Used specifically in the returnNoValueCollector wrapper function. @@ -689,16 +689,13 @@ export type InferNoValueCollectorType = ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector - : T extends 'AgreementCollector' - ? AgreementCollector - : NoValueCollectorBase<'NoValueCollector'>; + : NoValueCollectorBase<'NoValueCollector'>; export type NoValueCollectors = | NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector - | QrCodeCollector - | AgreementCollector; + | QrCodeCollector; export type NoValueCollector = InferNoValueCollectorType; diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index 1d890ac149..7cb924e9cd 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -24,8 +24,8 @@ import { returnSingleValueAutoCollector, returnObjectValueAutoCollector, returnQrCodeCollector, - returnAgreementCollector, normalizeReplacements, + returnBooleanCollector, returnValidatedBooleanCollector, } from './collector.utils.js'; import { returnPasswordPolicyValidator } from './password-policy.rules.js'; @@ -49,6 +49,7 @@ import type { AgreementField, } from './davinci.types.js'; import type { + BooleanCollector, MultiSelectCollector, PhoneNumberCollector, PhoneNumberExtensionCollector, @@ -1095,8 +1096,8 @@ describe('returnQrCodeCollector', () => { }); }); -describe('returnAgreementCollector', () => { - it('should return a valid AgreementCollector with all fields', () => { +describe('returnReadOnlyCollector with AGREEMENT field', () => { + it('should return a valid ReadOnlyCollector with content and title', () => { const mockField: AgreementField = { type: 'AGREEMENT', key: 'agreement-field', @@ -1109,31 +1110,56 @@ describe('returnAgreementCollector', () => { }, enabled: true, }; - const result = returnAgreementCollector(mockField, 0); + const result = returnReadOnlyCollector(mockField, 0); expect(result).toEqual({ category: 'NoValueCollector', error: null, - type: 'AgreementCollector', + type: 'ReadOnlyCollector', id: 'agreement-field-0', name: 'agreement-field-0', output: { key: 'agreement-field-0', label: 'Please accept the terms and conditions', type: 'AGREEMENT', - titleEnabled: true, + content: 'Please accept the terms and conditions', title: 'Terms and Conditions', - agreement: { - id: 'agreement-123', - useDynamicAgreement: false, - }, - enabled: true, }, }); }); + it('should return a ReadOnlyCollector with no title when title is disabled', () => { + const mockField: AgreementField = { + type: 'AGREEMENT', + key: 'agreement-field', + content: 'Please accept the terms and conditions', + titleEnabled: false, + title: 'Sample Title', + agreement: { + id: 'agreement-123', + useDynamicAgreement: false, + }, + enabled: true, + }; + const result = returnReadOnlyCollector(mockField, 0); + expect(result).toEqual({ + category: 'NoValueCollector', + error: null, + type: 'ReadOnlyCollector', + id: 'agreement-field-0', + name: 'agreement-field-0', + output: { + key: 'agreement-field-0', + label: 'Please accept the terms and conditions', + type: 'AGREEMENT', + content: 'Please accept the terms and conditions', + }, + }); + expect(result.output).not.toHaveProperty('title'); + }); + it('should set error when content is missing', () => { const mockField = { type: 'AGREEMENT', key: 'agreement-field' } as unknown as AgreementField; - const result = returnAgreementCollector(mockField, 0); + const result = returnReadOnlyCollector(mockField, 0); expect(result.error).toContain('Content is not found'); }); }); @@ -1833,3 +1859,84 @@ describe('returnValidatedBooleanCollector', () => { }); }); }); + +describe('returnBooleanCollector', () => { + it('should produce a BooleanCollector with SingleValueCollector category', () => { + const field: SingleCheckboxField = { + type: 'SINGLE_CHECKBOX', + inputType: 'BOOLEAN', + key: 'newsletter', + label: 'Subscribe to newsletter', + required: false, + appearance: 'checkbox', + }; + const result = returnBooleanCollector(field, 0); + expect(result satisfies BooleanCollector).toEqual({ + category: 'SingleValueCollector', + type: 'BooleanCollector', + error: null, + id: 'newsletter-0', + name: 'newsletter', + input: { + key: 'newsletter', + value: false, + type: 'SINGLE_CHECKBOX', + }, + output: { + key: 'newsletter', + label: 'Subscribe to newsletter', + type: 'SINGLE_CHECKBOX', + value: false, + appearance: 'checkbox', + }, + }); + }); + + it('should include richContent on output when field has richContent', () => { + const field: SingleCheckboxField = { + type: 'SINGLE_CHECKBOX', + inputType: 'BOOLEAN', + key: 'accept-terms', + label: 'Accept Terms', + required: false, + appearance: 'checkbox', + richContent: { + content: 'I agree to the {{link}}', + replacements: { + link: { + type: 'link', + value: 'terms and conditions', + href: 'https://example.com/terms', + target: '_blank', + }, + }, + }, + }; + const result = returnBooleanCollector(field, 0); + expect(result.output.richContent).toEqual({ + content: 'I agree to the {{link}}', + replacements: [ + { + key: 'link', + type: 'link', + value: 'terms and conditions', + href: 'https://example.com/terms', + target: '_blank', + }, + ], + }); + }); + + it('should omit richContent from output when field has no richContent', () => { + const field: SingleCheckboxField = { + type: 'SINGLE_CHECKBOX', + inputType: 'BOOLEAN', + key: 'accept-terms', + label: 'Accept Terms', + required: false, + appearance: 'checkbox', + }; + const result = returnBooleanCollector(field, 0); + expect(result.output).not.toHaveProperty('richContent'); + }); +}); diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index 1215aa094e..f98d8d5411 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -35,10 +35,10 @@ import type { ReadOnlyCollector, RichTextCollector, RichContentLink, - AgreementCollector, PhoneNumberExtensionOutputValue, PasswordCollector, ValidatedPasswordCollector, + BooleanCollector, } from './collector.types.js'; import type { DeviceAuthenticationField, @@ -274,6 +274,35 @@ export function returnSingleValueCollector< options: options, }, } as InferSingleValueCollectorType<'SingleSelectCollector'>; + } else if (collectorType === 'BooleanCollector') { + const richContent = + 'richContent' in field && field.richContent + ? { + content: field.richContent.content, + replacements: normalizeReplacements(field.richContent.replacements ?? {}), + } + : undefined; + + return { + category: 'SingleValueCollector', + error: error || null, + type: collectorType, + id: `${field.key}-${idx}`, + name: field.key, + input: { + key: field.key, + value: false, + type: field.type, + }, + output: { + key: field.key, + label: field.label, + type: field.type, + value: false, + appearance: ('appearance' in field && field.appearance) || '', + ...(richContent && { richContent }), + }, + } as InferSingleValueCollectorType<'BooleanCollector'>; } else if (collectorType === 'ValidatedBooleanCollector') { const validationArray = []; if ('required' in field && field.required === true) { @@ -585,6 +614,16 @@ export function returnSingleSelectCollector(field: SingleSelectField, idx: numbe return returnSingleValueCollector(field, idx, 'SingleSelectCollector', data); } +/** + * @function returnBooleanCollector - Creates a BooleanCollector (no validation). + * @param {SingleCheckboxField} field - The field object containing key, label, type, required, and validation. + * @param {number} idx - The index to be used in the id of the BooleanCollector. + * @returns {BooleanCollector} The constructed BooleanCollector object. + */ +export function returnBooleanCollector(field: SingleCheckboxField, idx: number): BooleanCollector { + return returnSingleValueCollector(field, idx, 'BooleanCollector') as BooleanCollector; +} + /** * @function returnValidatedBooleanCollector - Creates a ValidatedBooleanCollector object based on the provided field and index. * @param {SingleCheckboxField} field - The field object containing key, label, type, required, and validation. @@ -936,10 +975,10 @@ export function returnNoValueCollector< * @returns {ReadOnlyCollector | RichTextCollector} The constructed collector. */ export function returnReadOnlyCollector( - field: ReadOnlyField, + field: ReadOnlyField | AgreementField, idx: number, ): ReadOnlyCollector | RichTextCollector { - if (field.richContent) { + if (field.type === 'LABEL' && field.richContent) { const base = returnNoValueCollector(field, idx, 'RichTextCollector'); return { ...base, @@ -960,6 +999,7 @@ export function returnReadOnlyCollector( output: { ...base.output, content: field.content, + ...(field.type === 'AGREEMENT' && field.titleEnabled && { title: field.title ?? '' }), }, }; } @@ -983,29 +1023,6 @@ export function returnQrCodeCollector(field: QrCodeField, idx: number): QrCodeCo }; } -/** - * @function returnAgreementCollector - Creates an AgreementCollector object based on the provided field and index. - * @param {AgreementField} field - The field object containing key, label, type, and agreement details. - * @param {number} idx - The index to be used in the id of the AgreementCollector. - * @returns {AgreementCollector} The constructed AgreementCollector object. - */ -export function returnAgreementCollector(field: AgreementField, idx: number): AgreementCollector { - const base = returnNoValueCollector(field, idx, 'AgreementCollector'); - return { - ...base, - output: { - ...base.output, - titleEnabled: field.titleEnabled, - title: field.title, - agreement: { - id: field.agreement?.id ?? '', - useDynamicAgreement: field.agreement?.useDynamicAgreement ?? false, - }, - enabled: field.enabled ?? false, - }, - }; -} - /** * @function returnValidator - Creates a validator function based on the provided collector * @param {ValidatedTextCollector | | ValidatedBooleanCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors} collector - The collector to which the value will be validated diff --git a/packages/davinci-client/src/lib/node.reducer.test.ts b/packages/davinci-client/src/lib/node.reducer.test.ts index d1fcc95655..0d7109667e 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -20,9 +20,10 @@ import type { PollingCollector, ProtectCollector, QrCodeCollector, - AgreementCollector, + ReadOnlyCollector, SubmitCollector, TextCollector, + BooleanCollector, ValidatedBooleanCollector, } from './collector.types.js'; import type { FidoAuthenticationOptions, FidoRegistrationOptions } from './davinci.types.js'; @@ -484,22 +485,17 @@ describe('The node collector reducer', () => { { category: 'NoValueCollector', error: null, - type: 'AgreementCollector', + type: 'ReadOnlyCollector', id: 'agreement-field-0', name: 'agreement-field-0', output: { key: 'agreement-field-0', label: 'Please accept the terms and conditions', type: 'AGREEMENT', - titleEnabled: true, + content: 'Please accept the terms and conditions', title: 'Terms and Conditions', - agreement: { - id: 'agreement-123', - useDynamicAgreement: false, - }, - enabled: true, }, - } satisfies AgreementCollector, + } satisfies ReadOnlyCollector, ]); }); }); @@ -1618,7 +1614,7 @@ describe('The node collector reducer with ValidatedBooleanCollector', () => { inputType: 'BOOLEAN', key: 'accept-terms', label: 'Accept Terms', - required: false, + required: true, }, ], formData: {}, @@ -1636,7 +1632,7 @@ describe('The node collector reducer with ValidatedBooleanCollector', () => { key: 'accept-terms', value: false, type: 'SINGLE_CHECKBOX', - validation: [], + validation: [{ type: 'required', message: 'Value cannot be empty', rule: true }], }, output: { key: 'accept-terms', @@ -1778,7 +1774,7 @@ describe('The node collector reducer with ValidatedBooleanCollector', () => { inputType: 'BOOLEAN', key: 'accept-terms', label: 'Accept Terms', - required: false, + required: true, appearance: 'checkbox', richContent: { content: 'I agree to the {{tos}}', @@ -1808,7 +1804,7 @@ describe('The node collector reducer with ValidatedBooleanCollector', () => { key: 'accept-terms', value: false, type: 'SINGLE_CHECKBOX', - validation: [], + validation: [{ type: 'required', message: 'Value cannot be empty', rule: true }], }, output: { key: 'accept-terms', @@ -1834,6 +1830,164 @@ describe('The node collector reducer with ValidatedBooleanCollector', () => { }); }); +describe('The node collector reducer with BooleanCollector', () => { + it('should produce a BooleanCollector from a non-required SINGLE_CHECKBOX field', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + type: 'SINGLE_CHECKBOX', + inputType: 'BOOLEAN', + key: 'accept-terms', + label: 'Accept Terms', + required: false, + }, + ], + formData: {}, + }, + }; + const result = nodeCollectorReducer(undefined, action); + expect(result).toEqual([ + { + category: 'SingleValueCollector', + error: null, + type: 'BooleanCollector', + id: 'accept-terms-0', + name: 'accept-terms', + input: { + key: 'accept-terms', + value: false, + type: 'SINGLE_CHECKBOX', + }, + output: { + key: 'accept-terms', + label: 'Accept Terms', + type: 'SINGLE_CHECKBOX', + value: false, + appearance: '', + }, + } satisfies BooleanCollector, + ]); + }); + + it('should handle collector updates (toggle to true)', () => { + const action = { + type: 'node/update', + payload: { + id: 'accept-terms-0', + value: true, + }, + }; + const state: BooleanCollector[] = [ + { + category: 'SingleValueCollector', + error: null, + type: 'BooleanCollector', + id: 'accept-terms-0', + name: 'accept-terms', + input: { + key: 'accept-terms', + value: false, + type: 'SINGLE_CHECKBOX', + }, + output: { + key: 'accept-terms', + label: 'Accept Terms', + type: 'SINGLE_CHECKBOX', + value: false, + appearance: 'checkbox', + }, + }, + ]; + expect(nodeCollectorReducer(state, action)).toStrictEqual([ + { + category: 'SingleValueCollector', + error: null, + type: 'BooleanCollector', + id: 'accept-terms-0', + name: 'accept-terms', + input: { + key: 'accept-terms', + value: true, + type: 'SINGLE_CHECKBOX', + }, + output: { + key: 'accept-terms', + label: 'Accept Terms', + type: 'SINGLE_CHECKBOX', + value: false, + appearance: 'checkbox', + }, + }, + ]); + }); + + it('should normalise richContent replacements from Record to RichContentLink[]', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + type: 'SINGLE_CHECKBOX', + inputType: 'BOOLEAN', + key: 'accept-terms', + label: 'Accept Terms', + required: false, + appearance: 'checkbox', + richContent: { + content: 'I agree to the {{tos}}', + replacements: { + tos: { + type: 'link', + value: 'Terms of Service', + href: 'https://example.com/tos', + target: '_blank', + }, + }, + }, + }, + ], + formData: {}, + }, + }; + const result = nodeCollectorReducer(undefined, action); + expect(result).toEqual([ + { + category: 'SingleValueCollector', + error: null, + type: 'BooleanCollector', + id: 'accept-terms-0', + name: 'accept-terms', + input: { + key: 'accept-terms', + value: false, + type: 'SINGLE_CHECKBOX', + }, + output: { + key: 'accept-terms', + label: 'Accept Terms', + type: 'SINGLE_CHECKBOX', + value: false, + appearance: 'checkbox', + richContent: { + content: 'I agree to the {{tos}}', + replacements: [ + { + key: 'tos', + type: 'link', + value: 'Terms of Service', + href: 'https://example.com/tos', + target: '_blank', + }, + ], + }, + }, + } satisfies BooleanCollector, + ]); + }); +}); + describe('The node collector reducer with FidoAuthenticationFieldValue', () => { it('should handle collector updates ', () => { // todo: declare inputValue type as FidoAuthenticationInputValue diff --git a/packages/davinci-client/src/lib/node.reducer.ts b/packages/davinci-client/src/lib/node.reducer.ts index 1dd74a6d53..0348445eda 100644 --- a/packages/davinci-client/src/lib/node.reducer.ts +++ b/packages/davinci-client/src/lib/node.reducer.ts @@ -20,6 +20,7 @@ import { returnIdpCollector, returnSubmitCollector, returnTextCollector, + returnBooleanCollector, returnValidatedBooleanCollector, returnSingleSelectCollector, returnMultiSelectCollector, @@ -32,7 +33,6 @@ import { returnFidoRegistrationCollector, returnFidoAuthenticationCollector, returnQrCodeCollector, - returnAgreementCollector, } from './collector.utils.js'; import type { DaVinciField, UnknownField } from './davinci.types.js'; import type { PhoneNumberOutputValue, PhoneNumberExtensionOutputValue } from './collector.types.js'; @@ -81,7 +81,7 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build * Some collectors may not have the same properties as others; * LABEL field types are one of them, so let's catch them first. */ - if (field.type === 'LABEL') { + if (field.type === 'LABEL' || field.type === 'AGREEMENT') { return returnReadOnlyCollector(field, idx); } @@ -89,10 +89,6 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build return returnQrCodeCollector(field, idx); } - if (field.type === 'AGREEMENT') { - return returnAgreementCollector(field, idx); - } - // *Some* collectors may have default or existing data to display const data = action.payload.formData && @@ -137,7 +133,9 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build return returnObjectValueCollector(field, idx, prefillData); } case 'SINGLE_CHECKBOX': { - return returnValidatedBooleanCollector(field, idx); + return field.required === true + ? returnValidatedBooleanCollector(field, idx) + : returnBooleanCollector(field, idx); } case 'TEXT': { const str = data as string; @@ -206,7 +204,10 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build collector.category === 'ValidatedSingleValueCollector' || collector.category === 'SingleValueAutoCollector' ) { - if (collector.type === 'ValidatedBooleanCollector') { + if ( + collector.type === 'BooleanCollector' || + collector.type === 'ValidatedBooleanCollector' + ) { if (typeof action.payload.value !== 'boolean') { throw new Error('Value argument must be a boolean'); } diff --git a/packages/davinci-client/src/lib/node.types.test-d.ts b/packages/davinci-client/src/lib/node.types.test-d.ts index cab5ce8e90..78e3b2e006 100644 --- a/packages/davinci-client/src/lib/node.types.test-d.ts +++ b/packages/davinci-client/src/lib/node.types.test-d.ts @@ -16,7 +16,7 @@ import type { SuccessNode, } from './node.types.js'; import type { ErrorDetail, Links } from './davinci.types.js'; -import { +import type { ActionCollector, FlowCollector, MultiSelectCollector, @@ -24,6 +24,7 @@ import { ValidatedPasswordCollector, ReadOnlyCollector, RichTextCollector, + BooleanCollector, ValidatedBooleanCollector, SingleSelectCollector, SingleValueCollector, @@ -41,7 +42,6 @@ import { FidoRegistrationCollector, FidoAuthenticationCollector, QrCodeCollector, - AgreementCollector, } from './collector.types.js'; // ErrorDetail and Links are used as part of the DaVinciError and server._links types respectively @@ -241,6 +241,7 @@ describe('Node Types', () => { | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector + | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | ValidatedTextCollector @@ -249,7 +250,6 @@ describe('Node Types', () => { | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector - | AgreementCollector | UnknownCollector >(); diff --git a/packages/davinci-client/src/lib/node.types.ts b/packages/davinci-client/src/lib/node.types.ts index 657feb5085..61adfefbe0 100644 --- a/packages/davinci-client/src/lib/node.types.ts +++ b/packages/davinci-client/src/lib/node.types.ts @@ -14,6 +14,7 @@ import type { IdpCollector, SubmitCollector, ActionCollector, + BooleanCollector, ValidatedBooleanCollector, SingleValueCollector, SingleSelectCollector, @@ -30,7 +31,6 @@ import type { FidoRegistrationCollector, FidoAuthenticationCollector, QrCodeCollector, - AgreementCollector, PhoneNumberExtensionCollector, } from './collector.types.js'; import type { Links } from './davinci.types.js'; @@ -40,6 +40,7 @@ export type Collectors = | PasswordCollector | ValidatedPasswordCollector | TextCollector + | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector @@ -59,7 +60,6 @@ export type Collectors = | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector - | AgreementCollector | UnknownCollector; export interface CollectorErrors {