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/real-swans-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

Add MetadataCollector and error helper on client
36 changes: 36 additions & 0 deletions e2e/davinci-app/components/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import type { MetadataCollector, MetadataError, Updater } from '@forgerock/davinci-client/types';

export default function metadataComponent(
formEl: HTMLFormElement,
updater: Updater<MetadataCollector>,
getMetadataError: (errorDetails: MetadataError) => MetadataError,
submitForm: () => Promise<void>,
) {
const successBtn = document.createElement('button');
successBtn.type = 'button';
successBtn.innerHTML = 'Metadata Success';

const errorBtn = document.createElement('button');
errorBtn.type = 'button';
errorBtn.innerHTML = 'Metadata Error';

formEl.appendChild(successBtn);
formEl.appendChild(errorBtn);

successBtn.onclick = async () => {
updater({ status: 'succeeded' });
await submitForm();
};

errorBtn.onclick = async () => {
const metadataError = getMetadataError({ code: 'ERROR_CODE', message: 'Operation cancelled' });
updater(metadataError);
await submitForm();
};
Comment on lines +26 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle rejected submission promises.

If submitForm() rejects, both async event handlers produce an unhandled promise rejection, leaving the UI without an error path. Catch the failure and route it through the example app’s existing error handling or reporting mechanism.

🤖 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/metadata.ts` around lines 26 - 35, Update the
successBtn.onclick and errorBtn.onclick handlers to catch rejected submitForm()
promises and route the failure through the example app’s existing error handling
or reporting mechanism, while preserving their current updater calls and
submission behavior.

}
8 changes: 8 additions & 0 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import qrCodeComponent from './components/qr-code.js';
import formImageComponent from './components/form-image.js';
import pollingComponent from './components/polling.js';
import booleanComponent from './components/boolean.js';
import metadataComponent from './components/metadata.js';

const loggerFn = {
error: () => {
Expand Down Expand Up @@ -289,6 +290,13 @@ const urlParams = new URLSearchParams(window.location.search);
davinciClient.update(collector), // Returns an update function for this collector
submitForm,
);
} else if (collector.type === 'MetadataCollector') {
metadataComponent(
formEl, // You can ignore this; it's just for rendering
davinciClient.update(collector), // Returns an update function for this collector
davinciClient.getMetadataError,
submitForm,
);
} else if (collector.type === 'BooleanCollector') {
booleanComponent(formEl, collector, davinciClient.update(collector));
} else if (collector.type === 'ValidatedBooleanCollector') {
Expand Down
2 changes: 1 addition & 1 deletion e2e/davinci-app/server-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
},
},
/**
* Polling
* AutoCollectors: Polling, Metadata
*/
'31a587ce-9aa4-4f36-a09f-78cd8a0a74a0': {
clientId: '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0',
Expand Down
40 changes: 40 additions & 0 deletions e2e/davinci-suites/src/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';

test.describe('Metadata Collector', () => {
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
const davinciPolicy = '7793be21a14dd80c1d26b367e81ea985';

test('should submit with success status when metadata collection succeeds', async ({ page }) => {
const { navigate } = asyncEvents(page);
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);

await page.getByRole('button', { name: 'Sign On' }).click();
await expect(page.getByRole('button', { name: 'Metadata Success' })).toBeVisible();

await page.getByRole('button', { name: 'Metadata Success' }).click();

await expect(page.getByRole('heading', { name: 'Message' })).toBeVisible();
await expect(page.getByText('"status":"succeeded"')).toBeVisible();
});

test('should submit with error details when metadata collection fails', async ({ page }) => {
const { navigate } = asyncEvents(page);
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);

await page.getByRole('button', { name: 'Sign On' }).click();
await expect(page.getByRole('button', { name: 'Metadata Error' })).toBeVisible();

await page.getByRole('button', { name: 'Metadata Error' }).click();

await expect(page.getByRole('heading', { name: 'Message' })).toBeVisible();
await expect(page.getByText('"code":"ERROR_CODE"')).toBeVisible();
await expect(page.getByText('"message":"Operation cancelled"')).toBeVisible();
});
});
64 changes: 44 additions & 20 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export interface AutoCollector<C extends AutoCollectorCategories, T extends Auto
export type AutoCollectorCategories = 'SingleValueAutoCollector' | 'ObjectValueAutoCollector';

// @public (undocumented)
export type AutoCollectors = ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | SingleValueAutoCollector | ObjectValueAutoCollector;
export type AutoCollectors = ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | MetadataCollector | PollingCollector | SingleValueAutoCollector | ObjectValueAutoCollector;

// @public (undocumented)
export type AutoCollectorTypes = SingleValueAutoCollectorTypes | ObjectValueAutoCollectorTypes;
Expand Down Expand Up @@ -173,7 +173,7 @@ export interface CollectorRichContent {
}

// @public (undocumented)
export type Collectors = FlowCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | ImageCollector | UnknownCollector;
export type Collectors = FlowCollector | MetadataCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | ImageCollector | UnknownCollector;

// @public
export type CollectorValueType<T> = T extends {
Expand Down Expand Up @@ -211,6 +211,8 @@ export type CollectorValueType<T> = T extends {
} ? FidoRegistrationInputValue : T extends {
type: 'FidoAuthenticationCollector';
} ? FidoAuthenticationInputValue : T extends {
type: 'MetadataCollector';
} ? Record<string, unknown> | MetadataError : T extends {
category: 'SingleValueCollector';
} ? string : T extends {
category: 'ValidatedSingleValueCollector';
Expand All @@ -225,10 +227,10 @@ export type CollectorValueType<T> = T extends {
} ? never : CollectorValueTypes;

// @public
export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue;
export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError;

// @public (undocumented)
export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField;
export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField | MetadataField;

// @public (undocumented)
export interface ContinueNode {
Expand Down Expand Up @@ -283,16 +285,19 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
resume: (input: {
continueToken: string;
}) => Promise<InternalErrorResponse | NodeStates>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
pollStatus: (collector: PollingCollector) => Poller;
getMetadataError: (errorDetails: MetadataError) => MetadataError;
getClient: () => {
action: string;
collectors: Collectors[];
description?: string;
name?: string;
status: "continue";
} | {
status: "start";
} | {
action: string;
collectors: Collectors[];
Expand All @@ -301,8 +306,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
status: "error";
} | {
status: "failure";
} | {
status: "start";
} | {
authorization?: {
code?: string;
Expand All @@ -313,7 +316,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
getCollectors: () => Collectors[];
getError: () => DaVinciError | null;
getErrorCollectors: () => CollectorErrors[];
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
getServer: () => {
_links?: Links;
id?: string;
Expand All @@ -322,6 +325,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
href?: string;
eventName?: string;
status: "continue";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand All @@ -337,8 +342,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
interactionId?: string;
interactionToken?: string;
status: "failure";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand All @@ -355,14 +358,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand All @@ -379,14 +382,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "error"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand All @@ -407,14 +410,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand All @@ -431,14 +434,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "error"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand Down Expand Up @@ -636,6 +639,9 @@ export interface DaVinciRequest {
};
}

// @public
export type DaVinciRequestValueTypes = string | number | boolean | (string | number | boolean)[] | DeviceValue | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError;

// @public (undocumented)
export interface DaVinciSuccessResponse extends DaVinciBaseResponse {
// (undocumented)
Expand Down Expand Up @@ -968,7 +974,7 @@ export type ImageField = {
export type InferActionCollectorType<T extends ActionCollectorTypes> = T extends 'IdpCollector' ? IdpCollector : T extends 'SubmitCollector' ? SubmitCollector : T extends 'FlowCollector' ? FlowCollector : ActionCollectorWithUrl<'ActionCollector'> | ActionCollectorNoUrl<'ActionCollector'>;

// @public
export type InferAutoCollectorType<T extends AutoCollectorTypes> = T extends 'ProtectCollector' ? ProtectCollector : T extends 'PollingCollector' ? PollingCollector : T extends 'FidoRegistrationCollector' ? FidoRegistrationCollector : T extends 'FidoAuthenticationCollector' ? FidoAuthenticationCollector : T extends 'ObjectValueAutoCollector' ? ObjectValueAutoCollector : SingleValueAutoCollector;
export type InferAutoCollectorType<T extends AutoCollectorTypes> = T extends 'ProtectCollector' ? ProtectCollector : T extends 'PollingCollector' ? PollingCollector : T extends 'FidoRegistrationCollector' ? FidoRegistrationCollector : T extends 'FidoAuthenticationCollector' ? FidoAuthenticationCollector : T extends 'MetadataCollector' ? MetadataCollector : T extends 'ObjectValueAutoCollector' ? ObjectValueAutoCollector : SingleValueAutoCollector;

// @public
export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>;
Expand Down Expand Up @@ -1005,6 +1011,24 @@ export interface Links {

export { LogLevel }

// @public (undocumented)
export type MetadataCollector = AutoCollector<'ObjectValueAutoCollector', 'MetadataCollector', Record<string, unknown>, Record<string, unknown>>;

// @public (undocumented)
export interface MetadataError {
// (undocumented)
code: string;
// (undocumented)
message: string;
}

// @public (undocumented)
export type MetadataField = {
type: 'METADATA';
key: string;
payload: Record<string, unknown>;
};

// @public (undocumented)
export type MultiSelectCollector = MultiValueCollectorWithValue<'MultiSelectCollector'>;

Expand Down Expand Up @@ -1222,7 +1246,7 @@ export interface ObjectOptionsCollectorWithStringValue<T extends ObjectValueColl
export type ObjectValueAutoCollector = AutoCollector<'ObjectValueAutoCollector', 'ObjectValueAutoCollector', Record<string, unknown>>;

// @public (undocumented)
export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRegistrationCollector' | 'FidoAuthenticationCollector';
export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRegistrationCollector' | 'FidoAuthenticationCollector' | 'MetadataCollector';

// @public (undocumented)
export type ObjectValueCollector<T extends ObjectValueCollectorTypes> = ObjectOptionsCollectorWithObjectValue<T> | ObjectOptionsCollectorWithStringValue<T> | ObjectValueCollectorWithObjectValue<T>;
Expand Down
Loading
Loading