From 77ecd019d569f059d2dc34592ebe9fb308003b0b Mon Sep 17 00:00:00 2001 From: AJ Ancheta <7781450+ancheetah@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:52:40 -0400 Subject: [PATCH 1/3] feat(davinci-client): add support for extension in PhoneNumberCollector (SDKS-4670) --- .changeset/long-singers-do.md | 7 ++ e2e/davinci-app/components/object-value.ts | 1 + e2e/davinci-suites/src/form-fields.test.ts | 6 +- .../src/lib/collector.types.test-d.ts | 76 +++++++++++++++++++ .../davinci-client/src/lib/collector.types.ts | 47 ++++-------- .../src/lib/collector.utils.test.ts | 53 +++++++++++++ .../davinci-client/src/lib/collector.utils.ts | 5 ++ .../davinci-client/src/lib/davinci.types.ts | 3 +- .../src/lib/node.reducer.test.ts | 30 +++++++- 9 files changed, 192 insertions(+), 36 deletions(-) create mode 100644 .changeset/long-singers-do.md diff --git a/.changeset/long-singers-do.md b/.changeset/long-singers-do.md new file mode 100644 index 0000000000..986d246e0e --- /dev/null +++ b/.changeset/long-singers-do.md @@ -0,0 +1,7 @@ +--- +'@forgerock/davinci-client': minor +--- + +Add support for extension in PhoneNumberCollector + +BREAKING CHANGE: `ObjectValueCollectorWithObjectValue` type was removed diff --git a/e2e/davinci-app/components/object-value.ts b/e2e/davinci-app/components/object-value.ts index 77e964d765..f5e3ddc3e9 100644 --- a/e2e/davinci-app/components/object-value.ts +++ b/e2e/davinci-app/components/object-value.ts @@ -87,6 +87,7 @@ export default function objectValueComponent( updater({ phoneNumber: selectedValue, countryCode: collector.output.value?.countryCode || '', + extension: collector.output.value?.extension || '', } as any); }); diff --git a/e2e/davinci-suites/src/form-fields.test.ts b/e2e/davinci-suites/src/form-fields.test.ts index 0ad89d3e21..4800c68b23 100644 --- a/e2e/davinci-suites/src/form-fields.test.ts +++ b/e2e/davinci-suites/src/form-fields.test.ts @@ -42,9 +42,10 @@ test('Should render form fields', async ({ page }) => { await page.getByRole('button', { name: 'Submit' }).click(); const request = await requestPromise; - - const parsedData = JSON.parse(request.postData()); + const postData = request.postData(); + const parsedData = postData ? JSON.parse(postData) : {}; const data = parsedData.parameters.data; + expect(data.actionKey).toBe('submit'); expect(data.formData).toStrictEqual({ 'text-input-key': 'The input', @@ -55,6 +56,7 @@ test('Should render form fields', async ({ page }) => { 'phone-field': { phoneNumber: '1234567890', countryCode: 'GB', + extension: '4321', }, }); }); 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 5fe63303ad..5a9732fa8a 100644 --- a/packages/davinci-client/src/lib/collector.types.test-d.ts +++ b/packages/davinci-client/src/lib/collector.types.test-d.ts @@ -27,6 +27,12 @@ import type { ReadOnlyCollector, QrCodeCollector, AgreementCollector, + PhoneNumberCollector, + ObjectOptionsCollectorWithObjectValue, + InferValueObjectCollectorType, + PhoneNumberInputValue, + PhoneNumberOutputValue, + PhoneNumberOptions, } from './collector.types.js'; describe('Collector Types', () => { @@ -358,6 +364,76 @@ describe('Collector Types', () => { expectTypeOf(tCollector).toMatchTypeOf(); }); + + it('should correctly infer PhoneNumberCollector Type', () => { + const tCollector: InferValueObjectCollectorType<'PhoneNumberCollector'> = { + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberCollector', + id: '', + name: '', + input: { + key: '', + value: { countryCode: '', phoneNumber: '', extension: '' }, + type: '', + validation: null, + }, + output: { + key: '', + label: '', + type: '', + options: { showExtension: false }, + value: { countryCode: '', phoneNumber: '', extension: '' }, + }, + }; + + expectTypeOf(tCollector).toEqualTypeOf(); + }); + }); + + describe('ObjectValueCollector Types', () => { + it('should validate PhoneNumberCollector structure', () => { + expectTypeOf().toEqualTypeOf< + ObjectOptionsCollectorWithObjectValue< + 'PhoneNumberCollector', + PhoneNumberInputValue, + PhoneNumberOutputValue, + PhoneNumberOptions + > + >(); + expectTypeOf() + .toHaveProperty('category') + .toEqualTypeOf<'ObjectValueCollector'>(); + expectTypeOf() + .toHaveProperty('type') + .toEqualTypeOf<'PhoneNumberCollector'>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + it('should validate PhoneNumberCollector base type constraints', () => { + const collector: PhoneNumberCollector = { + category: 'ObjectValueCollector', + type: 'PhoneNumberCollector', + error: null, + id: 'test', + name: 'Test', + input: { + key: 'phone', + value: { countryCode: '+1', phoneNumber: '5555555555', extension: '' }, + type: 'string', + validation: null, + }, + output: { + key: 'phone', + label: 'Phone Number', + type: 'phone', + options: { showExtension: true }, + value: { countryCode: '+1', phoneNumber: '5555555555' }, + }, + }; + expectTypeOf(collector).toEqualTypeOf(); + }); }); describe('InferNoValueCollectorType', () => { diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index d99ebacfed..50f7a284e7 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -297,11 +297,17 @@ export interface DeviceValue { export interface PhoneNumberInputValue { countryCode: string; phoneNumber: string; + extension: string; } export interface PhoneNumberOutputValue { countryCode?: string; phoneNumber?: string; + extension?: string; +} + +export interface PhoneNumberOptions { + showExtension: boolean; } export interface ObjectOptionsCollectorWithStringValue< @@ -331,6 +337,7 @@ export interface ObjectOptionsCollectorWithObjectValue< T extends ObjectValueCollectorTypes, V = Record, D = Record, + O = Record, > { category: 'ObjectValueCollector'; error: string | null; @@ -341,38 +348,14 @@ export interface ObjectOptionsCollectorWithObjectValue< key: string; value: V; type: string; - validation: ValidationRequired[] | null; - }; - output: { - key: string; - label: string; - type: string; - options: DeviceOptionWithDefault[]; - value?: D | null; - }; -} - -export interface ObjectValueCollectorWithObjectValue< - T extends ObjectValueCollectorTypes, - IV = Record, - OV = Record, -> { - category: 'ObjectValueCollector'; - error: string | null; - type: T; - id: string; - name: string; - input: { - key: string; - value: IV; - type: string; validation: (ValidationRequired | ValidationPhoneNumber)[] | null; }; output: { key: string; label: string; type: string; - value?: OV | null; + options: O; + value?: D | null; }; } @@ -396,8 +379,7 @@ export type ObjectValueCollectors = export type ObjectValueCollector = | ObjectOptionsCollectorWithObjectValue - | ObjectOptionsCollectorWithStringValue - | ObjectValueCollectorWithObjectValue; + | ObjectOptionsCollectorWithStringValue; export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue< 'DeviceRegistrationCollector', @@ -405,12 +387,15 @@ export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue< >; export type DeviceAuthenticationCollector = ObjectOptionsCollectorWithObjectValue< 'DeviceAuthenticationCollector', - DeviceValue + DeviceValue, + Record, + DeviceOptionWithDefault[] >; -export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue< +export type PhoneNumberCollector = ObjectOptionsCollectorWithObjectValue< 'PhoneNumberCollector', PhoneNumberInputValue, - PhoneNumberOutputValue + PhoneNumberOutputValue, + PhoneNumberOptions >; /** ********************************************************************* diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index c9d1381857..c63d6dadb5 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -569,6 +569,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: true, validatePhoneNumber: true, + showExtension: false, }; const result = returnObjectValueCollector(mockField, 1, {}); @@ -583,6 +584,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: '', phoneNumber: '', + extension: '', }, type: mockField.type, validation: [ @@ -602,9 +604,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: '', phoneNumber: '', + extension: '', }, }, }); @@ -618,6 +622,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const result = returnObjectValueCollector(mockField, 1, {}); expect(result).toEqual({ @@ -631,6 +636,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: mockField.defaultCountryCode, phoneNumber: '', + extension: '', }, type: mockField.type, validation: null, @@ -639,9 +645,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: mockField.defaultCountryCode, phoneNumber: '', + extension: '', }, }, }); @@ -655,6 +663,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -671,6 +680,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: prefillMock.countryCode, phoneNumber: '', + extension: '', }, type: mockField.type, validation: null, @@ -679,9 +689,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: prefillMock.countryCode, phoneNumber: '', + extension: '', }, }, }); @@ -697,6 +709,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { phoneNumber: '1234567890', @@ -713,6 +726,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: '', phoneNumber: prefillMock.phoneNumber, + extension: '', }, type: mockField.type, validation: null, @@ -721,9 +735,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: '', phoneNumber: prefillMock.phoneNumber, + extension: '', }, }, }); @@ -737,6 +753,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -754,6 +771,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: prefillMock.countryCode, phoneNumber: prefillMock.phoneNumber, + extension: '', }, type: mockField.type, validation: null, @@ -762,13 +780,48 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: prefillMock.countryCode, phoneNumber: prefillMock.phoneNumber, + extension: '', }, }, }); }); + + it('showExtension is reflected in output options', () => { + const mockField: PhoneNumberField = { + key: 'phone-number-key', + defaultCountryCode: null, + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: false, + validatePhoneNumber: false, + showExtension: true, + }; + const result = returnObjectValueCollector(mockField, 1, {}); + expect(result.output.options).toEqual({ showExtension: true }); + }); + + it('prefilled extension is set on collector', () => { + const mockField: PhoneNumberField = { + key: 'phone-number-key', + defaultCountryCode: null, + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: false, + validatePhoneNumber: false, + showExtension: true, + }; + const prefillMock: PhoneNumberOutputValue = { + phoneNumber: '1234567890', + extension: '123', + }; + const result = returnObjectValueCollector(mockField, 1, prefillMock); + expect(result.input.value.extension).toBe('123'); + expect(result.output.value?.extension).toBe('123'); + }); }); describe('No Value Collectors', () => { diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index 6233c3202d..20f5ed22ed 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -655,11 +655,16 @@ export function returnObjectCollector< }); } + options = { showExtension: field.showExtension }; + const prefilledCountryCode = prefillData?.countryCode; const prefilledPhone = prefillData?.phoneNumber; + const prefilledExtension = prefillData?.extension; + defaultValue = { countryCode: prefilledCountryCode ? prefilledCountryCode : field.defaultCountryCode || '', phoneNumber: prefilledPhone || '', + extension: prefilledExtension || '', }; } diff --git a/packages/davinci-client/src/lib/davinci.types.ts b/packages/davinci-client/src/lib/davinci.types.ts index 2b0f21f24b..76a86db9d5 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -168,9 +168,10 @@ export type PhoneNumberField = { type: 'PHONE_NUMBER'; key: string; label: string; - defaultCountryCode: string | null; required: boolean; + defaultCountryCode: string | null; validatePhoneNumber: boolean; + showExtension: boolean; }; export type ProtectField = { diff --git a/packages/davinci-client/src/lib/node.reducer.test.ts b/packages/davinci-client/src/lib/node.reducer.test.ts index 723afbeaff..0692b74908 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -801,6 +801,7 @@ describe('The phone number collector reducer', () => { label: 'Phone Number', type: 'PHONE_NUMBER', required: false, + showExtension: false, }, ], }, @@ -817,6 +818,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: '', phoneNumber: '', + extension: '', }, type: 'PHONE_NUMBER', validation: null, @@ -825,9 +827,11 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + options: { showExtension: false }, value: { countryCode: '', phoneNumber: '', + extension: '', }, }, }, @@ -845,8 +849,18 @@ describe('The phone number collector reducer', () => { label: 'Phone Number', type: 'PHONE_NUMBER', required: false, + showExtension: true, }, ], + formData: { + value: { + 'phone-number-key': { + countryCode: 'US', + phoneNumber: '1234567890', + extension: '54321', + }, + }, + }, }, }; expect(nodeCollectorReducer(undefined, action)).toEqual([ @@ -860,7 +874,8 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', value: { countryCode: 'US', - phoneNumber: '', + phoneNumber: '1234567890', + extension: '54321', }, type: 'PHONE_NUMBER', validation: null, @@ -869,9 +884,11 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + options: { showExtension: true }, value: { countryCode: 'US', - phoneNumber: '', + phoneNumber: '1234567890', + extension: '54321', }, }, }, @@ -886,6 +903,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: 'US', phoneNumber: '555-555-5555', + extension: '54321', }, }, }; @@ -901,6 +919,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: '', phoneNumber: '', + extension: '', }, type: 'PHONE_NUMBER', validation: null, @@ -909,8 +928,11 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + options: { showExtension: true }, value: { countryCode: '', + phoneNumber: '', + extension: '', }, }, }, @@ -927,6 +949,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: 'US', phoneNumber: '555-555-5555', + extension: '54321', }, type: 'PHONE_NUMBER', validation: null, @@ -935,8 +958,11 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + options: { showExtension: true }, value: { countryCode: '', + phoneNumber: '', + extension: '', }, }, }, From ec5922f0e8d4f77babfb278947319929234ab899 Mon Sep 17 00:00:00 2001 From: AJ Ancheta <7781450+ancheetah@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:53:26 -0400 Subject: [PATCH 2/3] chore(davinci-client): add PhoneNumberExtensionCollector --- e2e/davinci-app/components/object-value.ts | 98 ++++++++- e2e/davinci-app/main.ts | 5 +- e2e/davinci-suites/src/form-fields.test.ts | 5 +- .../davinci-client/src/lib/client.store.ts | 2 + .../src/lib/collector.types.test-d.ts | 60 +++++- .../davinci-client/src/lib/collector.types.ts | 88 ++++++-- .../src/lib/collector.utils.test.ts | 44 ++-- .../davinci-client/src/lib/collector.utils.ts | 62 ++++-- .../davinci-client/src/lib/davinci.types.ts | 5 + .../src/lib/node.reducer.test.ts | 196 ++++++++++++++++-- .../davinci-client/src/lib/node.reducer.ts | 28 ++- .../src/lib/node.types.test-d.ts | 2 + packages/davinci-client/src/lib/node.types.ts | 2 + 13 files changed, 504 insertions(+), 93 deletions(-) diff --git a/e2e/davinci-app/components/object-value.ts b/e2e/davinci-app/components/object-value.ts index f5e3ddc3e9..f078f5d2a9 100644 --- a/e2e/davinci-app/components/object-value.ts +++ b/e2e/davinci-app/components/object-value.ts @@ -8,6 +8,9 @@ import type { DeviceAuthenticationCollector, DeviceRegistrationCollector, PhoneNumberCollector, + PhoneNumberExtensionCollector, + PhoneNumberExtensionInputValue, + PhoneNumberInputValue, Updater, } from '@forgerock/davinci-client/types'; @@ -19,11 +22,16 @@ import type { */ export default function objectValueComponent( formEl: HTMLFormElement, - collector: DeviceRegistrationCollector | DeviceAuthenticationCollector | PhoneNumberCollector, + collector: + | DeviceRegistrationCollector + | DeviceAuthenticationCollector + | PhoneNumberCollector + | PhoneNumberExtensionCollector, updater: | Updater | Updater - | Updater, + | Updater + | Updater, submitForm: () => void, ) { if ( @@ -61,7 +69,7 @@ export default function objectValueComponent( buttonEl.textContent = option.label; formEl.appendChild(buttonEl); } - } else { + } else if (collector.type === 'PhoneNumberCollector') { const phoneLabel = document.createElement('label'); phoneLabel.textContent = collector.output.label || 'Phone Number'; phoneLabel.className = 'object-options-title'; @@ -73,6 +81,9 @@ export default function objectValueComponent( phoneInput.setAttribute('name', 'phone-number-input'); phoneInput.setAttribute('placeholder', 'Enter phone number'); + formEl.appendChild(phoneLabel); + formEl.appendChild(phoneInput); + // Add change event listener phoneInput.addEventListener('change', (event) => { // Properly type the event target @@ -84,14 +95,85 @@ export default function objectValueComponent( return; } - updater({ + const phoneNumberInputValue: PhoneNumberInputValue = { phoneNumber: selectedValue, countryCode: collector.output.value?.countryCode || '', - extension: collector.output.value?.extension || '', - } as any); + }; + const phoneNumberUpdater = updater as Updater; + phoneNumberUpdater(phoneNumberInputValue); }); + } else if (collector.type === 'PhoneNumberExtensionCollector') { + const phoneLabel = document.createElement('label'); + phoneLabel.textContent = collector.output.label || 'Phone Number'; + phoneLabel.className = 'object-options-title'; + phoneLabel.setAttribute('for', 'phone-number-input-1'); - formEl.appendChild(phoneLabel); - formEl.appendChild(phoneInput); + const phoneInput = document.createElement('input'); + phoneInput.setAttribute('type', 'tel'); + phoneInput.setAttribute('id', 'phone-number-input-1'); + phoneInput.setAttribute('name', 'phone-number-input-1'); + phoneInput.setAttribute('placeholder', 'Enter phone number'); + + const extensionLabel = document.createElement('label'); + extensionLabel.textContent = collector.output.options.extensionLabel || 'Extension'; + extensionLabel.className = 'object-options-title'; + extensionLabel.setAttribute('for', 'extension-input-1'); + + const extensionInput = document.createElement('input'); + extensionInput.setAttribute('type', 'text'); + extensionInput.setAttribute('id', 'extension-input-1'); + extensionInput.setAttribute('name', 'extension-input-1'); + extensionInput.setAttribute('placeholder', 'Enter extension'); + + const divEl = document.createElement('div'); + divEl.style = 'display: flex; gap: 8px;'; + divEl.appendChild(phoneLabel); + divEl.appendChild(phoneInput); + divEl.appendChild(extensionLabel); + divEl.appendChild(extensionInput); + + formEl.appendChild(divEl); + + const phoneNumberExtensionUpdater = updater as Updater; + + // Add change event listener for phone number input + phoneInput.addEventListener('change', (event) => { + const target = event.target as HTMLInputElement; + const phoneValue = target.value; + const extensionValue = extensionInput.value; + + if (!phoneValue) { + console.error('No value found for phone number'); + return; + } + + const phoneNumberExtensionInputValue: PhoneNumberExtensionInputValue = { + phoneNumber: phoneValue, + countryCode: collector.output.value?.countryCode || '', + extension: extensionValue, + }; + + phoneNumberExtensionUpdater(phoneNumberExtensionInputValue); + }); + + // Add change event listener for extension input + extensionInput.addEventListener('change', (event) => { + const target = event.target as HTMLInputElement; + const extensionValue = target.value; + const phoneValue = phoneInput.value; + + if (!extensionValue) { + console.error('No value found for extension'); + return; + } + + const phoneNumberExtensionInputValue: PhoneNumberExtensionInputValue = { + phoneNumber: phoneValue, + countryCode: collector.output.value?.countryCode || '', + extension: extensionValue, + }; + + phoneNumberExtensionUpdater(phoneNumberExtensionInputValue); + }); } } diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index a1a93b54d4..3c577c168d 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -250,7 +250,10 @@ const urlParams = new URLSearchParams(window.location.search); formEl, // You can ignore this; it's just for rendering collector, // This is the plain object of the collector ); - } else if (collector.type === 'PhoneNumberCollector') { + } else if ( + collector.type === 'PhoneNumberCollector' || + collector.type === 'PhoneNumberExtensionCollector' + ) { objectValueComponent( formEl, // You can ignore this; it's just for rendering collector, // This is the plain object of the collector diff --git a/e2e/davinci-suites/src/form-fields.test.ts b/e2e/davinci-suites/src/form-fields.test.ts index 4800c68b23..4ec2999ef0 100644 --- a/e2e/davinci-suites/src/form-fields.test.ts +++ b/e2e/davinci-suites/src/form-fields.test.ts @@ -31,7 +31,8 @@ test('Should render form fields', async ({ page }) => { await page.locator('#combobox-field-key-3').check(); await page.locator('#combobox-field-key-2').uncheck(); - await page.locator('#phone-number-input').fill('1234567890'); + await page.locator('#phone-number-input-1').fill('1234567890'); + await page.locator('#extension-input-1').fill('7890'); await expect(page.getByRole('button', { name: 'Flow Button' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Flow Link' })).toBeVisible(); @@ -56,7 +57,7 @@ test('Should render form fields', async ({ page }) => { 'phone-field': { phoneNumber: '1234567890', countryCode: 'GB', - extension: '4321', + extension: '7890', }, }); }); diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index 3e2d3adcd8..ce65d85740 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -46,6 +46,7 @@ import type { MultiValueCollectors, FidoRegistrationInputValue, FidoAuthenticationInputValue, + PhoneNumberExtensionInputValue, } from './collector.types.js'; import type { InitFlow, @@ -338,6 +339,7 @@ export async function davinci({ | string | string[] | PhoneNumberInputValue + | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue, index?: number, 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 5a9732fa8a..b823ff73cc 100644 --- a/packages/davinci-client/src/lib/collector.types.test-d.ts +++ b/packages/davinci-client/src/lib/collector.types.test-d.ts @@ -28,11 +28,13 @@ import type { QrCodeCollector, AgreementCollector, PhoneNumberCollector, - ObjectOptionsCollectorWithObjectValue, + PhoneNumberExtensionCollector, + ObjectValueCollectorWithObjectValue, InferValueObjectCollectorType, PhoneNumberInputValue, PhoneNumberOutputValue, - PhoneNumberOptions, + PhoneNumberExtensionInputValue, + PhoneNumberExtensionOutputValue, } from './collector.types.js'; describe('Collector Types', () => { @@ -374,7 +376,7 @@ describe('Collector Types', () => { name: '', input: { key: '', - value: { countryCode: '', phoneNumber: '', extension: '' }, + value: { countryCode: '', phoneNumber: '' }, type: '', validation: null, }, @@ -382,8 +384,7 @@ describe('Collector Types', () => { key: '', label: '', type: '', - options: { showExtension: false }, - value: { countryCode: '', phoneNumber: '', extension: '' }, + value: { countryCode: '', phoneNumber: '' }, }, }; @@ -392,13 +393,52 @@ describe('Collector Types', () => { }); describe('ObjectValueCollector Types', () => { + it('should correctly infer PhoneNumberExtensionCollector Type', () => { + const tCollector: InferValueObjectCollectorType<'PhoneNumberExtensionCollector'> = { + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberExtensionCollector', + id: '', + name: '', + input: { + key: '', + value: { countryCode: '', phoneNumber: '', extension: '' }, + type: '', + validation: null, + }, + output: { + key: '', + label: '', + type: '', + options: { extensionLabel: '' }, + value: {}, + }, + }; + + expectTypeOf(tCollector).toEqualTypeOf(); + }); + + it('should validate PhoneNumberExtensionCollector structure', () => { + expectTypeOf() + .toHaveProperty('category') + .toEqualTypeOf<'ObjectValueCollector'>(); + expectTypeOf() + .toHaveProperty('type') + .toEqualTypeOf<'PhoneNumberExtensionCollector'>(); + expectTypeOf< + PhoneNumberExtensionCollector['input']['value'] + >().toEqualTypeOf(); + expectTypeOf< + PhoneNumberExtensionCollector['output']['value'] + >().toEqualTypeOf(); + }); + it('should validate PhoneNumberCollector structure', () => { expectTypeOf().toEqualTypeOf< - ObjectOptionsCollectorWithObjectValue< + ObjectValueCollectorWithObjectValue< 'PhoneNumberCollector', PhoneNumberInputValue, - PhoneNumberOutputValue, - PhoneNumberOptions + PhoneNumberOutputValue > >(); expectTypeOf() @@ -408,7 +448,6 @@ describe('Collector Types', () => { .toHaveProperty('type') .toEqualTypeOf<'PhoneNumberCollector'>(); expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); }); it('should validate PhoneNumberCollector base type constraints', () => { @@ -420,7 +459,7 @@ describe('Collector Types', () => { name: 'Test', input: { key: 'phone', - value: { countryCode: '+1', phoneNumber: '5555555555', extension: '' }, + value: { countryCode: '+1', phoneNumber: '5555555555' }, type: 'string', validation: null, }, @@ -428,7 +467,6 @@ describe('Collector Types', () => { key: 'phone', label: 'Phone Number', type: 'phone', - options: { showExtension: true }, value: { countryCode: '+1', phoneNumber: '5555555555' }, }, }; diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index 50f7a284e7..1d3b94166c 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -267,6 +267,7 @@ export type ObjectValueCollectorTypes = | 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' + | 'PhoneNumberExtensionCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector'; @@ -297,17 +298,27 @@ export interface DeviceValue { export interface PhoneNumberInputValue { countryCode: string; phoneNumber: string; - extension: string; } export interface PhoneNumberOutputValue { countryCode?: string; phoneNumber?: string; +} + +export interface PhoneNumberExtensionInputValue { + countryCode: string; + phoneNumber: string; + extension: string; +} + +export interface PhoneNumberExtensionOutputValue { + countryCode?: string; + phoneNumber?: string; extension?: string; } -export interface PhoneNumberOptions { - showExtension: boolean; +export interface PhoneNumberExtensionOptions { + extensionLabel: string; } export interface ObjectOptionsCollectorWithStringValue< @@ -337,7 +348,6 @@ export interface ObjectOptionsCollectorWithObjectValue< T extends ObjectValueCollectorTypes, V = Record, D = Record, - O = Record, > { category: 'ObjectValueCollector'; error: string | null; @@ -348,17 +358,62 @@ export interface ObjectOptionsCollectorWithObjectValue< key: string; value: V; type: string; - validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + validation: ValidationRequired[] | null; }; output: { key: string; label: string; type: string; - options: O; + options: DeviceOptionWithDefault[]; value?: D | null; }; } +export interface ObjectValueCollectorWithObjectValue< + T extends ObjectValueCollectorTypes, + IV = Record, + OV = Record, +> { + category: 'ObjectValueCollector'; + error: string | null; + type: T; + id: string; + name: string; + input: { + key: string; + value: IV; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + output: { + key: string; + label: string; + type: string; + value?: OV | null; + }; +} + +export interface PhoneNumberExtensionCollector { + category: 'ObjectValueCollector'; + error: string | null; + type: 'PhoneNumberExtensionCollector'; + id: string; + name: string; + input: { + key: string; + value: PhoneNumberExtensionInputValue; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + output: { + key: string; + label: string; + type: string; + options: PhoneNumberExtensionOptions; + value: PhoneNumberExtensionOutputValue; + }; +} + export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector @@ -366,20 +421,24 @@ export type InferValueObjectCollectorType = ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector - : - | ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> - | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; + : T extends 'PhoneNumberExtensionCollector' + ? PhoneNumberExtensionCollector + : + | ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> + | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; export type ObjectValueCollectors = | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector + | PhoneNumberExtensionCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; export type ObjectValueCollector = | ObjectOptionsCollectorWithObjectValue - | ObjectOptionsCollectorWithStringValue; + | ObjectOptionsCollectorWithStringValue + | ObjectValueCollectorWithObjectValue; export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue< 'DeviceRegistrationCollector', @@ -387,15 +446,12 @@ export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue< >; export type DeviceAuthenticationCollector = ObjectOptionsCollectorWithObjectValue< 'DeviceAuthenticationCollector', - DeviceValue, - Record, - DeviceOptionWithDefault[] + DeviceValue >; -export type PhoneNumberCollector = ObjectOptionsCollectorWithObjectValue< +export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue< 'PhoneNumberCollector', PhoneNumberInputValue, - PhoneNumberOutputValue, - PhoneNumberOptions + PhoneNumberOutputValue >; /** ********************************************************************* diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index c63d6dadb5..291787c620 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -31,6 +31,7 @@ import type { DeviceRegistrationField, FidoAuthenticationField, FidoRegistrationField, + PhoneNumberExtensionField, PhoneNumberField, ProtectField, QrCodeField, @@ -43,7 +44,9 @@ import type { import type { MultiSelectCollector, PhoneNumberCollector, + PhoneNumberExtensionCollector, PhoneNumberOutputValue, + PhoneNumberExtensionOutputValue, ValidatedTextCollector, } from './collector.types.js'; @@ -560,7 +563,7 @@ describe('Object value collectors', () => { }); }); -describe('returnPhoneNumberCollector', () => { +describe('returnObjectValueCollector with phone fields', () => { it('input value is empty when no prefill or default country code', () => { const mockField: PhoneNumberField = { key: 'phone-number-key', @@ -569,7 +572,6 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: true, validatePhoneNumber: true, - showExtension: false, }; const result = returnObjectValueCollector(mockField, 1, {}); @@ -584,7 +586,6 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: '', phoneNumber: '', - extension: '', }, type: mockField.type, validation: [ @@ -604,11 +605,9 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, - options: { showExtension: false }, value: { countryCode: '', phoneNumber: '', - extension: '', }, }, }); @@ -622,7 +621,6 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, - showExtension: false, }; const result = returnObjectValueCollector(mockField, 1, {}); expect(result).toEqual({ @@ -636,7 +634,6 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: mockField.defaultCountryCode, phoneNumber: '', - extension: '', }, type: mockField.type, validation: null, @@ -645,11 +642,9 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, - options: { showExtension: false }, value: { countryCode: mockField.defaultCountryCode, phoneNumber: '', - extension: '', }, }, }); @@ -663,7 +658,6 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, - showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -680,7 +674,6 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: prefillMock.countryCode, phoneNumber: '', - extension: '', }, type: mockField.type, validation: null, @@ -689,11 +682,9 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, - options: { showExtension: false }, value: { countryCode: prefillMock.countryCode, phoneNumber: '', - extension: '', }, }, }); @@ -709,7 +700,6 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, - showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { phoneNumber: '1234567890', @@ -726,7 +716,6 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: '', phoneNumber: prefillMock.phoneNumber, - extension: '', }, type: mockField.type, validation: null, @@ -735,11 +724,9 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, - options: { showExtension: false }, value: { countryCode: '', phoneNumber: prefillMock.phoneNumber, - extension: '', }, }, }); @@ -753,7 +740,6 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, - showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -771,7 +757,6 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: prefillMock.countryCode, phoneNumber: prefillMock.phoneNumber, - extension: '', }, type: mockField.type, validation: null, @@ -780,18 +765,16 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, - options: { showExtension: false }, value: { countryCode: prefillMock.countryCode, phoneNumber: prefillMock.phoneNumber, - extension: '', }, }, }); }); - it('showExtension is reflected in output options', () => { - const mockField: PhoneNumberField = { + it('showExtension true returns PhoneNumberExtensionCollector', () => { + const mockField: PhoneNumberExtensionField = { key: 'phone-number-key', defaultCountryCode: null, label: 'Phone Number', @@ -799,13 +782,14 @@ describe('returnPhoneNumberCollector', () => { required: false, validatePhoneNumber: false, showExtension: true, + extensionLabel: 'Extension', }; const result = returnObjectValueCollector(mockField, 1, {}); - expect(result.output.options).toEqual({ showExtension: true }); + expect(result.type).toBe('PhoneNumberExtensionCollector'); }); it('prefilled extension is set on collector', () => { - const mockField: PhoneNumberField = { + const mockField: PhoneNumberExtensionField = { key: 'phone-number-key', defaultCountryCode: null, label: 'Phone Number', @@ -813,14 +797,20 @@ describe('returnPhoneNumberCollector', () => { required: false, validatePhoneNumber: false, showExtension: true, + extensionLabel: 'Extension', }; - const prefillMock: PhoneNumberOutputValue = { + const prefillMock: PhoneNumberExtensionOutputValue = { phoneNumber: '1234567890', extension: '123', }; - const result = returnObjectValueCollector(mockField, 1, prefillMock); + const result = returnObjectValueCollector( + mockField, + 1, + prefillMock, + ) as PhoneNumberExtensionCollector; expect(result.input.value.extension).toBe('123'); expect(result.output.value?.extension).toBe('123'); + expect(result.output.options.extensionLabel).toBe('Extension'); }); }); diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index 20f5ed22ed..dc716446c3 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -31,6 +31,7 @@ import type { ObjectValueAutoCollectorTypes, QrCodeCollectorBase, AgreementCollector, + PhoneNumberExtensionOutputValue, } from './collector.types.js'; import type { DeviceAuthenticationField, @@ -49,6 +50,7 @@ import type { ValidatedField, AgreementField, ReadOnlyFields, + PhoneNumberExtensionField, } from './davinci.types.js'; /** @@ -576,9 +578,18 @@ export function returnMultiSelectCollector(field: MultiSelectField, idx: number, * @returns {ObjectCollector} The constructed ObjectCollector object. */ export function returnObjectCollector< - Field extends DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField, + Field extends + | DeviceAuthenticationField + | DeviceRegistrationField + | PhoneNumberField + | PhoneNumberExtensionField, CollectorType extends ObjectValueCollectorTypes = 'ObjectValueCollector', ->(field: Field, idx: number, collectorType: CollectorType, prefillData?: PhoneNumberOutputValue) { +>( + field: Field, + idx: number, + collectorType: CollectorType, + prefillData?: PhoneNumberOutputValue | PhoneNumberExtensionOutputValue, +) { let error = ''; if (!('key' in field)) { error = `${error}Key is not found in the field object. `; @@ -655,17 +666,28 @@ export function returnObjectCollector< }); } - options = { showExtension: field.showExtension }; - const prefilledCountryCode = prefillData?.countryCode; const prefilledPhone = prefillData?.phoneNumber; - const prefilledExtension = prefillData?.extension; - defaultValue = { - countryCode: prefilledCountryCode ? prefilledCountryCode : field.defaultCountryCode || '', - phoneNumber: prefilledPhone || '', - extension: prefilledExtension || '', - }; + if ('showExtension' in field && field.showExtension === true) { + const prefilledExtension = + prefillData && 'extension' in prefillData ? prefillData.extension : ''; + + // PhoneNumberExtensionCollector default value + defaultValue = { + countryCode: prefilledCountryCode ? prefilledCountryCode : field.defaultCountryCode || '', + phoneNumber: prefilledPhone || '', + extension: prefilledExtension ?? '', + }; + + options = { extensionLabel: field.extensionLabel || '' }; + } else { + // PhoneNumberCollector default value + defaultValue = { + countryCode: prefilledCountryCode ? prefilledCountryCode : field.defaultCountryCode || '', + phoneNumber: prefilledPhone || '', + }; + } } return { @@ -710,11 +732,25 @@ export function returnObjectSelectCollector( } export function returnObjectValueCollector( - field: PhoneNumberField, + field: PhoneNumberField | PhoneNumberExtensionField, idx: number, - prefillData: PhoneNumberOutputValue, + prefillData: PhoneNumberOutputValue | PhoneNumberExtensionOutputValue, ) { - return returnObjectCollector(field, idx, 'PhoneNumberCollector', prefillData); + if ('showExtension' in field && field.showExtension === true) { + return returnObjectCollector( + field, + idx, + 'PhoneNumberExtensionCollector', + prefillData as PhoneNumberExtensionOutputValue, + ); + } + + return returnObjectCollector( + field, + idx, + 'PhoneNumberCollector', + prefillData as PhoneNumberOutputValue, + ); } /** diff --git a/packages/davinci-client/src/lib/davinci.types.ts b/packages/davinci-client/src/lib/davinci.types.ts index 76a86db9d5..b73488fd3a 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -171,7 +171,11 @@ export type PhoneNumberField = { required: boolean; defaultCountryCode: string | null; validatePhoneNumber: boolean; +}; + +export type PhoneNumberExtensionField = PhoneNumberField & { showExtension: boolean; + extensionLabel: string; }; export type ProtectField = { @@ -249,6 +253,7 @@ export type ComplexValueFields = | DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField + | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField; diff --git a/packages/davinci-client/src/lib/node.reducer.test.ts b/packages/davinci-client/src/lib/node.reducer.test.ts index 0692b74908..254db3761e 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -14,6 +14,7 @@ import type { FidoRegistrationCollector, MultiSelectCollector, PhoneNumberCollector, + PhoneNumberExtensionCollector, PollingCollector, ProtectCollector, QrCodeCollector, @@ -801,7 +802,6 @@ describe('The phone number collector reducer', () => { label: 'Phone Number', type: 'PHONE_NUMBER', required: false, - showExtension: false, }, ], }, @@ -818,7 +818,6 @@ describe('The phone number collector reducer', () => { value: { countryCode: '', phoneNumber: '', - extension: '', }, type: 'PHONE_NUMBER', validation: null, @@ -827,11 +826,9 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', - options: { showExtension: false }, value: { countryCode: '', phoneNumber: '', - extension: '', }, }, }, @@ -849,7 +846,6 @@ describe('The phone number collector reducer', () => { label: 'Phone Number', type: 'PHONE_NUMBER', required: false, - showExtension: true, }, ], formData: { @@ -857,7 +853,6 @@ describe('The phone number collector reducer', () => { 'phone-number-key': { countryCode: 'US', phoneNumber: '1234567890', - extension: '54321', }, }, }, @@ -875,7 +870,6 @@ describe('The phone number collector reducer', () => { value: { countryCode: 'US', phoneNumber: '1234567890', - extension: '54321', }, type: 'PHONE_NUMBER', validation: null, @@ -884,11 +878,9 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', - options: { showExtension: true }, value: { countryCode: 'US', phoneNumber: '1234567890', - extension: '54321', }, }, }, @@ -903,7 +895,6 @@ describe('The phone number collector reducer', () => { value: { countryCode: 'US', phoneNumber: '555-555-5555', - extension: '54321', }, }, }; @@ -914,6 +905,183 @@ describe('The phone number collector reducer', () => { type: 'PhoneNumberCollector', id: 'phone-number-key-0', name: 'phone-number-key', + input: { + key: 'phone-number-key', + value: { + countryCode: '', + phoneNumber: '', + }, + type: 'PHONE_NUMBER', + validation: null, + }, + output: { + key: 'phone-number-key', + label: 'Phone Number', + type: 'PHONE_NUMBER', + value: { + countryCode: '', + phoneNumber: '', + }, + }, + }, + ]; + expect(nodeCollectorReducer(state, action)).toStrictEqual([ + { + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberCollector', + id: 'phone-number-key-0', + name: 'phone-number-key', + input: { + key: 'phone-number-key', + value: { + countryCode: 'US', + phoneNumber: '555-555-5555', + }, + type: 'PHONE_NUMBER', + validation: null, + }, + output: { + key: 'phone-number-key', + label: 'Phone Number', + type: 'PHONE_NUMBER', + value: { + countryCode: '', + phoneNumber: '', + }, + }, + }, + ]); + }); +}); + +describe('The phone number extension collector reducer', () => { + it('should populate phone number extension collector', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + key: 'phone-number-key', + defaultCountryCode: null, + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: false, + showExtension: true, + extensionLabel: 'Extension', + }, + ], + }, + }; + expect(nodeCollectorReducer(undefined, action)).toEqual([ + { + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberExtensionCollector', + id: 'phone-number-key-0', + name: 'phone-number-key', + input: { + key: 'phone-number-key', + value: { + countryCode: '', + phoneNumber: '', + extension: '', + }, + type: 'PHONE_NUMBER', + validation: null, + }, + output: { + key: 'phone-number-key', + label: 'Phone Number', + type: 'PHONE_NUMBER', + value: { + countryCode: '', + phoneNumber: '', + extension: '', + }, + options: { extensionLabel: 'Extension' }, + }, + }, + ]); + }); + + it('should populate phone number extension collector with default value', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + key: 'phone-number-key', + defaultCountryCode: 'US', + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: false, + showExtension: true, + extensionLabel: 'Extension', + }, + ], + formData: { + value: { + 'phone-number-key': { + countryCode: 'US', + phoneNumber: '1234567890', + extension: '123', + }, + }, + }, + }, + }; + expect(nodeCollectorReducer(undefined, action)).toEqual([ + { + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberExtensionCollector', + id: 'phone-number-key-0', + name: 'phone-number-key', + input: { + key: 'phone-number-key', + value: { + countryCode: 'US', + phoneNumber: '1234567890', + extension: '123', + }, + type: 'PHONE_NUMBER', + validation: null, + }, + output: { + key: 'phone-number-key', + label: 'Phone Number', + type: 'PHONE_NUMBER', + value: { + countryCode: 'US', + phoneNumber: '1234567890', + extension: '123', + }, + options: { extensionLabel: 'Extension' }, + }, + }, + ]); + }); + + it('should handle collector updates', () => { + const action = { + type: 'node/update', + payload: { + id: 'phone-number-key-0', + value: { + countryCode: 'US', + phoneNumber: '555-555-5555', + extension: '456', + }, + }, + }; + const state: PhoneNumberExtensionCollector[] = [ + { + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberExtensionCollector', + id: 'phone-number-key-0', + name: 'phone-number-key', input: { key: 'phone-number-key', value: { @@ -928,12 +1096,12 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', - options: { showExtension: true }, value: { countryCode: '', phoneNumber: '', extension: '', }, + options: { extensionLabel: 'Extension' }, }, }, ]; @@ -941,7 +1109,7 @@ describe('The phone number collector reducer', () => { { category: 'ObjectValueCollector', error: null, - type: 'PhoneNumberCollector', + type: 'PhoneNumberExtensionCollector', id: 'phone-number-key-0', name: 'phone-number-key', input: { @@ -949,7 +1117,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: 'US', phoneNumber: '555-555-5555', - extension: '54321', + extension: '456', }, type: 'PHONE_NUMBER', validation: null, @@ -958,12 +1126,12 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', - options: { showExtension: true }, value: { countryCode: '', phoneNumber: '', extension: '', }, + options: { extensionLabel: 'Extension' }, }, }, ]); diff --git a/packages/davinci-client/src/lib/node.reducer.ts b/packages/davinci-client/src/lib/node.reducer.ts index 9162351c8b..3c84780761 100644 --- a/packages/davinci-client/src/lib/node.reducer.ts +++ b/packages/davinci-client/src/lib/node.reducer.ts @@ -59,6 +59,9 @@ import type { FidoRegistrationInputValue, QrCodeCollector, AgreementCollector, + PhoneNumberExtensionOutputValue, + PhoneNumberExtensionCollector, + PhoneNumberExtensionInputValue, } from './collector.types.js'; /** @@ -77,6 +80,7 @@ export const updateCollectorValues = createAction<{ | string | string[] | PhoneNumberInputValue + | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; index?: number; @@ -99,6 +103,7 @@ const initialCollectorValues: ( | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector + | PhoneNumberExtensionCollector | ReadOnlyCollector | ValidatedTextCollector | UnknownCollector @@ -179,7 +184,9 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build return returnPasswordCollector(field, idx); } case 'PHONE_NUMBER': { - const prefillData = data as PhoneNumberOutputValue; + const prefillData = data as + | PhoneNumberOutputValue + | PhoneNumberExtensionOutputValue; return returnObjectValueCollector(field, idx, prefillData); } case 'TEXT': { @@ -319,6 +326,25 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build collector.input.value = action.payload.value; } + if (collector.type === 'PhoneNumberExtensionCollector') { + if (typeof action.payload.id !== 'string') { + throw new Error('Index argument must be a string'); + } + if (typeof action.payload.value !== 'object') { + throw new Error('Value argument must be an object'); + } + if ( + !('phoneNumber' in action.payload.value) || + !('countryCode' in action.payload.value) || + !('extension' in action.payload.value) + ) { + throw new Error( + 'Value argument must contain a phoneNumber, countryCode, and extension property', + ); + } + collector.input.value = action.payload.value; + } + if (collector.type === 'FidoRegistrationCollector') { if (typeof action.payload.id !== 'string') { throw new Error('Index argument must be a string'); 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 4fbd40911f..86c3a4ecea 100644 --- a/packages/davinci-client/src/lib/node.types.test-d.ts +++ b/packages/davinci-client/src/lib/node.types.test-d.ts @@ -31,6 +31,7 @@ import { DeviceRegistrationCollector, DeviceAuthenticationCollector, PhoneNumberCollector, + PhoneNumberExtensionCollector, UnknownCollector, ProtectCollector, PollingCollector, @@ -233,6 +234,7 @@ describe('Node Types', () => { | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector + | PhoneNumberExtensionCollector | ReadOnlyCollector | SingleSelectCollector | ValidatedTextCollector diff --git a/packages/davinci-client/src/lib/node.types.ts b/packages/davinci-client/src/lib/node.types.ts index a3572fba38..52759bf695 100644 --- a/packages/davinci-client/src/lib/node.types.ts +++ b/packages/davinci-client/src/lib/node.types.ts @@ -28,6 +28,7 @@ import type { FidoAuthenticationCollector, QrCodeCollector, AgreementCollector, + PhoneNumberExtensionCollector, } from './collector.types.js'; import type { Links } from './davinci.types.js'; @@ -44,6 +45,7 @@ export type Collectors = | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector + | PhoneNumberExtensionCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector From 507902e9c002f56a96eb94040450b3fd3f2c2797 Mon Sep 17 00:00:00 2001 From: AJ Ancheta <7781450+ancheetah@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:04:31 -0400 Subject: [PATCH 3/3] chore: pr revisions --- .changeset/long-singers-do.md | 4 +- .gitignore | 3 + e2e/davinci-app/components/object-value.ts | 2 +- e2e/davinci-suites/src/form-fields.test.ts | 2 +- .../src/phone-number-field.test.ts | 3 +- .../src/lib/collector.types.test-d.ts | 2 +- .../davinci-client/src/lib/collector.types.ts | 6 +- .../src/lib/collector.utils.test.ts | 68 ++++++++++++++++++- .../davinci-client/src/lib/collector.utils.ts | 4 +- .../src/lib/node.reducer.test.ts | 8 +-- 10 files changed, 84 insertions(+), 18 deletions(-) diff --git a/.changeset/long-singers-do.md b/.changeset/long-singers-do.md index 986d246e0e..b4214abfd1 100644 --- a/.changeset/long-singers-do.md +++ b/.changeset/long-singers-do.md @@ -2,6 +2,4 @@ '@forgerock/davinci-client': minor --- -Add support for extension in PhoneNumberCollector - -BREAKING CHANGE: `ObjectValueCollectorWithObjectValue` type was removed +A new PhoneNumberExtensionCollector has been added to support phone number fields that include an extension. When a DaVinci PHONE_NUMBER field has showExtension: true, the SDK now produces a PhoneNumberExtensionCollector instead of a PhoneNumberCollector. diff --git a/.gitignore b/.gitignore index 518e1a3770..327a931c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,6 @@ GEMINI.md .claude/worktrees .claude/settings.local.json .opensource + +# Polaris +.polaris-setup-progress.json diff --git a/e2e/davinci-app/components/object-value.ts b/e2e/davinci-app/components/object-value.ts index f078f5d2a9..e762bbcdcf 100644 --- a/e2e/davinci-app/components/object-value.ts +++ b/e2e/davinci-app/components/object-value.ts @@ -115,7 +115,7 @@ export default function objectValueComponent( phoneInput.setAttribute('placeholder', 'Enter phone number'); const extensionLabel = document.createElement('label'); - extensionLabel.textContent = collector.output.options.extensionLabel || 'Extension'; + extensionLabel.textContent = collector.output.extensionLabel || 'Extension'; extensionLabel.className = 'object-options-title'; extensionLabel.setAttribute('for', 'extension-input-1'); diff --git a/e2e/davinci-suites/src/form-fields.test.ts b/e2e/davinci-suites/src/form-fields.test.ts index 4ec2999ef0..8c009f6576 100644 --- a/e2e/davinci-suites/src/form-fields.test.ts +++ b/e2e/davinci-suites/src/form-fields.test.ts @@ -57,7 +57,7 @@ test('Should render form fields', async ({ page }) => { 'phone-field': { phoneNumber: '1234567890', countryCode: 'GB', - extension: '7890', + extension: '7890', // Tests PhoneNumberExtensionCollector }, }); }); diff --git a/e2e/davinci-suites/src/phone-number-field.test.ts b/e2e/davinci-suites/src/phone-number-field.test.ts index 2e55adbfb7..fc1017adb3 100644 --- a/e2e/davinci-suites/src/phone-number-field.test.ts +++ b/e2e/davinci-suites/src/phone-number-field.test.ts @@ -103,8 +103,9 @@ test.describe('Device registration tests', () => { await page.getByRole('button', { name: 'DEVICE_REGISTRATION' }).click(); await expect(page.getByText('SDK Automation - Device Registration')).toBeVisible(); await page.getByRole('button', { name: 'Text Message' }).click(); - await expect(page.getByText('SDK Automation - Enter Phone Number')).toBeVisible(); + await expect(page.getByText('SDK Automation [JS] - Enter Phone Number')).toBeVisible(); await page.getByRole('textbox', { name: 'Enter Phone Number' }).fill('3035550100'); + await expect(page.getByText('Extension')).not.toBeVisible(); // Tests standard PhoneNumberCollector await page.getByRole('button', { name: 'Submit' }).click(); await expect(page.getByText('SMS/Voice MFA Registered')).toBeVisible(); 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 b823ff73cc..ecaabcbc33 100644 --- a/packages/davinci-client/src/lib/collector.types.test-d.ts +++ b/packages/davinci-client/src/lib/collector.types.test-d.ts @@ -410,7 +410,7 @@ describe('Collector Types', () => { key: '', label: '', type: '', - options: { extensionLabel: '' }, + extensionLabel: '', value: {}, }, }; diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index 1d3b94166c..cb9f8f1787 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -317,10 +317,6 @@ export interface PhoneNumberExtensionOutputValue { extension?: string; } -export interface PhoneNumberExtensionOptions { - extensionLabel: string; -} - export interface ObjectOptionsCollectorWithStringValue< T extends ObjectValueCollectorTypes, V = string, @@ -409,7 +405,7 @@ export interface PhoneNumberExtensionCollector { key: string; label: string; type: string; - options: PhoneNumberExtensionOptions; + extensionLabel: string; value: PhoneNumberExtensionOutputValue; }; } diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index 291787c620..7b47a6f22f 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -788,6 +788,59 @@ describe('returnObjectValueCollector with phone fields', () => { expect(result.type).toBe('PhoneNumberExtensionCollector'); }); + it('creates a full PhoneNumberExtensionCollector with all fields', () => { + const mockField: PhoneNumberExtensionField = { + key: 'phone-number-key', + defaultCountryCode: 'US', + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: true, + validatePhoneNumber: true, + showExtension: true, + extensionLabel: 'Extension', + }; + const result = returnObjectValueCollector(mockField, 1, {}) as PhoneNumberExtensionCollector; + expect(result).toEqual({ + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberExtensionCollector', + id: 'phone-number-key-1', + name: 'phone-number-key', + input: { + key: mockField.key, + value: { + countryCode: 'US', + phoneNumber: '', + extension: '', + }, + type: mockField.type, + validation: [ + { + message: 'Value cannot be empty', + rule: true, + type: 'required', + }, + { + message: 'Phone number should be validated', + rule: true, + type: 'validatePhoneNumber', + }, + ], + }, + output: { + key: mockField.key, + label: mockField.label, + type: mockField.type, + extensionLabel: 'Extension', + value: { + countryCode: 'US', + phoneNumber: '', + extension: '', + }, + }, + }); + }); + it('prefilled extension is set on collector', () => { const mockField: PhoneNumberExtensionField = { key: 'phone-number-key', @@ -810,7 +863,20 @@ describe('returnObjectValueCollector with phone fields', () => { ) as PhoneNumberExtensionCollector; expect(result.input.value.extension).toBe('123'); expect(result.output.value?.extension).toBe('123'); - expect(result.output.options.extensionLabel).toBe('Extension'); + expect(result.output.extensionLabel).toBe('Extension'); + }); + + it('PhoneNumberCollector does not have extensionLabel in output', () => { + const mockField: PhoneNumberField = { + key: 'phone-number-key', + defaultCountryCode: null, + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: false, + validatePhoneNumber: false, + }; + const result = returnObjectValueCollector(mockField, 1, {}); + expect(result.output).not.toHaveProperty('extensionLabel'); }); }); diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index dc716446c3..4d639d9381 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -612,6 +612,7 @@ export function returnObjectCollector< let options; let defaultValue; + let extensionLabel: string | null = null; if (field.type === 'DEVICE_AUTHENTICATION') { if (!('options' in field)) { @@ -680,7 +681,7 @@ export function returnObjectCollector< extension: prefilledExtension ?? '', }; - options = { extensionLabel: field.extensionLabel || '' }; + extensionLabel = field.extensionLabel; } else { // PhoneNumberCollector default value defaultValue = { @@ -707,6 +708,7 @@ export function returnObjectCollector< label: field.label, type: field.type, ...(options && { options: options || [] }), + ...(extensionLabel !== null && { extensionLabel }), ...(defaultValue && { value: defaultValue }), }, } as InferValueObjectCollectorType; diff --git a/packages/davinci-client/src/lib/node.reducer.test.ts b/packages/davinci-client/src/lib/node.reducer.test.ts index 254db3761e..2d2ba361ab 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -994,12 +994,12 @@ describe('The phone number extension collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + extensionLabel: 'Extension', value: { countryCode: '', phoneNumber: '', extension: '', }, - options: { extensionLabel: 'Extension' }, }, }, ]); @@ -1052,12 +1052,12 @@ describe('The phone number extension collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + extensionLabel: 'Extension', value: { countryCode: 'US', phoneNumber: '1234567890', extension: '123', }, - options: { extensionLabel: 'Extension' }, }, }, ]); @@ -1101,7 +1101,7 @@ describe('The phone number extension collector reducer', () => { phoneNumber: '', extension: '', }, - options: { extensionLabel: 'Extension' }, + extensionLabel: 'Extension', }, }, ]; @@ -1126,12 +1126,12 @@ describe('The phone number extension collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + extensionLabel: 'Extension', value: { countryCode: '', phoneNumber: '', extension: '', }, - options: { extensionLabel: 'Extension' }, }, }, ]);