diff --git a/.changeset/long-singers-do.md b/.changeset/long-singers-do.md new file mode 100644 index 0000000000..b4214abfd1 --- /dev/null +++ b/.changeset/long-singers-do.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': minor +--- + +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 77e964d765..e762bbcdcf 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,13 +95,85 @@ export default function objectValueComponent( return; } - updater({ + const phoneNumberInputValue: PhoneNumberInputValue = { phoneNumber: selectedValue, countryCode: collector.output.value?.countryCode || '', - } 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.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 0ad89d3e21..8c009f6576 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(); @@ -42,9 +43,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 +57,7 @@ test('Should render form fields', async ({ page }) => { 'phone-field': { phoneNumber: '1234567890', countryCode: 'GB', + 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/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 5fe63303ad..ecaabcbc33 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,14 @@ import type { ReadOnlyCollector, QrCodeCollector, AgreementCollector, + PhoneNumberCollector, + PhoneNumberExtensionCollector, + ObjectValueCollectorWithObjectValue, + InferValueObjectCollectorType, + PhoneNumberInputValue, + PhoneNumberOutputValue, + PhoneNumberExtensionInputValue, + PhoneNumberExtensionOutputValue, } from './collector.types.js'; describe('Collector Types', () => { @@ -358,6 +366,112 @@ 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: '' }, + type: '', + validation: null, + }, + output: { + key: '', + label: '', + type: '', + value: { countryCode: '', phoneNumber: '' }, + }, + }; + + expectTypeOf(tCollector).toEqualTypeOf(); + }); + }); + + 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: '', + 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< + ObjectValueCollectorWithObjectValue< + 'PhoneNumberCollector', + PhoneNumberInputValue, + PhoneNumberOutputValue + > + >(); + expectTypeOf() + .toHaveProperty('category') + .toEqualTypeOf<'ObjectValueCollector'>(); + expectTypeOf() + .toHaveProperty('type') + .toEqualTypeOf<'PhoneNumberCollector'>(); + 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' }, + type: 'string', + validation: null, + }, + output: { + key: 'phone', + label: 'Phone Number', + type: 'phone', + 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..cb9f8f1787 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'; @@ -304,6 +305,18 @@ export interface PhoneNumberOutputValue { phoneNumber?: string; } +export interface PhoneNumberExtensionInputValue { + countryCode: string; + phoneNumber: string; + extension: string; +} + +export interface PhoneNumberExtensionOutputValue { + countryCode?: string; + phoneNumber?: string; + extension?: string; +} + export interface ObjectOptionsCollectorWithStringValue< T extends ObjectValueCollectorTypes, V = string, @@ -376,6 +389,27 @@ export interface ObjectValueCollectorWithObjectValue< }; } +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; + extensionLabel: string; + value: PhoneNumberExtensionOutputValue; + }; +} + export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector @@ -383,14 +417,17 @@ 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'>; diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index c9d1381857..7b47a6f22f 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', @@ -769,6 +772,112 @@ describe('returnPhoneNumberCollector', () => { }, }); }); + + it('showExtension true returns PhoneNumberExtensionCollector', () => { + const mockField: PhoneNumberExtensionField = { + key: 'phone-number-key', + defaultCountryCode: null, + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: false, + validatePhoneNumber: false, + showExtension: true, + extensionLabel: 'Extension', + }; + const result = returnObjectValueCollector(mockField, 1, {}); + 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', + defaultCountryCode: null, + label: 'Phone Number', + type: 'PHONE_NUMBER', + required: false, + validatePhoneNumber: false, + showExtension: true, + extensionLabel: 'Extension', + }; + const prefillMock: PhoneNumberExtensionOutputValue = { + phoneNumber: '1234567890', + extension: '123', + }; + 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.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'); + }); }); 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..4d639d9381 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. `; @@ -601,6 +612,7 @@ export function returnObjectCollector< let options; let defaultValue; + let extensionLabel: string | null = null; if (field.type === 'DEVICE_AUTHENTICATION') { if (!('options' in field)) { @@ -657,10 +669,26 @@ export function returnObjectCollector< const prefilledCountryCode = prefillData?.countryCode; const prefilledPhone = prefillData?.phoneNumber; - defaultValue = { - countryCode: prefilledCountryCode ? prefilledCountryCode : field.defaultCountryCode || '', - phoneNumber: prefilledPhone || '', - }; + + 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 ?? '', + }; + + extensionLabel = field.extensionLabel; + } else { + // PhoneNumberCollector default value + defaultValue = { + countryCode: prefilledCountryCode ? prefilledCountryCode : field.defaultCountryCode || '', + phoneNumber: prefilledPhone || '', + }; + } } return { @@ -680,6 +708,7 @@ export function returnObjectCollector< label: field.label, type: field.type, ...(options && { options: options || [] }), + ...(extensionLabel !== null && { extensionLabel }), ...(defaultValue && { value: defaultValue }), }, } as InferValueObjectCollectorType; @@ -705,11 +734,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 2b0f21f24b..b73488fd3a 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -168,11 +168,16 @@ export type PhoneNumberField = { type: 'PHONE_NUMBER'; key: string; label: string; - defaultCountryCode: string | null; required: boolean; + defaultCountryCode: string | null; validatePhoneNumber: boolean; }; +export type PhoneNumberExtensionField = PhoneNumberField & { + showExtension: boolean; + extensionLabel: string; +}; + export type ProtectField = { type: 'PROTECT'; key: string; @@ -248,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 723afbeaff..2d2ba361ab 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, @@ -847,6 +848,14 @@ describe('The phone number collector reducer', () => { required: false, }, ], + formData: { + value: { + 'phone-number-key': { + countryCode: 'US', + phoneNumber: '1234567890', + }, + }, + }, }, }; expect(nodeCollectorReducer(undefined, action)).toEqual([ @@ -860,7 +869,7 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', value: { countryCode: 'US', - phoneNumber: '', + phoneNumber: '1234567890', }, type: 'PHONE_NUMBER', validation: null, @@ -871,7 +880,7 @@ describe('The phone number collector reducer', () => { type: 'PHONE_NUMBER', value: { countryCode: 'US', - phoneNumber: '', + phoneNumber: '1234567890', }, }, }, @@ -911,6 +920,7 @@ describe('The phone number collector reducer', () => { type: 'PHONE_NUMBER', value: { countryCode: '', + phoneNumber: '', }, }, }, @@ -937,6 +947,190 @@ describe('The phone number collector reducer', () => { 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', + extensionLabel: 'Extension', + value: { + countryCode: '', + phoneNumber: '', + 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', + extensionLabel: 'Extension', + value: { + countryCode: 'US', + phoneNumber: '1234567890', + extension: '123', + }, + }, + }, + ]); + }); + + 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: { + countryCode: '', + phoneNumber: '', + extension: '', + }, + type: 'PHONE_NUMBER', + validation: null, + }, + output: { + key: 'phone-number-key', + label: 'Phone Number', + type: 'PHONE_NUMBER', + value: { + countryCode: '', + phoneNumber: '', + extension: '', + }, + extensionLabel: 'Extension', + }, + }, + ]; + expect(nodeCollectorReducer(state, action)).toStrictEqual([ + { + category: 'ObjectValueCollector', + error: null, + type: 'PhoneNumberExtensionCollector', + id: 'phone-number-key-0', + name: 'phone-number-key', + input: { + key: 'phone-number-key', + value: { + countryCode: 'US', + phoneNumber: '555-555-5555', + extension: '456', + }, + type: 'PHONE_NUMBER', + validation: null, + }, + output: { + key: 'phone-number-key', + label: 'Phone Number', + type: 'PHONE_NUMBER', + extensionLabel: 'Extension', + value: { + countryCode: '', + phoneNumber: '', + 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