Skip to content

Commit 9507c76

Browse files
committed
feat(reactjs-todo-davinci): add new collectors (SDKS-5052)
1 parent bc9ebef commit 9507c76

14 files changed

Lines changed: 480 additions & 59 deletions

File tree

javascript/reactjs-todo-davinci/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,24 @@ This sample code is provided "as is" and is not a supported product of Ping Iden
88

99
- TextCollector
1010
- PasswordCollector
11+
- ValidatedPasswordCollector
1112
- SingleSelectCollector
1213
- ReadOnlyCollector
1314
- PhoneNumberCollector
15+
- PhoneNumberExtensionCollector
1416
- DeviceRegistrationCollector
1517
- DeviceAuthenticationCollector
18+
- FidoRegistrationCollector
19+
- FidoAuthenticationCollector
1620
- IdpCollector
1721
- SubmitCollector
1822
- FlowCollector
1923
- ProtectCollector
24+
- QrCodeCollector
25+
- RichTextCollector
26+
- AgreementCollector
27+
- ValidatedBooleanCollector
28+
- PollingCollector
2029

2130
## Requirements
2231

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* ping-sample-web-react-davinci
3+
*
4+
* boolean.js
5+
*
6+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
7+
* This software may be modified and distributed under the terms
8+
* of the MIT license. See the LICENSE file for details.
9+
*/
10+
11+
import React, { useState } from 'react';
12+
import { interpolateRichContent } from '../utilities/rich-content';
13+
14+
export default function BooleanComponent({ collector, inputName, updater }) {
15+
const [isChecked, setIsChecked] = useState(collector.output.value);
16+
17+
const fieldId = collector.output.key || `${inputName}-checkbox-field`;
18+
19+
const { richContent } = collector.output;
20+
const label = richContent?.replacements?.length
21+
? interpolateRichContent(richContent)
22+
: collector.output.label || '';
23+
24+
const required = collector.input.validation?.some(
25+
(validation) => validation.type === 'required' && validation.rule === true,
26+
);
27+
28+
const handleChange = (event) => {
29+
const value = event.target.checked;
30+
setIsChecked(value);
31+
updater(value);
32+
};
33+
34+
return (
35+
<div className={'mb-5 form-check'}>
36+
<label htmlFor={fieldId} className="form-label">
37+
{label}
38+
</label>
39+
<input
40+
type="checkbox"
41+
id={fieldId}
42+
name={fieldId}
43+
checked={isChecked}
44+
onChange={handleChange}
45+
className="form-check-input"
46+
required={required}
47+
/>
48+
</div>
49+
);
50+
}

javascript/reactjs-todo-davinci/client/components/davinci-client/error.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/*
2+
* ping-sample-web-react-davinci
3+
*
4+
* error.js
5+
*
6+
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
7+
* This software may be modified and distributed under the terms
8+
* of the MIT license. See the LICENSE file for details.
9+
*/
10+
111
import React, { useEffect, useState } from 'react';
212

313
export default function Error({ getError }) {

javascript/reactjs-todo-davinci/client/components/davinci-client/form.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010
import React, { Fragment, useContext, useEffect, useState } from 'react';
1111
import { useNavigate } from 'react-router-dom';
12-
import Readonly from './readonly.js';
12+
import ReadOnly from './readonly.js';
1313
import Text from './text.js';
1414
import Error from './error.js';
1515
import Password from './password.js';
@@ -19,6 +19,10 @@ import Protect from './protect.js';
1919
import ObjectValueComponent from './object-value.js';
2020
import SingleSelect from './single-select.js';
2121
import FlowLink from './flow-link.js';
22+
import FidoComponent from './fido.js';
23+
import PollingComponent from './polling.js';
24+
import BooleanComponent from './boolean.js';
25+
import QrCode from './qr-code.js';
2226
import Unknown from './unknown.js';
2327
import Alert from './alert.js';
2428
import KeyIcon from '../icons/key-icon';
@@ -30,7 +34,6 @@ import { ProtectContext } from '../../context/protect.context.js';
3034
import { ThemeContext } from '../../context/theme.context.js';
3135
import useDavinci from './hooks/davinci.hook.js';
3236
import useOAuth from './hooks/oauth.hook.js';
33-
import FidoComponent from './fido.js';
3437

3538
/**
3639
* @function Form - React view for managing the user authentication journey
@@ -58,7 +61,7 @@ export default function Form() {
5861
const [user, setCode] = useOAuth();
5962
const [
6063
{ formName, formAction, node, collectors },
61-
{ getError, setNext, startNewFlow, updater, externalIdp },
64+
{ getError, setNext, startNewFlow, updater, pollStatus, externalIdp },
6265
] = useDavinci();
6366

6467
/**
@@ -169,17 +172,28 @@ export default function Form() {
169172
case 'ERROR_DISPLAY':
170173
return <Error key={idx + 'err'} getError={getError} />;
171174
case 'ReadOnlyCollector':
172-
return <Readonly key={idx + collectorName} collector={collector} />;
175+
case 'RichTextCollector':
176+
case 'AgreementCollector':
177+
return <ReadOnly key={idx + collectorName} collector={collector} />;
178+
case 'ValidatedBooleanCollector':
179+
return (
180+
<BooleanComponent
181+
key={idx + collectorName}
182+
collector={collector}
183+
updater={updater(collector)}
184+
inputName={idx + collectorName}
185+
/>
186+
);
173187
case 'PhoneNumberCollector':
188+
case 'PhoneNumberExtensionCollector':
174189
case 'DeviceRegistrationCollector':
175190
case 'DeviceAuthenticationCollector':
176191
return (
177192
<ObjectValueComponent
178-
inputName={collectorName}
193+
inputName={idx + collectorName}
179194
collector={collector}
180195
updater={updater(collector)}
181-
key={collectorName}
182-
submitForm={setNext}
196+
key={idx + collectorName}
183197
/>
184198
);
185199
case 'IdpCollector':
@@ -201,8 +215,21 @@ export default function Form() {
201215
submitForm={setNext}
202216
/>
203217
);
218+
case 'PollingCollector':
219+
return (
220+
<PollingComponent
221+
collector={collector}
222+
updater={updater(collector)}
223+
pollStatus={pollStatus(collector)}
224+
cacheKey={node?.cache?.key}
225+
key={collectorName}
226+
submitForm={setNext}
227+
/>
228+
);
204229
case 'ProtectCollector':
205230
return <Protect collector={collector} key={collectorName} />;
231+
case 'QrCodeCollector':
232+
return <QrCode collector={collector} key={collectorName} />;
206233
case 'SubmitCollector':
207234
return <SubmitButton collector={collector} isLoading={isLoading} key={collectorName} />;
208235
case 'FlowCollector':

javascript/reactjs-todo-davinci/client/components/davinci-client/hooks/davinci.hook.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ export default function useDavinci() {
9494
return davinciClient.update(collector);
9595
}
9696

97+
/**
98+
* @function pollStatus - Gets the DaVinci client pollStatus function for a collector
99+
* @returns {function} - A function to start challenge or continue polling
100+
*/
101+
function pollStatus(collector) {
102+
return davinciClient.pollStatus(collector);
103+
}
104+
97105
/**
98106
* @function setNext - Get the next node in the DaVinci flow
99107
* @returns {Promise<void>}
@@ -148,6 +156,7 @@ export default function useDavinci() {
148156
setNext,
149157
startNewFlow,
150158
updater,
159+
pollStatus,
151160
externalIdp: davinciClient && davinciClient.externalIdp(),
152161
getError: davinciClient && davinciClient.getError,
153162
},

javascript/reactjs-todo-davinci/client/components/davinci-client/object-value.js

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,38 @@
1+
/*
2+
* ping-sample-web-react-davinci
3+
*
4+
* object-value.js
5+
*
6+
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
7+
* This software may be modified and distributed under the terms
8+
* of the MIT license. See the LICENSE file for details.
9+
*/
10+
111
import React, { useEffect, useContext, useState } from 'react';
212
import { ThemeContext } from '../../context/theme.context.js';
313

4-
export default function ObjectValueComponent({ collector, updater }) {
5-
const [selected, setSelected] = useState(collector.output.options[0].value);
14+
export default function ObjectValueComponent({ collector, updater, inputName }) {
15+
const [selectedDevice, setSelectedDevice] = useState(
16+
collector.output.options?.[0]?.value ?? null,
17+
);
18+
const [phoneValue, setPhoneValue] = useState({
19+
phoneNumber: collector.input.value?.phoneNumber ?? '',
20+
extension: collector.input.value?.extension ?? '',
21+
});
622
const theme = useContext(ThemeContext);
723

824
useEffect(() => {
9-
updater(selected);
10-
}, [selected]);
25+
if (
26+
collector.type === 'DeviceAuthenticationCollector' ||
27+
collector.type === 'DeviceRegistrationCollector'
28+
) {
29+
updater(selectedDevice);
30+
}
31+
}, [selectedDevice, updater, collector.type]);
1132

12-
const handleChange = (event) => {
33+
const handleChangeDevice = (event) => {
1334
event.preventDefault();
14-
setSelected(event.target.value);
35+
setSelectedDevice(event.target.value);
1536
};
1637
if (
1738
collector.type === 'DeviceAuthenticationCollector' ||
@@ -28,8 +49,8 @@ export default function ObjectValueComponent({ collector, updater }) {
2849
<select
2950
id="device-select"
3051
className="form-select form-select-lg w-100"
31-
value={selected}
32-
onChange={handleChange}
52+
value={selectedDevice}
53+
onChange={handleChangeDevice}
3354
>
3455
{collector.output.options.map((option) => (
3556
<option key={option.value + option.label} value={option.value}>
@@ -41,25 +62,86 @@ export default function ObjectValueComponent({ collector, updater }) {
4162
</div>
4263
);
4364
} else if (collector.type === 'PhoneNumberCollector') {
65+
const phoneInputId = `${inputName}-phone-number`;
66+
const required = collector.input.validation?.some(
67+
(validation) => validation.type === 'required' && validation.rule === true,
68+
);
69+
4470
return (
4571
<>
46-
<label htmlFor={'form-label phone-number-input'} className={'mb-2 mt-2'}>
72+
<label htmlFor={phoneInputId} className={'form-label mb-2 mt-2'}>
4773
{collector.output.label || 'Phone Number'}
4874
</label>
4975
<input
5076
type="text"
5177
className={'mb-2 mt-2'}
52-
id="phone-number-input"
53-
name="phone-number-input"
78+
id={phoneInputId}
79+
name={phoneInputId}
80+
placeholder="Enter phone number"
81+
value={phoneValue.phoneNumber}
82+
onChange={(event) => {
83+
const updatedPhone = event.target.value;
84+
setPhoneValue((prev) => ({ ...prev, phoneNumber: updatedPhone }));
85+
updater({
86+
phoneNumber: updatedPhone,
87+
countryCode: collector.input.value?.countryCode,
88+
});
89+
}}
90+
required={required}
91+
/>
92+
</>
93+
);
94+
} else if (collector.type === 'PhoneNumberExtensionCollector') {
95+
const phoneInputId = `${inputName}-phone-number`;
96+
const extensionInputId = `${inputName}-extension`;
97+
const required = collector.input.validation?.some(
98+
(validation) => validation.type === 'required' && validation.rule === true,
99+
);
100+
return (
101+
<>
102+
<label htmlFor={phoneInputId} className={'form-label mb-2 mt-2'}>
103+
{collector.output.label || 'Phone Number'}
104+
</label>
105+
<input
106+
type="text"
107+
className={'mb-2'}
108+
id={phoneInputId}
109+
name={phoneInputId}
54110
placeholder="Enter phone number"
111+
value={phoneValue.phoneNumber}
112+
onChange={(event) => {
113+
const updatedPhoneNumber = event.target.value;
114+
const updatedPhoneValue = { ...phoneValue, phoneNumber: updatedPhoneNumber };
115+
setPhoneValue(updatedPhoneValue);
116+
updater({
117+
phoneNumber: updatedPhoneValue.phoneNumber,
118+
countryCode: collector.input.value?.countryCode,
119+
extension: updatedPhoneValue.extension,
120+
});
121+
}}
122+
required={required}
123+
/>
124+
<label htmlFor={extensionInputId} className={'form-label mb-2 mt-2'}>
125+
{collector.output.extensionLabel || 'Extension'}
126+
</label>
127+
<input
128+
type="text"
129+
className={'mb-2'}
130+
id={extensionInputId}
131+
name={extensionInputId}
132+
placeholder="Enter extension"
133+
value={phoneValue.extension}
55134
onChange={(event) => {
56-
const selectedValue = event.target.value;
57-
if (!selectedValue) {
58-
console.error('No value found for the selected option');
59-
return;
60-
}
61-
updater({ phoneNumber: selectedValue, countryCode: collector.input.value.countryCode });
135+
const updatedExtension = event.target.value;
136+
const updatedPhoneValue = { ...phoneValue, extension: updatedExtension };
137+
setPhoneValue(updatedPhoneValue);
138+
updater({
139+
phoneNumber: updatedPhoneValue.phoneNumber,
140+
countryCode: collector.input.value?.countryCode,
141+
extension: updatedPhoneValue.extension,
142+
});
62143
}}
144+
required={required}
63145
/>
64146
</>
65147
);

0 commit comments

Comments
 (0)