From 9aa183c8ce7ee63d9e0e0137a722795c813c38e0 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] 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 deafe24f8c..e2af60fa2c 100644 --- a/packages/davinci-client/src/lib/collector.types.test-d.ts +++ b/packages/davinci-client/src/lib/collector.types.test-d.ts @@ -23,6 +23,12 @@ import type { InferSingleValueCollectorType, InferMultiValueCollectorType, InferActionCollectorType, + PhoneNumberCollector, + ObjectOptionsCollectorWithObjectValue, + InferValueObjectCollectorType, + PhoneNumberInputValue, + PhoneNumberOutputValue, + PhoneNumberOptions, } from './collector.types.js'; describe('Collector Types', () => { @@ -354,5 +360,75 @@ 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(); + }); }); }); diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index e6c882a5c4..95c00b6122 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 ebc33f3c6b..3bd958a837 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -567,6 +567,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: true, validatePhoneNumber: true, + showExtension: false, }; const result = returnObjectValueCollector(mockField, 1, {}); @@ -581,6 +582,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: '', phoneNumber: '', + extension: '', }, type: mockField.type, validation: [ @@ -600,9 +602,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: '', phoneNumber: '', + extension: '', }, }, }); @@ -616,6 +620,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const result = returnObjectValueCollector(mockField, 1, {}); expect(result).toEqual({ @@ -629,6 +634,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: mockField.defaultCountryCode, phoneNumber: '', + extension: '', }, type: mockField.type, validation: null, @@ -637,9 +643,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: mockField.defaultCountryCode, phoneNumber: '', + extension: '', }, }, }); @@ -653,6 +661,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -669,6 +678,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: prefillMock.countryCode, phoneNumber: '', + extension: '', }, type: mockField.type, validation: null, @@ -677,9 +687,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: prefillMock.countryCode, phoneNumber: '', + extension: '', }, }, }); @@ -695,6 +707,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { phoneNumber: '1234567890', @@ -711,6 +724,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: '', phoneNumber: prefillMock.phoneNumber, + extension: '', }, type: mockField.type, validation: null, @@ -719,9 +733,11 @@ describe('returnPhoneNumberCollector', () => { key: mockField.key, label: mockField.label, type: mockField.type, + options: { showExtension: false }, value: { countryCode: '', phoneNumber: prefillMock.phoneNumber, + extension: '', }, }, }); @@ -735,6 +751,7 @@ describe('returnPhoneNumberCollector', () => { type: 'PHONE_NUMBER', required: false, validatePhoneNumber: false, + showExtension: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -752,6 +769,7 @@ describe('returnPhoneNumberCollector', () => { value: { countryCode: prefillMock.countryCode, phoneNumber: prefillMock.phoneNumber, + extension: '', }, type: mockField.type, validation: null, @@ -760,13 +778,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 bbbdc2fdde..4c464f23fe 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -652,11 +652,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 2f97c3c63b..0b2af7caa8 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -155,9 +155,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 b30d7d12b3..96bb1a3866 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -756,6 +756,7 @@ describe('The phone number collector reducer', () => { label: 'Phone Number', type: 'PHONE_NUMBER', required: false, + showExtension: false, }, ], }, @@ -772,6 +773,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: '', phoneNumber: '', + extension: '', }, type: 'PHONE_NUMBER', validation: null, @@ -780,9 +782,11 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + options: { showExtension: false }, value: { countryCode: '', phoneNumber: '', + extension: '', }, }, }, @@ -800,8 +804,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([ @@ -815,7 +829,8 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', value: { countryCode: 'US', - phoneNumber: '', + phoneNumber: '1234567890', + extension: '54321', }, type: 'PHONE_NUMBER', validation: null, @@ -824,9 +839,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', }, }, }, @@ -841,6 +858,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: 'US', phoneNumber: '555-555-5555', + extension: '54321', }, }, }; @@ -856,6 +874,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: '', phoneNumber: '', + extension: '', }, type: 'PHONE_NUMBER', validation: null, @@ -864,8 +883,11 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + options: { showExtension: true }, value: { countryCode: '', + phoneNumber: '', + extension: '', }, }, }, @@ -882,6 +904,7 @@ describe('The phone number collector reducer', () => { value: { countryCode: 'US', phoneNumber: '555-555-5555', + extension: '54321', }, type: 'PHONE_NUMBER', validation: null, @@ -890,8 +913,11 @@ describe('The phone number collector reducer', () => { key: 'phone-number-key', label: 'Phone Number', type: 'PHONE_NUMBER', + options: { showExtension: true }, value: { countryCode: '', + phoneNumber: '', + extension: '', }, }, },