Skip to content

Commit 44f9be3

Browse files
authored
Merge pull request #573 from ForgeRock/SDKS-4670-phone-ext
SDKS-4670: Add support for extension in PhoneNumberCollector
2 parents eedcca7 + b271274 commit 44f9be3

16 files changed

Lines changed: 649 additions & 28 deletions

.changeset/long-singers-do.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@forgerock/davinci-client': minor
3+
---
4+
5+
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.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,6 @@ GEMINI.md
9595
.claude/worktrees
9696
.claude/settings.local.json
9797
.opensource
98+
99+
# Polaris
100+
.polaris-setup-progress.json

e2e/davinci-app/components/object-value.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import type {
88
DeviceAuthenticationCollector,
99
DeviceRegistrationCollector,
1010
PhoneNumberCollector,
11+
PhoneNumberExtensionCollector,
12+
PhoneNumberExtensionInputValue,
13+
PhoneNumberInputValue,
1114
Updater,
1215
} from '@forgerock/davinci-client/types';
1316

@@ -19,11 +22,16 @@ import type {
1922
*/
2023
export default function objectValueComponent(
2124
formEl: HTMLFormElement,
22-
collector: DeviceRegistrationCollector | DeviceAuthenticationCollector | PhoneNumberCollector,
25+
collector:
26+
| DeviceRegistrationCollector
27+
| DeviceAuthenticationCollector
28+
| PhoneNumberCollector
29+
| PhoneNumberExtensionCollector,
2330
updater:
2431
| Updater<DeviceRegistrationCollector>
2532
| Updater<DeviceAuthenticationCollector>
26-
| Updater<PhoneNumberCollector>,
33+
| Updater<PhoneNumberCollector>
34+
| Updater<PhoneNumberExtensionCollector>,
2735
submitForm: () => void,
2836
) {
2937
if (
@@ -61,7 +69,7 @@ export default function objectValueComponent(
6169
buttonEl.textContent = option.label;
6270
formEl.appendChild(buttonEl);
6371
}
64-
} else {
72+
} else if (collector.type === 'PhoneNumberCollector') {
6573
const phoneLabel = document.createElement('label');
6674
phoneLabel.textContent = collector.output.label || 'Phone Number';
6775
phoneLabel.className = 'object-options-title';
@@ -73,6 +81,9 @@ export default function objectValueComponent(
7381
phoneInput.setAttribute('name', 'phone-number-input');
7482
phoneInput.setAttribute('placeholder', 'Enter phone number');
7583

84+
formEl.appendChild(phoneLabel);
85+
formEl.appendChild(phoneInput);
86+
7687
// Add change event listener
7788
phoneInput.addEventListener('change', (event) => {
7889
// Properly type the event target
@@ -84,13 +95,73 @@ export default function objectValueComponent(
8495
return;
8596
}
8697

87-
updater({
98+
const phoneNumberInputValue: PhoneNumberInputValue = {
8899
phoneNumber: selectedValue,
89100
countryCode: collector.output.value?.countryCode || '',
90-
} as any);
101+
};
102+
const phoneNumberUpdater = updater as Updater<PhoneNumberCollector>;
103+
phoneNumberUpdater(phoneNumberInputValue);
91104
});
105+
} else if (collector.type === 'PhoneNumberExtensionCollector') {
106+
const phoneLabel = document.createElement('label');
107+
phoneLabel.textContent = collector.output.label || 'Phone Number';
108+
phoneLabel.className = 'object-options-title';
109+
phoneLabel.setAttribute('for', 'phone-number-input-1');
92110

93-
formEl.appendChild(phoneLabel);
94-
formEl.appendChild(phoneInput);
111+
const phoneInput = document.createElement('input');
112+
phoneInput.setAttribute('type', 'tel');
113+
phoneInput.setAttribute('id', 'phone-number-input-1');
114+
phoneInput.setAttribute('name', 'phone-number-input-1');
115+
phoneInput.setAttribute('placeholder', 'Enter phone number');
116+
117+
const extensionLabel = document.createElement('label');
118+
extensionLabel.textContent = collector.output.extensionLabel || 'Extension';
119+
extensionLabel.className = 'object-options-title';
120+
extensionLabel.setAttribute('for', 'extension-input-1');
121+
122+
const extensionInput = document.createElement('input');
123+
extensionInput.setAttribute('type', 'text');
124+
extensionInput.setAttribute('id', 'extension-input-1');
125+
extensionInput.setAttribute('name', 'extension-input-1');
126+
extensionInput.setAttribute('placeholder', 'Enter extension');
127+
128+
const divEl = document.createElement('div');
129+
divEl.style = 'display: flex; gap: 8px;';
130+
divEl.appendChild(phoneLabel);
131+
divEl.appendChild(phoneInput);
132+
divEl.appendChild(extensionLabel);
133+
divEl.appendChild(extensionInput);
134+
135+
formEl.appendChild(divEl);
136+
137+
const phoneNumberExtensionUpdater = updater as Updater<PhoneNumberExtensionCollector>;
138+
139+
// Add change event listener for phone number input
140+
phoneInput.addEventListener('change', (event) => {
141+
const target = event.target as HTMLInputElement;
142+
const phoneValue = target.value;
143+
const extensionValue = extensionInput.value;
144+
const phoneNumberExtensionInputValue: PhoneNumberExtensionInputValue = {
145+
phoneNumber: phoneValue,
146+
countryCode: collector.output.value?.countryCode || '',
147+
extension: extensionValue,
148+
};
149+
150+
phoneNumberExtensionUpdater(phoneNumberExtensionInputValue);
151+
});
152+
153+
// Add change event listener for extension input
154+
extensionInput.addEventListener('change', (event) => {
155+
const target = event.target as HTMLInputElement;
156+
const extensionValue = target.value;
157+
const phoneValue = phoneInput.value;
158+
const phoneNumberExtensionInputValue: PhoneNumberExtensionInputValue = {
159+
phoneNumber: phoneValue,
160+
countryCode: collector.output.value?.countryCode || '',
161+
extension: extensionValue,
162+
};
163+
164+
phoneNumberExtensionUpdater(phoneNumberExtensionInputValue);
165+
});
95166
}
96167
}

e2e/davinci-app/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ const urlParams = new URLSearchParams(window.location.search);
250250
formEl, // You can ignore this; it's just for rendering
251251
collector, // This is the plain object of the collector
252252
);
253-
} else if (collector.type === 'PhoneNumberCollector') {
253+
} else if (
254+
collector.type === 'PhoneNumberCollector' ||
255+
collector.type === 'PhoneNumberExtensionCollector'
256+
) {
254257
objectValueComponent(
255258
formEl, // You can ignore this; it's just for rendering
256259
collector, // This is the plain object of the collector

e2e/davinci-suites/src/form-fields.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ test('Should render form fields', async ({ page }) => {
3131
await page.locator('#combobox-field-key-3').check();
3232
await page.locator('#combobox-field-key-2').uncheck();
3333

34-
await page.locator('#phone-number-input').fill('1234567890');
34+
await page.locator('#phone-number-input-1').fill('1234567890');
35+
await page.locator('#extension-input-1').fill('7890');
3536

3637
await expect(page.getByRole('button', { name: 'Flow Button' })).toBeVisible();
3738
await expect(page.getByRole('button', { name: 'Flow Link' })).toBeVisible();
@@ -42,9 +43,10 @@ test('Should render form fields', async ({ page }) => {
4243

4344
await page.getByRole('button', { name: 'Submit' }).click();
4445
const request = await requestPromise;
45-
46-
const parsedData = JSON.parse(request.postData());
46+
const postData = request.postData();
47+
const parsedData = postData ? JSON.parse(postData) : {};
4748
const data = parsedData.parameters.data;
49+
4850
expect(data.actionKey).toBe('submit');
4951
expect(data.formData).toStrictEqual({
5052
'text-input-key': 'The input',
@@ -55,6 +57,7 @@ test('Should render form fields', async ({ page }) => {
5557
'phone-field': {
5658
phoneNumber: '1234567890',
5759
countryCode: 'GB',
60+
extension: '7890', // Tests PhoneNumberExtensionCollector
5861
},
5962
});
6063
});

e2e/davinci-suites/src/phone-number-field.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ test.describe('Device registration tests', () => {
105105
await page.getByRole('button', { name: 'Text Message' }).click();
106106
await expect(page.getByText('SDK Automation [JS] - Enter Phone Number')).toBeVisible();
107107
await page.getByRole('textbox', { name: 'Enter Phone Number' }).fill('3035550100');
108+
await expect(page.getByText('Extension')).not.toBeVisible(); // Tests standard PhoneNumberCollector
108109
await page.getByRole('button', { name: 'Submit' }).click();
109110

110111
await expect(page.getByText('SMS/Voice MFA Registered')).toBeVisible();

packages/davinci-client/src/lib/client.store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import type {
4646
MultiValueCollectors,
4747
FidoRegistrationInputValue,
4848
FidoAuthenticationInputValue,
49+
PhoneNumberExtensionInputValue,
4950
} from './collector.types.js';
5051
import type {
5152
InitFlow,
@@ -338,6 +339,7 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
338339
| string
339340
| string[]
340341
| PhoneNumberInputValue
342+
| PhoneNumberExtensionInputValue
341343
| FidoRegistrationInputValue
342344
| FidoAuthenticationInputValue,
343345
index?: number,

packages/davinci-client/src/lib/collector.types.test-d.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ import type {
2727
ReadOnlyCollector,
2828
QrCodeCollector,
2929
AgreementCollector,
30+
PhoneNumberCollector,
31+
PhoneNumberExtensionCollector,
32+
ObjectValueCollectorWithObjectValue,
33+
InferValueObjectCollectorType,
34+
PhoneNumberInputValue,
35+
PhoneNumberOutputValue,
36+
PhoneNumberExtensionInputValue,
37+
PhoneNumberExtensionOutputValue,
3038
} from './collector.types.js';
3139

3240
describe('Collector Types', () => {
@@ -358,6 +366,112 @@ describe('Collector Types', () => {
358366

359367
expectTypeOf(tCollector).toMatchTypeOf<FlowCollector>();
360368
});
369+
370+
it('should correctly infer PhoneNumberCollector Type', () => {
371+
const tCollector: InferValueObjectCollectorType<'PhoneNumberCollector'> = {
372+
category: 'ObjectValueCollector',
373+
error: null,
374+
type: 'PhoneNumberCollector',
375+
id: '',
376+
name: '',
377+
input: {
378+
key: '',
379+
value: { countryCode: '', phoneNumber: '' },
380+
type: '',
381+
validation: null,
382+
},
383+
output: {
384+
key: '',
385+
label: '',
386+
type: '',
387+
value: { countryCode: '', phoneNumber: '' },
388+
},
389+
};
390+
391+
expectTypeOf(tCollector).toEqualTypeOf<PhoneNumberCollector>();
392+
});
393+
});
394+
395+
describe('ObjectValueCollector Types', () => {
396+
it('should correctly infer PhoneNumberExtensionCollector Type', () => {
397+
const tCollector: InferValueObjectCollectorType<'PhoneNumberExtensionCollector'> = {
398+
category: 'ObjectValueCollector',
399+
error: null,
400+
type: 'PhoneNumberExtensionCollector',
401+
id: '',
402+
name: '',
403+
input: {
404+
key: '',
405+
value: { countryCode: '', phoneNumber: '', extension: '' },
406+
type: '',
407+
validation: null,
408+
},
409+
output: {
410+
key: '',
411+
label: '',
412+
type: '',
413+
extensionLabel: '',
414+
value: {},
415+
},
416+
};
417+
418+
expectTypeOf(tCollector).toEqualTypeOf<PhoneNumberExtensionCollector>();
419+
});
420+
421+
it('should validate PhoneNumberExtensionCollector structure', () => {
422+
expectTypeOf<PhoneNumberExtensionCollector>()
423+
.toHaveProperty('category')
424+
.toEqualTypeOf<'ObjectValueCollector'>();
425+
expectTypeOf<PhoneNumberExtensionCollector>()
426+
.toHaveProperty('type')
427+
.toEqualTypeOf<'PhoneNumberExtensionCollector'>();
428+
expectTypeOf<
429+
PhoneNumberExtensionCollector['input']['value']
430+
>().toEqualTypeOf<PhoneNumberExtensionInputValue>();
431+
expectTypeOf<
432+
PhoneNumberExtensionCollector['output']['value']
433+
>().toEqualTypeOf<PhoneNumberExtensionOutputValue>();
434+
});
435+
436+
it('should validate PhoneNumberCollector structure', () => {
437+
expectTypeOf<PhoneNumberCollector>().toEqualTypeOf<
438+
ObjectValueCollectorWithObjectValue<
439+
'PhoneNumberCollector',
440+
PhoneNumberInputValue,
441+
PhoneNumberOutputValue
442+
>
443+
>();
444+
expectTypeOf<PhoneNumberCollector>()
445+
.toHaveProperty('category')
446+
.toEqualTypeOf<'ObjectValueCollector'>();
447+
expectTypeOf<PhoneNumberCollector>()
448+
.toHaveProperty('type')
449+
.toEqualTypeOf<'PhoneNumberCollector'>();
450+
expectTypeOf<PhoneNumberCollector['input']['value']>().toEqualTypeOf<PhoneNumberInputValue>();
451+
});
452+
453+
it('should validate PhoneNumberCollector base type constraints', () => {
454+
const collector: PhoneNumberCollector = {
455+
category: 'ObjectValueCollector',
456+
type: 'PhoneNumberCollector',
457+
error: null,
458+
id: 'test',
459+
name: 'Test',
460+
input: {
461+
key: 'phone',
462+
value: { countryCode: '+1', phoneNumber: '5555555555' },
463+
type: 'string',
464+
validation: null,
465+
},
466+
output: {
467+
key: 'phone',
468+
label: 'Phone Number',
469+
type: 'phone',
470+
value: { countryCode: '+1', phoneNumber: '5555555555' },
471+
},
472+
};
473+
expectTypeOf(collector).toEqualTypeOf<PhoneNumberCollector>();
474+
});
361475
});
362476

363477
describe('InferNoValueCollectorType', () => {

0 commit comments

Comments
 (0)