Skip to content

Commit a2633f1

Browse files
committed
feat(davinci-client): add QRCode collector support
Add QR_CODE field type support to the DaVinci client SDK, enabling applications to render QR codes returned by DaVinci flows. - Add QrCodeField type and QrCodeCollectorBase interface - Add returnQrCodeCollector factory with defensive fallbacks - Wire QR_CODE dispatch in node reducer (early return, same as LABEL) - Export QrCodeCollector in public API types - Add QR code component and config to sample davinci-app - Add unit tests for factory and reducer integration
1 parent e09904f commit a2633f1

13 files changed

Lines changed: 244 additions & 6 deletions

File tree

.changeset/fast-ways-rest.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+
Add QR code collector support to davinci-client
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 { QrCodeCollector } from '@forgerock/davinci-client/types';
8+
9+
export default function (formEl: HTMLFormElement, collector: QrCodeCollector) {
10+
if (collector.error) {
11+
const errorEl = document.createElement('p');
12+
errorEl.innerText = `QR Code error: ${collector.error}`;
13+
formEl.appendChild(errorEl);
14+
return;
15+
}
16+
17+
const container = document.createElement('div');
18+
19+
const img = document.createElement('img');
20+
img.src = collector.output.src;
21+
img.alt = 'QR Code';
22+
img.setAttribute('data-testid', 'qr-code-image');
23+
container.appendChild(img);
24+
25+
if (collector.output.fallbackText) {
26+
const fallback = document.createElement('p');
27+
fallback.innerText = `Manual code: ${collector.output.fallbackText}`;
28+
fallback.setAttribute('data-testid', 'qr-code-fallback');
29+
container.appendChild(fallback);
30+
}
31+
32+
formEl.appendChild(container);
33+
}

e2e/davinci-app/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import multiValueComponent from './components/multi-value.js';
3232
import labelComponent from './components/label.js';
3333
import objectValueComponent from './components/object-value.js';
3434
import fidoComponent from './components/fido.js';
35+
import qrCodeComponent from './components/qr-code.js';
3536

3637
const loggerFn = {
3738
error: () => {
@@ -223,6 +224,8 @@ const urlParams = new URLSearchParams(window.location.search);
223224
formEl, // You can ignore this; it's just for rendering
224225
collector, // This is the plain object of the collector
225226
);
227+
} else if (collector.type === 'QrCodeCollector') {
228+
qrCodeComponent(formEl, collector);
226229
} else if (collector.type === 'TextCollector') {
227230
textComponent(
228231
formEl, // You can ignore this; it's just for rendering

e2e/davinci-app/server-configs.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,21 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
5757
},
5858
/**
5959
* Phone Number Input With Email and Password
60-
*
6160
*/
6261
'20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0': {
6362
clientId: '20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0',
6463
redirectUri: window.location.origin + '/',
65-
scope: 'openid profile email revoke',
64+
scope: 'openid profile email',
65+
serverConfig: {
66+
wellknown:
67+
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
68+
},
69+
},
70+
/** QR Code policy id : aa3c00c3ec25a9721be078f7bf44678d **/
71+
'c12743f9-08e8-4420-a624-71bbb08e9fe1': {
72+
clientId: 'c12743f9-08e8-4420-a624-71bbb08e9fe1',
73+
redirectUri: window.location.origin + '/',
74+
scope: 'openid profile email',
6675
serverConfig: {
6776
wellknown:
6877
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>;
482482
/**
483483
* @interface NoValueCollector - Represents a collector that collects no value; text only for display.
484484
*/
485-
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'NoValueCollector';
485+
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'NoValueCollector' | 'QrCodeCollector';
486486

487487
export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
488488
category: 'NoValueCollector';
@@ -497,6 +497,21 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
497497
};
498498
}
499499

500+
export interface QrCodeCollectorBase {
501+
category: 'NoValueCollector';
502+
error: string | null;
503+
type: 'QrCodeCollector';
504+
id: string;
505+
name: string;
506+
output: {
507+
key: string;
508+
label: string;
509+
type: string;
510+
src: string;
511+
fallbackText: string;
512+
};
513+
}
514+
500515
/**
501516
* Type to help infer the collector based on the collector type
502517
* Used specifically in the returnNoValueCollector wrapper function.
@@ -507,16 +522,21 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
507522
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> =
508523
T extends 'ReadOnlyCollector'
509524
? NoValueCollectorBase<'ReadOnlyCollector'>
510-
: NoValueCollectorBase<'NoValueCollector'>;
525+
: T extends 'QrCodeCollector'
526+
? QrCodeCollectorBase
527+
: NoValueCollectorBase<'NoValueCollector'>;
511528

512529
export type NoValueCollectors =
513530
| NoValueCollectorBase<'NoValueCollector'>
514-
| NoValueCollectorBase<'ReadOnlyCollector'>;
531+
| NoValueCollectorBase<'ReadOnlyCollector'>
532+
| QrCodeCollectorBase;
515533

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

518536
export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>;
519537

538+
export type QrCodeCollector = QrCodeCollectorBase;
539+
520540
export type UnknownCollector = {
521541
category: 'UnknownCollector';
522542
error: string | null;

packages/davinci-client/src/lib/collector.utils.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
returnObjectValueCollector,
2323
returnSingleValueAutoCollector,
2424
returnObjectValueAutoCollector,
25+
returnQrCodeCollector,
2526
} from './collector.utils.js';
2627
import type {
2728
DaVinciField,
@@ -31,6 +32,7 @@ import type {
3132
FidoRegistrationField,
3233
PhoneNumberField,
3334
ProtectField,
35+
QrCodeField,
3436
ReadOnlyField,
3537
RedirectField,
3638
StandardField,
@@ -809,6 +811,80 @@ describe('No Value Collectors', () => {
809811
});
810812
});
811813

814+
describe('returnQrCodeCollector', () => {
815+
it('should return a valid QrCodeCollector with src and fallbackText', () => {
816+
const mockField: QrCodeField = {
817+
type: 'QR_CODE',
818+
key: 'qr-code-field',
819+
content: 'data:image/png;base64,abc123',
820+
fallbackText: '04ZKS2KCIWKXT8FHRX',
821+
};
822+
const result = returnQrCodeCollector(mockField, 2);
823+
expect(result).toEqual({
824+
category: 'NoValueCollector',
825+
error: null,
826+
type: 'QrCodeCollector',
827+
id: 'qr-code-field-2',
828+
name: 'qr-code-field-2',
829+
output: {
830+
key: 'qr-code-field-2',
831+
label: 'data:image/png;base64,abc123',
832+
type: 'QR_CODE',
833+
src: 'data:image/png;base64,abc123',
834+
fallbackText: '04ZKS2KCIWKXT8FHRX',
835+
},
836+
});
837+
});
838+
839+
it('should handle missing fallbackText gracefully', () => {
840+
const mockField: QrCodeField = {
841+
type: 'QR_CODE',
842+
key: 'qr-code-field',
843+
content: 'data:image/png;base64,abc123',
844+
};
845+
const result = returnQrCodeCollector(mockField, 0);
846+
expect(result).toEqual({
847+
category: 'NoValueCollector',
848+
error: null,
849+
type: 'QrCodeCollector',
850+
id: 'qr-code-field-0',
851+
name: 'qr-code-field-0',
852+
output: {
853+
key: 'qr-code-field-0',
854+
label: 'data:image/png;base64,abc123',
855+
type: 'QR_CODE',
856+
src: 'data:image/png;base64,abc123',
857+
fallbackText: '',
858+
},
859+
});
860+
});
861+
862+
it('should set error when content is missing', () => {
863+
const mockField = { type: 'QR_CODE', key: 'qr-code-field' } as unknown as QrCodeField;
864+
const result = returnQrCodeCollector(mockField, 0);
865+
expect(result.error).toContain('Content is not found');
866+
expect(result.output.src).toBe('');
867+
});
868+
869+
it('should set error and fall back to type when key is missing', () => {
870+
const mockField = {
871+
type: 'QR_CODE',
872+
content: 'data:image/png;base64,abc123',
873+
} as unknown as QrCodeField;
874+
const result = returnQrCodeCollector(mockField, 0);
875+
expect(result.error).toContain('Key is not found');
876+
expect(result.id).toBe('QR_CODE-0');
877+
expect(result.name).toBe('QR_CODE-0');
878+
});
879+
880+
it('should accumulate multiple errors when key and content are missing', () => {
881+
const mockField = { type: 'QR_CODE' } as unknown as QrCodeField;
882+
const result = returnQrCodeCollector(mockField, 0);
883+
expect(result.error).toContain('Content is not found');
884+
expect(result.error).toContain('Key is not found');
885+
});
886+
});
887+
812888
describe('returnSingleValueAutoCollector', () => {
813889
it('should create a valid ProtectCollector', () => {
814890
const mockField: ProtectField = {

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
AutoCollectors,
3030
SingleValueAutoCollectorTypes,
3131
ObjectValueAutoCollectorTypes,
32+
QrCodeCollectorBase,
3233
} from './collector.types.js';
3334
import type {
3435
DeviceAuthenticationField,
@@ -38,6 +39,7 @@ import type {
3839
MultiSelectField,
3940
PhoneNumberField,
4041
ProtectField,
42+
QrCodeField,
4143
ReadOnlyField,
4244
RedirectField,
4345
SingleSelectField,
@@ -713,6 +715,42 @@ export function returnReadOnlyCollector(field: ReadOnlyField, idx: number) {
713715
return returnNoValueCollector(field, idx, 'ReadOnlyCollector');
714716
}
715717

718+
/**
719+
* @function returnQrCodeCollector - Creates a QrCodeCollector object for displaying QR code images.
720+
* @param {QrCodeField} field - The field object containing key, content, type, and optional fallbackText.
721+
* @param {number} idx - The index to be used in the id of the QrCodeCollector.
722+
* @returns {QrCodeCollectorBase} The constructed QrCodeCollector object.
723+
*/
724+
export function returnQrCodeCollector(field: QrCodeField, idx: number): QrCodeCollectorBase {
725+
let error = '';
726+
if (!('content' in field) || !field.content) {
727+
error = `${error}Content is not found in the field object. `;
728+
}
729+
if (!('key' in field) || !field.key) {
730+
error = `${error}Key is not found in the field object. `;
731+
}
732+
if (!('type' in field)) {
733+
error = `${error}Type is not found in the field object. `;
734+
}
735+
736+
const key = field.key || field.type;
737+
738+
return {
739+
category: 'NoValueCollector',
740+
error: error || null,
741+
type: 'QrCodeCollector',
742+
id: `${key}-${idx}`,
743+
name: `${key}-${idx}`,
744+
output: {
745+
key: `${key}-${idx}`,
746+
label: field.content || '',
747+
type: field.type,
748+
src: field.content || '',
749+
fallbackText: field.fallbackText || '',
750+
},
751+
};
752+
}
753+
716754
/**
717755
* @function returnValidator - Creates a validator function based on the provided collector
718756
* @param {ValidatedTextCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors} collector - The collector to which the value will be validated

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ export type ReadOnlyField = {
7474
key?: string;
7575
};
7676

77+
export type QrCodeField = {
78+
type: 'QR_CODE';
79+
key: string;
80+
content: string;
81+
fallbackText?: string;
82+
};
83+
7784
export type RedirectField = {
7885
type: 'SOCIAL_LOGIN_BUTTON';
7986
key: string;
@@ -221,7 +228,7 @@ export type ComplexValueFields =
221228
| FidoRegistrationField
222229
| FidoAuthenticationField;
223230
export type MultiValueFields = MultiSelectField;
224-
export type ReadOnlyFields = ReadOnlyField;
231+
export type ReadOnlyFields = ReadOnlyField | QrCodeField;
225232
export type RedirectFields = RedirectField;
226233
export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField;
227234

packages/davinci-client/src/lib/node.reducer.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
MultiSelectCollector,
1616
PhoneNumberCollector,
1717
ProtectCollector,
18+
QrCodeCollector,
1819
SubmitCollector,
1920
TextCollector,
2021
} from './collector.types.js';
@@ -415,6 +416,40 @@ describe('The node collector reducer', () => {
415416
'ActionCollectors are read-only',
416417
);
417418
});
419+
420+
it('should handle QR_CODE field type', () => {
421+
const action = {
422+
type: 'node/next',
423+
payload: {
424+
fields: [
425+
{
426+
type: 'QR_CODE',
427+
key: 'qr-code-field',
428+
content: 'data:image/png;base64,abc123',
429+
fallbackText: '04ZKS2KCIWKXT8FHRX',
430+
},
431+
],
432+
formData: {},
433+
},
434+
};
435+
const result = nodeCollectorReducer(undefined, action);
436+
expect(result).toEqual([
437+
{
438+
category: 'NoValueCollector',
439+
error: null,
440+
type: 'QrCodeCollector',
441+
id: 'qr-code-field-0',
442+
name: 'qr-code-field-0',
443+
output: {
444+
key: 'qr-code-field-0',
445+
label: 'data:image/png;base64,abc123',
446+
type: 'QR_CODE',
447+
src: 'data:image/png;base64,abc123',
448+
fallbackText: '04ZKS2KCIWKXT8FHRX',
449+
},
450+
} satisfies QrCodeCollector,
451+
]);
452+
});
418453
});
419454

420455
describe('The node collector reducer with MultiValueCollector', () => {

packages/davinci-client/src/lib/node.reducer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
returnUnknownCollector,
2929
returnFidoRegistrationCollector,
3030
returnFidoAuthenticationCollector,
31+
returnQrCodeCollector,
3132
} from './collector.utils.js';
3233
import type { DaVinciField, UnknownField } from './davinci.types.js';
3334
import type {
@@ -53,6 +54,7 @@ import type {
5354
FidoAuthenticationCollector,
5455
FidoAuthenticationInputValue,
5556
FidoRegistrationInputValue,
57+
QrCodeCollector,
5658
} from './collector.types.js';
5759

5860
/**
@@ -98,6 +100,7 @@ const initialCollectorValues: (
98100
| ProtectCollector
99101
| FidoRegistrationCollector
100102
| FidoAuthenticationCollector
103+
| QrCodeCollector
101104
)[] = [];
102105

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

130+
if (field.type === 'QR_CODE') {
131+
return returnQrCodeCollector(field, idx);
132+
}
133+
127134
// *Some* collectors may have default or existing data to display
128135
const data =
129136
action.payload.formData &&

0 commit comments

Comments
 (0)