DaVinci client deep dive for JavaScript
Configure DaVinci client properties to connect to PingOne and step through an associated DaVinci flow.
Installing and importing the DaVinci client
To install and import the DaVinci client:
-
Install the DaVinci client into your JavaScript apps using
npm
:Install the DaVinci clientnpm install @forgerock/davinci-client
-
Import the DaVinci client as a named import:
Import the DaVinci clientimport { davinci } from '@forgerock/davinci-client';
Configuring the DaVinci client
Configure DaVinci client for JavaScript properties to connect to PingOne and step through an associated DaVinci flow.
The following shows a full DaVinci client configuration:
import { davinci } from '@forgerock/davinci';
const davinciClient = await davinci({
config: {
clientId: '6c7eb89a-66e9-ab12-cd34-eeaf795650b2',
serverConfig: {
wellknown: 'https://auth.pingone.com/3072206d-c6ce-ch15-m0nd-f87e972c7cc3/as/.well-known/openid-configuration',
timeout: 3000,
},
scope: 'openid profile email phone',
responseType: 'code',
},
});
For information on the properties available, refer to Configure DaVinci client for JavaScript properties.
Stepping through DaVinci flows
To authenticate your users the Ping SDK for JavaScript DaVinci client must start the flow, and step through each node.
For information on which connectors and fields the DaVinci client supports, refer to Compatibility. |
Starting a DaVinci flow
To start a DaVinci flow, call the start()
method on your new client object:
let node = await davinciClient.start();
Adding custom parameters
When starting a DaVinci client you can add additional key-pair parameters. The DaVinci client will append these parameters as query strings to the initial OAuth 2.0 call to the /authorize
endpoint.
You can access these additional OAuth 2.0 parameters in your DaVinci flows by using the Learn more in Referencing PingOne data in the flow. |
To add parameters when starting the client, create an object of the key-value pairs and pass it as a query
parameter to the start()
function:
const query = {
customKey: 'customValue'
}
let node = await davinciClient.start(query);
You can add any parameters to the request as required. For example, you could add |
Determining DaVinci flow node type
Each step of the flow returns one of four node types:
continue
-
This type indicates there is input required from the client. The
node
object for this type contains a list ofcollector
objects, which describe the information it requires from the client.Learn more in Handling DaVinci flow collectors in continue nodes.
success
-
This type indicates the flow is complete, and authentication was successful.
Learn more in Handling DaVinci flow success nodes.
error
-
This type indicates an error in the data sent to the server. For example, an email address in an incorrect format, or a password that does not meet complexity requirements.
You can correct the error and resubmit to continue the flow.
Learn more in Handling DaVinci flow error nodes.
failure
-
This type indicates that the flow could not be completed and must be restarted. This can be caused by a server error, or a timeout.
Learn more in Handling DaVinci flow failure nodes.
Use node.status
to determine which node type the server has returned:
node.status
propertylet node = await davinciClient.start();
switch (node.status) {
case 'continue':
return renderContinue();
case 'success':
return renderSuccess();
case 'error':
return renderError();
default: // Handle 'failure' node type
return renderFailure();
}
Handling DaVinci flow collectors in continue nodes
The continue
node type contains a list of collector
objects. These collectors define what information or action to request from the user, or browser.
For a list of supported collectors, refer to Supported PingOne fields and collectors. |
The Ping SDK for JavaScript groups collectors that have similar traits together into categories. For example, collectors that only require a single primitive value to be returned to the server, such as a username or password string, or a single value from a drop-down list are grouped together in a single value collectors category.
To complete a DaVinci flow, we recommend that you either implement a component for a category of collectors, or implement a component for each collector type that you will encounter in the flow.
Your app iterates through the flow and handles each collector as you encounter it.
const collectors = davinciClient.getCollectors();
collectors.forEach((collector) => {
if (collector.type === 'TextCollector') {
textComponent(
collector, // Object with the collector details
davinciClient.update(collector), // Returns an update function for this collector
davinciClient.validate(collector), // Returns a validate function for this collector
);
} else if (collector.type === 'PasswordCollector') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
collector;
passwordComponent(
collector, // Object with the collector details
davinciClient.update(collector), // Returns an update function for this collector
);
} else if (collector.type === 'SubmitCollector') {
submitButtonComponent(
collector, // Object with the collector details
);
} else if (collector.type === 'FlowCollector') {
flowLinkComponent(
collector, // Object with the collector details
davinciClient.flow({
// Returns a function to call the flow from within component
action: collector.output.key,
}),
renderForm, // Enable re-rendering the form
);
}
});
Example 1. Handling TextCollector
with a component
This example shows how to update a collector with a value gathered from your user.
Pass both a collector
and updater
object into a component that renders the appropriate user interface, captures the user’s input, and then updates the collector, ready to return to the server.
TextCollector
mappingconst collectors = davinciClient.getCollectors();
collectors.map((collector) => {
if (collector.type === 'TextCollector') {
renderTextCollector(collector, davinciClient.update(collector));
}
});
Mutating the The internal data the client stores is immutable and can only be updated using the provided APIs, not through property assignment. |
Your renderTextCollector
would resemble the following:
TextCollector
updater componentfunction renderTextCollector(collector, updater) {
// ... component logic
function onClick(event) {
updater(event.target.value);
}
// render code
}
Example 2. Handling FlowCollector
with a component
This example shows how change from the current flow to an alternate flow, such as a reset password or registration flow.
To switch flows, call the flow
method on the davinciClient
passing the key
property to identify the new flow.
FlowCollector
mappingconst collectors = davinciClient.getCollectors();
collectors.map((collector) => {
if (collector.type === 'FlowCollector') {
renderFlowCollector(collector, davinciClient.flow(collector));
}
});
This returns a function you can call when the user interacts with it.
flowCollector
componentfunction renderFlowCollector(collector, startFlow) {
// ... component logic
function onClick(event) {
startFlow();
}
// render code
}
Example 3. Handling SubmitCollector
with a component
This example shows how submit the current node and its collected values back to the server. The collection of the data is already complete so an updater component is not required. This collector only renders the button for the user to submit the collected data.
SubmitCollector
mappingconst collectors = davinciClient.getCollectors();
collectors.map((collector) => {
if (collector.type === 'SubmitCollector') {
renderSubmitCollector(
collector, // This is the only argument you will need to pass
);
}
});
Continuing a DaVinci flow
After collecting the data for a node you can proceed to the next node in the flow by calling the next()
method on your DaVinci client object.
This can be the result of a user clicking on the button rendered from the SubmitCollector
, from the submit
event of an HTML form, or by programmatically triggering the submission in the application layer.
next()
let nextStep = davinciClient.next();
You do not need to pass any parameters into the |
The server responds with a new node
object, just like when starting a flow initially.
Loop again through conditional checks on the new node’s type to render the appropriate UI or take the appropriate action.
Handling DaVinci flow error nodes
DaVinci flows return the error
node type when it receives data that is incorrect, but you can fix the data and resubmit. For example, an email value submitted in an invalid format or a new password that is too short.
This is different than a failure
node type which you cannot resubmit and instead you must restart the entire flow.
You can retain a reference to the node
you submit in case the next node
you receive is an error
type. If so, you can re-render the previous form, and inject the error information from the new error
node.
After the user revises the data call next()
as you did before.
Handling DaVinci flow failure nodes
DaVinci flows return the failure
node type if there has been an issue that prevents the flow from continuing. For example, the flow times out or suffers an HTTP 500 server error.
You should offer to restart the flow on receipt of a failure
node type.
failure
node typeconst node = await davinciClient.next();
if (node.status === 'failure') {
const error = davinciClient.getError();
renderError(error);
// ... user clicks button to restart flow
const freshNode = davinciClient.start();
}
Handling DaVinci flow success nodes
DaVinci flows return the success
node type when the user completes the flow and PingOne issues them a session.
On receipt of a success
node type you should use the OAuth 2.0 authorization Code and state properties from the node and use them to obtain an access token on behalf of the user.
To obtain an access token, leverage the Ping SDK for JavaScript.
// ... other imports
import { davinci } from '@forgerock/davinci-client';
import { Config, TokenManager } from '@forgerock/javascript-sdk';
// ... other config or initialization code
// This Config.set accepts the same config schema as the davinci function
Config.set(config);
const node = await davinciClient.next();
if (node.status === 'success') {
const clientInfo = davinciClient.getClient();
const code = clientInfo.authorization?.code || '';
const state = clientInfo.authorization?.state || '';
const tokens = await TokenManager.getTokens({
query: {
code, state
}
});
// user now has session and OIDC tokens
}