diff --git a/.changeset/lemon-monkeys-fail.md b/.changeset/lemon-monkeys-fail.md new file mode 100644 index 0000000000..a140889076 --- /dev/null +++ b/.changeset/lemon-monkeys-fail.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': minor +--- + +Add support for MFA OTP field support with added collectors diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index ccf8a0e968..a415572479 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -28,6 +28,7 @@ import type { SingleValueCollectors, IdpCollector, MultiSelectCollector, + ObjectValueCollectors, } from './collector.types.js'; import type { InitFlow, Updater, Validator } from './client.types.js'; import { returnValidator } from './collector.utils.js'; @@ -167,7 +168,9 @@ export async function davinci({ * @param {SingleValueCollector} collector - the collector to update * @returns {function} - a function to call for updating collector value */ - update: (collector: SingleValueCollectors | MultiSelectCollector): Updater => { + update: ( + collector: SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors, + ): Updater => { if (!collector.id) { console.error('Argument for `collector` has no ID'); return function () { @@ -197,7 +200,8 @@ export async function davinci({ if ( collectorToUpdate.category !== 'MultiValueCollector' && collectorToUpdate.category !== 'SingleValueCollector' && - collectorToUpdate.category !== 'ValidatedSingleValueCollector' + collectorToUpdate.category !== 'ValidatedSingleValueCollector' && + collectorToUpdate.category !== 'ObjectValueCollector' ) { console.error( 'Collector is not a MultiValueCollector, SingleValueCollector or ValidatedSingleValueCollector and cannot be updated', diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index e334491f60..43af1ec057 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -4,6 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ + /** ********************************************************************* * SINGLE-VALUE COLLECTORS */ @@ -15,10 +16,11 @@ export type SingleValueCollectorTypes = | 'PasswordCollector' | 'SingleValueCollector' | 'SingleSelectCollector' + | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; -interface SelectorOptions { +interface SelectorOption { label: string; value: string; } @@ -90,7 +92,7 @@ export interface SingleSelectCollectorWithValue = ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector - : T extends 'PasswordCollector' - ? PasswordCollector - : /** - * 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 'ValidatedTextCollector' + ? ValidatedTextCollector + : T extends 'PasswordCollector' + ? PasswordCollector + : /** + * 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 @@ -198,7 +202,7 @@ export interface MultiValueCollectorWithValue label: string; type: string; value: string[]; - options: SelectorOptions[]; + options: SelectorOption[]; }; } @@ -246,6 +250,101 @@ export type MultiValueCollector = export type MultiSelectCollector = MultiValueCollectorWithValue<'MultiSelectCollector'>; +/** ********************************************************************* + * OBJECT COLLECTORS + */ + +export type ObjectValueCollectorTypes = + | 'DeviceAuthenticationCollector' + | 'DeviceRegistrationCollector' + | 'ObjectValueCollector' + | 'ObjectSelectCollector'; + +interface ObjectOptionWithValue { + type: string; + label: string; + content: string; + default: boolean; + value: string; + key: string; +} + +interface ObjectOptionNoValue { + type: string; + label: string; + content: string; + value: string; + key: string; +} + +interface ObjectValue { + type: string; + id: string; + value: string; +} + +export interface ObjectValueCollectorNoValue { + category: 'ObjectValueCollector'; + error: string | null; + type: T; + id: string; + name: string; + input: { + key: string; + value: string | null; + type: string; + }; + output: { + key: string; + label: string; + type: string; + options: ObjectOptionNoValue[]; + }; +} + +export interface ObjectValueCollectorWithValue { + category: 'ObjectValueCollector'; + error: string | null; + type: T; + id: string; + name: string; + input: { + key: string; + value: ObjectValue | null; + type: string; + }; + output: { + key: string; + label: string; + type: string; + options: ObjectOptionWithValue[]; + }; +} + +export type InferValueObjectCollectorType = + T extends 'DeviceAuthenticationCollector' + ? DeviceAuthenticationCollector + : T extends 'DeviceRegistrationCollector' + ? DeviceRegistrationCollector + : + | ObjectValueCollectorWithValue<'ObjectValueCollector'> + | ObjectValueCollectorNoValue<'ObjectValueCollector'>; + +export type ObjectValueCollectors = + | ObjectValueCollectorWithValue<'DeviceAuthenticationCollector'> + | ObjectValueCollectorNoValue<'DeviceRegistrationCollector'> + | ObjectValueCollectorWithValue<'ObjectSelectCollector'> + | ObjectValueCollectorNoValue<'ObjectSelectCollector'>; + +export type ObjectValueCollector = + | ObjectValueCollectorWithValue + | ObjectValueCollectorNoValue; + +export type DeviceRegistrationCollector = + ObjectValueCollectorNoValue<'DeviceRegistrationCollector'>; +export type DeviceAuthenticationCollector = + ObjectValueCollectorWithValue<'DeviceAuthenticationCollector'>; + /** ********************************************************************* * ACTION COLLECTORS */ diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index fd9f2a01e6..786d1dca45 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -18,12 +18,15 @@ import { returnValidator, returnReadOnlyCollector, returnNoValueCollector, + returnObjectSelectCollector, } from './collector.utils.js'; import type { DaVinciField, - ReadOnlyFieldValue, - RedirectFieldValue, - StandardFieldValue, + DeviceAuthenticationField, + DeviceRegistrationField, + ReadOnlyField, + RedirectField, + StandardField, } from './davinci.types.js'; import { ValidatedTextCollector } from './collector.types.js'; @@ -52,7 +55,7 @@ describe('Action Collectors', () => { }); it('should handle error cases properly', () => { - const invalidField = {} as StandardFieldValue; + const invalidField = {} as StandardField; const result = returnFlowCollector(invalidField, 1); expect(result.error).toContain('Label is not found'); expect(result.error).toContain('Type is not found'); @@ -60,7 +63,7 @@ describe('Action Collectors', () => { }); describe('returnIdpCollector', () => { - const mockSocialField: RedirectFieldValue = { + const mockSocialField: RedirectField = { key: 'google-login', label: 'Continue with Google', type: 'SOCIAL_LOGIN_BUTTON', @@ -105,7 +108,7 @@ describe('Action Collectors', () => { }); it('should handle error cases properly', () => { - const invalidField = {} as StandardFieldValue; + const invalidField = {} as StandardField; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const result = returnIdpCollector(invalidField, 1); @@ -137,7 +140,7 @@ describe('Action Collectors', () => { }); it('should handle error cases properly', () => { - const invalidField = {} as StandardFieldValue; + const invalidField = {} as StandardField; const result = returnSubmitCollector(invalidField, 1); expect(result.error).toContain('Key is not found'); expect(result.type).toBe('SubmitCollector'); @@ -145,7 +148,7 @@ describe('Action Collectors', () => { }); describe('returnActionCollector', () => { - const mockField: StandardFieldValue = { + const mockField: StandardField = { key: 'testKey', label: 'Test Label', type: 'TEXT', @@ -227,6 +230,12 @@ describe('Action Collectors', () => { }); }); + it('creates an action collector from flow link field type', () => { + const result = returnFlowCollector(mockField, 1); + expect(result.type).toBe('FlowCollector'); + expect(result.output).not.toHaveProperty('value'); + }); + it('handles missing authentication URL for social login', () => { const result = returnActionCollector(mockField, 1, 'IdpCollector'); if ('url' in result.output) { @@ -237,7 +246,7 @@ describe('Action Collectors', () => { it('should return an error message when field is missing key, label, or type', () => { const field = {}; const idx = 3; - const result = returnActionCollector(field as StandardFieldValue, idx, 'ActionCollector'); + const result = returnActionCollector(field as StandardField, idx, 'ActionCollector'); expect(result.error).toBe( 'Label is not found in the field object. Type is not found in the field object. Key is not found in the field object. ', ); @@ -301,7 +310,7 @@ describe('Single Value Collectors', () => { const field = {}; const idx = 3; const result = returnSingleValueCollector( - field as StandardFieldValue, + field as StandardField, idx, 'SingleValueCollector', ); @@ -373,7 +382,11 @@ describe('Single Value Collectors', () => { expect(result.type).toBe('SingleSelectCollector'); expect(result.output).toHaveProperty('value', ''); }); + }); +}); +describe('Multi-Value Collectors', () => { + describe('Specialized Multi-Select Collectors', () => { it('creates a multi-select collector from combobox field type', () => { const comboField: DaVinciField = { type: 'COMBOBOX', @@ -396,17 +409,124 @@ describe('Single Value Collectors', () => { expect(result.type).toBe('MultiSelectCollector'); expect(result.output).toHaveProperty('value', []); }); + }); +}); - it('creates an action collector from flow link field type', () => { - const result = returnFlowCollector(mockField, 1); - expect(result.type).toBe('FlowCollector'); - expect(result.output).not.toHaveProperty('value'); +describe('Object value collectors', () => { + describe('returnDeviceAuthenticationCollector', () => { + const mockField: DeviceAuthenticationField = { + key: 'device-auth-key', + label: 'Device Authentication', + type: 'DEVICE_AUTHENTICATION', + devices: [ + { + type: 'device1', + iconSrc: 'icon1.png', + title: 'Device 1', + id: '123123', + default: true, + value: 'device1-value', + }, + { + type: 'device2', + iconSrc: 'icon2.png', + title: 'Device 2', + id: '345345', + default: false, + value: 'device2-value', + }, + ], + required: true, + }; + + const transformedDevices = mockField.devices.map((device) => ({ + label: device.title, + value: device.id, + content: device.value, + type: device.type, + key: device.id, + default: device.default, + })); + + it('should create a valid DeviceAuthenticationCollector', () => { + const result = returnObjectSelectCollector(mockField, 1); + expect(result).toEqual({ + category: 'ObjectValueCollector', + error: null, + type: 'DeviceAuthenticationCollector', + id: 'device-auth-key-1', + name: 'device-auth-key', + input: { + key: mockField.key, + value: null, + type: mockField.type, + }, + output: { + key: mockField.key, + label: mockField.label, + type: mockField.type, + options: transformedDevices, + }, + }); + }); + }); + + describe('returnDeviceRegistrationCollector', () => { + const mockField: DeviceRegistrationField = { + key: 'device-reg-key', + label: 'Device Registration', + type: 'DEVICE_REGISTRATION', + devices: [ + { + type: 'device1', + iconSrc: 'icon1.png', + title: 'Device 1', + description: 'Device 1 Description', + }, + { + type: 'device2', + iconSrc: 'icon2.png', + title: 'Device 2', + description: 'Device 2 Description', + }, + ], + required: true, + }; + + const transformedDevices = mockField.devices.map((device, idx) => ({ + label: device.title, + value: device.type, + content: device.description, + type: device.type, + key: `${device.type}-${idx}`, + })); + + it('should create a valid DeviceRegistrationCollector', () => { + const result = returnObjectSelectCollector(mockField, 1); + expect(result).toEqual({ + category: 'ObjectValueCollector', + error: null, + type: 'DeviceRegistrationCollector', + id: 'device-reg-key-1', + name: 'device-reg-key', + input: { + key: mockField.key, + value: null, + type: mockField.type, + }, + output: { + key: mockField.key, + label: mockField.label, + type: mockField.type, + options: transformedDevices, + }, + }); }); }); }); describe('No Value Collectors', () => { - const mockField: ReadOnlyFieldValue = { + const mockField: ReadOnlyField = { content: 'Test Label', type: 'LABEL', }; diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index dfbc27f87d..94395bca84 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -19,14 +19,18 @@ import type { InferNoValueCollectorType, ValidatedSingleValueCollectorWithValue, ValidatedTextCollector, + InferValueObjectCollectorType, + ObjectValueCollectorTypes, } from './collector.types.js'; import type { - MultiSelectFieldValue, - ReadOnlyFieldValue, - RedirectFieldValue, - SingleSelectFieldValue, - StandardFieldValue, - ValidatedFieldValue, + DeviceAuthenticationField, + DeviceRegistrationField, + MultiSelectField, + ReadOnlyField, + RedirectField, + SingleSelectField, + StandardField, + ValidatedField, } from './davinci.types.js'; /** @@ -37,7 +41,7 @@ import type { * @returns {ActionCollector} The constructed ActionCollector object. */ export function returnActionCollector( - field: RedirectFieldValue | StandardFieldValue, + field: RedirectField | StandardField, idx: number, collectorType: CollectorType, ): ActionCollectors { @@ -95,7 +99,7 @@ export function returnActionCollector(field: Field, idx: number, collectorType: CollectorType, data?: string) { let error = ''; @@ -253,7 +257,7 @@ export function returnSingleValueCollector< * @param {number} idx - The index to be used in the id of the PasswordCollector. * @returns {PasswordCollector} The constructed PasswordCollector object. */ -export function returnPasswordCollector(field: StandardFieldValue, idx: number) { +export function returnPasswordCollector(field: StandardField, idx: number) { return returnSingleValueCollector(field, idx, 'PasswordCollector'); } @@ -263,7 +267,7 @@ export function returnPasswordCollector(field: StandardFieldValue, idx: number) * @param {number} idx - The index to be used in the id of the TextCollector. * @returns {TextCollector} The constructed TextCollector object. */ -export function returnTextCollector(field: StandardFieldValue, idx: number, data: string) { +export function returnTextCollector(field: StandardField, idx: number, data: string) { return returnSingleValueCollector(field, idx, 'TextCollector', data); } /** @@ -272,11 +276,7 @@ export function returnTextCollector(field: StandardFieldValue, idx: number, data * @param {number} idx - The index to be used in the id of the SingleCollector. * @returns {SingleValueCollector} The constructed SingleCollector object. */ -export function returnSingleSelectCollector( - field: SingleSelectFieldValue, - idx: number, - data: string, -) { +export function returnSingleSelectCollector(field: SingleSelectField, idx: number, data: string) { return returnSingleValueCollector(field, idx, 'SingleSelectCollector', data); } @@ -288,7 +288,7 @@ export function returnSingleSelectCollector( * @returns {MultiValueCollector} The constructed MultiValueCollector object. */ export function returnMultiValueCollector< - Field extends MultiSelectFieldValue, + Field extends MultiSelectField, CollectorType extends MultiValueCollectorTypes = 'MultiValueCollector', >(field: Field, idx: number, collectorType: CollectorType, data?: string[]) { let error = ''; @@ -332,23 +332,108 @@ export function returnMultiValueCollector< * @param {number} idx - The index to be used in the id of the DropDownCollector. * @returns {SingleValueCollector} The constructed DropDownCollector object. */ -export function returnMultiSelectCollector( - field: MultiSelectFieldValue, +export function returnMultiSelectCollector(field: MultiSelectField, idx: number, data: string[]) { + return returnMultiValueCollector(field, idx, 'MultiSelectCollector', data); +} + +/** + * @function returnObjectCollector - Creates a ObjectCollector object based on the provided field, index, and optional collector type. + * @param {DaVinciField} field - The field object containing key, label, type, and links. + * @param {number} idx - The index to be used in the id of the ObjectCollector. + * @param {ObjectValueCollectorTypes} [collectorType] - Optional type of the ObjectCollector. + * @returns {ObjectCollector} The constructed ObjectCollector object. + */ +export function returnObjectCollector< + Field extends DeviceAuthenticationField | DeviceRegistrationField, + CollectorType extends ObjectValueCollectorTypes = 'ObjectValueCollector', +>(field: Field, idx: number, collectorType: CollectorType) { + let error = ''; + if (!('key' in field)) { + error = `${error}Key is not found in the field object. `; + } + if (!('label' in field)) { + error = `${error}Label is not found in the field object. `; + } + if (!('type' in field)) { + error = `${error}Type is not found in the field object. `; + } + if (!('devices' in field)) { + error = `${error}Options are not found in the field object. `; + } + + let devices; + if (Array.isArray(field.devices) && field.devices.length === 0) { + error = `${error}Options are not an array or is empty. `; + } + if (field.type === 'DEVICE_AUTHENTICATION') { + // Map DaVinci spec to normalized SDK API + devices = field.devices.map((device) => ({ + type: device.type, + label: device.title, + content: device.value, + value: device.id, + key: device.id, + default: device.default, + })); + } else { + // Map DaVinci spec to normalized SDK API + devices = field.devices.map((device, idx) => ({ + type: device.type, + label: device.title, + content: device.description, + value: device.type, + key: `${device.type}-${idx}`, + })); + } + + return { + category: 'ObjectValueCollector', + error: error || null, + type: collectorType || 'ObjectValueCollector', + id: `${field.key}-${idx}`, + name: field.key, + input: { + key: field.key, + value: null, + type: field.type, + }, + output: { + key: field.key, + label: field.label, + type: field.type, + options: devices || [], + }, + } as InferValueObjectCollectorType; +} + +/** + * @function returnObjectSelectCollector - Creates a DropDownCollector object based on the provided field and index. + * @param {DaVinciField} field - The field object containing key, label, type, and links. + * @param {number} idx - The index to be used in the id of the DropDownCollector. + * @returns {SingleValueCollector} The constructed DropDownCollector object. + */ +export function returnObjectSelectCollector( + field: DeviceAuthenticationField | DeviceRegistrationField, idx: number, - data: string[], ) { - return returnMultiValueCollector(field, idx, 'MultiSelectCollector', data); + return returnObjectCollector( + field, + idx, + field.type === 'DEVICE_AUTHENTICATION' + ? 'DeviceAuthenticationCollector' + : 'DeviceRegistrationCollector', + ); } /** - * @function returnMultiValueCollector - Creates a MultiValueCollector object based on the provided field, index, and optional collector type. + * @function returnNoValueCollector - Creates a NoValueCollector object based on the provided field, index, and optional collector type. * @param {DaVinciField} field - The field object containing key, label, type, and links. - * @param {number} idx - The index to be used in the id of the MultiValueCollector. - * @param {MultiValueCollectorTypes} [collectorType] - Optional type of the MultiValueCollector. - * @returns {MultiValueCollector} The constructed MultiValueCollector object. + * @param {number} idx - The index to be used in the id of the NoValueCollector. + * @param {NoValueCollectorTypes} [collectorType] - Optional type of the NoValueCollector. + * @returns {NoValueCollector} The constructed NoValueCollector object. */ export function returnNoValueCollector< - Field extends ReadOnlyFieldValue, + Field extends ReadOnlyField, CollectorType extends NoValueCollectorTypes = 'NoValueCollector', >(field: Field, idx: number, collectorType: CollectorType) { let error = ''; @@ -374,12 +459,12 @@ export function returnNoValueCollector< } /** - * @function returnMultiSelectCollector - Creates a DropDownCollector object based on the provided field and index. + * @function returnReadOnlyCollector - Creates a ReadOnlyCollector object based on the provided field and index. * @param {DaVinciField} field - The field object containing key, label, type, and links. - * @param {number} idx - The index to be used in the id of the DropDownCollector. - * @returns {SingleValueCollector} The constructed DropDownCollector object. + * @param {number} idx - The index to be used in the id of the ReadOnlyCollector. + * @returns {ReadOnlyCollector} The constructed ReadOnlyCollector object. */ -export function returnReadOnlyCollector(field: ReadOnlyFieldValue, idx: number) { +export function returnReadOnlyCollector(field: ReadOnlyField, idx: number) { return returnNoValueCollector(field, idx, 'ReadOnlyCollector'); } diff --git a/packages/davinci-client/src/lib/davinci.types.ts b/packages/davinci-client/src/lib/davinci.types.ts index ccb1bcfd5d..6a3cef7ee0 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -52,7 +52,7 @@ export interface Links { }; } -export type StandardFieldValue = { +export type StandardField = { type: | 'PASSWORD' | 'PASSWORD_VERIFY' @@ -68,20 +68,20 @@ export type StandardFieldValue = { required?: boolean; }; -export type ReadOnlyFieldValue = { +export type ReadOnlyField = { type: 'LABEL'; content: string; key?: string; }; -export type RedirectFieldValue = { +export type RedirectField = { type: 'SOCIAL_LOGIN_BUTTON'; key: string; label: string; links: Links; }; -export type ValidatedFieldValue = { +export type ValidatedField = { type: 'TEXT'; key: string; label: string; @@ -92,7 +92,7 @@ export type ValidatedFieldValue = { }; }; -export type SingleSelectFieldValue = { +export type SingleSelectField = { inputType: 'SINGLE_SELECT'; key: string; label: string; @@ -104,7 +104,7 @@ export type SingleSelectFieldValue = { type: 'RADIO' | 'DROPDOWN'; }; -export type MultiSelectFieldValue = { +export type MultiSelectField = { inputType: 'MULTI_SELECT'; key: string; label: string; @@ -116,13 +116,46 @@ export type MultiSelectFieldValue = { type: 'CHECKBOX' | 'COMBOBOX'; }; +export type DeviceAuthenticationField = { + type: 'DEVICE_AUTHENTICATION'; + key: string; + label: string; + devices: { + type: string; + iconSrc: string; + title: string; + id: string; + default: boolean; + value: string; + }[]; + required: boolean; +}; + +export type DeviceRegistrationField = { + type: 'DEVICE_REGISTRATION'; + key: string; + label: string; + devices: { + type: string; + iconSrc: string; + title: string; + description: string; + }[]; + required: boolean; +}; + +export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField; +export type MultiValueFields = MultiSelectField; +export type ReadOnlyFields = ReadOnlyField; +export type RedirectFields = RedirectField; +export type SingleValueFields = StandardField | ValidatedField | SingleSelectField; + export type DaVinciField = - | StandardFieldValue - | ReadOnlyFieldValue - | RedirectFieldValue - | ValidatedFieldValue - | SingleSelectFieldValue - | MultiSelectFieldValue; + | ComplexValueFields + | MultiValueFields + | ReadOnlyFields + | RedirectFields + | SingleValueFields; /** * Next or Continuation Response DaVinci API diff --git a/packages/davinci-client/src/lib/node.reducer.test.ts b/packages/davinci-client/src/lib/node.reducer.test.ts index fa45041de5..81fd1dcb2c 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -7,7 +7,13 @@ import { describe, it, expect } from 'vitest'; import { nodeCollectorReducer } from './node.reducer.js'; -import { MultiSelectCollector, SubmitCollector, TextCollector } from './collector.types.js'; +import { + DeviceAuthenticationCollector, + DeviceRegistrationCollector, + MultiSelectCollector, + SubmitCollector, + TextCollector, +} from './collector.types.js'; describe('The node collector reducer', () => { it('should return the initial state', () => { @@ -485,3 +491,205 @@ describe('The node collector reducer with MultiValueCollector', () => { ]); }); }); + +describe('The node collector reducer with DeviceAuthenticationFieldValue', () => { + it('should handle collector updates ', () => { + const action = { + type: 'node/update', + payload: { + id: 'device-0', + value: '42036625-37a5-4c7a-b7c4-ef778838c8e1', + }, + }; + const state: DeviceAuthenticationCollector[] = [ + { + category: 'ObjectValueCollector', + error: null, + type: 'DeviceAuthenticationCollector', + id: 'device-0', + name: 'device', + input: { + key: 'device', + value: null, + type: 'TEXT', + }, + output: { + key: 'device', + label: 'First Name', + type: 'TEXT', + options: [ + { + type: 'SMS', + label: 'Text Message', + value: '55122b45-b192-4a6e-ad54-e2fd8f1a0a47', + default: true, + content: '***-***-6036', + key: '55122b45-b192-4a6e-ad54-e2fd8f1a0a47', + }, + { + type: 'EMAIL', + label: 'Email', + value: '42036625-37a5-4c7a-b7c4-ef778838c8e1', + default: false, + content: 's***********1@pingidentity.com', + key: '42036625-37a5-4c7a-b7c4-ef778838c8e1', + }, + { + type: 'VOICE', + label: 'Voice', + value: 'e958e8c4-505a-4db1-9726-45bf38bed4da', + default: false, + content: '***-***-6036', + key: 'e958e8c4-505a-4db1-9726-45bf38bed4da', + }, + ], + }, + }, + ]; + expect(nodeCollectorReducer(state, action)).toStrictEqual([ + { + category: 'ObjectValueCollector', + error: null, + type: 'DeviceAuthenticationCollector', + id: 'device-0', + name: 'device', + input: { + key: 'device', + value: { + type: 'EMAIL', + id: '42036625-37a5-4c7a-b7c4-ef778838c8e1', + value: 's***********1@pingidentity.com', + }, + type: 'TEXT', + }, + output: { + key: 'device', + label: 'First Name', + type: 'TEXT', + options: [ + { + type: 'SMS', + label: 'Text Message', + value: '55122b45-b192-4a6e-ad54-e2fd8f1a0a47', + default: true, + content: '***-***-6036', + key: '55122b45-b192-4a6e-ad54-e2fd8f1a0a47', + }, + { + type: 'EMAIL', + label: 'Email', + value: '42036625-37a5-4c7a-b7c4-ef778838c8e1', + default: false, + content: 's***********1@pingidentity.com', + key: '42036625-37a5-4c7a-b7c4-ef778838c8e1', + }, + { + type: 'VOICE', + label: 'Voice', + value: 'e958e8c4-505a-4db1-9726-45bf38bed4da', + default: false, + content: '***-***-6036', + key: 'e958e8c4-505a-4db1-9726-45bf38bed4da', + }, + ], + }, + }, + ]); + }); +}); + +describe('The node collector reducer with DeviceRegistrationFieldValue', () => { + it('should handle collector updates ', () => { + const action = { + type: 'node/update', + payload: { + id: 'device-0', + value: 'EMAIL', + }, + }; + const state: DeviceRegistrationCollector[] = [ + { + category: 'ObjectValueCollector', + error: null, + type: 'DeviceRegistrationCollector', + id: 'device-0', + name: 'device', + input: { + key: 'device', + value: null, + type: 'TEXT', + }, + output: { + key: 'device', + label: 'First Name', + type: 'TEXT', + options: [ + { + type: 'EMAIL', + label: 'Email', + content: 'Receive an authentication passcode in your email.', + value: 'EMAIL', + key: 'VOICE-0', + }, + { + type: 'SMS', + label: 'Text Message', + content: 'Receive an authentication passcode in a text message.', + value: 'SMS', + key: 'SMS-1', + }, + { + type: 'VOICE', + label: 'Voice', + content: 'Receive a phone call with an authentication passcode.', + value: 'VOICE', + key: 'VOICE-2', + }, + ], + }, + }, + ]; + expect(nodeCollectorReducer(state, action)).toStrictEqual([ + { + category: 'ObjectValueCollector', + error: null, + type: 'DeviceRegistrationCollector', + id: 'device-0', + name: 'device', + input: { + key: 'device', + value: 'EMAIL', + type: 'TEXT', + }, + output: { + key: 'device', + label: 'First Name', + type: 'TEXT', + options: [ + { + type: 'EMAIL', + label: 'Email', + content: 'Receive an authentication passcode in your email.', + value: 'EMAIL', + key: 'VOICE-0', + }, + { + type: 'SMS', + label: 'Text Message', + content: 'Receive an authentication passcode in a text message.', + value: 'SMS', + key: 'SMS-1', + }, + { + type: 'VOICE', + label: 'Voice', + content: 'Receive a phone call with an authentication passcode.', + value: 'VOICE', + key: 'VOICE-2', + }, + ], + }, + }, + ]); + }); +}); diff --git a/packages/davinci-client/src/lib/node.reducer.ts b/packages/davinci-client/src/lib/node.reducer.ts index 39d4199f23..f1ab7a9f0d 100644 --- a/packages/davinci-client/src/lib/node.reducer.ts +++ b/packages/davinci-client/src/lib/node.reducer.ts @@ -23,6 +23,7 @@ import { returnSingleSelectCollector, returnMultiSelectCollector, returnReadOnlyCollector, + returnObjectSelectCollector, } from './collector.utils.js'; import type { DaVinciField } from './davinci.types.js'; import { @@ -37,6 +38,8 @@ import { TextCollector, ReadOnlyCollector, ValidatedTextCollector, + DeviceAuthenticationCollector, + DeviceRegistrationCollector, } from './collector.types.js'; /** @@ -68,6 +71,8 @@ const initialCollectorValues: ( | SingleValueCollector<'SingleValueCollector'> | SingleSelectCollector | MultiSelectCollector + | DeviceAuthenticationCollector + | DeviceRegistrationCollector | ReadOnlyCollector | ValidatedTextCollector )[] = []; @@ -122,6 +127,11 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build // No data to send return returnFlowCollector(field, idx); } + case 'DEVICE_AUTHENTICATION': + case 'DEVICE_REGISTRATION': { + // Intentional fall-through + return returnObjectSelectCollector(field, idx); + } case 'PASSWORD': case 'PASSWORD_VERIFY': { // No data to send @@ -194,5 +204,42 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build } return; } + + if (collector.type === 'DeviceAuthenticationCollector') { + if (typeof action.payload.id !== 'string') { + throw new Error('Index argument must be a string'); + } + // Iterate through the options object and find option to update + const option = collector.output.options.find( + (option) => option.value === action.payload.value, + ); + + if (!option) { + throw new Error('No option found matching value to update'); + } + + // Remap values back to DaVinci spec + collector.input.value = { + type: option.type, + id: option.value, + value: option.content, + }; + } + + if (collector.type === 'DeviceRegistrationCollector') { + if (typeof action.payload.id !== 'string') { + throw new Error('Index argument must be a string'); + } + // Iterate through the options object and find option to update + const option = collector.output.options.find( + (option) => option.value === action.payload.value, + ); + + if (!option) { + throw new Error('No option found matching value to update'); + } + + collector.input.value = option.type; + } }); }); 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 6780d7a0bd..8228f02f5e 100644 --- a/packages/davinci-client/src/lib/node.types.test-d.ts +++ b/packages/davinci-client/src/lib/node.types.test-d.ts @@ -28,6 +28,8 @@ import { SubmitCollector, TextCollector, ValidatedTextCollector, + DeviceRegistrationCollector, + DeviceAuthenticationCollector, } from './collector.types.js'; // ErrorDetail and Links are used as part of the DaVinciError and server._links types respectively @@ -220,6 +222,8 @@ describe('Node Types', () => { | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector + | DeviceAuthenticationCollector + | DeviceRegistrationCollector | ReadOnlyCollector | SingleSelectCollector | ValidatedTextCollector diff --git a/packages/davinci-client/src/lib/node.types.ts b/packages/davinci-client/src/lib/node.types.ts index 3f454194ef..3067667d65 100644 --- a/packages/davinci-client/src/lib/node.types.ts +++ b/packages/davinci-client/src/lib/node.types.ts @@ -14,6 +14,8 @@ import type { SingleValueCollector, SingleSelectCollector, MultiSelectCollector, + DeviceAuthenticationCollector, + DeviceRegistrationCollector, ReadOnlyCollector, ValidatedTextCollector, } from './collector.types.js'; @@ -30,6 +32,8 @@ export type Collectors = | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector + | DeviceAuthenticationCollector + | DeviceRegistrationCollector | ReadOnlyCollector | ValidatedTextCollector;