Skip to content

Commit ae9b433

Browse files
committed
RPS-1172: Check your answers screen (#651)
* RPS-1172: added mocks for address lookup api * RPS-1172: fixed journey up until payment screen * RPS-1172: fixed address lookup flow * RPS-1172: fixed validation on address lookup flow * RPS-1172: added conditional email field * RPS-1172: pr cleanup * RPS-1172: pr cleanup #2 * RPS-1172: pr cleanup #3 * RPS-1172: fixed change links
1 parent 6fb16b1 commit ae9b433

40 files changed

Lines changed: 1945 additions & 517 deletions

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Development environment variables
2+
# Copy this file to .env.local and fill in the values for your local development environment
3+
4+
SESSION_COOKIE_PASSWORD=604c565d2037e89c5fc3ed6bb20343148ccd2e5aa1abb93d
5+
RPS_API_BASE_URL=http://localhost
6+
RPS_TENANT_ID=test
7+
RPS_CLIENT_ID=test-client
8+
RPS_CLIENT_SECRET=test-secret
9+
RPS_SCOPES=test-scope

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ npm-debug.log
1313

1414
# Environment variables
1515
**/.env
16+
**/.env.local
1617
**/.env_mysql
1718

1819
# Build output

docker-compose.dev.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,15 @@ services:
2525
- /usr/src/app/runner/dist
2626
- ./runner:/usr/src/app/runner
2727
command: yarn runner dev
28+
extra_hosts:
29+
- "host.docker.internal:host-gateway"
2830
environment:
29-
- SESSION_COOKIE_PASSWORD=604c565d2037e89c5fc3ed6bb20343148ccd2e5aa1abb93d
31+
# Forms can reference ${VAR_NAME} placeholders (e.g. addressLookupConfig in
32+
# order-a-radon-risk-report.json). In development these are resolved from
33+
# environment variables of the same name by resolvePlaceholders.ts.
34+
- SESSION_COOKIE_PASSWORD
35+
- RPS_API_BASE_URL
36+
- RPS_TENANT_ID
37+
- RPS_CLIENT_ID
38+
- RPS_CLIENT_SECRET
39+
- RPS_SCOPES

model/src/components/types.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export enum ComponentTypeEnum {
2727
ContextComponent = "ContextComponent",
2828
ContentWithState = "ContentWithState",
2929
DisplayAddress = "DisplayAddress",
30+
HiddenField = "HiddenField",
3031
}
3132

3233
export type ComponentType =
@@ -58,7 +59,8 @@ export type ComponentType =
5859
| "WebsiteField"
5960
| "ContextComponent"
6061
| "ContentWithState"
61-
| "DisplayAddress";
62+
| "DisplayAddress"
63+
| "HiddenField";
6264

6365
export type ComponentSubType = "field" | "content";
6466

@@ -311,6 +313,16 @@ export interface DisplayAddressComponent extends ContentFieldBase {
311313
type: "DisplayAddress";
312314
}
313315

316+
export interface HiddenFieldComponent {
317+
type: "HiddenField";
318+
name: string;
319+
options: {
320+
value?: string;
321+
exposeToContext?: boolean;
322+
};
323+
schema?: {};
324+
}
325+
314326
// List Fields
315327
export interface ListComponent extends ListFieldBase {
316328
type: "List";
@@ -337,7 +349,9 @@ export interface RadiosFieldComponent extends ListFieldBase {
337349

338350
export interface SelectFieldComponent extends ListFieldBase {
339351
type: "SelectField";
340-
options: ListFieldBase["options"] & { autocomplete?: string } & { preselected?: { text: string; value: string | number | boolean } };
352+
options: ListFieldBase["options"] & { autocomplete?: string } & {
353+
preselected?: { text: string; value: string | number | boolean };
354+
};
341355
subType?: "listField";
342356
}
343357

@@ -376,7 +390,8 @@ export type ComponentDef =
376390
| WebsiteFieldComponent
377391
| ContextComponent
378392
| ContentWithStateComponent
379-
| DisplayAddressComponent;
393+
| DisplayAddressComponent
394+
| HiddenFieldComponent;
380395

381396
// Components that render inputs.
382397
export type InputFieldsComponentsDef =

model/src/data-model/types.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,64 @@ export interface AddressLookupConfig extends MsalAuthorizerConfig {
215215
subscriptionKey?: string;
216216
}
217217

218+
export interface SummaryDeclaration {
219+
/** Checkbox label rendered on the summary page. */
220+
label: string;
221+
/** Overrides the default "You must declare…" flash error when unchecked. */
222+
errorMessage?: string;
223+
/** Hides the h2 Declaration heading in the summary page */
224+
hideDeclarationHeading?: boolean;
225+
}
226+
227+
/**
228+
* Merges multiple named fields into a single summary row.
229+
* The resulting row uses `to` as its field name and joins the source values with `joiner`.
230+
*/
231+
export interface SummaryMergeField {
232+
names: string[];
233+
to: string;
234+
joiner: string;
235+
}
236+
237+
/** A synthetic row that can be appended to the last summary section via a conditional rule. */
238+
export interface SummaryAppendSection {
239+
name: string;
240+
label: string;
241+
value: string;
242+
/** When true the user cannot return to change this value from the summary. */
243+
immutable?: boolean;
244+
}
245+
246+
export interface SummaryConditionalRowCondition {
247+
field: string;
248+
value?: string;
249+
isEmpty?: boolean;
250+
}
251+
252+
export interface SummaryConditionalRow {
253+
when: SummaryConditionalRowCondition;
254+
removeFields?: string[];
255+
appendToLastSection?: SummaryAppendSection;
256+
}
257+
258+
/**
259+
* Data-driven configuration for the summary page, set at the form-definition level.
260+
* Transforms are applied in order: merge → remove → relabel → value transform → conditional rules.
261+
*/
262+
export interface SummaryConfig {
263+
/** Overrides the default "Confirm and submit" button label. */
264+
submitLabel?: string;
265+
declaration?: SummaryDeclaration;
266+
/** Field names to strip from the summary rows entirely. */
267+
removeFields?: string[];
268+
mergeFields?: Array<SummaryMergeField>;
269+
/** Map of field name → new display label. */
270+
relabelFields?: Record<string, string>;
271+
/** Map of field name → { rawValue → replacement display value }. */
272+
valueTransforms?: Record<string, Record<string, string>>;
273+
conditionalRows?: Array<SummaryConditionalRow>;
274+
}
275+
218276
/**
219277
* `FormDefinition` is a typescript representation of `Schema`
220278
*/
@@ -254,4 +312,5 @@ export type FormDefinition = {
254312
returnTo?: boolean | undefined;
255313
addressLookupConfig?: AddressLookupConfig;
256314
error500ContactEmail?: string | undefined;
315+
summaryConfig?: SummaryConfig;
257316
};

model/src/schema/schema.ts

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,7 @@ const specialPagesSchema = joi.object().keys({
178178
paymentSkippedWarningPage: paymentSkippedWarningPage.optional(),
179179
});
180180

181-
const listItemSchema = joi.object().keys({
182-
text: localisedString,
181+
const listItemBaseKeys = {
183182
value: joi.alternatives().try(joi.number(), joi.string()),
184183
checkpointDisplayValue: joi.alternatives().try(joi.number(), joi.string()),
185184
description: localisedString.optional(),
@@ -196,7 +195,17 @@ const listItemSchema = joi.object().keys({
196195
.allow(null)
197196
.optional(),
198197
condition: joi.string().allow(null, "").optional(),
199-
});
198+
};
199+
200+
// Either `html` or `text` must be present, but not both — .oxor enforces this.
201+
const listItemSchema = joi
202+
.object()
203+
.keys({
204+
...listItemBaseKeys,
205+
html: localisedString,
206+
text: localisedString,
207+
})
208+
.oxor("html", "text");
200209

201210
const listSchema = joi.object().keys({
202211
name: joi.string().required(),
@@ -345,6 +354,60 @@ const addressLookupConfigSchema = msalAuthorizeConfigSchema.concat(
345354
})
346355
);
347356

357+
const summaryConfigSchema = joi.object().keys({
358+
submitLabel: joi.string().optional(),
359+
declaration: joi
360+
.object()
361+
.keys({
362+
label: joi.string().required(),
363+
errorMessage: joi.string().optional(),
364+
hideDeclarationHeading: joi.boolean().optional(),
365+
})
366+
.optional(),
367+
removeFields: joi.array().items(joi.string()).optional(),
368+
mergeFields: joi
369+
.array()
370+
.items(
371+
joi.object().keys({
372+
names: joi.array().items(joi.string()).required(),
373+
to: joi.string().required(),
374+
joiner: joi.string().required(),
375+
})
376+
)
377+
.optional(),
378+
relabelFields: joi.object().pattern(joi.string(), joi.string()).optional(),
379+
valueTransforms: joi
380+
.object()
381+
.pattern(joi.string(), joi.object().pattern(joi.string(), joi.string()))
382+
.optional(),
383+
conditionalRows: joi
384+
.array()
385+
.items(
386+
joi.object().keys({
387+
when: joi
388+
.object()
389+
.keys({
390+
field: joi.string().required(),
391+
value: joi.string().optional(),
392+
isEmpty: joi.boolean().optional(),
393+
})
394+
.or("value", "isEmpty")
395+
.required(),
396+
removeFields: joi.array().items(joi.string()).optional(),
397+
appendToLastSection: joi
398+
.object()
399+
.keys({
400+
name: joi.string().required(),
401+
label: joi.string().required(),
402+
value: joi.string().required(),
403+
immutable: joi.boolean().optional(),
404+
})
405+
.optional(),
406+
})
407+
)
408+
.optional(),
409+
});
410+
348411
export const Schema = joi
349412
.object()
350413
.required()
@@ -392,6 +455,7 @@ export const Schema = joi
392455
secureFormSubmissionConfig: secureFormSubmissionConfig.optional(),
393456
addressLookupConfig: addressLookupConfigSchema.optional(),
394457
error500ContactEmail: joi.string().optional(),
458+
summaryConfig: summaryConfigSchema.optional(),
395459
});
396460

397461
/**

0 commit comments

Comments
 (0)