diff --git a/src/Context.js b/src/Context.js
index 9bd99b470..165d19a77 100644
--- a/src/Context.js
+++ b/src/Context.js
@@ -23,6 +23,12 @@ const FormContext = React.createContext({
});
FormContext.displayName = 'FormContext';
+const AnalyticsToolsConfigContext = React.createContext({
+ govmetricSourceId: '',
+ govmetricSecureGuid: '',
+ enableGovmetricAnalytics: false,
+});
+
const ConfigContext = React.createContext({
baseUrl: '',
clientBaseUrl: window.location.href,
@@ -43,4 +49,10 @@ FormioTranslations.displayName = 'FormioTranslations';
const SubmissionContext = React.createContext({submission: null});
SubmissionContext.displayName = 'SubmissionContext';
-export {FormContext, ConfigContext, FormioTranslations, SubmissionContext};
+export {
+ FormContext,
+ ConfigContext,
+ FormioTranslations,
+ SubmissionContext,
+ AnalyticsToolsConfigContext,
+};
diff --git a/src/api-mocks/analytics.js b/src/api-mocks/analytics.js
new file mode 100644
index 000000000..560f78356
--- /dev/null
+++ b/src/api-mocks/analytics.js
@@ -0,0 +1,15 @@
+import {rest} from 'msw';
+
+import {BASE_URL} from './base';
+
+export const mockAnalyticsToolConfigGet = (overrides = {}) =>
+ rest.get(`${BASE_URL}analytics/analytics_tools_config_info`, (req, res, ctx) =>
+ res(
+ ctx.json({
+ govmetricSourceId: '',
+ govmetricSecureGuid: '',
+ enableGovmetricAnalytics: false,
+ ...overrides,
+ })
+ )
+ );
diff --git a/src/api-mocks/index.js b/src/api-mocks/index.js
index 667b865a1..de04d3314 100644
--- a/src/api-mocks/index.js
+++ b/src/api-mocks/index.js
@@ -1,3 +1,4 @@
export {BASE_URL} from './base';
export {buildForm, mockFormGet} from './forms';
export {buildSubmission} from './submissions';
+export {mockAnalyticsToolConfigGet} from './analytics';
diff --git a/src/components/AbortionButton/AbortionButton.js b/src/components/AbortionButton/AbortionButton.js
new file mode 100644
index 000000000..106d15206
--- /dev/null
+++ b/src/components/AbortionButton/AbortionButton.js
@@ -0,0 +1,66 @@
+import PropTypes from 'prop-types';
+import React, {useContext} from 'react';
+import {FormattedMessage, useIntl} from 'react-intl';
+
+import {AnalyticsToolsConfigContext} from 'Context';
+import {OFButton} from 'components/Button';
+import {buildGovMetricUrl} from 'components/analytics/utils';
+import useFormContext from 'hooks/useFormContext';
+
+const AbortionButton = ({isAuthenticated, onDestroySession}) => {
+ const intl = useIntl();
+ const analyticsToolsConfig = useContext(AnalyticsToolsConfigContext);
+ const form = useFormContext();
+
+ const confirmationMessage = isAuthenticated
+ ? intl.formatMessage({
+ description: 'log out confirmation prompt',
+ defaultMessage: 'Are you sure that you want to logout?',
+ })
+ : intl.formatMessage({
+ description: 'Abort confirmation prompt',
+ defaultMessage:
+ 'Are you sure that you want to abort this submission? You will lose your progress if you continue.',
+ });
+
+ const callback = async event => {
+ event.preventDefault();
+
+ if (!window.confirm(confirmationMessage)) return;
+
+ await onDestroySession();
+
+ if (analyticsToolsConfig.enableGovmetricAnalytics) {
+ const govmetricUrl = buildGovMetricUrl(
+ analyticsToolsConfig.govmetricSourceId,
+ form.slug,
+ analyticsToolsConfig.govmetricSecureGuid
+ );
+
+ window.open(govmetricUrl);
+ }
+ };
+
+ const message = isAuthenticated ? (
+
+ ) : (
+
+ );
+
+ if (!isAuthenticated && !analyticsToolsConfig.enableGovmetricAnalytics) return null;
+
+ return (
+
+ {message}
+
+ );
+};
+
+AbortionButton.propTypes = {
+ isAuthenticated: PropTypes.bool.isRequired,
+ onDestroySession: PropTypes.func.isRequired,
+};
+export default AbortionButton;
diff --git a/src/components/AbortionButton/AbortionButton.mdx b/src/components/AbortionButton/AbortionButton.mdx
new file mode 100644
index 000000000..60dab2cc9
--- /dev/null
+++ b/src/components/AbortionButton/AbortionButton.mdx
@@ -0,0 +1,35 @@
+import {ArgTypes, Canvas, Meta, Story} from '@storybook/blocks';
+
+import * as AbortionButtonStories from './AbortionButton.stories';
+
+
+
+# Abortion Button
+
+The abortion button can be used to abort a submission whether the user is authenticated or not.
+
+- If the user is authenticated, then the button will have the text 'Log out' to comply with Logius
+ directives that whenever a user is logged in with DigiD a 'Log out' option must be available. When
+ GovMetric is enabled, a new window will open where the user can give feedback.
+- If the analytics tool 'GovMetric' is enabled, aborting the submission will open a new window where
+ the user can give feedback about why they decided to not fill the form until the end.
+
+
+
+
+
+
+
+
+
+## Props
+
+
diff --git a/src/components/AbortionButton/AbortionButton.stories.js b/src/components/AbortionButton/AbortionButton.stories.js
new file mode 100644
index 000000000..6681a6ebe
--- /dev/null
+++ b/src/components/AbortionButton/AbortionButton.stories.js
@@ -0,0 +1,74 @@
+import {expect} from '@storybook/jest';
+import {within} from '@storybook/testing-library';
+
+import {AnalyticsToolsDecorator} from 'story-utils/decorators';
+
+import AbortionButton from './AbortionButton';
+
+export default {
+ title: 'Private API / Abortion button',
+ component: AbortionButton,
+ decorators: [AnalyticsToolsDecorator],
+};
+
+export const Authenticated = {
+ args: {
+ isAuthenticated: true,
+ },
+ play: async ({canvasElement}) => {
+ const canvas = within(canvasElement);
+
+ const abortButton = await canvas.findByRole('button', {name: 'Uitloggen'});
+ await expect(abortButton).toBeVisible();
+ },
+};
+
+export const GovMetricEnabled = {
+ args: {
+ isAuthenticated: false,
+ },
+ parameters: {
+ analyticsToolsParams: {
+ govmetricSourceId: '1234',
+ govmetricSecureGuid: '',
+ enableGovmetricAnalytics: true,
+ },
+ },
+ play: async ({canvasElement}) => {
+ const canvas = within(canvasElement);
+
+ const abortButton = await canvas.findByRole('button', {name: 'Abort submission'});
+ await expect(abortButton).toBeVisible();
+ },
+};
+
+export const AuthenticatedAndGovmetric = {
+ args: {
+ isAuthenticated: true,
+ },
+ parameters: {
+ analyticsToolsParams: {
+ govmetricSourceId: '1234',
+ govmetricSecureGuid: '',
+ enableGovmetricAnalytics: true,
+ },
+ },
+ play: async ({canvasElement}) => {
+ const canvas = within(canvasElement);
+
+ const abortButton = await canvas.findByRole('button', {name: 'Uitloggen'});
+ await expect(abortButton).toBeVisible();
+ },
+};
+
+export const NotAuthenticatedNoGovMetric = {
+ args: {
+ isAuthenticated: false,
+ },
+ play: async ({canvasElement}) => {
+ const canvas = within(canvasElement);
+
+ const abortButton = await canvas.queryByRole('button');
+ await expect(abortButton).toBeNull();
+ },
+};
diff --git a/src/components/AbortionButton/index.js b/src/components/AbortionButton/index.js
new file mode 100644
index 000000000..d9d353e4e
--- /dev/null
+++ b/src/components/AbortionButton/index.js
@@ -0,0 +1,3 @@
+import AbortionButton from './AbortionButton';
+
+export default AbortionButton;
diff --git a/src/components/App.stories.js b/src/components/App.stories.js
index 3c50e8e9f..e910aa6e8 100644
--- a/src/components/App.stories.js
+++ b/src/components/App.stories.js
@@ -3,7 +3,7 @@ import {userEvent, waitForElementToBeRemoved, within} from '@storybook/testing-l
import {RouterProvider, createMemoryRouter} from 'react-router-dom';
import {FormContext} from 'Context';
-import {BASE_URL} from 'api-mocks';
+import {BASE_URL, mockAnalyticsToolConfigGet} from 'api-mocks';
import {buildForm} from 'api-mocks';
import {
mockSubmissionCheckLogicPost,
@@ -79,6 +79,7 @@ export default {
{code: 'en', name: 'English'},
]),
mockLanguageChoicePut,
+ mockAnalyticsToolConfigGet(),
],
},
},
@@ -189,6 +190,7 @@ export const ActiveSubmission = {
{code: 'en', name: 'English'},
]),
mockLanguageChoicePut,
+ mockAnalyticsToolConfigGet(),
],
},
},
diff --git a/src/components/ButtonsToolbar/index.js b/src/components/ButtonsToolbar/index.js
index 8caf0f08d..8f682a5fa 100644
--- a/src/components/ButtonsToolbar/index.js
+++ b/src/components/ButtonsToolbar/index.js
@@ -5,7 +5,6 @@ import {OFButton} from 'components/Button';
import Link from 'components/Link';
import {Literal, LiteralsProvider} from 'components/Literal';
import Loader from 'components/Loader';
-import LogoutButton from 'components/LogoutButton';
import {Toolbar, ToolbarList} from 'components/Toolbar';
import {SUBMISSION_ALLOWED} from 'components/constants';
@@ -14,12 +13,10 @@ const ButtonsToolbar = ({
canSubmitStep,
canSubmitForm,
canSuspendForm,
- isAuthenticated,
isLastStep,
isCheckingLogic,
onNavigatePrevPage,
onFormSave,
- onLogout,
previousPage,
}) => {
const showSubmitButton = !(canSubmitForm === SUBMISSION_ALLOWED.noWithoutOverview && isLastStep);
@@ -65,7 +62,6 @@ const ButtonsToolbar = ({
- {isAuthenticated ? : null}
>
);
};
@@ -76,11 +72,9 @@ ButtonsToolbar.propTypes = {
canSubmitForm: PropTypes.string.isRequired,
canSuspendForm: PropTypes.bool.isRequired,
isLastStep: PropTypes.bool.isRequired,
- isAuthenticated: PropTypes.bool.isRequired,
isCheckingLogic: PropTypes.bool.isRequired,
onNavigatePrevPage: PropTypes.func,
onFormSave: PropTypes.func.isRequired,
- onLogout: PropTypes.func.isRequired,
previousPage: PropTypes.string,
};
diff --git a/src/components/ExistingSubmissionOptions.js b/src/components/ExistingSubmissionOptions.js
index 5cb11daab..ea0eeb35c 100644
--- a/src/components/ExistingSubmissionOptions.js
+++ b/src/components/ExistingSubmissionOptions.js
@@ -1,16 +1,30 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {FormattedMessage} from 'react-intl';
+import {FormattedMessage, useIntl} from 'react-intl';
import {useNavigate} from 'react-router-dom';
import {OFButton} from 'components/Button';
import {Toolbar, ToolbarList} from 'components/Toolbar';
import Types from 'types';
-const ExistingSubmissionOptions = ({form, onFormAbort}) => {
+const ExistingSubmissionOptions = ({form, onDestroySession}) => {
const navigate = useNavigate();
+ const intl = useIntl();
const firstStepRoute = `/stap/${form.steps[0].slug}`;
+ const confirmationMessage = intl.formatMessage({
+ description: 'Abort confirmation prompt',
+ defaultMessage:
+ 'Are you sure that you want to abort this submission? You will lose your progress if you continue.',
+ });
+
+ const onFormAbort = async event => {
+ event.preventDefault();
+
+ if (!window.confirm(confirmationMessage)) return;
+
+ await onDestroySession();
+ };
return (
@@ -36,7 +50,7 @@ const ExistingSubmissionOptions = ({form, onFormAbort}) => {
ExistingSubmissionOptions.propTypes = {
form: Types.Form.isRequired,
- onFormAbort: PropTypes.func.isRequired,
+ onDestroySession: PropTypes.func.isRequired,
};
export default ExistingSubmissionOptions;
diff --git a/src/components/Form.js b/src/components/Form.js
index 63211ae6d..b015e02cd 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -2,10 +2,11 @@ import React, {useContext, useEffect} from 'react';
import {useIntl} from 'react-intl';
import {Navigate, Route, Routes, useLocation, useMatch, useNavigate} from 'react-router-dom';
import {usePrevious} from 'react-use';
+import {useAsync} from 'react-use';
import {useImmerReducer} from 'use-immer';
-import {ConfigContext} from 'Context';
-import {destroy} from 'api';
+import {AnalyticsToolsConfigContext, ConfigContext} from 'Context';
+import {destroy, get} from 'api';
import ErrorBoundary from 'components/Errors/ErrorBoundary';
import FormStart from 'components/FormStart';
import FormStep from 'components/FormStep';
@@ -140,6 +141,10 @@ const Form = () => {
flagNoActiveSubmission();
});
+ const {value: analyticsToolsConfigInfo, loading: loadingAnalyticsConfig} = useAsync(async () => {
+ return await get(`${config.baseUrl}analytics/analytics_tools_config_info`);
+ }, [intl.locale]);
+
useEffect(
() => {
if (prevLocale === undefined) return;
@@ -210,40 +215,15 @@ const Form = () => {
}
};
- const destroySession = async confirmationMessage => {
- if (!window.confirm(confirmationMessage)) {
- return;
- }
-
+ const onDestroySession = async () => {
await destroy(`${config.baseUrl}authentication/${state.submission.id}/session`);
removeSubmissionId();
- navigate('/');
- // TODO: replace with a proper reset of the state instead of a page reload.
- window.location.reload();
- };
-
- const onFormAbort = async event => {
- event.preventDefault();
-
- const confirmationQuestion = intl.formatMessage({
- description: 'Abort confirmation prompt',
- defaultMessage:
- 'Are you sure that you want to abort this submission? You will lose your progress if you continue.',
- });
-
- await destroySession(confirmationQuestion);
- };
-
- const onLogout = async event => {
- event.preventDefault();
-
- const confirmationQuestion = intl.formatMessage({
- description: 'log out confirmation prompt',
- defaultMessage: 'Are you sure that you want to logout?',
+ dispatch({
+ type: 'RESET',
+ payload: initialStateFromProps,
});
-
- await destroySession(confirmationQuestion);
+ navigate('/');
};
const onProcessingFailure = errorMessage => {
@@ -269,7 +249,7 @@ const Form = () => {
);
}
- if (loading || shouldAutomaticallyRedirect) {
+ if (loading || loadingAnalyticsConfig || shouldAutomaticallyRedirect) {
return ;
}
@@ -358,7 +338,7 @@ const Form = () => {
form={form}
hasActiveSubmission={!!state.submission}
onFormStart={onFormStart}
- onFormAbort={onFormAbort}
+ onDestroySession={onDestroySession}
/>
}
@@ -374,9 +354,9 @@ const Form = () => {
form={form}
processingError={state.processingError}
onConfirm={onSubmitForm}
- onLogout={onLogout}
component={SubmissionSummary}
onClearProcessingErrors={() => dispatch({type: 'CLEAR_PROCESSING_ERROR'})}
+ onDestroySession={onDestroySession}
/>
@@ -425,13 +405,13 @@ const Form = () => {
dispatch({type: 'SUBMISSION_LOADED', payload: submission})
}
onStepSubmitted={onStepSubmitted}
- onLogout={onLogout}
onSessionDestroyed={() => {
resetSession();
navigate('/');
dispatch({type: 'RESET', payload: initialStateFromProps});
}}
component={FormStep}
+ onDestroySession={onDestroySession}
/>
@@ -441,7 +421,13 @@ const Form = () => {
);
// render the form step if there's an active submission (and no summary)
- return {router};
+ return (
+
+
+ {router}
+
+
+ );
};
Form.propTypes = {};
diff --git a/src/components/FormStart/index.js b/src/components/FormStart/index.js
index 69fb24dc8..6686426ca 100644
--- a/src/components/FormStart/index.js
+++ b/src/components/FormStart/index.js
@@ -40,7 +40,7 @@ const FormStartMessage = ({form}) => {
* This is shown when the form is initially loaded and provides the explicit user
* action to start the form, or present the login button (DigiD, eHerkenning...)
*/
-const FormStart = ({form, hasActiveSubmission, onFormStart, onFormAbort}) => {
+const FormStart = ({form, hasActiveSubmission, onFormStart, onDestroySession}) => {
const doStart = useStartSubmission();
const outagePluginId = useDetectAuthenticationOutage();
const authErrors = useDetectAuthErrorMessages();
@@ -126,7 +126,7 @@ const FormStart = ({form, hasActiveSubmission, onFormStart, onFormAbort}) => {
{hasActiveSubmission ? (
-
+
) : (
)}
@@ -139,7 +139,7 @@ FormStart.propTypes = {
form: Types.Form.isRequired,
hasActiveSubmission: PropTypes.bool.isRequired,
onFormStart: PropTypes.func.isRequired,
- onFormAbort: PropTypes.func.isRequired,
+ onDestroySession: PropTypes.func.isRequired,
};
export default FormStart;
diff --git a/src/components/FormStart/tests.spec.js b/src/components/FormStart/tests.spec.js
index 941aa3903..dac3e20f8 100644
--- a/src/components/FormStart/tests.spec.js
+++ b/src/components/FormStart/tests.spec.js
@@ -45,7 +45,7 @@ it('Form start page start if _start parameter is present', () => {
useQuery.mockReturnValue(testLocation);
const onFormStart = jest.fn();
- const onFormAbort = jest.fn();
+ const onDestroySession = jest.fn();
act(() => {
root.render(
@@ -53,7 +53,7 @@ it('Form start page start if _start parameter is present', () => {
@@ -65,7 +65,7 @@ it('Form start page start if _start parameter is present', () => {
it('Form start does not start if there are auth errors', () => {
const onFormStart = jest.fn();
- const onFormAbort = jest.fn();
+ const onDestroySession = jest.fn();
const testQueries = {
'_digid-message=error':
@@ -86,7 +86,7 @@ it('Form start does not start if there are auth errors', () => {
@@ -101,14 +101,14 @@ it('Form start does not start if there are auth errors', () => {
it('Form start page does not show login buttons if an active submission is present', () => {
useQuery.mockReturnValue(new URLSearchParams());
const onFormStart = jest.fn();
- const onFormAbort = jest.fn();
+ const onDestroySession = jest.fn();
renderTest(
,
diff --git a/src/components/FormStep/FormStep.stories.js b/src/components/FormStep/FormStep.stories.js
index 4a98f3174..63c9aeabc 100644
--- a/src/components/FormStep/FormStep.stories.js
+++ b/src/components/FormStep/FormStep.stories.js
@@ -1,10 +1,12 @@
+import {expect} from '@storybook/jest';
+import {within} from '@storybook/testing-library';
import produce from 'immer';
import {getWorker} from 'msw-storybook-addon';
import {withRouter} from 'storybook-addon-react-router-v6';
import {v4 as uuid4} from 'uuid';
import {buildForm, buildSubmission} from 'api-mocks';
-import {ConfigDecorator} from 'story-utils/decorators';
+import {AnalyticsToolsDecorator, ConfigDecorator} from 'story-utils/decorators';
import FormStep from '.';
import {
@@ -16,7 +18,7 @@ import {
export default {
title: 'Private API / FormStep',
component: FormStep,
- decorators: [ConfigDecorator, withRouter],
+ decorators: [ConfigDecorator, withRouter, AnalyticsToolsDecorator],
argTypes: {
submission: {control: false},
form: {control: false},
@@ -41,8 +43,8 @@ const render = ({
submission,
onLogicChecked,
onStepSubmitted,
- onLogout,
onSessionDestroyed,
+ onDestroySession,
// story args
formioConfiguration,
}) => {
@@ -66,8 +68,8 @@ const render = ({
submission={submission}
onLogicChecked={onLogicChecked}
onStepSubmitted={onStepSubmitted}
- onLogout={onLogout}
onSessionDestroyed={onSessionDestroyed}
+ onDestroySession={onDestroySession}
/>
);
};
@@ -128,3 +130,45 @@ export const SuspensionDisallowed = {
submission: buildSubmission(),
},
};
+
+export const govmetricEnabled = {
+ name: 'GovMetric enabled',
+ render,
+ args: {
+ formioConfiguration: {
+ display: 'form',
+ components: [
+ {
+ type: 'textfield',
+ key: 'text1',
+ label: 'Simple text field',
+ description: 'A help text for the text field',
+ },
+ {
+ type: 'radio',
+ key: 'radio1',
+ label: 'Radio choices',
+ values: [
+ {value: 'option1', label: 'Option1'},
+ {value: 'option2', label: 'Option2'},
+ ],
+ },
+ ],
+ },
+ form: buildForm(),
+ submission: buildSubmission(),
+ },
+ parameters: {
+ analyticsToolsParams: {
+ govmetricSourceId: '1234',
+ govmetricSecureGuid: '',
+ enableGovmetricAnalytics: true,
+ },
+ },
+ play: async ({canvasElement}) => {
+ const canvas = within(canvasElement);
+
+ const abortButton = await canvas.findByRole('button', {name: 'Abort submission'});
+ await expect(abortButton).toBeVisible();
+ },
+};
diff --git a/src/components/FormStep/index.js b/src/components/FormStep/index.js
index 07c0c17cb..8ebbe5f7f 100644
--- a/src/components/FormStep/index.js
+++ b/src/components/FormStep/index.js
@@ -33,12 +33,14 @@ import {useNavigate, useParams} from 'react-router-dom';
import {useAsync} from 'react-use';
import {useImmerReducer} from 'use-immer';
-import {ConfigContext, FormioTranslations} from 'Context';
+import {AnalyticsToolsConfigContext, ConfigContext, FormioTranslations} from 'Context';
import {get, post, put} from 'api';
+import AbortionButton from 'components/AbortionButton';
import ButtonsToolbar from 'components/ButtonsToolbar';
import Card, {CardTitle} from 'components/Card';
import FormStepDebug from 'components/FormStepDebug';
import Loader from 'components/Loader';
+import {Toolbar, ToolbarList} from 'components/Toolbar';
import FormStepSaveModal from 'components/modals/FormStepSaveModal';
import {
eventTriggeredBySubmitButton,
@@ -47,11 +49,10 @@ import {
} from 'components/utils';
import {ValidationError} from 'errors';
import {PREFIX} from 'formio/constants';
+import hooks from 'formio/hooks';
import useTitle from 'hooks/useTitle';
import Types from 'types';
-import hooks from '../../formio/hooks';
-
/**
* Debounce interval in milliseconds (1000ms equals 1s) to prevent excessive amount of logic checks.
* @type {number}
@@ -287,7 +288,7 @@ const reducer = (draft, action) => {
* @param {Object} submission
* @param {Function} onLogicChecked
* @param {Function} onStepSubmitted
- * @param {Function} onLogout
+ * @param {Function} onDestroySession
* @param {Function} onSessionDestroyed
* @throws {Error} Throws errors from state so the error boundaries can pick them up.
* @return {React.ReactNode}
@@ -297,12 +298,13 @@ const FormStep = ({
submission,
onLogicChecked,
onStepSubmitted,
- onLogout,
onSessionDestroyed,
+ onDestroySession,
}) => {
const intl = useIntl();
const config = useContext(ConfigContext);
const formioTranslations = useContext(FormioTranslations);
+ const analyticsToolsConfig = useContext(AnalyticsToolsConfigContext);
/* component state */
const formRef = useRef(null);
@@ -818,6 +820,9 @@ const FormStep = ({
dispatch({type: 'FORMIO_CHANGE_HANDLED'});
};
+ const showExtraToolbar =
+ submission.isAuthenticated || analyticsToolsConfig.enableGovmetricAnalytics;
+
const isLoadingSomething = loading || isNavigating;
return (
<>
@@ -871,15 +876,23 @@ const FormStep = ({
canSubmitStep={canSubmit}
canSubmitForm={submission.submissionAllowed}
canSuspendForm={form.suspensionAllowed}
- isAuthenticated={submission.isAuthenticated}
isLastStep={isLastStep(currentStepIndex, submission)}
isCheckingLogic={logicChecking}
loginRequired={form.loginRequired}
onFormSave={onFormSave}
- onLogout={onLogout}
onNavigatePrevPage={onPrevPage}
previousPage={getPreviousPageHref()}
/>
+ {showExtraToolbar && (
+
+
+
+
+
+ )}
>
) : null}
@@ -902,8 +915,8 @@ FormStep.propTypes = {
submission: PropTypes.object.isRequired,
onLogicChecked: PropTypes.func.isRequired,
onStepSubmitted: PropTypes.func.isRequired,
- onLogout: PropTypes.func.isRequired,
onSessionDestroyed: PropTypes.func.isRequired,
+ onDestroySession: PropTypes.func.isRequired,
};
export default FormStep;
diff --git a/src/components/LogoutButton.js b/src/components/LogoutButton.js
deleted file mode 100644
index 6c476fd7f..000000000
--- a/src/components/LogoutButton.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import {FormattedMessage} from 'react-intl';
-
-import {OFButton} from 'components/Button';
-import {Toolbar, ToolbarList} from 'components/Toolbar';
-
-const LogoutButton = ({onLogout}) => {
- return (
-
-
-
-
-
-
-
- );
-};
-
-LogoutButton.propTypes = {
- onLogout: PropTypes.func.isRequired,
-};
-
-export default LogoutButton;
diff --git a/src/components/PostCompletionViews/ConfirmationView.js b/src/components/PostCompletionViews/ConfirmationView.js
index 2e280bd99..87070a498 100644
--- a/src/components/PostCompletionViews/ConfirmationView.js
+++ b/src/components/PostCompletionViews/ConfirmationView.js
@@ -5,6 +5,7 @@ import {useLocation} from 'react-router-dom';
import Body from 'components/Body';
import ErrorMessage from 'components/Errors/ErrorMessage';
+import {GovMetricSnippet} from 'components/analytics';
import PostCompletionView from './PostCompletionView';
import StatusUrlPoller, {SubmissionStatusContext} from './StatusUrlPoller';
@@ -88,6 +89,7 @@ const ConfirmationViewDisplay = ({downloadPDFText}) => {
body={body}
mainWebsiteUrl={mainWebsiteUrl}
reportDownloadUrl={reportDownloadUrl}
+ extraBottom={}
/>
);
};
diff --git a/src/components/PostCompletionViews/ConfirmationView.stories.js b/src/components/PostCompletionViews/ConfirmationView.stories.js
index 57f773a2d..1b6b02263 100644
--- a/src/components/PostCompletionViews/ConfirmationView.stories.js
+++ b/src/components/PostCompletionViews/ConfirmationView.stories.js
@@ -2,14 +2,14 @@ import {expect} from '@storybook/jest';
import {within} from '@storybook/testing-library';
import {withRouter} from 'storybook-addon-react-router-v6';
-import {withSubmissionPollInfo} from 'story-utils/decorators';
+import {AnalyticsToolsDecorator, withForm, withSubmissionPollInfo} from 'story-utils/decorators';
import {ConfirmationViewDisplay} from './ConfirmationView';
export default {
title: 'Private API / Post completion views / Confirmation view',
component: ConfirmationViewDisplay,
- decorators: [withSubmissionPollInfo, withRouter],
+ decorators: [withForm, AnalyticsToolsDecorator, withSubmissionPollInfo, withRouter],
argTypes: {
paymentUrl: {control: false},
},
@@ -81,3 +81,34 @@ export const WithFailedPayment = {
expect(errorNode).toHaveClass('utrecht-alert__message');
},
};
+
+export const WithGovMetric = {
+ name: 'With GovMetric',
+ args: {
+ paymentUrl: '',
+ publicReference: 'OF-1234',
+ reportDownloadUrl: '#',
+ confirmationPageContent: 'Your answers are submitted. Hurray!',
+ mainWebsiteUrl: '#',
+ downloadPDFText: 'Download a PDF of your submitted answers',
+ form: {
+ slug: 'a-test-form',
+ },
+ },
+ parameters: {
+ analyticsToolsParams: {
+ govmetricSourceId: '1234',
+ govmetricSecureGuid: '',
+ enableGovmetricAnalytics: true,
+ },
+ },
+ play: async ({canvasElement}) => {
+ const canvas = within(canvasElement);
+
+ const feedbackLink = await canvas.findByRole('link', {name: 'Give feedback'});
+ await expect(feedbackLink).toBeVisible();
+ await expect(feedbackLink.href).toEqual(
+ 'https://websurveys2.govmetric.com/theme/kf/1234?Q_Formid=a-test-form'
+ );
+ },
+};
diff --git a/src/components/PostCompletionViews/PostCompletionView.js b/src/components/PostCompletionViews/PostCompletionView.js
index 8ae21f5bc..243c847ea 100644
--- a/src/components/PostCompletionViews/PostCompletionView.js
+++ b/src/components/PostCompletionViews/PostCompletionView.js
@@ -17,6 +17,7 @@ const PostCompletionView = ({
body,
mainWebsiteUrl,
reportDownloadUrl,
+ extraBottom,
}) => {
useTitle(pageTitle);
@@ -49,6 +50,7 @@ const PostCompletionView = ({
) : null}
+ {extraBottom}
);
};
@@ -60,6 +62,7 @@ PostCompletionView.propTypes = {
body: PropTypes.node,
mainWebsiteUrl: PropTypes.string,
reportDownloadUrl: PropTypes.string,
+ extraBottom: PropTypes.node,
};
export default PostCompletionView;
diff --git a/src/components/PostCompletionViews/index.mdx b/src/components/PostCompletionViews/index.mdx
index fa667df02..c3f17870c 100644
--- a/src/components/PostCompletionViews/index.mdx
+++ b/src/components/PostCompletionViews/index.mdx
@@ -20,6 +20,11 @@ When no payment is required, after submitting the form the user sees this view:
+In the case where GovMetric (analytics tool) is enabled, then there is an extra button to provide
+feedback about the experience of filling in the form.
+
+
+
## `ConfirmationViewDisplay` with payment required
After a successful payment, the user is redirected to this view:
diff --git a/src/components/ProgressIndicator/progressIndicator.spec.js b/src/components/ProgressIndicator/progressIndicator.spec.js
index 6ad2c7dd8..312d102e3 100644
--- a/src/components/ProgressIndicator/progressIndicator.spec.js
+++ b/src/components/ProgressIndicator/progressIndicator.spec.js
@@ -5,7 +5,7 @@ import {IntlProvider} from 'react-intl';
import {RouterProvider, createMemoryRouter} from 'react-router-dom';
import {ConfigContext, FormContext} from 'Context';
-import {BASE_URL, buildForm} from 'api-mocks';
+import {BASE_URL, buildForm, mockAnalyticsToolConfigGet} from 'api-mocks';
import {getDefaultFactory} from 'api-mocks/base';
import {FORM_DEFAULTS} from 'api-mocks/forms';
import mswServer from 'api-mocks/msw-server';
@@ -57,7 +57,7 @@ afterEach(() => {
describe('The progress indicator component', () => {
it('displays the available submission/form steps and hardcoded steps (without payment)', async () => {
- mswServer.use(mockSubmissionPost(buildSubmission()));
+ mswServer.use(mockSubmissionPost(buildSubmission()), mockAnalyticsToolConfigGet());
const user = userEvent.setup({delay: null});
const form = buildForm();
@@ -78,7 +78,7 @@ describe('The progress indicator component', () => {
});
it('displays the available submission/form steps and hardcoded steps (with payment)', async () => {
- mswServer.use(mockSubmissionPost(buildSubmission()));
+ mswServer.use(mockSubmissionPost(buildSubmission()), mockAnalyticsToolConfigGet());
const user = userEvent.setup({delay: null});
const form = buildForm({paymentRequired: true});
@@ -101,7 +101,7 @@ describe('The progress indicator component', () => {
});
it('renders steps in the correct order', async () => {
- mswServer.use(mockSubmissionPost(buildSubmission()));
+ mswServer.use(mockSubmissionPost(buildSubmission()), mockAnalyticsToolConfigGet());
const user = userEvent.setup({delay: null});
const form = buildForm();
diff --git a/src/components/Summary/GenericSummary.js b/src/components/Summary/GenericSummary.js
index 6962d51d7..aaf42b80e 100644
--- a/src/components/Summary/GenericSummary.js
+++ b/src/components/Summary/GenericSummary.js
@@ -2,13 +2,14 @@ import {Form, Formik} from 'formik';
import PropTypes from 'prop-types';
import React from 'react';
+import AbortionButton from 'components/AbortionButton';
import Card from 'components/Card';
import ErrorMessage from 'components/Errors/ErrorMessage';
import FormStepSummary from 'components/FormStepSummary';
import Loader from 'components/Loader';
-import LogoutButton from 'components/LogoutButton';
import Price from 'components/Price';
import SummaryConfirmation from 'components/SummaryConfirmation';
+import {Toolbar, ToolbarList} from 'components/Toolbar';
import {SUBMISSION_ALLOWED} from 'components/constants';
const GenericSummary = ({
@@ -16,6 +17,7 @@ const GenericSummary = ({
submissionAllowed,
summaryData = [],
showPaymentInformation,
+ showExtraToolbar,
amountToPay,
editStepText,
isLoading,
@@ -23,7 +25,7 @@ const GenericSummary = ({
prevPage,
errors = [],
onSubmit,
- onLogout,
+ onDestroySession,
onPrevPage = null,
}) => {
const Wrapper = submissionAllowed === SUBMISSION_ALLOWED.yes ? Form : 'div';
@@ -60,7 +62,16 @@ const GenericSummary = ({
prevPage={prevPage}
onPrevPage={onPrevPage}
/>
- {isAuthenticated ? : null}
+ {showExtraToolbar && (
+
+
+
+
+
+ )}
@@ -94,6 +105,7 @@ GenericSummary.propTypes = {
})
),
showPaymentInformation: PropTypes.bool,
+ showExtraToolbar: PropTypes.bool,
amountToPay: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
editStepText: PropTypes.string,
isLoading: PropTypes.bool,
@@ -101,7 +113,7 @@ GenericSummary.propTypes = {
errors: PropTypes.arrayOf(PropTypes.string),
prevPage: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
- onLogout: PropTypes.func.isRequired,
+ onDestroySession: PropTypes.func.isRequired,
onPrevPage: PropTypes.func,
};
diff --git a/src/components/Summary/SubmissionSummary.js b/src/components/Summary/SubmissionSummary.js
index 89029c3a5..afc33947e 100644
--- a/src/components/Summary/SubmissionSummary.js
+++ b/src/components/Summary/SubmissionSummary.js
@@ -1,10 +1,11 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {useContext} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useNavigate} from 'react-router-dom';
import {useAsync} from 'react-use';
import {useImmerReducer} from 'use-immer';
+import {AnalyticsToolsConfigContext} from 'Context';
import {post} from 'api';
import {LiteralsProvider} from 'components/Literal';
import {SUBMISSION_ALLOWED} from 'components/constants';
@@ -37,12 +38,13 @@ const SubmissionSummary = ({
submission,
processingError = '',
onConfirm,
- onLogout,
onClearProcessingErrors,
+ onDestroySession,
}) => {
const [state, dispatch] = useImmerReducer(reducer, initialState);
const navigate = useNavigate();
const intl = useIntl();
+ const analyticsToolsConfig = useContext(AnalyticsToolsConfigContext);
const refreshedSubmission = useRefreshSubmission(submission);
@@ -109,6 +111,9 @@ const SubmissionSummary = ({
return errors;
};
+ const showExtraToolbar =
+ refreshedSubmission.isAuthenticated || analyticsToolsConfig.enableGovmetricAnalytics;
+
return (
);
@@ -140,8 +146,8 @@ SubmissionSummary.propTypes = {
submission: Types.Submission.isRequired,
processingError: PropTypes.string,
onConfirm: PropTypes.func.isRequired,
- onLogout: PropTypes.func.isRequired,
onClearProcessingErrors: PropTypes.func.isRequired,
+ onDestroySession: PropTypes.func.isRequired,
};
export default SubmissionSummary;
diff --git a/src/components/Summary/Summary.stories.js b/src/components/Summary/Summary.stories.js
index 16e73824b..0f6db0cf2 100644
--- a/src/components/Summary/Summary.stories.js
+++ b/src/components/Summary/Summary.stories.js
@@ -95,6 +95,7 @@ export default {
},
],
showPaymentInformation: true,
+ showExtraToolbar: true,
amountToPay: 54.05,
showPreviousPageLink: true,
isLoading: false,
@@ -139,6 +140,7 @@ const render = ({
submissionAllowed,
summaryData,
showPaymentInformation,
+ showExtraToolbar,
amountToPay,
editStepText,
isLoading,
@@ -172,6 +174,7 @@ const render = ({
submissionAllowed={submissionAllowed}
summaryData={summaryData}
showPaymentInformation={showPaymentInformation}
+ showExtraToolbar={showExtraToolbar}
amountToPay={amountToPay}
editStepText={editStepText}
isLoading={isLoading}
diff --git a/src/components/Summary/test.spec.js b/src/components/Summary/test.spec.js
index a58e534fe..74740ca70 100644
--- a/src/components/Summary/test.spec.js
+++ b/src/components/Summary/test.spec.js
@@ -62,7 +62,7 @@ it('Summary displays logout button if isAuthenticated is true', () => {
...SUBMISSION,
isAuthenticated: true,
};
- const onLogout = jest.fn();
+ const onDestroySession = jest.fn();
const onConfirm = jest.fn();
useAsync.mockReturnValue({loading: false, value: []});
@@ -75,7 +75,7 @@ it('Summary displays logout button if isAuthenticated is true', () => {
form={testForm}
submission={SUBMISSION}
onConfirm={onConfirm}
- onLogout={onLogout}
+ onDestroySession={onDestroySession}
onClearProcessingErrors={() => {}}
/>
@@ -90,11 +90,11 @@ it('Summary does not display logout button if loginRequired is false', () => {
...testForm,
loginRequired: false,
};
- const onLogout = jest.fn();
+ const onDestroySession = jest.fn();
const onConfirm = jest.fn();
useAsync.mockReturnValue({loading: false, value: []});
- useRefreshSubmission.mockReturnValue(SUBMISSION);
+ useRefreshSubmission.mockReturnValue({...SUBMISSION, isAuthenticated: false});
act(() => {
root.render(
@@ -103,7 +103,7 @@ it('Summary does not display logout button if loginRequired is false', () => {
form={formLoginRequired}
submission={SUBMISSION}
onConfirm={onConfirm}
- onLogout={onLogout}
+ onDestroySession={onDestroySession}
onClearProcessingErrors={() => {}}
/>
diff --git a/src/components/analytics/GovMetricSnippet.js b/src/components/analytics/GovMetricSnippet.js
new file mode 100644
index 000000000..1b1fb8621
--- /dev/null
+++ b/src/components/analytics/GovMetricSnippet.js
@@ -0,0 +1,43 @@
+import {ButtonLink} from '@utrecht/component-library-react';
+import {useContext} from 'react';
+import {FormattedMessage, useIntl} from 'react-intl';
+
+import {AnalyticsToolsConfigContext} from 'Context';
+import {Toolbar, ToolbarList} from 'components/Toolbar';
+import useFormContext from 'hooks/useFormContext';
+
+import {buildGovMetricUrl} from './utils';
+
+const GovMetricSnippet = () => {
+ const analyticsToolsConfig = useContext(AnalyticsToolsConfigContext);
+ const form = useFormContext();
+ const intl = useIntl();
+
+ if (!analyticsToolsConfig.enableGovmetricAnalytics) return null;
+
+ const govmetricUrl = buildGovMetricUrl(
+ analyticsToolsConfig.govmetricSourceId,
+ form.slug,
+ analyticsToolsConfig.govmetricSecureGuid
+ );
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default GovMetricSnippet;
diff --git a/src/components/analytics/constants.js b/src/components/analytics/constants.js
new file mode 100644
index 000000000..af0d19815
--- /dev/null
+++ b/src/components/analytics/constants.js
@@ -0,0 +1 @@
+export const GOVMETRIC_URL = 'https://websurveys2.govmetric.com';
diff --git a/src/components/analytics/index.js b/src/components/analytics/index.js
new file mode 100644
index 000000000..15498d7e2
--- /dev/null
+++ b/src/components/analytics/index.js
@@ -0,0 +1,3 @@
+import GovMetricSnippet from './GovMetricSnippet';
+
+export {GovMetricSnippet};
diff --git a/src/components/analytics/utils.js b/src/components/analytics/utils.js
new file mode 100644
index 000000000..87ef5b1c5
--- /dev/null
+++ b/src/components/analytics/utils.js
@@ -0,0 +1,13 @@
+import {GOVMETRIC_URL} from './constants';
+
+const buildGovMetricUrl = (govmetricSourceId, formSlug, govmetricSecureGuid) => {
+ let govmetricStopknop = new URL(`/theme/kf/${govmetricSourceId}`, GOVMETRIC_URL);
+ govmetricStopknop.searchParams.append('Q_Formid', formSlug);
+ if (govmetricSecureGuid) {
+ govmetricStopknop.searchParams.append('GUID', govmetricSecureGuid);
+ }
+
+ return govmetricStopknop.href;
+};
+
+export {buildGovMetricUrl};
diff --git a/src/components/analytics/utils.spec.js b/src/components/analytics/utils.spec.js
new file mode 100644
index 000000000..da9cdbfc3
--- /dev/null
+++ b/src/components/analytics/utils.spec.js
@@ -0,0 +1,17 @@
+import {buildGovMetricUrl} from './utils';
+
+describe('Test analytics utils', () => {
+ test('Test build URL without GUID', () => {
+ const url = buildGovMetricUrl('1234', 'form-slug');
+
+ expect(url).toEqual('https://websurveys2.govmetric.com/theme/kf/1234?Q_Formid=form-slug');
+ });
+
+ test('Test build URL with GUID', () => {
+ const url = buildGovMetricUrl('1234', 'form-slug', '4321');
+
+ expect(url).toEqual(
+ 'https://websurveys2.govmetric.com/theme/kf/1234?Q_Formid=form-slug&GUID=4321'
+ );
+ });
+});
diff --git a/src/sdk.spec.js b/src/sdk.spec.js
index c2ff4e1e6..0febfe035 100644
--- a/src/sdk.spec.js
+++ b/src/sdk.spec.js
@@ -1,12 +1,12 @@
import {act, waitForElementToBeRemoved, within} from '@testing-library/react';
-import {BASE_URL, buildForm, mockFormGet} from 'api-mocks';
+import {BASE_URL, buildForm, mockAnalyticsToolConfigGet, mockFormGet} from 'api-mocks';
import mswServer from 'api-mocks/msw-server';
import {mockFormioTranslations, mockLanguageInfoGet} from 'components/LanguageSelection/mocks';
import {OpenForm} from './sdk.js';
-// scrollIntoView is not not supported in Jest
+// scrollIntoView is not supported in Jest
let scrollIntoViewMock = jest.fn();
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
@@ -19,6 +19,7 @@ const apiMocks = [
mockFormGet(buildForm({translationEnabled: true})),
mockLanguageInfoGet(LANGUAGES),
mockFormioTranslations,
+ mockAnalyticsToolConfigGet(),
];
describe('OpenForm', () => {
@@ -90,7 +91,8 @@ describe('OpenForm', () => {
mockFormGet(formNL, true),
mockFormGet(formEN, true),
mockLanguageInfoGet(LANGUAGES),
- mockFormioTranslations
+ mockFormioTranslations,
+ mockAnalyticsToolConfigGet()
);
const form = new OpenForm(formRoot, {
diff --git a/src/story-utils/decorators.js b/src/story-utils/decorators.js
index c6246d5da..9a8728296 100644
--- a/src/story-utils/decorators.js
+++ b/src/story-utils/decorators.js
@@ -3,7 +3,7 @@ import {Formik} from 'formik';
import merge from 'lodash/merge';
import {MemoryRouter, Route, Routes} from 'react-router-dom';
-import {ConfigContext, FormContext} from 'Context';
+import {AnalyticsToolsConfigContext, ConfigContext, FormContext} from 'Context';
import {BASE_URL, buildForm} from 'api-mocks';
import Card from 'components/Card';
import {LiteralsProvider} from 'components/Literal';
@@ -30,6 +30,20 @@ export const ConfigDecorator = (Story, {parameters}) => {
);
};
+export const AnalyticsToolsDecorator = (Story, {parameters, args}) => {
+ const defaults = {
+ govmetricSourceId: '',
+ govmetricSecureGuid: '',
+ enableGovmetricAnalytics: false,
+ };
+
+ return (
+
+
+
+ );
+};
+
const RouterStoryWrapper = ({route = '', children}) => {
if (!route) {
return <>{children}>;