Skip to content

Commit 2690d5b

Browse files
committed
feat(davinci-client): support form agreements with AgreementCollector
1 parent b28b6b0 commit 2690d5b

12 files changed

Lines changed: 274 additions & 5 deletions

File tree

.changeset/silent-ideas-joke.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+
Support form agreements with AgreementCollector
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2026 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 { AgreementCollector } from '@forgerock/davinci-client/types';
8+
9+
export default function (formEl: HTMLFormElement, collector: AgreementCollector) {
10+
const output = collector.output;
11+
const componentEnabled = output.enabled;
12+
13+
if (!componentEnabled) {
14+
return;
15+
}
16+
17+
const content = output.label;
18+
const titleEnabled = output.titleEnabled;
19+
const title = output.title;
20+
21+
if (titleEnabled) {
22+
const titleEl = document.createElement('h3');
23+
titleEl.innerText = title;
24+
formEl?.appendChild(titleEl);
25+
}
26+
27+
const agreement = document.createElement('p');
28+
agreement.innerText = content;
29+
formEl?.appendChild(agreement);
30+
}

e2e/davinci-app/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import labelComponent from './components/label.js';
3333
import objectValueComponent from './components/object-value.js';
3434
import fidoComponent from './components/fido.js';
3535
import qrCodeComponent from './components/qr-code.js';
36+
import agreementComponent from './components/agreement.js';
3637
import pollingComponent from './components/polling.js';
3738

3839
const loggerFn = {
@@ -227,6 +228,8 @@ const urlParams = new URLSearchParams(window.location.search);
227228
);
228229
} else if (collector.type === 'QrCodeCollector') {
229230
qrCodeComponent(formEl, collector);
231+
} else if (collector.type === 'AgreementCollector') {
232+
agreementComponent(formEl, collector);
230233
} else if (collector.type === 'TextCollector') {
231234
textComponent(
232235
formEl, // You can ignore this; it's just for rendering

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import type {
2323
InferSingleValueCollectorType,
2424
InferMultiValueCollectorType,
2525
InferActionCollectorType,
26+
InferNoValueCollectorType,
27+
ReadOnlyCollector,
28+
QrCodeCollector,
29+
AgreementCollector,
2630
} from './collector.types.js';
2731

2832
describe('Collector Types', () => {
@@ -355,4 +359,65 @@ describe('Collector Types', () => {
355359
expectTypeOf(tCollector).toMatchTypeOf<FlowCollector>();
356360
});
357361
});
362+
363+
describe('InferNoValueCollectorType', () => {
364+
it('should correctly infer ReadOnlyCollector Type', () => {
365+
const tCollector: InferNoValueCollectorType<'ReadOnlyCollector'> = {
366+
category: 'NoValueCollector',
367+
error: null,
368+
type: 'ReadOnlyCollector',
369+
id: 'read-only-0',
370+
name: 'read-only',
371+
output: {
372+
key: 'read-only',
373+
label: 'Read Only Field',
374+
type: 'READ_ONLY',
375+
},
376+
};
377+
378+
expectTypeOf(tCollector).toEqualTypeOf<ReadOnlyCollector>();
379+
});
380+
381+
it('should correctly infer QrCodeCollector Type', () => {
382+
const tCollector: InferNoValueCollectorType<'QrCodeCollector'> = {
383+
category: 'NoValueCollector',
384+
error: null,
385+
type: 'QrCodeCollector',
386+
id: 'qr-code-0',
387+
name: 'qr-code-0',
388+
output: {
389+
key: 'qr-code-0',
390+
label: 'FALLBACK TEXT',
391+
type: 'QR_CODE',
392+
src: 'data:image/png;base64,abc123',
393+
},
394+
};
395+
396+
expectTypeOf(tCollector).toEqualTypeOf<QrCodeCollector>();
397+
});
398+
399+
it('should correctly infer AgreementCollector Type', () => {
400+
const tCollector: InferNoValueCollectorType<'AgreementCollector'> = {
401+
category: 'NoValueCollector',
402+
error: null,
403+
type: 'AgreementCollector',
404+
id: 'agreement-0',
405+
name: 'agreement-0',
406+
output: {
407+
key: 'agreement-0',
408+
label: 'Please accept the terms and conditions',
409+
type: 'AGREEMENT',
410+
titleEnabled: true,
411+
title: 'Terms and Conditions',
412+
agreement: {
413+
id: 'agreement-123',
414+
useDynamicAgreement: false,
415+
},
416+
enabled: true,
417+
},
418+
};
419+
420+
expectTypeOf(tCollector).toEqualTypeOf<AgreementCollector>();
421+
});
422+
});
358423
});

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,11 @@ 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' | 'QrCodeCollector';
485+
export type NoValueCollectorTypes =
486+
| 'ReadOnlyCollector'
487+
| 'NoValueCollector'
488+
| 'QrCodeCollector'
489+
| 'AgreementCollector';
486490

487491
export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
488492
category: 'NoValueCollector';
@@ -511,6 +515,21 @@ export interface QrCodeCollectorBase {
511515
};
512516
}
513517

518+
export interface AgreementCollector extends NoValueCollectorBase<'AgreementCollector'> {
519+
output: {
520+
key: string;
521+
label: string;
522+
type: string;
523+
titleEnabled: boolean;
524+
title: string;
525+
agreement: {
526+
id: string;
527+
useDynamicAgreement: boolean;
528+
};
529+
enabled: boolean;
530+
};
531+
}
532+
514533
/**
515534
* Type to help infer the collector based on the collector type
516535
* Used specifically in the returnNoValueCollector wrapper function.
@@ -523,19 +542,26 @@ export type InferNoValueCollectorType<T extends NoValueCollectorTypes> =
523542
? NoValueCollectorBase<'ReadOnlyCollector'>
524543
: T extends 'QrCodeCollector'
525544
? QrCodeCollectorBase
526-
: NoValueCollectorBase<'NoValueCollector'>;
545+
: T extends 'AgreementCollector'
546+
? AgreementCollector
547+
: NoValueCollectorBase<'NoValueCollector'>;
527548

528549
export type NoValueCollectors =
529550
| NoValueCollectorBase<'NoValueCollector'>
530551
| NoValueCollectorBase<'ReadOnlyCollector'>
531-
| QrCodeCollectorBase;
552+
| QrCodeCollectorBase
553+
| AgreementCollector;
532554

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

535557
export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>;
536558

537559
export type QrCodeCollector = QrCodeCollectorBase;
538560

561+
/** *********************************************************************
562+
* UNKNOWN COLLECTOR
563+
*/
564+
539565
export type UnknownCollector = {
540566
category: 'UnknownCollector';
541567
error: string | null;

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
returnSingleValueAutoCollector,
2424
returnObjectValueAutoCollector,
2525
returnQrCodeCollector,
26+
returnAgreementCollector,
2627
} from './collector.utils.js';
2728
import type {
2829
DaVinciField,
@@ -37,6 +38,7 @@ import type {
3738
ReadOnlyField,
3839
RedirectField,
3940
StandardField,
41+
AgreementField,
4042
} from './davinci.types.js';
4143
import type {
4244
MultiSelectCollector,
@@ -883,6 +885,49 @@ describe('returnQrCodeCollector', () => {
883885
});
884886
});
885887

888+
describe('returnAgreementCollector', () => {
889+
it('should return a valid AgreementCollector with all fields', () => {
890+
const mockField: AgreementField = {
891+
type: 'AGREEMENT',
892+
key: 'agreement-field',
893+
content: 'Please accept the terms and conditions',
894+
titleEnabled: true,
895+
title: 'Terms and Conditions',
896+
agreement: {
897+
id: 'agreement-123',
898+
useDynamicAgreement: false,
899+
},
900+
enabled: true,
901+
};
902+
const result = returnAgreementCollector(mockField, 0);
903+
expect(result).toEqual({
904+
category: 'NoValueCollector',
905+
error: null,
906+
type: 'AgreementCollector',
907+
id: 'agreement-field-0',
908+
name: 'agreement-field-0',
909+
output: {
910+
key: 'agreement-field-0',
911+
label: 'Please accept the terms and conditions',
912+
type: 'AGREEMENT',
913+
titleEnabled: true,
914+
title: 'Terms and Conditions',
915+
agreement: {
916+
id: 'agreement-123',
917+
useDynamicAgreement: false,
918+
},
919+
enabled: true,
920+
},
921+
});
922+
});
923+
924+
it('should set error when content is missing', () => {
925+
const mockField = { type: 'AGREEMENT', key: 'agreement-field' } as unknown as AgreementField;
926+
const result = returnAgreementCollector(mockField, 0);
927+
expect(result.error).toContain('Content is not found');
928+
});
929+
});
930+
886931
describe('returnSingleValueAutoCollector', () => {
887932
it('should create a valid ProtectCollector', () => {
888933
const mockField: ProtectField = {

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import type {
4646
SingleSelectField,
4747
StandardField,
4848
ValidatedField,
49+
AgreementField,
4950
} from './davinci.types.js';
5051

5152
/**
@@ -717,7 +718,7 @@ export function returnObjectValueCollector(
717718
* @returns {NoValueCollector} The constructed NoValueCollector object.
718719
*/
719720
export function returnNoValueCollector<
720-
Field extends ReadOnlyField | QrCodeField,
721+
Field extends ReadOnlyField | QrCodeField | AgreementField,
721722
CollectorType extends NoValueCollectorTypes = 'NoValueCollector',
722723
>(field: Field, idx: number, collectorType: CollectorType) {
723724
let error = '';
@@ -728,6 +729,20 @@ export function returnNoValueCollector<
728729
error = `${error}Type is not found in the field object. `;
729730
}
730731

732+
let output = {};
733+
734+
if (collectorType === 'AgreementCollector' && field.type === 'AGREEMENT') {
735+
output = {
736+
titleEnabled: field.titleEnabled,
737+
title: field.title,
738+
agreement: {
739+
id: field.agreement?.id ?? '',
740+
useDynamicAgreement: field.agreement?.useDynamicAgreement ?? false,
741+
},
742+
enabled: field.enabled ?? false,
743+
};
744+
}
745+
731746
return {
732747
category: 'NoValueCollector',
733748
error: error || null,
@@ -738,6 +753,7 @@ export function returnNoValueCollector<
738753
key: `${field.key || field.type}-${idx}`,
739754
label: field.content,
740755
type: field.type,
756+
...output,
741757
},
742758
} as InferNoValueCollectorType<CollectorType>;
743759
}
@@ -771,6 +787,16 @@ export function returnQrCodeCollector(field: QrCodeField, idx: number): QrCodeCo
771787
};
772788
}
773789

790+
/**
791+
* @function returnAgreementCollector - Creates an AgreementCollector object based on the provided field and index.
792+
* @param {AgreementField} field - The field object containing key, label, type, and agreement details.
793+
* @param {number} idx - The index to be used in the id of the AgreementCollector.
794+
* @returns {AgreementCollector} The constructed AgreementCollector object.
795+
*/
796+
export function returnAgreementCollector(field: AgreementField, idx: number) {
797+
return returnNoValueCollector(field, idx, 'AgreementCollector');
798+
}
799+
774800
/**
775801
* @function returnValidator - Creates a validator function based on the provided collector
776802
* @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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ export type QrCodeField = {
8181
fallbackText?: string;
8282
};
8383

84+
export type AgreementField = {
85+
type: 'AGREEMENT';
86+
key: string;
87+
content: string;
88+
titleEnabled: boolean;
89+
title: string;
90+
agreement: {
91+
id: string;
92+
useDynamicAgreement: boolean;
93+
};
94+
enabled: boolean;
95+
};
96+
8497
export type RedirectField = {
8598
type: 'SOCIAL_LOGIN_BUTTON';
8699
key: string;
@@ -238,7 +251,7 @@ export type ComplexValueFields =
238251
| FidoAuthenticationField
239252
| PollingField;
240253
export type MultiValueFields = MultiSelectField;
241-
export type ReadOnlyFields = ReadOnlyField | QrCodeField;
254+
export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField;
242255
export type RedirectFields = RedirectField;
243256
export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField;
244257

0 commit comments

Comments
 (0)