Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/long-singers-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mark this as major if it is a breaking change?

---

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.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,6 @@ GEMINI.md
.claude/worktrees
.claude/settings.local.json
.opensource

# Polaris
.polaris-setup-progress.json
85 changes: 78 additions & 7 deletions e2e/davinci-app/components/object-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import type {
DeviceAuthenticationCollector,
DeviceRegistrationCollector,
PhoneNumberCollector,
PhoneNumberExtensionCollector,
PhoneNumberExtensionInputValue,
PhoneNumberInputValue,
Updater,
} from '@forgerock/davinci-client/types';

Expand All @@ -19,11 +22,16 @@ import type {
*/
export default function objectValueComponent(
formEl: HTMLFormElement,
collector: DeviceRegistrationCollector | DeviceAuthenticationCollector | PhoneNumberCollector,
collector:
| DeviceRegistrationCollector
| DeviceAuthenticationCollector
| PhoneNumberCollector
| PhoneNumberExtensionCollector,
updater:
| Updater<DeviceRegistrationCollector>
| Updater<DeviceAuthenticationCollector>
| Updater<PhoneNumberCollector>,
| Updater<PhoneNumberCollector>
| Updater<PhoneNumberExtensionCollector>,
submitForm: () => void,
) {
if (
Expand Down Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -84,13 +95,73 @@ export default function objectValueComponent(
return;
}

updater({
const phoneNumberInputValue: PhoneNumberInputValue = {
phoneNumber: selectedValue,
countryCode: collector.output.value?.countryCode || '',
} as any);
};
const phoneNumberUpdater = updater as Updater<PhoneNumberCollector>;
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;';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

TypeScript lib.dom.d.ts ElementCSSInlineStyle style readonly CSSStyleDeclaration 2025

💡 Result:

In TypeScript's lib.dom.d.ts (as of 2024/2025 DOM updates), the ElementCSSInlineStyle interface declares the style property as readonly CSSStyleDeclaration. This means you cannot reassign the style property itself (e.g., element.style = someOtherDeclaration), but you can mutate the returned CSSStyleDeclaration object by setting individual properties (e.g., element.style.color = 'red'), using cssText (e.g., element.style.cssText = 'color: red'), or calling methods like setProperty. This design matches the DOM spec and browser behavior: the style property returns a live CSSStyleDeclaration object tied to the element's inline styles, which is mutable despite the readonly getter. Direct string assignment like element.style = 'color: red' works in browsers (forwarded to cssText), but TypeScript types it as readonly to prevent replacing the object, avoiding type errors for incompatible assignments. A long-open GitHub issue (#38838) requests allowing string setters per spec, but it remains open in the Backlog as of 2025. No changes in 2025 DOM updates (e.g., PR #60987, #61647) alter this; recent updates add CSS properties and accessors but keep style readonly. For dynamic keys, cast or use setProperty. Example: const el = document.createElement('div') as HTMLElement; el.style.color = 'red'; // OK el.style.cssText = 'color: red; font-size: 16px'; // OK el.style.setProperty('background', 'blue'); // OK el.style = /* CSSStyleDeclaration */; // Error: readonly

Citations:


🏁 Script executed:

# Check if the file exists and read around line 129
file_path="e2e/davinci-app/components/object-value.ts"
if [ -f "$file_path" ]; then
  echo "=== File exists ==="
  # Show lines around 129 with context
  sed -n '120,140p' "$file_path" | cat -n
else
  echo "File not found: $file_path"
  # Try to find similar files
  find . -name "object-value.ts" -type f 2>/dev/null
fi

Repository: ForgeRock/ping-javascript-sdk

Length of output: 1080


🏁 Script executed:

# Check for tsconfig files and TypeScript configuration
echo "=== Looking for tsconfig files ==="
find . -name "tsconfig*.json" -type f 2>/dev/null | head -20

echo ""
echo "=== Checking project structure around the file ==="
ls -la e2e/davinci-app/components/ 2>/dev/null | head -20

Repository: ForgeRock/ping-javascript-sdk

Length of output: 1851


🏁 Script executed:

# Check the tsconfig files for davinci-app
echo "=== e2e/davinci-app/tsconfig.json ==="
cat e2e/davinci-app/tsconfig.json

echo ""
echo "=== e2e/davinci-app/tsconfig.app.json ==="
cat e2e/davinci-app/tsconfig.app.json

Repository: ForgeRock/ping-javascript-sdk

Length of output: 1186


divEl.style = '...' is a TypeScript compile error (Cannot assign to 'style' because it is a read-only property).

The style property is typed as readonly CSSStyleDeclaration in TypeScript's DOM type definitions. Direct string assignment is not permitted, even though browsers forward it to cssText. Use divEl.style.cssText instead.

🛠 Proposed fix
-    divEl.style = 'display: flex; gap: 8px;';
+    divEl.style.cssText = 'display: flex; gap: 8px;';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
divEl.style = 'display: flex; gap: 8px;';
divEl.style.cssText = 'display: flex; gap: 8px;';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/davinci-app/components/object-value.ts` at line 129, Replace the invalid
assignment to the readonly style property by setting the element's cssText: find
the statement assigning to divEl.style (the line "divEl.style = 'display: flex;
gap: 8px;';") and change it to assign the CSS string to divEl.style.cssText so
TypeScript's readonly CSSStyleDeclaration typing is respected.

divEl.appendChild(phoneLabel);
divEl.appendChild(phoneInput);
divEl.appendChild(extensionLabel);
divEl.appendChild(extensionInput);

formEl.appendChild(divEl);

const phoneNumberExtensionUpdater = updater as Updater<PhoneNumberExtensionCollector>;

// 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;
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;
const phoneNumberExtensionInputValue: PhoneNumberExtensionInputValue = {
phoneNumber: phoneValue,
countryCode: collector.output.value?.countryCode || '',
extension: extensionValue,
};

phoneNumberExtensionUpdater(phoneNumberExtensionInputValue);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
5 changes: 4 additions & 1 deletion e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions e2e/davinci-suites/src/form-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;

Comment on lines +46 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard parameters.data access when POST body is absent.

At Line 47, parsedData can be {}, but Line 48 still assumes parsedData.parameters.data exists. This can fail with a TypeError and hide the real failure cause.

🔧 Proposed fix
 const postData = request.postData();
-const parsedData = postData ? JSON.parse(postData) : {};
-const data = parsedData.parameters.data;
+expect(postData).toBeTruthy();
+const parsedData = JSON.parse(postData!);
+const data = parsedData?.parameters?.data;
+expect(data).toBeTruthy();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const postData = request.postData();
const parsedData = postData ? JSON.parse(postData) : {};
const data = parsedData.parameters.data;
const postData = request.postData();
expect(postData).toBeTruthy();
const parsedData = JSON.parse(postData!);
const data = parsedData?.parameters?.data;
expect(data).toBeTruthy();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/davinci-suites/src/form-fields.test.ts` around lines 46 - 49, The test
accesses parsedData.parameters.data without guarding for missing POST body;
update the extraction of data (variables postData/parsedData/data) to safely
handle absent or malformed bodies by checking postData and parsedData.parameters
exist before accessing .data (e.g., use optional chaining or provide default
objects), and ensure data is assigned a safe default when missing so no
TypeError is thrown during the test.

expect(data.actionKey).toBe('submit');
expect(data.formData).toStrictEqual({
'text-input-key': 'The input',
Expand All @@ -55,6 +57,7 @@ test('Should render form fields', async ({ page }) => {
'phone-field': {
phoneNumber: '1234567890',
countryCode: 'GB',
extension: '7890', // Tests PhoneNumberExtensionCollector
},
});
});
Expand Down
3 changes: 2 additions & 1 deletion e2e/davinci-suites/src/phone-number-field.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import type {
MultiValueCollectors,
FidoRegistrationInputValue,
FidoAuthenticationInputValue,
PhoneNumberExtensionInputValue,
} from './collector.types.js';
import type {
InitFlow,
Expand Down Expand Up @@ -338,6 +339,7 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
| string
| string[]
| PhoneNumberInputValue
| PhoneNumberExtensionInputValue
| FidoRegistrationInputValue
| FidoAuthenticationInputValue,
index?: number,
Expand Down
114 changes: 114 additions & 0 deletions packages/davinci-client/src/lib/collector.types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ import type {
ReadOnlyCollector,
QrCodeCollector,
AgreementCollector,
PhoneNumberCollector,
PhoneNumberExtensionCollector,
ObjectValueCollectorWithObjectValue,
InferValueObjectCollectorType,
PhoneNumberInputValue,
PhoneNumberOutputValue,
PhoneNumberExtensionInputValue,
PhoneNumberExtensionOutputValue,
} from './collector.types.js';

describe('Collector Types', () => {
Expand Down Expand Up @@ -358,6 +366,112 @@ describe('Collector Types', () => {

expectTypeOf(tCollector).toMatchTypeOf<FlowCollector>();
});

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<PhoneNumberCollector>();
});
});

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<PhoneNumberExtensionCollector>();
});

it('should validate PhoneNumberExtensionCollector structure', () => {
expectTypeOf<PhoneNumberExtensionCollector>()
.toHaveProperty('category')
.toEqualTypeOf<'ObjectValueCollector'>();
expectTypeOf<PhoneNumberExtensionCollector>()
.toHaveProperty('type')
.toEqualTypeOf<'PhoneNumberExtensionCollector'>();
expectTypeOf<
PhoneNumberExtensionCollector['input']['value']
>().toEqualTypeOf<PhoneNumberExtensionInputValue>();
expectTypeOf<
PhoneNumberExtensionCollector['output']['value']
>().toEqualTypeOf<PhoneNumberExtensionOutputValue>();
});

it('should validate PhoneNumberCollector structure', () => {
expectTypeOf<PhoneNumberCollector>().toEqualTypeOf<
ObjectValueCollectorWithObjectValue<
'PhoneNumberCollector',
PhoneNumberInputValue,
PhoneNumberOutputValue
>
>();
expectTypeOf<PhoneNumberCollector>()
.toHaveProperty('category')
.toEqualTypeOf<'ObjectValueCollector'>();
expectTypeOf<PhoneNumberCollector>()
.toHaveProperty('type')
.toEqualTypeOf<'PhoneNumberCollector'>();
expectTypeOf<PhoneNumberCollector['input']['value']>().toEqualTypeOf<PhoneNumberInputValue>();
});

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<PhoneNumberCollector>();
});
});

describe('InferNoValueCollectorType', () => {
Expand Down
Loading
Loading