From a6abc9c48cf95a82a78565d79554e6c8856d5efc Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 1 Nov 2024 17:25:02 +0100 Subject: [PATCH 1/8] :sparkles: [#4320] Set up dedicated route for cosign start --- src/Context.js | 1 + src/components/CoSign/Cosign.jsx | 2 + src/components/CoSign/CosignStart.jsx | 80 +++++++++++++++++++++++++++ src/components/routingActions.jsx | 6 ++ 4 files changed, 89 insertions(+) create mode 100644 src/components/CoSign/CosignStart.jsx diff --git a/src/Context.js b/src/Context.js index d6dc386f3..5272d7ecc 100644 --- a/src/Context.js +++ b/src/Context.js @@ -10,6 +10,7 @@ const FormContext = React.createContext({ introductionPageContent: '', loginRequired: false, loginOptions: [], + cosignLoginOptions: [], maintenanceMode: false, showProgressIndicator: true, showSummaryProgress: false, diff --git a/src/components/CoSign/Cosign.jsx b/src/components/CoSign/Cosign.jsx index 6b22bbe95..ea371fd38 100644 --- a/src/components/CoSign/Cosign.jsx +++ b/src/components/CoSign/Cosign.jsx @@ -9,6 +9,7 @@ import {CosignSummary} from 'components/Summary'; import useFormContext from 'hooks/useFormContext'; import CosignDone from './CosignDone'; +import CosignStart from './CosignStart'; const initialState = { submission: null, @@ -60,6 +61,7 @@ const Cosign = () => { return ( + } /> { + const form = useFormContext(); + + const outagePluginId = useDetectAuthenticationOutage(); + const authErrors = useDetectAuthErrorMessages(); + + if (!form.active) { + throw new UnprocessableEntity('Unprocessable Entity', 422, 'Form not active', 'form-inactive'); + } + + const userIsFormDesigner = IsFormDesigner.getValue(); + if (!userIsFormDesigner && form.maintenanceMode) { + return ; + } + + if (outagePluginId) { + const loginOption = form.cosignLoginOptions.find( + option => option.identifier === outagePluginId + ); + if (!loginOption) throw new Error('Unknown login plugin identifier'); + return ( + + } + > + + + ); + } + + return ( + + + {userIsFormDesigner && form.maintenanceMode && } + + {!!authErrors ? : null} + + + + + + {}} + /> + + + ); +}; + +export default CosignStart; diff --git a/src/components/routingActions.jsx b/src/components/routingActions.jsx index 32a94b68f..b3cadb727 100644 --- a/src/components/routingActions.jsx +++ b/src/components/routingActions.jsx @@ -7,6 +7,12 @@ */ export const getRedirectParams = (action, actionParams) => { switch (action) { + case 'cosign-init': { + return { + path: 'cosign/start', + query: new URLSearchParams(actionParams), + }; + } case 'cosign': return { path: 'cosign/check', From 7a2d26ed56e41b0268f440695c0bb94866f7b2d3 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 1 Nov 2024 17:44:34 +0100 Subject: [PATCH 2/8] :sparkles: [open-formulieren/open-forms#4320] Only display cosign login options if links in emails are not allowed The link in emails are proper deep-links that bring the user directly to the required start page. --- src/components/FormStart/index.jsx | 8 +++++++- src/types/Form.js | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/FormStart/index.jsx b/src/components/FormStart/index.jsx index e229c9d8d..d0713c563 100644 --- a/src/components/FormStart/index.jsx +++ b/src/components/FormStart/index.jsx @@ -125,7 +125,13 @@ const FormStart = ({form, submission, onFormStart, onDestroySession, initialData isAuthenticated={isAuthenticated} /> ) : ( - + )} diff --git a/src/types/Form.js b/src/types/Form.js index 594910331..567642906 100644 --- a/src/types/Form.js +++ b/src/types/Form.js @@ -20,6 +20,7 @@ const Form = PropTypes.shape({ loginRequired: PropTypes.bool.isRequired, loginOptions: PropTypes.arrayOf(LoginOption).isRequired, cosignLoginOptions: PropTypes.arrayOf(LoginOption), + cosignHasLinkInEmail: PropTypes.bool, product: PropTypes.string, slug: PropTypes.string.isRequired, url: PropTypes.string.isRequired, From d25b835b39c954124230a1cc08a5f1f4a71a0cff Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 1 Nov 2024 17:54:30 +0100 Subject: [PATCH 3/8] :sparkles: [open-formulieren/open-forms#4320] Support optional code for cosign login URL If a code query parameter is present in the route params, use it to pre-populate the code parameter for the ?next param, which redirects to the submission look-up page on the backend. This will allow the backend to shortcut the form by auto-populating it and validating the authentication state, requiring less user input. This page also works without the parameter and follows the 'old' flow. The login screens will be updated accordingly to redirect to this page. --- src/components/CoSign/CosignStart.stories.js | 51 +++++++++ .../LoginOptions/LoginOptions.stories.jsx | 8 +- src/components/LoginOptions/index.jsx | 8 +- src/components/LoginOptions/tests.spec.jsx | 105 ++++++++---------- src/components/utils/index.jsx | 9 ++ 5 files changed, 118 insertions(+), 63 deletions(-) create mode 100644 src/components/CoSign/CosignStart.stories.js diff --git a/src/components/CoSign/CosignStart.stories.js b/src/components/CoSign/CosignStart.stories.js new file mode 100644 index 000000000..bd95c7a55 --- /dev/null +++ b/src/components/CoSign/CosignStart.stories.js @@ -0,0 +1,51 @@ +import {withRouter} from 'storybook-addon-remix-react-router'; + +import {buildForm} from 'api-mocks'; +import {withForm} from 'story-utils/decorators'; + +import CosignStart from './CosignStart'; + +export default { + title: 'Private API / Cosign / Start', + component: CosignStart, + decorators: [withForm, withRouter], + parameters: { + formContext: { + form: buildForm({ + loginRequired: true, + loginOptions: [ + { + identifier: 'digid', + label: 'DigiD', + url: '#', + logo: { + title: 'DigiD simulatie', + imageSrc: './digid.png', + href: 'https://www.digid.nl/', + appearance: 'dark', + }, + isForGemachtigde: false, + }, + ], + cosignLoginOptions: [ + { + identifier: 'digid', + label: 'DigiD', + url: 'http://localhost:8000/auth/digid/?next=http://localhost:8000/cosign&code=123', + logo: { + title: 'DigiD simulatie', + imageSrc: './digid.png', + href: 'https://www.digid.nl/', + appearance: 'dark', + }, + isForGemachtigde: false, + }, + ], + }), + }, + }, +}; + +export const Default = { + name: 'CosignStart', +}; diff --git a/src/components/LoginOptions/LoginOptions.stories.jsx b/src/components/LoginOptions/LoginOptions.stories.jsx index e2526a15a..96a8789bf 100644 --- a/src/components/LoginOptions/LoginOptions.stories.jsx +++ b/src/components/LoginOptions/LoginOptions.stories.jsx @@ -1,4 +1,5 @@ import {expect, fn, userEvent, waitFor, within} from '@storybook/test'; +import {withRouter} from 'storybook-addon-remix-react-router'; import {buildForm} from 'api-mocks'; import {LiteralDecorator} from 'story-utils/decorators'; @@ -9,7 +10,10 @@ import LoginOptionsDisplay from './LoginOptionsDisplay'; export default { title: 'Composites / Login Options', component: LoginOptions, - decorators: [LiteralDecorator], + decorators: [LiteralDecorator, withRouter], + args: { + onFormStart: fn(), + }, argTypes: { form: {table: {disable: true}}, }, @@ -239,7 +243,7 @@ export const WithCoSignOption = { { identifier: 'digid', label: 'DigiD', - url: '#', + url: 'http://localhost:8000/auth/digid/?next=http://localhost:3000/form?_start=1', logo: { title: 'DigiD simulatie', imageSrc: './digid.png', diff --git a/src/components/LoginOptions/index.jsx b/src/components/LoginOptions/index.jsx index 32a742150..21a5b4cc0 100644 --- a/src/components/LoginOptions/index.jsx +++ b/src/components/LoginOptions/index.jsx @@ -4,13 +4,15 @@ import {FormattedMessage} from 'react-intl'; import {ConfigContext} from 'Context'; import Literal from 'components/Literal'; -import {getLoginUrl} from 'components/utils'; +import {getCosignLoginUrl, getLoginUrl} from 'components/utils'; +import useQuery from 'hooks/useQuery'; import Types from 'types'; import LoginOptionsDisplay from './LoginOptionsDisplay'; const LoginOptions = ({form, onFormStart, extraNextParams = {}}) => { const config = useContext(ConfigContext); + const queryParams = useQuery(); const loginAsYourselfOptions = []; const loginAsGemachtigdeOptions = []; @@ -35,9 +37,12 @@ const LoginOptions = ({form, onFormStart, extraNextParams = {}}) => { }); if (form.cosignLoginOptions) { + const cosignCode = queryParams.get('code'); form.cosignLoginOptions.forEach(option => { + const loginUrl = getCosignLoginUrl(option, cosignCode ? {code: cosignCode} : undefined); cosignLoginOptions.push({ ...option, + url: loginUrl, label: ( { e.preventDefault(); onFormStart(e, true); }, + 'data-testid': 'start-form', }; return ( diff --git a/src/components/LoginOptions/tests.spec.jsx b/src/components/LoginOptions/tests.spec.jsx index 0c1cbddee..1e947afbe 100644 --- a/src/components/LoginOptions/tests.spec.jsx +++ b/src/components/LoginOptions/tests.spec.jsx @@ -1,56 +1,56 @@ -import {fireEvent} from '@testing-library/react'; +import {render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import messagesNL from 'i18n/compiled/nl.json'; import React from 'react'; -import {createRoot} from 'react-dom/client'; -import {act} from 'react-dom/test-utils'; import {IntlProvider} from 'react-intl'; +import {RouterProvider, createMemoryRouter} from 'react-router-dom'; import {buildForm} from 'api-mocks'; import {LiteralsProvider} from 'components/Literal'; import LoginOptions from './index'; -let container = null; -let root = null; -beforeEach(() => { - // setup a DOM element as a render target - container = document.createElement('div'); - document.body.appendChild(container); - root = createRoot(container); -}); - -afterEach(() => { - // cleanup on exiting - act(() => { - root.unmount(); - container.remove(); - root = null; - container = null; - }); -}); +const Wrapper = ({form, onFormStart}) => { + const router = createMemoryRouter( + [ + { + path: '/', + element: , + }, + ], + { + initialEntries: ['/'], + initialIndex: 0, + } + ); + + return ( + + + + + + ); +}; -it('Login not required, options wrapped in form tag', () => { +it('Login not required, options wrapped in form tag', async () => { + const user = userEvent.setup(); const form = buildForm({loginRequired: false, loginOptions: [], cosignLoginOptions: []}); const onFormStart = jest.fn(e => e.preventDefault()); - act(() => { - root.render( - - - - ); - }); + render(); - expect(container.firstChild.nodeName).toEqual('FORM'); - const anonymousStartButton = container.getElementsByTagName('button')[0]; - expect(anonymousStartButton).not.toBeUndefined(); + expect(await screen.findByTestId('start-form')).toBeInTheDocument(); - fireEvent.click(anonymousStartButton); + const anonymousStartButton = screen.getByRole('button', {name: 'Begin Form'}); + expect(anonymousStartButton).toBeVisible(); + + await user.click(anonymousStartButton); expect(onFormStart).toHaveBeenCalled(); }); -it('Login required, options not wrapped in form tag', () => { +it('Login required, options not wrapped in form tag', async () => { const form = buildForm({ loginRequired: true, loginOptions: [ @@ -77,28 +77,22 @@ it('Login required, options not wrapped in form tag', () => { href: 'https://open-forms.nl/digid-form/', }; - act(() => { - root.render( - - - - ); - }); + render(); const expectedUrl = new URL(form.loginOptions[0].url); expectedUrl.searchParams.set('next', 'https://open-forms.nl/digid-form/?_start=1'); - expect(container.firstChild.nodeName).toEqual('DIV'); - const anonymousStartButton = container.getElementsByTagName('button')[0]; - expect(anonymousStartButton).toBeUndefined(); + const digidLoginLink = await screen.findByRole('link', {name: 'Inloggen met DigiD'}); + expect(digidLoginLink).toBeVisible(); + expect(digidLoginLink.href).toEqual(expectedUrl.toString()); - const digidLoginButton = container.getElementsByTagName('a')[0]; - expect(digidLoginButton.href).toEqual(expectedUrl.toString()); + expect(screen.queryByTestId('start-form')).not.toBeInTheDocument(); + expect(screen.queryAllByRole('button')).toHaveLength(0); window.location = location; }); -it('Login button has the right URL after cancelling log in', () => { +it('Login button has the right URL after cancelling log in', async () => { const form = buildForm({ loginRequired: true, loginOptions: [ @@ -126,23 +120,14 @@ it('Login button has the right URL after cancelling log in', () => { href: 'https://open-forms.nl/digid-form/?_start=1&_digid-message=login-cancelled', }; - act(() => { - root.render( - - - - ); - }); + render(); const expectedUrl = new URL(form.loginOptions[0].url); expectedUrl.searchParams.set('next', 'https://open-forms.nl/digid-form/?_start=1'); - expect(container.firstChild.nodeName).toEqual('DIV'); - const anonymousStartButton = container.getElementsByTagName('button')[0]; - expect(anonymousStartButton).toBeUndefined(); - - const digidLoginButton = container.getElementsByTagName('a')[0]; - expect(digidLoginButton.href).toEqual(expectedUrl.toString()); + const digidLoginLink = await screen.findByRole('link', {name: 'Inloggen met DigiD'}); + expect(digidLoginLink).toBeVisible(); + expect(digidLoginLink.href).toEqual(expectedUrl.toString()); window.location = location; }); diff --git a/src/components/utils/index.jsx b/src/components/utils/index.jsx index 33c904405..86695ce91 100644 --- a/src/components/utils/index.jsx +++ b/src/components/utils/index.jsx @@ -43,6 +43,14 @@ const getLoginUrl = (loginOption, extraParams = {}, extraNextParams = {}) => { return loginUrl.toString(); }; +const getCosignLoginUrl = (loginOption, extraParams = {}) => { + const loginUrl = new URL(loginOption.url); + const nextUrl = new URL(loginUrl.searchParams.get('next')); + Object.entries(extraParams).forEach(([key, value]) => nextUrl.searchParams.set(key, value)); + loginUrl.searchParams.set('next', nextUrl.toString()); + return loginUrl.toString(); +}; + const getLoginRedirectUrl = form => { // Automatically redirect the user to a specific login option (if configured) if (form.autoLoginAuthenticationBackend) { @@ -72,5 +80,6 @@ export { isLastStep, getLoginRedirectUrl, getLoginUrl, + getCosignLoginUrl, eventTriggeredBySubmitButton, }; From 1f16218fc10dd3619404dcbcb455999c818ee6a4 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 11 Nov 2024 17:05:10 +0100 Subject: [PATCH 4/8] :speech_balloon: [open-formulieren/open-forms#4320] Update introduction text for cosigning Added extra paragraph/body clarifying the cosign context. On the CosignStart page, this is displayed as is, without extra heading or background color because the regular login options are (by definition) not displayed on this page. On the regular form start page, the extra paragraph is included under the heading. This entire section is only displayed when no links are included in the cosign email. --- src/components/CoSign/CosignStart.jsx | 4 +- .../LoginOptions/LoginOptionsDisplay.jsx | 30 ++++++++--- src/components/LoginOptions/index.jsx | 4 +- src/i18n/compiled/en.json | 52 ++++++++++++------- src/i18n/compiled/nl.json | 52 ++++++++++++------- src/i18n/messages/en.json | 15 ++++-- src/i18n/messages/nl.json | 15 ++++-- 7 files changed, 113 insertions(+), 59 deletions(-) diff --git a/src/components/CoSign/CosignStart.jsx b/src/components/CoSign/CosignStart.jsx index 2f85738ed..3e1125df1 100644 --- a/src/components/CoSign/CosignStart.jsx +++ b/src/components/CoSign/CosignStart.jsx @@ -61,7 +61,8 @@ const CosignStart = () => { @@ -71,6 +72,7 @@ const CosignStart = () => { // dummy - we don't actually start a new submission, but the next URL is baked // into the login URLs. onFormStart={() => {}} + isolateCosignOptions={false} /> diff --git a/src/components/LoginOptions/LoginOptionsDisplay.jsx b/src/components/LoginOptions/LoginOptionsDisplay.jsx index 0a2c3ecaa..2526fe6a2 100644 --- a/src/components/LoginOptions/LoginOptionsDisplay.jsx +++ b/src/components/LoginOptions/LoginOptionsDisplay.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {FormattedMessage} from 'react-intl'; +import Body from 'components/Body'; import LoginButton from 'components/LoginButton'; import FormattedLoginOption from 'types/FormattedLoginOption'; import {getBEMClassName} from 'utils'; @@ -10,6 +11,7 @@ const LoginOptionsDisplay = ({ loginAsYourselfOptions, loginAsGemachtigdeOptions, cosignLoginOptions, + isolateCosignOptions = true, }) => { return (
@@ -37,13 +39,26 @@ const LoginOptionsDisplay = ({ )} {cosignLoginOptions?.length > 0 && ( -
-

- -

+
+ {isolateCosignOptions && ( + <> +

+ +

+ + + + + )}
{cosignLoginOptions.map(option => ( @@ -60,6 +75,7 @@ LoginOptionsDisplay.propTypes = { loginAsYourselfOptions: PropTypes.arrayOf(FormattedLoginOption).isRequired, loginAsGemachtigdeOptions: PropTypes.arrayOf(FormattedLoginOption).isRequired, cosignLoginOptions: PropTypes.arrayOf(FormattedLoginOption), + isolateCosignOptions: PropTypes.bool, }; export default LoginOptionsDisplay; diff --git a/src/components/LoginOptions/index.jsx b/src/components/LoginOptions/index.jsx index 21a5b4cc0..fc20ca24b 100644 --- a/src/components/LoginOptions/index.jsx +++ b/src/components/LoginOptions/index.jsx @@ -10,7 +10,7 @@ import Types from 'types'; import LoginOptionsDisplay from './LoginOptionsDisplay'; -const LoginOptions = ({form, onFormStart, extraNextParams = {}}) => { +const LoginOptions = ({form, onFormStart, extraNextParams = {}, isolateCosignOptions = true}) => { const config = useContext(ConfigContext); const queryParams = useQuery(); @@ -79,6 +79,7 @@ const LoginOptions = ({form, onFormStart, extraNextParams = {}}) => { loginAsYourselfOptions={loginAsYourselfOptions} loginAsGemachtigdeOptions={loginAsGemachtigdeOptions} cosignLoginOptions={cosignLoginOptions} + isolateCosignOptions={isolateCosignOptions} /> ); @@ -89,6 +90,7 @@ LoginOptions.propTypes = { onFormStart: PropTypes.func.isRequired, extraParams: PropTypes.object, extraNextParams: PropTypes.object, + isolateCosignOptions: PropTypes.bool, }; export default LoginOptions; diff --git a/src/i18n/compiled/en.json b/src/i18n/compiled/en.json index 26079fe83..faeda5988 100644 --- a/src/i18n/compiled/en.json +++ b/src/i18n/compiled/en.json @@ -231,6 +231,26 @@ "value": "." } ], + "7v8PR6": [ + { + "type": 0, + "value": "Your session has expired. " + }, + { + "children": [ + { + "type": 0, + "value": "Click here to restart" + } + ], + "type": 8, + "value": "link" + }, + { + "type": 0, + "value": "." + } + ], "8DHd4/": [ { "type": 0, @@ -881,6 +901,12 @@ "value": "isApplicable" } ], + "LSB7FE": [ + { + "type": 0, + "value": "Did you receive an email with a request to cosign? Start the cosigning by logging in." + } + ], "LiRh7j": [ { "type": 0, @@ -1327,26 +1353,6 @@ "value": "No" } ], - "abtbgK": [ - { - "type": 0, - "value": "Your session has expired. " - }, - { - "children": [ - { - "type": 0, - "value": "Click here to restart" - } - ], - "type": 8, - "value": "link" - }, - { - "type": 0, - "value": "." - } - ], "ay8sO5": [ { "type": 0, @@ -1395,6 +1401,12 @@ "value": "Back to form start" } ], + "eAmrdi": [ + { + "type": 0, + "value": "Loading..." + } + ], "eO6Ysb": [ { "type": 0, diff --git a/src/i18n/compiled/nl.json b/src/i18n/compiled/nl.json index 3140a37ca..9588f3c5c 100644 --- a/src/i18n/compiled/nl.json +++ b/src/i18n/compiled/nl.json @@ -231,6 +231,26 @@ "value": "." } ], + "7v8PR6": [ + { + "type": 0, + "value": "Your session has expired. " + }, + { + "children": [ + { + "type": 0, + "value": "Click here to restart" + } + ], + "type": 8, + "value": "link" + }, + { + "type": 0, + "value": "." + } + ], "8DHd4/": [ { "type": 0, @@ -881,6 +901,12 @@ "value": "isApplicable" } ], + "LSB7FE": [ + { + "type": 0, + "value": "Heb je een e-mail ontvangen om mede te ondertekenen? Start het mede-ondertekenen door in te loggen." + } + ], "LiRh7j": [ { "type": 0, @@ -1331,26 +1357,6 @@ "value": "Nee" } ], - "abtbgK": [ - { - "type": 0, - "value": "Je sessie is verlopen. " - }, - { - "children": [ - { - "type": 0, - "value": "Klik hier om opnieuw te beginnen" - } - ], - "type": 8, - "value": "link" - }, - { - "type": 0, - "value": "." - } - ], "ay8sO5": [ { "type": 0, @@ -1399,6 +1405,12 @@ "value": "Terug naar begin" } ], + "eAmrdi": [ + { + "type": 0, + "value": "Laden..." + } + ], "eO6Ysb": [ { "type": 0, diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 589574630..d3349f361 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -124,6 +124,11 @@ "description": "ZOD 'invalid_enum_value' error message", "originalDefault": "Invalid enum value. Expected {expected}, received {received}." }, + "7v8PR6": { + "defaultMessage": "Your session has expired. Click here to restart.", + "description": "Session expired error message", + "originalDefault": "Your session has expired. Click here to restart." + }, "8DHd4/": { "defaultMessage": "Array must contain {exact, select, true {exactly} other {{inclusive, select, true {at most} other {less than}}} } {maximum, plural, one {{maximum} item} other {{maximum} items}}.", "description": "ZOD 'too_big' error message, for arrays", @@ -429,6 +434,11 @@ "description": "Step label in progress indicator", "originalDefault": "{isApplicable, select, false {{label} (n/a)} other {{label}} }" }, + "LSB7FE": { + "defaultMessage": "Did you receive an email with a request to cosign? Start the cosigning by logging in.", + "description": "Cosign start explanation message", + "originalDefault": "Did you receive an email with a request to cosign? Start the cosigning by logging in." + }, "LiRh7j": { "defaultMessage": "Confirmation: {reference}", "description": "Confirmation page title", @@ -639,11 +649,6 @@ "description": "'False' display", "originalDefault": "No" }, - "abtbgK": { - "defaultMessage": "Your session has expired. Click here to restart.", - "description": "Session expired error message", - "originalDefault": "Your session has expired. Click here to restart." - }, "ay8sO5": { "defaultMessage": "Contact details", "description": "Appointments: contact details step title", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 7f37c38b7..b0f07e185 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -126,6 +126,11 @@ "description": "ZOD 'invalid_enum_value' error message", "originalDefault": "Invalid enum value. Expected {expected}, received {received}." }, + "7v8PR6": { + "defaultMessage": "Your session has expired. Click here to restart.", + "description": "Session expired error message", + "originalDefault": "Your session has expired. Click here to restart." + }, "8DHd4/": { "defaultMessage": "Er mogen {exact, select, true {precies} other {{inclusive, select, true {maximaal} other {minder dan}}} } {maximum, plural, one {{maximum} element} other {{maximum} elementen}} in de lijst zijn.", "description": "ZOD 'too_big' error message, for arrays", @@ -434,6 +439,11 @@ "description": "Step label in progress indicator", "originalDefault": "{isApplicable, select, false {{label} (n/a)} other {{label}} }" }, + "LSB7FE": { + "defaultMessage": "Heb je een e-mail ontvangen om mede te ondertekenen? Start het mede-ondertekenen door in te loggen.", + "description": "Cosign start explanation message", + "originalDefault": "Did you receive an email with a request to cosign? Start the cosigning by logging in." + }, "LiRh7j": { "defaultMessage": "Bevestiging: {reference}", "description": "Confirmation page title", @@ -648,11 +658,6 @@ "description": "'False' display", "originalDefault": "No" }, - "abtbgK": { - "defaultMessage": "Je sessie is verlopen. Klik hier om opnieuw te beginnen.", - "description": "Session expired error message", - "originalDefault": "Your session has expired. Click here to restart." - }, "ay8sO5": { "defaultMessage": "Je gegevens", "description": "Appointments: contact details step title", From 80b93a20ab200815926037d629e7877271be2d34 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 11 Nov 2024 17:08:37 +0100 Subject: [PATCH 5/8] :truck: [open-formulieren/open-forms#4320] Move cosign stories to shared location --- src/components/CoSign/CoSign.stories.jsx | 4 ++-- src/components/CoSign/CosignStart.stories.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/CoSign/CoSign.stories.jsx b/src/components/CoSign/CoSign.stories.jsx index da5afda48..93942c86e 100644 --- a/src/components/CoSign/CoSign.stories.jsx +++ b/src/components/CoSign/CoSign.stories.jsx @@ -1,7 +1,7 @@ import CosignDone from './CosignDone'; export default { - title: 'Views / Co-sign done', + title: 'Views / Cosign / Done', component: CosignDone, args: { reportDownloadUrl: '#', @@ -9,5 +9,5 @@ export default { }; export const CoSignDone = { - name: 'Co-sign done', + name: 'CosignDone', }; diff --git a/src/components/CoSign/CosignStart.stories.js b/src/components/CoSign/CosignStart.stories.js index bd95c7a55..fa481b98f 100644 --- a/src/components/CoSign/CosignStart.stories.js +++ b/src/components/CoSign/CosignStart.stories.js @@ -6,7 +6,7 @@ import {withForm} from 'story-utils/decorators'; import CosignStart from './CosignStart'; export default { - title: 'Private API / Cosign / Start', + title: 'Views / Cosign / Start', component: CosignStart, decorators: [withForm, withRouter], parameters: { From 1c7faeb85f9d57b7d41cb9c73de05351355f4582 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 12 Nov 2024 18:27:34 +0100 Subject: [PATCH 6/8] :sparkles: [open-formulieren/open-forms#4320] Make it possible to let the backend drive the content of the confirmation page more Added example with simulated backend data for the cosign confirmation page. --- .../PostCompletionViews/ConfirmationView.jsx | 21 ++++--- .../ConfirmationViewCosign.stories.js | 62 +++++++++++++++++++ .../PostCompletionViews/StatusUrlPoller.jsx | 11 +++- src/story-utils/decorators.jsx | 1 + 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 src/components/PostCompletionViews/ConfirmationViewCosign.stories.js diff --git a/src/components/PostCompletionViews/ConfirmationView.jsx b/src/components/PostCompletionViews/ConfirmationView.jsx index 4b73d251c..fcf20653c 100644 --- a/src/components/PostCompletionViews/ConfirmationView.jsx +++ b/src/components/PostCompletionViews/ConfirmationView.jsx @@ -44,8 +44,13 @@ const ConfirmationViewDisplay = ({downloadPDFText}) => { const paymentStatus = location?.state?.status; const userAction = location?.state?.userAction; - const {publicReference, reportDownloadUrl, confirmationPageContent, mainWebsiteUrl} = - useContext(SubmissionStatusContext); + const { + publicReference, + reportDownloadUrl, + confirmationPageTitle, + confirmationPageContent, + mainWebsiteUrl, + } = useContext(SubmissionStatusContext); const paymentStatusMessage = STATUS_MESSAGES[paymentStatus]; let Wrapper = React.Fragment; @@ -80,11 +85,13 @@ const ConfirmationViewDisplay = ({downloadPDFText}) => { defaultMessage: 'Confirmation', })} header={ - + confirmationPageTitle || ( + + ) } body={body} mainWebsiteUrl={mainWebsiteUrl} diff --git a/src/components/PostCompletionViews/ConfirmationViewCosign.stories.js b/src/components/PostCompletionViews/ConfirmationViewCosign.stories.js new file mode 100644 index 000000000..30bbbd67c --- /dev/null +++ b/src/components/PostCompletionViews/ConfirmationViewCosign.stories.js @@ -0,0 +1,62 @@ +import {withRouter} from 'storybook-addon-remix-react-router'; + +import {buildForm} from 'api-mocks'; +import {AnalyticsToolsDecorator, withForm, withSubmissionPollInfo} from 'story-utils/decorators'; + +import {ConfirmationViewDisplay} from './ConfirmationView'; + +export default { + title: 'Views / Cosign / Submission completed', + component: ConfirmationViewDisplay, + decorators: [withForm, AnalyticsToolsDecorator, withSubmissionPollInfo, withRouter], + args: { + publicReference: 'OF-1234', + reportDownloadUrl: '/dummy', + confirmationPageTitle: 'Request incomplete', + confirmationPageContent: ` +

Your request is not yet complete.

+

Cosigning required

+

+ We've sent an email with a cosign request to + info@example.com. Once the submission has + been cosigned we will start processing your request. +

+

+ If you need to contact us about this submission, you can use the reference + OF-1234. +

+ `, + mainWebsiteUrl: 'https://example.com', + paymentUrl: '', + }, + argTypes: { + paymentUrl: {control: false}, + }, + parameters: { + formContext: { + form: buildForm({ + cosignLoginOptions: [ + { + identifier: 'digid', + label: 'DigiD', + url: 'http://localhost:8000/auth/digid/?next=http://localhost:8000/cosign&code=123', + logo: { + title: 'DigiD simulatie', + imageSrc: './digid.png', + href: 'https://www.digid.nl/', + appearance: 'dark', + }, + isForGemachtigde: false, + }, + ], + }), + }, + reactRouter: { + location: {state: {}}, + }, + }, +}; + +export const Default = { + name: 'Submission completed', +}; diff --git a/src/components/PostCompletionViews/StatusUrlPoller.jsx b/src/components/PostCompletionViews/StatusUrlPoller.jsx index 559e75f85..c1cd3652d 100644 --- a/src/components/PostCompletionViews/StatusUrlPoller.jsx +++ b/src/components/PostCompletionViews/StatusUrlPoller.jsx @@ -11,7 +11,14 @@ import usePoll from 'hooks/usePoll'; const RESULT_FAILED = 'failed'; const RESULT_SUCCESS = 'success'; -const SubmissionStatusContext = React.createContext(); +const SubmissionStatusContext = React.createContext({ + publicReference: '', + paymentUrl: '', + reportDownloadUrl: '', + confirmationPageTitle: '', + confirmationPageContent: '', + mainWebsiteUrl: '', +}); SubmissionStatusContext.displayName = 'SubmissionStatusContext'; const StatusUrlPoller = ({statusUrl, onFailure, onConfirmed, children}) => { @@ -74,6 +81,7 @@ const StatusUrlPoller = ({statusUrl, onFailure, onConfirmed, children}) => { paymentUrl, publicReference, reportDownloadUrl, + confirmationPageTitle, confirmationPageContent, mainWebsiteUrl, } = statusResponse; @@ -88,6 +96,7 @@ const StatusUrlPoller = ({statusUrl, onFailure, onConfirmed, children}) => { publicReference, paymentUrl, reportDownloadUrl, + confirmationPageTitle, confirmationPageContent, mainWebsiteUrl, }} diff --git a/src/story-utils/decorators.jsx b/src/story-utils/decorators.jsx index 8a78481d9..418692e59 100644 --- a/src/story-utils/decorators.jsx +++ b/src/story-utils/decorators.jsx @@ -158,6 +158,7 @@ export const withSubmissionPollInfo = (Story, {parameters, args}) => { publicReference: args.publicReference, paymentUrl: args.paymentUrl, reportDownloadUrl: args.reportDownloadUrl, + confirmationPageTitle: args.confirmationPageTitle, confirmationPageContent: args.confirmationPageContent, mainWebsiteUrl: args.mainWebsiteUrl, }} From ac85cfe6cefe8ece649102285c4c3e7a94eb6fa9 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 15 Nov 2024 12:25:50 +0100 Subject: [PATCH 7/8] :technologist: Fix button stories/documentation page The was deprecated earlier, which caused the buttons not to be properly displayed any longer. --- src/components/Button/OFButton.mdx | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/components/Button/OFButton.mdx b/src/components/Button/OFButton.mdx index 68e09c49d..6bfc521f1 100644 --- a/src/components/Button/OFButton.mdx +++ b/src/components/Button/OFButton.mdx @@ -16,33 +16,25 @@ aria attribute instead. More information on the Utrecht buttons can be found [here](https://nl-design-system.github.io/themes/?path=/docs/button--gemeente-utrecht). - - - - - - + + + + ### Links that look like buttons - - - - - + + + ### Button that looks like a link - - - + ## Icon buttons - - - - + + ## Disabled state From c0098efa02b4e1adb4fb9e433944f78d4e261828 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 15 Nov 2024 12:47:22 +0100 Subject: [PATCH 8/8] :children_crossing: [#4320] Update stories and UX of completion view The backend will gain the ability to include the cosign link/button, which will be rendered as a primary button. To point out the right call to actions, the 'return to website' link has been made a secondary button/action. Stories have been updated to reflect the default templates in the backend. --- .../ConfirmationViewCosign.stories.js | 11 +++++++++++ .../PostCompletionViews/PostCompletionView.jsx | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/PostCompletionViews/ConfirmationViewCosign.stories.js b/src/components/PostCompletionViews/ConfirmationViewCosign.stories.js index 30bbbd67c..6e3f7e3be 100644 --- a/src/components/PostCompletionViews/ConfirmationViewCosign.stories.js +++ b/src/components/PostCompletionViews/ConfirmationViewCosign.stories.js @@ -16,6 +16,17 @@ export default { confirmationPageContent: `

Your request is not yet complete.

Cosigning required

+

+ You can start the cosigning immediately by clicking the button below. +

+

+ + + +

+

Alternative instructions

We've sent an email with a cosign request to info@example.com. Once the submission has diff --git a/src/components/PostCompletionViews/PostCompletionView.jsx b/src/components/PostCompletionViews/PostCompletionView.jsx index a3a9de15e..648a08ab9 100644 --- a/src/components/PostCompletionViews/PostCompletionView.jsx +++ b/src/components/PostCompletionViews/PostCompletionView.jsx @@ -42,7 +42,7 @@ const PostCompletionView = ({ - +