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

Add QR code collector support to davinci-client
33 changes: 33 additions & 0 deletions e2e/davinci-app/components/qr-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 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 { QrCodeCollector } from '@forgerock/davinci-client/types';

export default function (formEl: HTMLFormElement, collector: QrCodeCollector) {
if (collector.error) {
const errorEl = document.createElement('p');
errorEl.innerText = `QR Code error: ${collector.error}`;
formEl.appendChild(errorEl);
return;
}

const container = document.createElement('div');

const img = document.createElement('img');
img.src = collector.output.src;
img.alt = 'QR Code';
img.setAttribute('data-testid', 'qr-code-image');
container.appendChild(img);

if (collector.output.fallbackText) {
const fallback = document.createElement('p');
fallback.innerText = `Manual code: ${collector.output.fallbackText}`;
fallback.setAttribute('data-testid', 'qr-code-fallback');
container.appendChild(fallback);
}

formEl.appendChild(container);
}
3 changes: 3 additions & 0 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import multiValueComponent from './components/multi-value.js';
import labelComponent from './components/label.js';
import objectValueComponent from './components/object-value.js';
import fidoComponent from './components/fido.js';
import qrCodeComponent from './components/qr-code.js';

const loggerFn = {
error: () => {
Expand Down Expand Up @@ -223,6 +224,8 @@ 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 === 'QrCodeCollector') {
qrCodeComponent(formEl, collector);
} else if (collector.type === 'TextCollector') {
textComponent(
formEl, // You can ignore this; it's just for rendering
Expand Down
11 changes: 10 additions & 1 deletion e2e/davinci-app/server-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
},
/**
* Phone Number Input With Email and Password
*
*/
'20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0': {
clientId: '20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0',
Expand All @@ -68,4 +67,14 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
},
},
/** QR Code policy id : aa3c00c3ec25a9721be078f7bf44678d **/
'c12743f9-08e8-4420-a624-71bbb08e9fe1': {
clientId: 'c12743f9-08e8-4420-a624-71bbb08e9fe1',
redirectUri: window.location.origin + '/',
scope: 'openid profile email',
serverConfig: {
wellknown:
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
},
},
};
26 changes: 23 additions & 3 deletions packages/davinci-client/src/lib/collector.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>;
/**
* @interface NoValueCollector - Represents a collector that collects no value; text only for display.
*/
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'NoValueCollector';
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'NoValueCollector' | 'QrCodeCollector';

export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
category: 'NoValueCollector';
Expand All @@ -497,6 +497,21 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
};
}

export interface QrCodeCollectorBase {
category: 'NoValueCollector';
error: string | null;
type: 'QrCodeCollector';
id: string;
name: string;
output: {
key: string;
label: string;
type: string;
src: string;
fallbackText: string;
};
}

/**
* Type to help infer the collector based on the collector type
* Used specifically in the returnNoValueCollector wrapper function.
Expand All @@ -507,16 +522,21 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> =
T extends 'ReadOnlyCollector'
? NoValueCollectorBase<'ReadOnlyCollector'>
: NoValueCollectorBase<'NoValueCollector'>;
: T extends 'QrCodeCollector'
? QrCodeCollectorBase
: NoValueCollectorBase<'NoValueCollector'>;

export type NoValueCollectors =
| NoValueCollectorBase<'NoValueCollector'>
| NoValueCollectorBase<'ReadOnlyCollector'>;
| NoValueCollectorBase<'ReadOnlyCollector'>
| QrCodeCollectorBase;

export type NoValueCollector<T extends NoValueCollectorTypes> = NoValueCollectorBase<T>;

export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>;

export type QrCodeCollector = QrCodeCollectorBase;

export type UnknownCollector = {
category: 'UnknownCollector';
error: string | null;
Expand Down
76 changes: 76 additions & 0 deletions packages/davinci-client/src/lib/collector.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
returnObjectValueCollector,
returnSingleValueAutoCollector,
returnObjectValueAutoCollector,
returnQrCodeCollector,
} from './collector.utils.js';
import type {
DaVinciField,
Expand All @@ -31,6 +32,7 @@ import type {
FidoRegistrationField,
PhoneNumberField,
ProtectField,
QrCodeField,
ReadOnlyField,
RedirectField,
StandardField,
Expand Down Expand Up @@ -809,6 +811,80 @@ describe('No Value Collectors', () => {
});
});

describe('returnQrCodeCollector', () => {
it('should return a valid QrCodeCollector with src and fallbackText', () => {
const mockField: QrCodeField = {
type: 'QR_CODE',
key: 'qr-code-field',
content: 'data:image/png;base64,abc123',
fallbackText: '04ZKS2KCIWKXT8FHRX',
};
const result = returnQrCodeCollector(mockField, 2);
expect(result).toEqual({
category: 'NoValueCollector',
error: null,
type: 'QrCodeCollector',
id: 'qr-code-field-2',
name: 'qr-code-field-2',
output: {
key: 'qr-code-field-2',
label: 'data:image/png;base64,abc123',
type: 'QR_CODE',
src: 'data:image/png;base64,abc123',
fallbackText: '04ZKS2KCIWKXT8FHRX',
},
});
});

it('should handle missing fallbackText gracefully', () => {
const mockField: QrCodeField = {
type: 'QR_CODE',
key: 'qr-code-field',
content: 'data:image/png;base64,abc123',
};
const result = returnQrCodeCollector(mockField, 0);
expect(result).toEqual({
category: 'NoValueCollector',
error: null,
type: 'QrCodeCollector',
id: 'qr-code-field-0',
name: 'qr-code-field-0',
output: {
key: 'qr-code-field-0',
label: 'data:image/png;base64,abc123',
type: 'QR_CODE',
src: 'data:image/png;base64,abc123',
fallbackText: '',
},
});
});

it('should set error when content is missing', () => {
const mockField = { type: 'QR_CODE', key: 'qr-code-field' } as unknown as QrCodeField;
const result = returnQrCodeCollector(mockField, 0);
expect(result.error).toContain('Content is not found');
expect(result.output.src).toBe('');
});

it('should set error and fall back to type when key is missing', () => {
const mockField = {
type: 'QR_CODE',
content: 'data:image/png;base64,abc123',
} as unknown as QrCodeField;
const result = returnQrCodeCollector(mockField, 0);
expect(result.error).toContain('Key is not found');
expect(result.id).toBe('QR_CODE-0');
expect(result.name).toBe('QR_CODE-0');
});

it('should accumulate multiple errors when key and content are missing', () => {
const mockField = { type: 'QR_CODE' } as unknown as QrCodeField;
const result = returnQrCodeCollector(mockField, 0);
expect(result.error).toContain('Content is not found');
expect(result.error).toContain('Key is not found');
});
});

describe('returnSingleValueAutoCollector', () => {
it('should create a valid ProtectCollector', () => {
const mockField: ProtectField = {
Expand Down
38 changes: 38 additions & 0 deletions packages/davinci-client/src/lib/collector.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
AutoCollectors,
SingleValueAutoCollectorTypes,
ObjectValueAutoCollectorTypes,
QrCodeCollectorBase,
} from './collector.types.js';
import type {
DeviceAuthenticationField,
Expand All @@ -38,6 +39,7 @@ import type {
MultiSelectField,
PhoneNumberField,
ProtectField,
QrCodeField,
ReadOnlyField,
RedirectField,
SingleSelectField,
Expand Down Expand Up @@ -713,6 +715,42 @@ export function returnReadOnlyCollector(field: ReadOnlyField, idx: number) {
return returnNoValueCollector(field, idx, 'ReadOnlyCollector');
}

/**
* @function returnQrCodeCollector - Creates a QrCodeCollector object for displaying QR code images.
* @param {QrCodeField} field - The field object containing key, content, type, and optional fallbackText.
* @param {number} idx - The index to be used in the id of the QrCodeCollector.
* @returns {QrCodeCollectorBase} The constructed QrCodeCollector object.
*/
export function returnQrCodeCollector(field: QrCodeField, idx: number): QrCodeCollectorBase {
let error = '';
if (!('content' in field) || !field.content) {
error = `${error}Content is not found in the field object. `;
}
if (!('key' in field) || !field.key) {
error = `${error}Key is not found in the field object. `;
}
if (!('type' in field)) {
error = `${error}Type is not found in the field object. `;
}

const key = field.key || field.type;

return {
category: 'NoValueCollector',
error: error || null,
type: 'QrCodeCollector',
id: `${key}-${idx}`,
name: `${key}-${idx}`,
output: {
key: `${key}-${idx}`,
label: field.content || '',
type: field.type,
src: field.content || '',
fallbackText: field.fallbackText || '',
},
};
}

/**
* @function returnValidator - Creates a validator function based on the provided collector
* @param {ValidatedTextCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors} collector - The collector to which the value will be validated
Expand Down
9 changes: 8 additions & 1 deletion packages/davinci-client/src/lib/davinci.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ export type ReadOnlyField = {
key?: string;
};

export type QrCodeField = {
type: 'QR_CODE';
key: string;
content: string;
fallbackText?: string;
};

export type RedirectField = {
type: 'SOCIAL_LOGIN_BUTTON';
key: string;
Expand Down Expand Up @@ -221,7 +228,7 @@ export type ComplexValueFields =
| FidoRegistrationField
| FidoAuthenticationField;
export type MultiValueFields = MultiSelectField;
export type ReadOnlyFields = ReadOnlyField;
export type ReadOnlyFields = ReadOnlyField | QrCodeField;
export type RedirectFields = RedirectField;
export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField;

Expand Down
35 changes: 35 additions & 0 deletions packages/davinci-client/src/lib/node.reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
MultiSelectCollector,
PhoneNumberCollector,
ProtectCollector,
QrCodeCollector,
SubmitCollector,
TextCollector,
} from './collector.types.js';
Expand Down Expand Up @@ -415,6 +416,40 @@ describe('The node collector reducer', () => {
'ActionCollectors are read-only',
);
});

it('should handle QR_CODE field type', () => {
const action = {
type: 'node/next',
payload: {
fields: [
{
type: 'QR_CODE',
key: 'qr-code-field',
content: 'data:image/png;base64,abc123',
fallbackText: '04ZKS2KCIWKXT8FHRX',
},
],
formData: {},
},
};
const result = nodeCollectorReducer(undefined, action);
expect(result).toEqual([
{
category: 'NoValueCollector',
error: null,
type: 'QrCodeCollector',
id: 'qr-code-field-0',
name: 'qr-code-field-0',
output: {
key: 'qr-code-field-0',
label: 'data:image/png;base64,abc123',
type: 'QR_CODE',
src: 'data:image/png;base64,abc123',
fallbackText: '04ZKS2KCIWKXT8FHRX',
},
} satisfies QrCodeCollector,
]);
});
});

describe('The node collector reducer with MultiValueCollector', () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/davinci-client/src/lib/node.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
returnUnknownCollector,
returnFidoRegistrationCollector,
returnFidoAuthenticationCollector,
returnQrCodeCollector,
} from './collector.utils.js';
import type { DaVinciField, UnknownField } from './davinci.types.js';
import type {
Expand All @@ -53,6 +54,7 @@ import type {
FidoAuthenticationCollector,
FidoAuthenticationInputValue,
FidoRegistrationInputValue,
QrCodeCollector,
} from './collector.types.js';

/**
Expand Down Expand Up @@ -98,6 +100,7 @@ const initialCollectorValues: (
| ProtectCollector
| FidoRegistrationCollector
| FidoAuthenticationCollector
| QrCodeCollector
)[] = [];

/**
Expand All @@ -124,6 +127,10 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build
return returnReadOnlyCollector(field, idx);
}

if (field.type === 'QR_CODE') {
return returnQrCodeCollector(field, idx);
}

// *Some* collectors may have default or existing data to display
const data =
action.payload.formData &&
Expand Down
Loading
Loading