Skip to content

Commit f375a7f

Browse files
add screen for privacy statement (#164)
1 parent 6e85754 commit f375a7f

File tree

8 files changed

+199
-2
lines changed

8 files changed

+199
-2
lines changed

frontend/app/.server/locales/protected-en.json

+5
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,10 @@
3131
"next": "Next",
3232
"first-name": "Enter first name",
3333
"last-name": "Enter last name"
34+
},
35+
"privacy-statement": {
36+
"confirm-privacy-notice-checkbox": "I agree to the terms and conditions",
37+
"page-title": "Privacy Statement",
38+
"title": "<span>Priv</span>acy Statement"
3439
}
3540
}

frontend/app/.server/locales/protected-fr.json

+5
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,10 @@
3131
"next": "Suivant",
3232
"first-name": "Entrez le prénom",
3333
"last-name": "Entrez le nom de famille"
34+
},
35+
"privacy-statement": {
36+
"confirm-privacy-notice-checkbox": "J'accepte les termes et conditions.",
37+
"page-title": "Déclaration de confidentialité",
38+
"title": "<span>Décl</span>aration de confidentialité"
3439
}
3540
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as v from 'valibot';
2+
3+
import { mapIssueErrorMessage } from '~/.server/utils/validation-utils';
4+
5+
/**
6+
* Interface for customizable error messages in first name validation
7+
*/
8+
export interface ConfirmPrivacyNoticeSchemaErrorMessages extends Record<string, string | undefined> {
9+
required_error?: string;
10+
}
11+
12+
/**
13+
* Localized error message configuration
14+
*/
15+
const DEFAULT_MESSAGES = {
16+
en: {
17+
required_error: 'Privacy notice statement is required.',
18+
},
19+
fr: {
20+
required_error: 'Un avis de confidentialité est requis.',
21+
},
22+
} as const satisfies Record<Language, Required<ConfirmPrivacyNoticeSchemaErrorMessages>>;
23+
24+
/**
25+
* Configuration options for first name schema validation
26+
*/
27+
export interface ConfirmPrivacyNoticeSchemaOptions {
28+
/** Custom error messages to override defaults */
29+
errorMessages?: ConfirmPrivacyNoticeSchemaErrorMessages;
30+
}
31+
32+
/**
33+
* Creates a Valibot schema for validating first names
34+
*
35+
* @param options - Validation configuration options
36+
* @returns Valibot schema for first name validation
37+
*/
38+
export function confirmPrivacyNoticeSchema(options: ConfirmPrivacyNoticeSchemaOptions = {}) {
39+
const { errorMessages = {} } = options;
40+
41+
return v.pipe(
42+
// Base string validation with required error
43+
v.string((issue) => mapIssueErrorMessage(issue, errorMessages, 'required_error', DEFAULT_MESSAGES)),
44+
v.trim(),
45+
v.nonEmpty((issue) => mapIssueErrorMessage(issue, errorMessages, 'required_error', DEFAULT_MESSAGES)),
46+
);
47+
}

frontend/app/@types/express-session.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ declare module 'express-session' {
2121
inPersonSINCase: {
2222
firstName?: string;
2323
lastName?: string;
24+
confirmPrivacyNotice?: string;
2425
};
2526
}
2627
}

frontend/app/i18n-routes.ts

+5
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export const i18nRoutes = [
5959
file: 'routes/protected/person-case/last-name.tsx',
6060
paths: { en: '/en/protected/person-case/last-name', fr: '/fr/protege/person-case/last-name' },
6161
},
62+
{
63+
id: 'PROT-0006',
64+
file: 'routes/protected/person-case/privacy-statement.tsx',
65+
paths: { en: '/en/protected/person-case/privacy-statement', fr: '/fr/protege/person-case/privacy-statement' },
66+
},
6267
],
6368
},
6469
{

frontend/app/routes/protected/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function Index() {
4343
<h2 className="mt-10 mb-2 text-2xl font-bold text-slate-700">{t('protected:dashboard.get-started')}</h2>
4444
<ButtonLink
4545
className="flex w-80 items-center justify-between rounded-none"
46-
file="routes/protected/person-case/first-name.tsx"
46+
file="routes/protected/person-case/privacy-statement.tsx"
4747
>
4848
<span className="text-bold flex flex-col text-slate-700">
4949
<span className="text-xl">{t('protected:in-person.title')}</span>

frontend/app/routes/protected/person-case/first-name.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ export default function FirstName({ loaderData, actionData, params }: Route.Comp
8585
type="text"
8686
/>
8787
<div className="mt-8 flex flex-wrap items-center gap-3">
88-
<ButtonLink id="back-button" file="routes/protected/index.tsx" params={params} disabled={isSubmitting}>
88+
<ButtonLink
89+
id="back-button"
90+
file="routes/protected/person-case/privacy-statement.tsx"
91+
params={params}
92+
disabled={isSubmitting}
93+
>
8994
{t('protected:person-case.previous')}
9095
</ButtonLink>
9196
<Button variant="primary" type="submit" id="continue-first-name-button" disabled={isSubmitting}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { data, useFetcher } from 'react-router';
2+
import type { RouteHandle } from 'react-router';
3+
4+
import { Trans, useTranslation } from 'react-i18next';
5+
import * as v from 'valibot';
6+
7+
import type { Route, Info } from './+types/privacy-statement';
8+
9+
import { requireAuth } from '~/.server/utils/auth-utils';
10+
import { i18nRedirect } from '~/.server/utils/route-utils';
11+
import { confirmPrivacyNoticeSchema } from '~/.server/validation/confirmPrivacyNoticeSchema';
12+
import { Button } from '~/components/button';
13+
import { ButtonLink } from '~/components/button-link';
14+
import { ErrorSummary } from '~/components/error-summary';
15+
import { InputCheckbox } from '~/components/input-checkbox';
16+
import { Progress } from '~/components/progress';
17+
import { getFixedT } from '~/i18n-config.server';
18+
import { handle as parentHandle } from '~/routes/protected/layout';
19+
import { getLanguage } from '~/utils/i18n-utils';
20+
21+
export const handle = {
22+
i18nNamespace: [...parentHandle.i18nNamespace, 'protected'],
23+
} as const satisfies RouteHandle;
24+
25+
export async function loader({ context, request }: Route.LoaderArgs) {
26+
requireAuth(context.session, new URL(request.url), ['user']);
27+
const t = await getFixedT(request, handle.i18nNamespace);
28+
29+
return {
30+
documentTitle: t('protected:privacy-statement.page-title'),
31+
defaultFormValues: {
32+
confirmPrivacyNotice: context.session.inPersonSINCase?.confirmPrivacyNotice,
33+
},
34+
};
35+
}
36+
37+
export const meta: Route.MetaFunction = ({ data }) => {
38+
return [{ title: data.documentTitle }];
39+
};
40+
41+
export async function action({ context, request }: Route.ActionArgs) {
42+
requireAuth(context.session, new URL(request.url), ['user']);
43+
const lang = getLanguage(request);
44+
45+
const formData = await request.formData();
46+
const input = { confirmPrivacyNotice: formData.get('confirmPrivacyNotice') as string };
47+
48+
const schema = v.object({ confirmPrivacyNotice: confirmPrivacyNoticeSchema() });
49+
const parsedDataResult = v.safeParse(schema, input, { lang });
50+
51+
if (!parsedDataResult.success) {
52+
return data({ errors: v.flatten<typeof schema>(parsedDataResult.issues).nested }, { status: 400 });
53+
}
54+
55+
// If the first name is valid, store it in the session and redirect to the next page
56+
context.session.inPersonSINCase = {
57+
...(context.session.inPersonSINCase ?? {}),
58+
...input,
59+
};
60+
return i18nRedirect('routes/protected/person-case/first-name.tsx', request);
61+
}
62+
63+
export default function PrivacyStatement({ loaderData, actionData, params }: Route.ComponentProps) {
64+
const { t } = useTranslation(handle.i18nNamespace);
65+
66+
const fetcher = useFetcher<Info['actionData']>();
67+
const isSubmitting = fetcher.state !== 'idle';
68+
const errors = fetcher.data?.errors;
69+
70+
return (
71+
<>
72+
<Progress className="mt-8 mb-8" label="" value={20} />
73+
<span className="text-md">{t('protected:in-person.title')}</span>
74+
<h1 className="mb-8 text-2xl font-bold text-slate-700">
75+
<Trans
76+
i18nKey="protected:privacy-statement.title"
77+
components={{ span: <span className="underline decoration-red-800 underline-offset-8" /> }}
78+
/>
79+
</h1>
80+
81+
<fetcher.Form method="post" noValidate>
82+
<div className="space-y-6">
83+
<ErrorSummary errors={errors} />
84+
<p>
85+
Lorem ipsum odor amet, consectetuer adipiscing elit. Gravida pulvinar fringilla augue per lacinia cubilia aliquam.
86+
Nibh egestas pharetra; at sit ipsum aliquet fames pellentesque. Posuere mauris pretium commodo hendrerit maecenas
87+
neque imperdiet. Phasellus tempor metus phasellus eu malesuada. Mi fusce dapibus nam metus est sagittis nisl sem
88+
fringilla. Iaculis gravida netus aptent mattis dignissim massa. Dolor curae donec hac dui, neque proin erat. Nullam
89+
sem ullamcorper commodo phasellus hendrerit ex. Curabitur venenatis ex, vitae fermentum finibus nibh.
90+
</p>
91+
92+
<p>
93+
Himenaeos turpis id pretium mauris pellentesque quis curae. Facilisi sollicitudin justo erat habitasse turpis
94+
consequat taciti. Scelerisque suspendisse hac dictumst mattis in odio. Molestie molestie parturient arcu iaculis
95+
lacinia. Ut est vel massa fusce congue laoreet posuere pulvinar. Consectetur pharetra ipsum tortor cubilia ut.
96+
Placerat a pellentesque commodo bibendum posuere vivamus. Senectus imperdiet sit praesent adipiscing accumsan nibh
97+
consequat per. Aliquam turpis ut libero non malesuada tortor ac maximus dictum. Non vestibulum pellentesque posuere
98+
dapibus eleifend cras tempus potenti.
99+
</p>
100+
101+
<p>
102+
Rhoncus semper dolor; scelerisque euismod justo integer. Rhoncus et cras cursus velit diam. Vehicula magna sem eget
103+
urna vitae donec phasellus dignissim volutpat. Arcu mi neque ad nulla; dui maximus. Nulla ligula ultrices facilisi
104+
urna rhoncus platea per platea. Nascetur nec dapibus augue dictum volutpat tristique nec dis. Dis ante metus tortor
105+
lacus porta.
106+
</p>
107+
108+
<InputCheckbox
109+
id="confirmPrivacyNotice"
110+
name="confirmPrivacyNotice"
111+
errorMessage={errors?.confirmPrivacyNotice?.[0]}
112+
defaultChecked={loaderData.defaultFormValues.confirmPrivacyNotice === 'on'}
113+
required
114+
>
115+
{t('protected:privacy-statement.confirm-privacy-notice-checkbox')}
116+
</InputCheckbox>
117+
</div>
118+
<div className="mt-8 flex flex-wrap items-center gap-3">
119+
<ButtonLink id="back-button" file="routes/protected/index.tsx" params={params} disabled={isSubmitting}>
120+
{t('protected:person-case.previous')}
121+
</ButtonLink>
122+
<Button variant="primary" type="submit" id="continue-button" disabled={isSubmitting}>
123+
{t('protected:person-case.next')}
124+
</Button>
125+
</div>
126+
</fetcher.Form>
127+
</>
128+
);
129+
}

0 commit comments

Comments
 (0)