Skip to content

Commit 4756348

Browse files
committed
feat(davinci-client): implement phone number field support
Also fixed some minor bugs with device auth and registration as well as implemented component support for phone, device auth and reg in sample/test app.
1 parent 6bc605b commit 4756348

File tree

16 files changed

+549
-61
lines changed

16 files changed

+549
-61
lines changed

.changeset/clever-chicken-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@forgerock/davinci-client': patch
3+
---
4+
5+
Fixed bugs related to device auth and registration

.changeset/plain-books-beam.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+
Implemented phone number collector to support phone number field
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import type { ReadOnlyCollector } from '@forgerock/davinci-client/types';
8+
9+
export default function (formEl: HTMLFormElement, collector: ReadOnlyCollector) {
10+
// create paragraph element with text of "Loading ... "
11+
const p = document.createElement('p');
12+
13+
p.innerText = collector.output.label;
14+
formEl?.appendChild(p);
15+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import type {
8+
DeviceAuthenticationCollector,
9+
DeviceRegistrationCollector,
10+
Updater,
11+
} from '@forgerock/davinci-client/types';
12+
13+
/**
14+
* Creates a dropdown component based on the provided data and attaches it to the form
15+
* @param {HTMLFormElement} formEl - The form element to attach the dropdown to
16+
* @param {SingleSelectCollector} collector - Contains the dropdown options and configuration
17+
* @param {Updater} updater - Function to call when selection changes
18+
*/
19+
export default function objectValueComponent(
20+
formEl: HTMLFormElement,
21+
collector: DeviceRegistrationCollector | DeviceAuthenticationCollector,
22+
updater: Updater,
23+
submitForm: () => void,
24+
) {
25+
// Create the label element
26+
const paragraphEl = document.createElement('p');
27+
paragraphEl.textContent = collector.output.label || 'Select an option';
28+
paragraphEl.className = 'object-options-title';
29+
30+
// Append elements to the form
31+
formEl.appendChild(paragraphEl);
32+
33+
// Add all options from the data
34+
for (const option of collector.output.options) {
35+
const buttonEl = document.createElement('button');
36+
37+
// Add change event listener
38+
buttonEl.addEventListener('click', (event) => {
39+
// Properly type the event target
40+
const target = event.target as HTMLButtonElement;
41+
const selectedValue = target.getAttribute('data-id');
42+
43+
if (!selectedValue) {
44+
console.error('No value found for the selected option');
45+
return;
46+
}
47+
updater(selectedValue);
48+
submitForm();
49+
});
50+
51+
buttonEl.setAttribute('data-id', option.value);
52+
buttonEl.textContent = option.label;
53+
formEl.appendChild(buttonEl);
54+
}
55+
}

e2e/davinci-app/main.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import socialLoginButtonComponent from './components/social-login-button.js';
1919
import { serverConfigs } from './server-configs.js';
2020
import singleValueComponent from './components/single-value.js';
2121
import multiValueComponent from './components/multi-value.js';
22+
import labelComponent from './components/label.js';
23+
import objectValueComponent from './components/object-value.js';
2224

2325
const qs = window.location.search;
2426
const searchParams = new URLSearchParams(qs);
@@ -58,6 +60,7 @@ const urlParams = new URLSearchParams(window.location.search);
5860
// different middleware type than the old legacy config
5961
await Config.setAsync(config as any);
6062
}
63+
6164
function renderComplete() {
6265
const clientInfo = davinciClient.getClient();
6366
const serverInfo = davinciClient.getServer();
@@ -164,6 +167,21 @@ const urlParams = new URLSearchParams(window.location.search);
164167
collector, // This is the plain object of the collector
165168
davinciClient.update(collector), // Returns an update function for this collector
166169
);
170+
} else if (
171+
collector.type === 'DeviceRegistrationCollector' ||
172+
collector.type === 'DeviceAuthenticationCollector'
173+
) {
174+
objectValueComponent(
175+
formEl, // You can ignore this; it's just for rendering
176+
collector, // This is the plain object of the collector
177+
davinciClient.update(collector), // Returns an update function for this collector
178+
submitForm,
179+
);
180+
} else if (collector.type === 'ReadOnlyCollector') {
181+
labelComponent(
182+
formEl, // You can ignore this; it's just for rendering
183+
collector, // This is the plain object of the collector
184+
);
167185
} else if (collector.type === 'TextCollector') {
168186
textComponent(
169187
formEl, // You can ignore this; it's just for rendering
@@ -206,17 +224,21 @@ const urlParams = new URLSearchParams(window.location.search);
206224
});
207225

208226
if (davinciClient.getCollectors().find((collector) => collector.name === 'protectsdk')) {
209-
const newNode = await davinciClient.next();
210-
211-
if (newNode.status === 'continue') {
212-
renderForm();
213-
} else if (newNode.status === 'success') {
214-
renderComplete();
215-
} else if (newNode.status === 'error') {
216-
renderForm();
217-
} else {
218-
console.error('Unknown node status', newNode);
219-
}
227+
submitForm();
228+
}
229+
}
230+
231+
async function submitForm() {
232+
const newNode = await davinciClient.next();
233+
234+
if (newNode.status === 'continue') {
235+
renderForm();
236+
} else if (newNode.status === 'success') {
237+
renderComplete();
238+
} else if (newNode.status === 'error') {
239+
renderForm();
240+
} else {
241+
console.error('Unknown node status', newNode);
220242
}
221243
}
222244

e2e/davinci-app/server-configs.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,13 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
5555
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
5656
},
5757
},
58+
'20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0': {
59+
clientId: '20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0',
60+
redirectUri: window.location.origin + '/',
61+
scope: 'openid profile email revoke',
62+
serverConfig: {
63+
wellknown:
64+
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
65+
},
66+
},
5867
};

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

Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,12 @@ export type MultiSelectCollector = MultiValueCollectorWithValue<'MultiSelectColl
257257
export type ObjectValueCollectorTypes =
258258
| 'DeviceAuthenticationCollector'
259259
| 'DeviceRegistrationCollector'
260+
| 'PhoneNumberCollector'
261+
| 'ObjectOptionsCollector'
260262
| 'ObjectValueCollector'
261263
| 'ObjectSelectCollector';
262264

263-
interface ObjectOptionWithValue {
265+
interface DeviceOptionWithDefault {
264266
type: string;
265267
label: string;
266268
content: string;
@@ -269,55 +271,96 @@ interface ObjectOptionWithValue {
269271
key: string;
270272
}
271273

272-
interface ObjectOptionNoValue {
274+
interface DeviceOptionNoDefault {
273275
type: string;
274276
label: string;
275277
content: string;
276278
value: string;
277279
key: string;
278280
}
279281

280-
interface ObjectValue {
282+
export interface DeviceValue {
281283
type: string;
282284
id: string;
283285
value: string;
284286
}
285287

286-
export interface ObjectValueCollectorNoValue<T extends ObjectValueCollectorTypes> {
288+
export interface PhoneNumberInputValue {
289+
countryCode: string;
290+
phoneNumber: string;
291+
}
292+
293+
interface PhoneNumberOutputValue {
294+
countryCode?: string;
295+
phoneNumber?: string;
296+
}
297+
298+
export interface ObjectOptionsCollectorWithStringValue<
299+
T extends ObjectValueCollectorTypes,
300+
V = string,
301+
> {
302+
category: 'ObjectValueCollector';
303+
error: string | null;
304+
type: T;
305+
id: string;
306+
name: string;
307+
input: {
308+
key: string;
309+
value: V;
310+
type: string;
311+
};
312+
output: {
313+
key: string;
314+
label: string;
315+
type: string;
316+
options: DeviceOptionNoDefault[];
317+
};
318+
}
319+
320+
export interface ObjectOptionsCollectorWithObjectValue<
321+
T extends ObjectValueCollectorTypes,
322+
V = Record<string, string>,
323+
D = Record<string, string>,
324+
> {
287325
category: 'ObjectValueCollector';
288326
error: string | null;
289327
type: T;
290328
id: string;
291329
name: string;
292330
input: {
293331
key: string;
294-
value: string | null;
332+
value: V;
295333
type: string;
296334
};
297335
output: {
298336
key: string;
299337
label: string;
300338
type: string;
301-
options: ObjectOptionNoValue[];
339+
options: DeviceOptionWithDefault[];
340+
value?: D | null;
302341
};
303342
}
304343

305-
export interface ObjectValueCollectorWithValue<T extends ObjectValueCollectorTypes> {
344+
export interface ObjectValueCollectorWithObjectValue<
345+
T extends ObjectValueCollectorTypes,
346+
IV = Record<string, string>,
347+
OV = Record<string, string>,
348+
> {
306349
category: 'ObjectValueCollector';
307350
error: string | null;
308351
type: T;
309352
id: string;
310353
name: string;
311354
input: {
312355
key: string;
313-
value: ObjectValue | null;
356+
value: IV;
314357
type: string;
315358
};
316359
output: {
317360
key: string;
318361
label: string;
319362
type: string;
320-
options: ObjectOptionWithValue[];
363+
value?: OV | null;
321364
};
322365
}
323366

@@ -326,24 +369,37 @@ export type InferValueObjectCollectorType<T extends ObjectValueCollectorTypes> =
326369
? DeviceAuthenticationCollector
327370
: T extends 'DeviceRegistrationCollector'
328371
? DeviceRegistrationCollector
329-
:
330-
| ObjectValueCollectorWithValue<'ObjectValueCollector'>
331-
| ObjectValueCollectorNoValue<'ObjectValueCollector'>;
372+
: T extends 'PhoneNumberCollector'
373+
? PhoneNumberCollector
374+
:
375+
| ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'>
376+
| ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;
332377

333378
export type ObjectValueCollectors =
334-
| ObjectValueCollectorWithValue<'DeviceAuthenticationCollector'>
335-
| ObjectValueCollectorNoValue<'DeviceRegistrationCollector'>
336-
| ObjectValueCollectorWithValue<'ObjectSelectCollector'>
337-
| ObjectValueCollectorNoValue<'ObjectSelectCollector'>;
379+
| DeviceAuthenticationCollector
380+
| DeviceRegistrationCollector
381+
| PhoneNumberCollector
382+
| ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'>
383+
| ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>;
338384

339385
export type ObjectValueCollector<T extends ObjectValueCollectorTypes> =
340-
| ObjectValueCollectorWithValue<T>
341-
| ObjectValueCollectorNoValue<T>;
342-
343-
export type DeviceRegistrationCollector =
344-
ObjectValueCollectorNoValue<'DeviceRegistrationCollector'>;
345-
export type DeviceAuthenticationCollector =
346-
ObjectValueCollectorWithValue<'DeviceAuthenticationCollector'>;
386+
| ObjectOptionsCollectorWithObjectValue<T>
387+
| ObjectOptionsCollectorWithStringValue<T>
388+
| ObjectValueCollectorWithObjectValue<T>;
389+
390+
export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue<
391+
'DeviceRegistrationCollector',
392+
string
393+
>;
394+
export type DeviceAuthenticationCollector = ObjectOptionsCollectorWithObjectValue<
395+
'DeviceAuthenticationCollector',
396+
DeviceValue
397+
>;
398+
export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<
399+
'PhoneNumberCollector',
400+
PhoneNumberInputValue,
401+
PhoneNumberOutputValue
402+
>;
347403

348404
/** *********************************************************************
349405
* ACTION COLLECTORS
@@ -448,3 +504,16 @@ export type NoValueCollectors =
448504
export type NoValueCollector<T extends NoValueCollectorTypes> = NoValueCollectorBase<T>;
449505

450506
export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>;
507+
508+
export type UnknownCollector = {
509+
category: 'UnknownCollector';
510+
error: string | null;
511+
type: 'UnknownCollector';
512+
id: string;
513+
name: string;
514+
output: {
515+
key: string;
516+
label: string;
517+
type: string;
518+
};
519+
};

0 commit comments

Comments
 (0)