From 74f3838b280f1a2552a7dcd3ca0350a799ce5a33 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 4 Jan 2024 15:04:20 +0100 Subject: [PATCH 1/8] :sparkles: [open-formulieren/open-forms#3276] Rework payment and confirmation views --- src/components/Form.js | 43 ++++--- src/components/ProgressIndicator/utils.js | 19 +-- src/components/constants.js | 4 + .../postCompletionViews/ConfirmationView.js | 115 ++++++++++++++++++ .../{ => postCompletionViews}/PaymentForm.js | 0 .../postCompletionViews/PostCompletionView.js | 65 ++++++++++ .../postCompletionViews/StartPayment.js | 49 ++++++++ .../postCompletionViews/StartPaymentView.js | 79 ++++++++++++ .../postCompletionViews/StatusUrlPoller.js | 108 ++++++++++++++++ src/components/postCompletionViews/index.js | 5 + 10 files changed, 461 insertions(+), 26 deletions(-) create mode 100644 src/components/postCompletionViews/ConfirmationView.js rename src/components/{ => postCompletionViews}/PaymentForm.js (100%) create mode 100644 src/components/postCompletionViews/PostCompletionView.js create mode 100644 src/components/postCompletionViews/StartPayment.js create mode 100644 src/components/postCompletionViews/StartPaymentView.js create mode 100644 src/components/postCompletionViews/StatusUrlPoller.js create mode 100644 src/components/postCompletionViews/index.js diff --git a/src/components/Form.js b/src/components/Form.js index 3ee7be829..9193b95ac 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -10,13 +10,12 @@ import ErrorBoundary from 'components/Errors/ErrorBoundary'; import FormStart from 'components/FormStart'; import FormStep from 'components/FormStep'; import Loader from 'components/Loader'; -import PaymentOverview from 'components/PaymentOverview'; import ProgressIndicator from 'components/ProgressIndicator'; import RequireSubmission from 'components/RequireSubmission'; import {RequireSession} from 'components/Sessions'; -import SubmissionConfirmation from 'components/SubmissionConfirmation'; import SubmissionSummary from 'components/Summary'; import {START_FORM_QUERY_PARAM} from 'components/constants'; +import {ConfirmationView, StartPaymentView} from 'components/postCompletionViews'; import {findNextApplicableStep} from 'components/utils'; import {createSubmission, flagActiveSubmission, flagNoActiveSubmission} from 'data/submissions'; import useAutomaticRedirect from 'hooks/useAutomaticRedirect'; @@ -105,6 +104,7 @@ const Form = () => { // TODO replace absolute path check with relative const stepMatch = useMatch('/stap/:step'); const summaryMatch = useMatch('/overzicht'); + const paymentMatch = useMatch('/betalen'); const confirmationMatch = useMatch('/bevestiging'); // extract the declared properties and configuration @@ -153,8 +153,6 @@ const Form = () => { [intl.locale, prevLocale, removeSubmissionId, state.submission] // eslint-disable-line react-hooks/exhaustive-deps ); - const paymentOverviewMatch = useMatch('/betaaloverzicht'); - /** * When the form is started, create a submission and add it to the state. * @@ -204,7 +202,12 @@ const Form = () => { processingStatusUrl, }, }); - navigate('/bevestiging'); + + if (form.paymentRequired && !state.submission.payment.hasPaid) { + navigate('/betalen'); + } else { + navigate('/bevestiging'); + } }; const destroySession = async confirmationMessage => { @@ -256,10 +259,11 @@ const Form = () => { return ( ); @@ -271,13 +275,13 @@ const Form = () => { // Progress Indicator - const isStartPage = !summaryMatch && stepMatch == null && !confirmationMatch; + const isStartPage = !summaryMatch && stepMatch == null && !paymentMatch; const submissionAllowedSpec = state.submission?.submissionAllowed ?? form.submissionAllowed; const showOverview = submissionAllowedSpec !== SUBMISSION_ALLOWED.noWithoutOverview; - const showConfirmation = submissionAllowedSpec === SUBMISSION_ALLOWED.yes; const submission = state.submission || state.submittedSubmission; const isCompleted = state.completed; const formName = form.name; + const needsPayment = form.paymentRequired; // Figure out the slug from the currently active step IF we're looking at a step const stepSlug = stepMatch ? stepMatch.params.step : ''; @@ -288,8 +292,8 @@ const Form = () => { activeStepTitle = intl.formatMessage(STEP_LABELS.login); } else if (summaryMatch) { activeStepTitle = intl.formatMessage(STEP_LABELS.overview); - } else if (confirmationMatch) { - activeStepTitle = intl.formatMessage(STEP_LABELS.confirmation); + } else if (paymentMatch) { + activeStepTitle = intl.formatMessage(STEP_LABELS.payment); } else { const step = steps.find(step => step.slug === stepSlug); activeStepTitle = step.formDefinition; @@ -324,14 +328,14 @@ const Form = () => { submission, currentPathname, showOverview, - showConfirmation, + needsPayment, isCompleted ); // Show the progress indicator if enabled on the form AND we're not in the payment - // status/overview screen. + // confirmation screen. const progressIndicator = - form.showProgressIndicator && !paymentOverviewMatch ? ( + form.showProgressIndicator && !confirmationMatch ? ( { /> { statusUrl={state.processingStatusUrl} onFailure={onProcessingFailure} onConfirmed={() => dispatch({type: 'PROCESSING_SUCCEEDED'})} - component={SubmissionConfirmation} + component={StartPaymentView} donwloadPDFText={form.submissionReportDownloadLinkTitle} /> @@ -396,10 +400,15 @@ const Form = () => { /> - + dispatch({type: 'PROCESSING_SUCCEEDED'})} + downloadPDFText={form.submissionReportDownloadLinkTitle} + /> } /> diff --git a/src/components/ProgressIndicator/utils.js b/src/components/ProgressIndicator/utils.js index 0bf0ae1fb..ff4e39640 100644 --- a/src/components/ProgressIndicator/utils.js +++ b/src/components/ProgressIndicator/utils.js @@ -35,11 +35,11 @@ const addFixedSteps = ( submission, currentPathname, showOverview, - showConfirmation, + needsPayment, completed = false ) => { const hasSubmission = !!submission; - const isConfirmation = checkMatchesPath(currentPathname, 'bevestiging'); + const isPayment = checkMatchesPath(currentPathname, 'betalen'); const applicableSteps = hasSubmission ? submission.steps.filter(step => step.isApplicable) : []; const applicableAndCompletedSteps = applicableSteps.filter(step => step.completed); const applicableCompleted = @@ -57,24 +57,25 @@ const addFixedSteps = ( const summaryStep = { to: 'overzicht', label: intl.formatMessage(STEP_LABELS.overview), - isCompleted: isConfirmation, + isCompleted: isPayment, isApplicable: true, isCurrent: checkMatchesPath(currentPathname, 'overzicht'), canNavigateTo: applicableCompleted, }; - const confirmationStep = { - to: 'bevestiging', - label: intl.formatMessage(STEP_LABELS.confirmation), - isCompleted: completed, - isCurrent: checkMatchesPath(currentPathname, 'bevestiging'), + const paymentStep = { + to: 'betalen', + label: intl.formatMessage(STEP_LABELS.payment), + isCompleted: false, + isCurrent: isPayment, + canNavigateTo: completed, }; const finalSteps = [ startPageStep, ...steps, showOverview && summaryStep, - showConfirmation && confirmationStep, + needsPayment && paymentStep, ].filter(Boolean); return finalSteps; diff --git a/src/components/constants.js b/src/components/constants.js index 780bd3789..d7574ad13 100644 --- a/src/components/constants.js +++ b/src/components/constants.js @@ -19,6 +19,10 @@ const STEP_LABELS = defineMessages({ description: 'Confirmation page title', defaultMessage: 'Confirmation', }, + payment: { + description: 'Payment page title', + defaultMessage: 'Payment', + }, }); const START_FORM_QUERY_PARAM = '_start'; diff --git a/src/components/postCompletionViews/ConfirmationView.js b/src/components/postCompletionViews/ConfirmationView.js new file mode 100644 index 000000000..2e280bd99 --- /dev/null +++ b/src/components/postCompletionViews/ConfirmationView.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React, {useContext} from 'react'; +import {FormattedMessage, defineMessage, useIntl} from 'react-intl'; +import {useLocation} from 'react-router-dom'; + +import Body from 'components/Body'; +import ErrorMessage from 'components/Errors/ErrorMessage'; + +import PostCompletionView from './PostCompletionView'; +import StatusUrlPoller, {SubmissionStatusContext} from './StatusUrlPoller'; + +// see openforms.payments.constants.UserAction in the backend +const USER_ACTIONS = ['accept', 'exception', 'cancel', 'unknown']; + +// see openforms.payments.constants.PaymentStatus in the backend +const STATUS_MESSAGES = { + started: defineMessage({ + description: 'payment started status', + defaultMessage: "You've started the payment process.", + }), + processing: defineMessage({ + description: 'payment processing status', + defaultMessage: 'Your payment is currently processing.', + }), + failed: defineMessage({ + description: 'payment failed status', + defaultMessage: + 'The payment has failed. If you aborted the payment, please complete payment from the confirmation email.', + }), + completed: defineMessage({ + description: 'payment completed status', + defaultMessage: 'Your payment has been received.', + }), + registered: defineMessage({ + description: 'payment registered status', + defaultMessage: 'Your payment is received and processed.', + }), +}; + +const ConfirmationViewDisplay = ({downloadPDFText}) => { + const intl = useIntl(); + const location = useLocation(); + const paymentStatus = location?.state?.status; + const userAction = location?.state?.userAction; + + const {publicReference, reportDownloadUrl, confirmationPageContent, mainWebsiteUrl} = + useContext(SubmissionStatusContext); + + const paymentStatusMessage = STATUS_MESSAGES[paymentStatus]; + let Wrapper = React.Fragment; + if (paymentStatus) { + if (!USER_ACTIONS.includes(userAction)) throw new Error('Unknown payment user action'); + + if (!paymentStatusMessage) throw new Error('Unknown payment status'); + + if (paymentStatus === 'failed') Wrapper = ErrorMessage; + } + + const body = ( + <> + {paymentStatus && ( + + {intl.formatMessage(paymentStatusMessage)} + + )} + + + ); + + return ( + + } + body={body} + mainWebsiteUrl={mainWebsiteUrl} + reportDownloadUrl={reportDownloadUrl} + /> + ); +}; + +ConfirmationViewDisplay.propTypes = { + downloadPDFText: PropTypes.node, +}; + +const ConfirmationView = ({statusUrl, onFailure, onConfirmed, downloadPDFText}) => { + return ( + + + + ); +}; + +ConfirmationView.propTypes = { + statusUrl: PropTypes.string, + onFailure: PropTypes.func, + onConfirmed: PropTypes.func, + downloadPDFText: PropTypes.node, +}; + +export {ConfirmationViewDisplay}; +export default ConfirmationView; diff --git a/src/components/PaymentForm.js b/src/components/postCompletionViews/PaymentForm.js similarity index 100% rename from src/components/PaymentForm.js rename to src/components/postCompletionViews/PaymentForm.js diff --git a/src/components/postCompletionViews/PostCompletionView.js b/src/components/postCompletionViews/PostCompletionView.js new file mode 100644 index 000000000..8ae21f5bc --- /dev/null +++ b/src/components/postCompletionViews/PostCompletionView.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import Anchor from 'components/Anchor'; +import Body from 'components/Body'; +import {OFButton} from 'components/Button'; +import Card from 'components/Card'; +import FAIcon from 'components/FAIcon'; +import {Toolbar, ToolbarList} from 'components/Toolbar'; +import useTitle from 'hooks/useTitle'; + +const PostCompletionView = ({ + downloadPDFText, + pageTitle, + header, + body, + mainWebsiteUrl, + reportDownloadUrl, +}) => { + useTitle(pageTitle); + + const linkTitle = downloadPDFText || ( + + ); + + return ( + + {body} + + + + ); +}; + +PostCompletionView.propTypes = { + downloadPDFText: PropTypes.string, + pageTitle: PropTypes.node, + header: PropTypes.node, + body: PropTypes.node, + mainWebsiteUrl: PropTypes.string, + reportDownloadUrl: PropTypes.string, +}; + +export default PostCompletionView; diff --git a/src/components/postCompletionViews/StartPayment.js b/src/components/postCompletionViews/StartPayment.js new file mode 100644 index 000000000..38e0efd8b --- /dev/null +++ b/src/components/postCompletionViews/StartPayment.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {useAsync} from 'react-use'; + +import {post} from 'api'; +import Loader from 'components/Loader'; + +import PaymentForm from './PaymentForm'; + +const getStartPaymentUrl = apiUrl => { + const nextUrl = new URL(window.location.href); + const startPaymentUrl = new URL(apiUrl); + startPaymentUrl.searchParams.set('next', nextUrl.toString()); + return startPaymentUrl.toString(); +}; + +/** + * Given a start URL, fetches the payment start information and renders the UI controls + * to enter the payment flow. + * @param {String} startUrl The payment start URL received from the backend. + * @return {JSX} + */ +const StartPayment = ({startUrl}) => { + const fullUrl = getStartPaymentUrl(startUrl); + + const {loading, value} = useAsync(async () => { + const resp = await post(fullUrl); + if (!resp.ok) throw new Error('Could not start payment'); + return resp.data; + }, [fullUrl]); + + if (loading) { + return ; + } + + return ( + <> + {value ? ( + + ) : null} + + ); +}; + +StartPayment.propTypes = { + startUrl: PropTypes.string.isRequired, +}; + +export {StartPayment}; diff --git a/src/components/postCompletionViews/StartPaymentView.js b/src/components/postCompletionViews/StartPaymentView.js new file mode 100644 index 000000000..d8b5c565f --- /dev/null +++ b/src/components/postCompletionViews/StartPaymentView.js @@ -0,0 +1,79 @@ +import PropTypes from 'prop-types'; +import React, {useContext} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import Body from 'components/Body'; +import ErrorBoundary from 'components/Errors/ErrorBoundary'; + +import PostCompletionView from './PostCompletionView'; +import {StartPayment} from './StartPayment'; +import StatusUrlPoller, {SubmissionStatusContext} from './StatusUrlPoller'; + +const StartPaymentViewDisplay = ({downloadPDFText}) => { + const intl = useIntl(); + const {publicReference, paymentUrl, reportDownloadUrl, mainWebsiteUrl} = + useContext(SubmissionStatusContext); + + const body = ( + <> + + + + + + + + + + + + + ); + return ( + + } + body={body} + mainWebsiteUrl={mainWebsiteUrl} + reportDownloadUrl={reportDownloadUrl} + /> + ); +}; + +StartPaymentViewDisplay.propTypes = { + downloadPDFText: PropTypes.node, +}; + +const StartPaymentView = ({statusUrl, onFailure, onConfirmed, downloadPDFText}) => { + return ( + + + + ); +}; + +StartPaymentView.propTypes = { + statusUrl: PropTypes.string, + onFailure: PropTypes.func, + onConfirmed: PropTypes.func, + downloadPDFText: PropTypes.node, +}; + +export default StartPaymentView; +export {StartPaymentViewDisplay}; diff --git a/src/components/postCompletionViews/StatusUrlPoller.js b/src/components/postCompletionViews/StatusUrlPoller.js new file mode 100644 index 000000000..559e75f85 --- /dev/null +++ b/src/components/postCompletionViews/StatusUrlPoller.js @@ -0,0 +1,108 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; +import {useLocation} from 'react-router-dom'; + +import Body from 'components/Body'; +import Card from 'components/Card'; +import Loader from 'components/Loader'; +import usePoll from 'hooks/usePoll'; + +const RESULT_FAILED = 'failed'; +const RESULT_SUCCESS = 'success'; + +const SubmissionStatusContext = React.createContext(); +SubmissionStatusContext.displayName = 'SubmissionStatusContext'; + +const StatusUrlPoller = ({statusUrl, onFailure, onConfirmed, children}) => { + const intl = useIntl(); + const location = useLocation(); + + const genericErrorMessage = intl.formatMessage({ + description: 'Generic submission error', + defaultMessage: 'Something went wrong while submitting the form.', + }); + + const { + loading, + error, + response: statusResponse, + } = usePoll( + statusUrl || location?.state?.statusUrl, + 1000, + response => response.status === 'done', + response => { + if (response.result === RESULT_FAILED) { + const errorMessage = response.errorMessage || genericErrorMessage; + onFailure && onFailure(errorMessage); + } else if (response.result === RESULT_SUCCESS) { + onConfirmed && onConfirmed(); + } + } + ); + + if (loading) { + return ( + + } + > + + + + + + ); + } + + // FIXME: https://github.com/open-formulieren/open-forms/issues/3255 + // errors (bad gateway 502, for example) appear to result in infinite loading + // spinners + if (error) { + console.error(error); + } + + const { + result, + paymentUrl, + publicReference, + reportDownloadUrl, + confirmationPageContent, + mainWebsiteUrl, + } = statusResponse; + + if (result === RESULT_FAILED) { + throw new Error('Failure should have been handled in the onFailure prop.'); + } + + return ( + + {children} + + ); +}; + +StatusUrlPoller.propTypes = { + statusUrl: PropTypes.string, + onFailure: PropTypes.func, + onConfirmed: PropTypes.func, + children: PropTypes.node, +}; + +export {SubmissionStatusContext}; +export default StatusUrlPoller; diff --git a/src/components/postCompletionViews/index.js b/src/components/postCompletionViews/index.js new file mode 100644 index 000000000..925dceba6 --- /dev/null +++ b/src/components/postCompletionViews/index.js @@ -0,0 +1,5 @@ +import ConfirmationView from './ConfirmationView'; +import StartPaymentView from './StartPaymentView'; +import {SubmissionStatusContext} from './StatusUrlPoller'; + +export {StartPaymentView, ConfirmationView, SubmissionStatusContext}; From 7be643663579636e6541eca3fff79007c246e6bb Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 4 Jan 2024 16:54:06 +0100 Subject: [PATCH 2/8] :memo: [open-formulieren/open-forms#3726] Stories for the post completion screens --- src/api-mocks/submissions.js | 11 +++- .../ConfirmationView.stories.js | 47 +++++++++++++ .../PostCompletionView.stories.js | 19 ++++++ .../StartPaymentView.stories.js | 35 ++++++++++ .../postCompletionViews.mdx | 27 ++++++++ .../viewsWithPolling.stories.js | 66 +++++++++++++++++++ src/story-utils/decorators.js | 17 +++++ 7 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/components/postCompletionViews/ConfirmationView.stories.js create mode 100644 src/components/postCompletionViews/PostCompletionView.stories.js create mode 100644 src/components/postCompletionViews/StartPaymentView.stories.js create mode 100644 src/components/postCompletionViews/postCompletionViews.mdx create mode 100644 src/components/postCompletionViews/viewsWithPolling.stories.js diff --git a/src/api-mocks/submissions.js b/src/api-mocks/submissions.js index 1708aba32..7407b744b 100644 --- a/src/api-mocks/submissions.js +++ b/src/api-mocks/submissions.js @@ -84,7 +84,7 @@ export const mockSubmissionCheckLogicPost = () => }); /** - * Simulate a succesful backend processing status without payment. + * Simulate a successful backend processing status without payment. */ export const mockSubmissionProcessingStatusGet = rest.get( `${BASE_URL}submissions/:uuid/:token/status`, @@ -97,7 +97,7 @@ export const mockSubmissionProcessingStatusGet = rest.get( publicReference: 'OF-L337', confirmationPageContent: `

Thank you for doing the thing.`, reportDownloadUrl: '#', - paymentUrl: '', + paymentUrl: `${BASE_URL}payment/4b0e86a8-dc5f-41cc-b812-c89857b9355b/demo/start`, mainWebsiteUrl: '#', }) ) @@ -136,3 +136,10 @@ export const mockSubmissionProcessingStatusErrorGet = rest.get( }) ) ); + +export const mockSubmissionPaymentStartGet = rest.post( + `${BASE_URL}payment/4b0e86a8-dc5f-41cc-b812-c89857b9355b/demo/start`, + (req, res, ctx) => { + return res(ctx.json({data: {method: 'get', action: 'http://example.com'}})); + } +); diff --git a/src/components/postCompletionViews/ConfirmationView.stories.js b/src/components/postCompletionViews/ConfirmationView.stories.js new file mode 100644 index 000000000..8bfa590bc --- /dev/null +++ b/src/components/postCompletionViews/ConfirmationView.stories.js @@ -0,0 +1,47 @@ +import {withRouter} from 'storybook-addon-react-router-v6'; + +import {withSubmissionPollInfo} from 'story-utils/decorators'; + +import {ConfirmationViewDisplay} from './ConfirmationView'; + +export default { + title: 'Private API / Post completion views / Confirmation view', + component: ConfirmationViewDisplay, + decorators: [withSubmissionPollInfo, withRouter], + argTypes: { + paymentUrl: {control: false}, + }, + parameters: { + reactRouter: { + routeState: {}, + }, + }, +}; + +export const WithoutPayment = { + args: { + paymentUrl: '', + publicReference: 'OF-1234', + reportDownloadUrl: '#', + confirmationPageContent: 'Your answers are submitted. Hurray!', + mainWebsiteUrl: '#', + downloadPDFText: 'Download a PDF of your submitted answers', + }, +}; + +export const WithPayment = { + args: { + publicReference: 'OF-1234', + reportDownloadUrl: '#', + confirmationPageContent: 'Your answers are submitted. Hurray!', + mainWebsiteUrl: '#', + downloadPDFText: 'Download a PDF of your submitted answers', + }, + parameters: { + reactRouter: { + routeState: { + status: 'completed', + }, + }, + }, +}; diff --git a/src/components/postCompletionViews/PostCompletionView.stories.js b/src/components/postCompletionViews/PostCompletionView.stories.js new file mode 100644 index 000000000..0aed6f1d9 --- /dev/null +++ b/src/components/postCompletionViews/PostCompletionView.stories.js @@ -0,0 +1,19 @@ +import Body from 'components/Body'; + +import PostCompletionView from './PostCompletionView'; + +export default { + title: 'Private API / Post completion views ', + render: ({body, ...args}) => {body}} />, +}; + +export const Generic = { + args: { + pageTitle: 'Confirmation', + header: 'Confirmation!', + body: 'This is some text for the body', + mainWebsiteUrl: '#', + reportDownloadUrl: '#', + downloadPDFText: 'Download a PDF of your submitted answers', + }, +}; diff --git a/src/components/postCompletionViews/StartPaymentView.stories.js b/src/components/postCompletionViews/StartPaymentView.stories.js new file mode 100644 index 000000000..cf4e98917 --- /dev/null +++ b/src/components/postCompletionViews/StartPaymentView.stories.js @@ -0,0 +1,35 @@ +import {BASE_URL} from 'api-mocks'; +import { + mockSubmissionPaymentStartGet, + mockSubmissionProcessingStatusGet, +} from 'api-mocks/submissions'; +import {withSubmissionPollInfo} from 'story-utils/decorators'; + +import {StartPaymentViewDisplay} from './StartPaymentView'; + +export default { + title: 'Private API / Post completion views / Start payment', + render: StartPaymentViewDisplay, + decorators: [withSubmissionPollInfo], + args: { + paymentUrl: `${BASE_URL}payment/4b0e86a8-dc5f-41cc-b812-c89857b9355b/demo/start`, + }, + argTypes: { + paymentUrl: {control: false}, + }, + parameters: { + msw: { + handlers: [mockSubmissionProcessingStatusGet, mockSubmissionPaymentStartGet], + }, + }, +}; + +export const Default = { + args: { + publicReference: 'OF-1234', + reportDownloadUrl: '#', + confirmationPageContent: 'This is some confirmation text.', + mainWebsiteUrl: '#', + downloadPDFText: 'Download a PDF of your submitted answers', + }, +}; diff --git a/src/components/postCompletionViews/postCompletionViews.mdx b/src/components/postCompletionViews/postCompletionViews.mdx new file mode 100644 index 000000000..fa5d15d4d --- /dev/null +++ b/src/components/postCompletionViews/postCompletionViews.mdx @@ -0,0 +1,27 @@ +import {Canvas, Meta} from '@storybook/blocks'; + +import * as ConfirmationStories from './ConfirmationView.stories'; +import * as PostCompletionStories from './PostCompletionView.stories'; +import * as StartPaymentStories from './StartPaymentView.stories'; + + + +# Post completion views + +## `StartPaymentViewDisplay` + +This view is displayed to the user when a submission is completed and a payment is required. + + + +## `ConfirmationViewDisplay` without payment required + +When no payment is required, after submitting the form the user sees this view: + + + +## `ConfirmationViewDisplay` with payment required + +After payment, the user is redirected to this view: + + diff --git a/src/components/postCompletionViews/viewsWithPolling.stories.js b/src/components/postCompletionViews/viewsWithPolling.stories.js new file mode 100644 index 000000000..93495c62f --- /dev/null +++ b/src/components/postCompletionViews/viewsWithPolling.stories.js @@ -0,0 +1,66 @@ +import {expect} from '@storybook/jest'; +import {waitFor, within} from '@storybook/testing-library'; +import {withRouter} from 'storybook-addon-react-router-v6'; + +import {BASE_URL} from 'api-mocks'; +import { + mockSubmissionProcessingStatusGet, + mockSubmissionProcessingStatusPendingGet, +} from 'api-mocks/submissions'; +import {withSubmissionPollInfo} from 'story-utils/decorators'; + +import ConfirmationView from './ConfirmationView'; + +export default { + title: 'Private API / Post completion views / With Polling', + component: ConfirmationView, + decorators: [withSubmissionPollInfo, withRouter], + argTypes: { + statusUrl: {control: false}, + }, + args: { + statusUrl: `${BASE_URL}submissions/4b0e86a8-dc5f-41cc-b812-c89857b9355b/-token-/status`, + }, + parameters: { + msw: { + handlers: [mockSubmissionProcessingStatusGet], + }, + reactRouter: { + routeState: {}, + }, + }, +}; + +export const withoutPayment = { + play: async ({canvasElement, args}) => { + const canvas = within(canvasElement); + + await waitFor( + async () => { + expect(canvas.getByRole('button', {name: 'Terug naar de website'})).toBeVisible(); + }, + { + timeout: 2000, + interval: 100, + } + ); + expect(canvas.getByText(/OF-L337/)).toBeVisible(); + expect(args.onConfirmed).toBeCalledTimes(1); + }, +}; + +export const withPayment = { + parameters: { + reactRouter: { + routeState: {status: 'completed'}, + }, + }, +}; + +export const Pending = { + parameters: { + msw: { + handlers: [mockSubmissionProcessingStatusPendingGet], + }, + }, +}; diff --git a/src/story-utils/decorators.js b/src/story-utils/decorators.js index a9e97f605..ba877e741 100644 --- a/src/story-utils/decorators.js +++ b/src/story-utils/decorators.js @@ -7,6 +7,7 @@ import {ConfigContext, FormContext} from 'Context'; import {BASE_URL, buildForm} from 'api-mocks'; import Card from 'components/Card'; import {LiteralsProvider} from 'components/Literal'; +import {SubmissionStatusContext} from 'components/postCompletionViews'; export const ConfigDecorator = (Story, {parameters}) => { const defaults = { @@ -134,3 +135,19 @@ export const withForm = (Story, {parameters, args}) => { ); }; + +export const withSubmissionPollInfo = (Story, {parameters, args}) => { + return ( + + + + ); +}; From 92d7b9a3d9371fb7b749ec58273fd1b3f99d028b Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 4 Jan 2024 17:40:58 +0100 Subject: [PATCH 3/8] :fire: [open-formulieren/open-forms#3726] Remove old components --- .../__snapshots__/test.spec.js.snap | 122 ----------- src/components/PaymentOverview/index.js | 110 ---------- src/components/PaymentOverview/test.spec.js | 118 ---------- src/components/SubmissionConfirmation.js | 202 ------------------ .../SubmissionConfirmation.stories.js | 55 ----- 5 files changed, 607 deletions(-) delete mode 100644 src/components/PaymentOverview/__snapshots__/test.spec.js.snap delete mode 100644 src/components/PaymentOverview/index.js delete mode 100644 src/components/PaymentOverview/test.spec.js delete mode 100644 src/components/SubmissionConfirmation.js delete mode 100644 src/components/SubmissionConfirmation.stories.js diff --git a/src/components/PaymentOverview/__snapshots__/test.spec.js.snap b/src/components/PaymentOverview/__snapshots__/test.spec.js.snap deleted file mode 100644 index c46f395d1..000000000 --- a/src/components/PaymentOverview/__snapshots__/test.spec.js.snap +++ /dev/null @@ -1,122 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`navigation without context displays a generic error message 1`] = ` -

-
-

- Payment overview -

-
-
- -
-
- -
-
- We can't display any payment information - did you maybe get here by accident? -
-
- -
-
-`; - -exports[`on valid redirect renders an error message on failure 1`] = ` -
-
-

- Payment overview -

-
-
- -
-
-
- -
-
- The payment has failed. If you aborted the payment, please complete payment from the confirmation email. -
-
-
- - Back to form start - - -
-
-`; - -exports[`on valid redirect renders the status information 1`] = ` -
-
-

- Payment overview -

-
-
- -
- Your payment has been received. -
- - Back to form start - - -
-
-`; diff --git a/src/components/PaymentOverview/index.js b/src/components/PaymentOverview/index.js deleted file mode 100644 index 972363f31..000000000 --- a/src/components/PaymentOverview/index.js +++ /dev/null @@ -1,110 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage, defineMessage, useIntl} from 'react-intl'; -import {useLocation} from 'react-router-dom'; - -import Body from 'components/Body'; -import Card from 'components/Card'; -import ErrorMessage from 'components/Errors/ErrorMessage'; -import Link from 'components/Link'; -import useTitle from 'hooks/useTitle'; - -// see openforms.payments.constants.UserAction in the backend -const USER_ACTIONS = ['accept', 'exception', 'cancel', 'unknown']; - -// see openforms.payments.constants.PaymentStatus in the backend -const STATUS_MESSAGES = { - started: defineMessage({ - description: 'payment started status', - defaultMessage: "You've started the payment process.", - }), - processing: defineMessage({ - description: 'payment processing status', - defaultMessage: 'Your payment is currently processing.', - }), - failed: defineMessage({ - description: 'payment failed status', - defaultMessage: - 'The payment has failed. If you aborted the payment, please complete payment from the confirmation email.', - }), - completed: defineMessage({ - description: 'payment completed status', - defaultMessage: 'Your payment has been received.', - }), - registered: defineMessage({ - description: 'payment registered status', - defaultMessage: 'Your payment is received and processed.', - }), -}; - -const Container = ({children}) => ( - - } - > - {children} - -); - -Container.propTypes = { - children: PropTypes.node, -}; - -const PaymentOverview = () => { - const intl = useIntl(); - const {state: {status, userAction} = {}} = useLocation(); - - const pageTitle = intl.formatMessage({ - description: 'Payment overview page title', - defaultMessage: 'Payment overview', - }); - useTitle(pageTitle); - - // display a warning if someone navigates directly to this URL. - if (!status || !userAction) { - return ( - - - - - - ); - } - - if (!USER_ACTIONS.includes(userAction)) { - throw new Error('Unknown payment user action'); - } - - const statusMsg = STATUS_MESSAGES[status]; - if (!statusMsg) { - throw new Error('Unknown payment status'); - } - - let Wrapper = React.Fragment; - if (status === 'failed') { - Wrapper = ErrorMessage; - } - - return ( - - - {intl.formatMessage(statusMsg)} - - - - - - - ); -}; - -PaymentOverview.propTypes = {}; - -export default PaymentOverview; diff --git a/src/components/PaymentOverview/test.spec.js b/src/components/PaymentOverview/test.spec.js deleted file mode 100644 index 71dbe5306..000000000 --- a/src/components/PaymentOverview/test.spec.js +++ /dev/null @@ -1,118 +0,0 @@ -import {render} from '@testing-library/react'; -import messagesEN from 'i18n/compiled/en.json'; -import React from 'react'; -import {IntlProvider} from 'react-intl'; -import {useLocation} from 'react-router-dom'; -import {MemoryRouter} from 'react-router-dom'; -import renderer from 'react-test-renderer'; - -import PaymentOverview from 'components/PaymentOverview'; - -/** - * Set up mocks - */ -jest.mock('../../map/rd', () => jest.fn()); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useLocation: jest.fn(), - Link: () => some link, -})); - -const Wrap = ({children}) => ( - - {children} - -); - -describe('navigation without context', () => { - it('displays a generic error message', () => { - useLocation.mockImplementation(() => ({ - pathname: 'localhost:3000/betaaloverzicht', - })); - const tree = renderer - .create( - - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('throws on unexpected values (status)', () => { - useLocation.mockImplementation(() => ({ - pathname: 'localhost:3000/betaaloverzicht', - state: { - status: 'nope', - userAction: 'accept', - }, - })); - - expect(() => - render( - - - - ) - ).toThrow(); - }); - - it('throws on unexpected values (userAction)', () => { - useLocation.mockImplementation(() => ({ - pathname: 'localhost:3000/betaaloverzicht', - state: { - status: 'completed', - userAction: 'nope', - }, - })); - - expect(() => - render( - - - - ) - ).toThrow(); - }); -}); - -describe('on valid redirect', () => { - it('renders the status information', () => { - useLocation.mockImplementation(() => ({ - pathname: 'localhost:3000/betaaloverzicht', - state: { - status: 'completed', - userAction: 'accept', - }, - })); - - const tree = renderer - .create( - - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('renders an error message on failure', () => { - useLocation.mockImplementation(() => ({ - pathname: 'localhost:3000/betaaloverzicht', - state: { - status: 'failed', - userAction: 'accept', - }, - })); - - const tree = renderer - .create( - - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/src/components/SubmissionConfirmation.js b/src/components/SubmissionConfirmation.js deleted file mode 100644 index e6cd97e4b..000000000 --- a/src/components/SubmissionConfirmation.js +++ /dev/null @@ -1,202 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; -import {useAsync} from 'react-use'; - -import {post} from 'api'; -import Anchor from 'components/Anchor'; -import Body from 'components/Body'; -import {OFButton} from 'components/Button'; -import Card from 'components/Card'; -import ErrorBoundary from 'components/Errors/ErrorBoundary'; -import FAIcon from 'components/FAIcon'; -import Loader from 'components/Loader'; -import PaymentForm from 'components/PaymentForm'; -import {Toolbar, ToolbarList} from 'components/Toolbar'; -import usePoll from 'hooks/usePoll'; -import useTitle from 'hooks/useTitle'; - -const RESULT_FAILED = 'failed'; -const RESULT_SUCCESS = 'success'; - -const getStartPaymentUrl = apiUrl => { - const nextUrl = new URL(window.location.href); - const startPaymentUrl = new URL(apiUrl); - startPaymentUrl.searchParams.set('next', nextUrl.toString()); - return startPaymentUrl.toString(); -}; - -/** - * Given a start URL, fetches the payment start information and renders the UI controls - * to enter the payment flow. - * @param {String} startUrl The payment start URL received from the backend. - * @return {JSX} - */ -const StartPayment = ({startUrl}) => { - const fullUrl = getStartPaymentUrl(startUrl); - const {loading, value} = useAsync(async () => { - const resp = await post(fullUrl); - if (!resp.ok) throw new Error('Could not start payment'); - return resp.data; - }, [fullUrl]); - - return ( - - - - - {loading ? ( - - ) : value ? ( - - ) : null} - - ); -}; - -StartPayment.propTypes = { - startUrl: PropTypes.string.isRequired, -}; - -/** - * Renders the confirmation page displayed after submitting a form. - * @param {String} statusUrl The URL where to check if the processing of the submission is complete - * @param {Function} onFailure Callback to invoke if the background processing result is failure. - * @param {Function} onConfirmed Callback to invoke if the background processing result is success. - */ -const SubmissionConfirmation = ({statusUrl, onFailure, onConfirmed, donwloadPDFText}) => { - const intl = useIntl(); - const pageTitle = intl.formatMessage({ - description: 'Confirmation page title', - defaultMessage: 'Confirmation', - }); - useTitle(pageTitle); - - const genericErrorMessage = intl.formatMessage({ - description: 'Generic submission error', - defaultMessage: 'Something went wrong while submitting the form.', - }); - - const { - loading, - error, - response: statusResponse, - } = usePoll( - statusUrl, - 1000, - response => response.status === 'done', - response => { - if (response.result === RESULT_FAILED) { - const errorMessage = response.errorMessage || genericErrorMessage; - onFailure && onFailure(errorMessage); - } else if (response.result === RESULT_SUCCESS) { - onConfirmed && onConfirmed(); - } - } - ); - - // FIXME: https://github.com/open-formulieren/open-forms/issues/3255 - // errors (bad gateway 502, for example) appear to result in infinite loading - // spinners - if (error) { - console.error(error); - } - - if (loading) { - return ( - - } - > - - - - - - ); - } - - // process API output now that processing is done - const { - result, - paymentUrl, - publicReference, - reportDownloadUrl, - confirmationPageContent, - mainWebsiteUrl, - } = statusResponse; - - if (result === RESULT_FAILED) { - throw new Error('Failure should have been handled in the onFailure prop.'); - } - - const showBackToMainWebsite = mainWebsiteUrl && !paymentUrl; - - // Fall back to the literal message in code for backwards compatibility. - const linkTitle = donwloadPDFText || ( - - ); - - return ( - <> - - } - > - - - - - - {paymentUrl ? : null} - - ); -}; - -SubmissionConfirmation.propTypes = { - statusUrl: PropTypes.string.isRequired, - onFailure: PropTypes.func, - onConfirmed: PropTypes.func, - donwloadPDFText: PropTypes.node, -}; - -export default SubmissionConfirmation; diff --git a/src/components/SubmissionConfirmation.stories.js b/src/components/SubmissionConfirmation.stories.js deleted file mode 100644 index d8329c571..000000000 --- a/src/components/SubmissionConfirmation.stories.js +++ /dev/null @@ -1,55 +0,0 @@ -import {expect} from '@storybook/jest'; -import {waitFor, within} from '@storybook/testing-library'; - -import {BASE_URL} from 'api-mocks'; -import { - mockSubmissionProcessingStatusGet, - mockSubmissionProcessingStatusPendingGet, -} from 'api-mocks/submissions'; -import {LayoutDecorator, withCard} from 'story-utils/decorators'; - -import SubmissionConfirmation from './SubmissionConfirmation'; - -export default { - title: 'Private API / SubmissionConfirmation', - component: SubmissionConfirmation, - decorators: [LayoutDecorator, withCard], - args: { - statusUrl: `${BASE_URL}submissions/4b0e86a8-dc5f-41cc-b812-c89857b9355b/-token-/status`, - donwloadPDFText: 'Download your details as PDF', - }, - argTypes: { - statusUrl: {control: false}, - }, - parameters: { - msw: { - handlers: [mockSubmissionProcessingStatusGet], - }, - }, -}; - -export const Success = { - play: async ({canvasElement, args}) => { - const canvas = within(canvasElement); - - await waitFor( - async () => { - expect(canvas.getByRole('button', {name: 'Terug naar de website'})).toBeVisible(); - }, - { - timeout: 2000, - interval: 100, - } - ); - expect(canvas.getByText(/OF-L337/)).toBeVisible(); - expect(args.onConfirmed).toBeCalledTimes(1); - }, -}; - -export const Pending = { - parameters: { - msw: { - handlers: [mockSubmissionProcessingStatusPendingGet], - }, - }, -}; From 5b6c82d34ae3f35bd68ed90614de3f2d78f37b61 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 8 Jan 2024 14:58:56 +0100 Subject: [PATCH 4/8] :white_check_mark: [open-formulieren/open-forms#3726] Update/Add tests --- src/api-mocks/forms.js | 2 +- .../progressIndicator.spec.js | 39 +++++++++++++++---- .../ProgressIndicator/utils.spec.js | 6 +-- .../CreateAppointment/Confirmation.js | 6 +-- .../ConfirmationView.stories.js | 38 +++++++++++++++++- .../viewsWithPolling.stories.js | 2 +- 6 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/api-mocks/forms.js b/src/api-mocks/forms.js index 2534e6d88..f8fc62784 100644 --- a/src/api-mocks/forms.js +++ b/src/api-mocks/forms.js @@ -5,7 +5,7 @@ import {PRIVACY_POLICY_ACCEPTED} from 'components/SummaryConfirmation/mocks'; import {BASE_URL, getDefaultFactory} from './base'; -const FORM_DEFAULTS = { +export const FORM_DEFAULTS = { uuid: 'e450890a-4166-410e-8d64-0a54ad30ba01', name: 'Mock form', slug: 'mock', diff --git a/src/components/ProgressIndicator/progressIndicator.spec.js b/src/components/ProgressIndicator/progressIndicator.spec.js index 04912c2b8..d419bc8e3 100644 --- a/src/components/ProgressIndicator/progressIndicator.spec.js +++ b/src/components/ProgressIndicator/progressIndicator.spec.js @@ -6,6 +6,8 @@ import {RouterProvider, createMemoryRouter} from 'react-router-dom'; import {ConfigContext, FormContext} from 'Context'; import {BASE_URL, buildForm} from 'api-mocks'; +import {getDefaultFactory} from 'api-mocks/base'; +import {FORM_DEFAULTS} from 'api-mocks/forms'; import mswServer from 'api-mocks/msw-server'; import {buildSubmission, mockSubmissionPost} from 'api-mocks/submissions'; import App, {routes as nestedRoutes} from 'components/App'; @@ -18,8 +20,7 @@ const routes = [ }, ]; -const renderApp = (initialRoute = '/') => { - const form = buildForm(); +const renderApp = (form, initialRoute = '/') => { const router = createMemoryRouter(routes, { initialEntries: [initialRoute], initialIndex: [0], @@ -55,11 +56,12 @@ afterEach(() => { }); describe('The progress indicator component', () => { - it('displays the available submission/form steps and hardcoded steps', async () => { + it('displays the available submission/form steps and hardcoded steps (without payment)', async () => { mswServer.use(mockSubmissionPost(buildSubmission())); const user = userEvent.setup({delay: null}); + const form = buildForm(); - renderApp(); + renderApp(form); const startFormLink = await screen.findByRole('link', {name: 'Start page'}); user.click(startFormLink); @@ -73,15 +75,37 @@ describe('The progress indicator component', () => { expect(stepPageItem).toBeVisible(); const summaryPageItem = await screen.findByText('Summary'); expect(summaryPageItem).toBeVisible(); - const confirmationPageItem = await screen.findByText('Confirmation'); - expect(confirmationPageItem).toBeVisible(); + }); + + it('displays the available submission/form steps and hardcoded steps (with payment)', async () => { + mswServer.use(mockSubmissionPost(buildSubmission())); + const user = userEvent.setup({delay: null}); + const form = getDefaultFactory({...FORM_DEFAULTS, paymentRequired: true})(); + + renderApp(form); + + const startFormLink = await screen.findByRole('link', {name: 'Start page'}); + user.click(startFormLink); + + const progressIndicator = await screen.findByText('Progress'); + expect(progressIndicator).toBeVisible(); + + const startPageItem = await screen.findByText('Start page'); + expect(startPageItem).toBeVisible(); + const stepPageItem = await screen.findByText('Step 1'); + expect(stepPageItem).toBeVisible(); + const summaryPageItem = await screen.findByText('Summary'); + expect(summaryPageItem).toBeVisible(); + const paymentPageItem = await screen.findByText('Payment'); + expect(paymentPageItem).toBeVisible(); }); it('renders steps in the correct order', async () => { mswServer.use(mockSubmissionPost(buildSubmission())); const user = userEvent.setup({delay: null}); + const form = buildForm(); - renderApp(); + renderApp(form); const startFormLink = await screen.findByRole('link', {name: 'Start page'}); user.click(startFormLink); @@ -91,6 +115,5 @@ describe('The progress indicator component', () => { expect(progressIndicatorSteps[0]).toHaveTextContent('Start page'); expect(progressIndicatorSteps[1]).toHaveTextContent('Step 1'); expect(progressIndicatorSteps[2]).toHaveTextContent('Summary'); - expect(progressIndicatorSteps[3]).toHaveTextContent('Confirmation'); }); }); diff --git a/src/components/ProgressIndicator/utils.spec.js b/src/components/ProgressIndicator/utils.spec.js index 2b85a29d4..b2846f7fb 100644 --- a/src/components/ProgressIndicator/utils.spec.js +++ b/src/components/ProgressIndicator/utils.spec.js @@ -20,7 +20,7 @@ const formSteps = [ ]; describe('Transforming form steps and injecting fixed steps', () => { - it('prepends start page and appends summary and confirmation steps', () => { + it('prepends start page and appends summary and payment steps', () => { const submission = buildSubmission(); const updatedSteps = getStepsInfo(formSteps, submission, '/stap/step-1'); const stepsToRender = addFixedSteps(intl, updatedSteps, submission, '/stap/step-1', true, true); @@ -36,10 +36,10 @@ describe('Transforming form steps and injecting fixed steps', () => { expect(stepsToRender[1].canNavigateTo).toEqual(true); expect(stepsToRender[2].to).toEqual('overzicht'); - expect(stepsToRender[3].to).toEqual('bevestiging'); + expect(stepsToRender[3].to).toEqual('betalen'); }); - it('accepts parameters to not append summary or confirmation', () => { + it('accepts parameters to not append summary or payment', () => { const submission = buildSubmission(); const updatedSteps = getStepsInfo(formSteps, submission, '/stap/step-1'); const stepsToRender = addFixedSteps( diff --git a/src/components/appointments/CreateAppointment/Confirmation.js b/src/components/appointments/CreateAppointment/Confirmation.js index 5f0a7bd8e..bbf4fab6a 100644 --- a/src/components/appointments/CreateAppointment/Confirmation.js +++ b/src/components/appointments/CreateAppointment/Confirmation.js @@ -1,7 +1,7 @@ import React from 'react'; import {useNavigate, useSearchParams} from 'react-router-dom'; -import SubmissionConfirmation from 'components/SubmissionConfirmation'; +import {ConfirmationView} from 'components/postCompletionViews'; import useFormContext from 'hooks/useFormContext'; import {useCreateAppointmentContext} from './CreateAppointmentState'; @@ -20,11 +20,11 @@ const Confirmation = () => { }; return ( - ); }; diff --git a/src/components/postCompletionViews/ConfirmationView.stories.js b/src/components/postCompletionViews/ConfirmationView.stories.js index 8bfa590bc..57f773a2d 100644 --- a/src/components/postCompletionViews/ConfirmationView.stories.js +++ b/src/components/postCompletionViews/ConfirmationView.stories.js @@ -1,3 +1,5 @@ +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'; @@ -29,7 +31,7 @@ export const WithoutPayment = { }, }; -export const WithPayment = { +export const WithSuccessfulPayment = { args: { publicReference: 'OF-1234', reportDownloadUrl: '#', @@ -41,7 +43,41 @@ export const WithPayment = { reactRouter: { routeState: { status: 'completed', + userAction: 'accept', }, }, }, + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + expect(canvas.getByText('Bevestiging: OF-1234')).toBeVisible(); + expect(canvas.getByText('Uw betaling is ontvangen.')).toBeVisible(); + }, +}; + +export const WithFailedPayment = { + args: { + publicReference: 'OF-1234', + reportDownloadUrl: '#', + confirmationPageContent: 'Your answers are submitted. Hurray!', + mainWebsiteUrl: '#', + downloadPDFText: 'Download a PDF of your submitted answers', + }, + parameters: { + reactRouter: { + routeState: { + status: 'failed', + userAction: 'exception', + }, + }, + }, + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + expect(canvas.getByText('Bevestiging: OF-1234')).toBeVisible(); + + const errorNode = canvas.queryByText(/^De betaling is mislukt./i); + expect(errorNode).not.toBeNull(); + expect(errorNode).toHaveClass('utrecht-alert__message'); + }, }; diff --git a/src/components/postCompletionViews/viewsWithPolling.stories.js b/src/components/postCompletionViews/viewsWithPolling.stories.js index 93495c62f..bbf6da683 100644 --- a/src/components/postCompletionViews/viewsWithPolling.stories.js +++ b/src/components/postCompletionViews/viewsWithPolling.stories.js @@ -52,7 +52,7 @@ export const withoutPayment = { export const withPayment = { parameters: { reactRouter: { - routeState: {status: 'completed'}, + routeState: {status: 'completed', userAction: 'accept'}, }, }, }; From 2240b0afecaabc03133531a3d869b528b5f9709d Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Fri, 5 Jan 2024 15:32:11 +0100 Subject: [PATCH 5/8] :sparkles: [open-formulieren/open-forms#3726] Support hash based routing for payment --- src/components/routingActions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/routingActions.js b/src/components/routingActions.js index 28b9fad6d..32a94b68f 100644 --- a/src/components/routingActions.js +++ b/src/components/routingActions.js @@ -24,6 +24,11 @@ export const getRedirectParams = (action, actionParams) => { path: `stap/${actionParams.step_slug}`, query: new URLSearchParams({submission_uuid: actionParams.submission_uuid}), }; + case 'payment': + return { + path: 'betalen', + query: new URLSearchParams(actionParams), + }; default: return {}; } From 398400211826ba1a7adead453a25795707816337 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Fri, 5 Jan 2024 15:32:42 +0100 Subject: [PATCH 6/8] :white_check_mark: [open-formulieren/open-forms#3726] Test payment with hash based routing --- src/sdk.spec.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/sdk.spec.js b/src/sdk.spec.js index 128895a09..c2ff4e1e6 100644 --- a/src/sdk.spec.js +++ b/src/sdk.spec.js @@ -199,6 +199,12 @@ describe('OpenForm', () => { )}&unrelated_q=1`, 'http://localhost/base-path/?unrelated_q=1#/stap/step-1?submission_uuid=abc', ], + [ + `/base-path/?_of_action=payment&_of_action_params=${encodeURIComponent( + JSON.stringify({of_payment_status: 'completed'}) + )}&unrelated_q=1`, + 'http://localhost/base-path/?unrelated_q=1#/betalen?of_payment_status=completed', + ], // Without a base path: [ // Omitting submission_uuid for simplicity @@ -223,6 +229,12 @@ describe('OpenForm', () => { )}&unrelated_q=1`, 'http://localhost/?unrelated_q=1#/stap/step-1?submission_uuid=abc', ], + [ + `/?_of_action=payment&_of_action_params=${encodeURIComponent( + JSON.stringify({of_payment_status: 'completed'}) + )}&unrelated_q=1`, + 'http://localhost/?unrelated_q=1#/betalen?of_payment_status=completed', + ], ])( 'should handle action redirects correctly (hash based routing)', async (initialUrl, expected) => { From 2189d4889c72c96072f27a718c7449072311497d Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 8 Jan 2024 15:13:50 +0100 Subject: [PATCH 7/8] :globe_with_meridians: [open-formulieren/open-forms#3726] Update translations --- src/i18n/compiled/en.json | 78 +++++++++++++++++++-------------------- src/i18n/compiled/nl.json | 78 +++++++++++++++++++-------------------- src/i18n/messages/en.json | 55 +++++++++++++-------------- src/i18n/messages/nl.json | 55 +++++++++++++-------------- 4 files changed, 126 insertions(+), 140 deletions(-) diff --git a/src/i18n/compiled/en.json b/src/i18n/compiled/en.json index 4c78bd5bd..2db3c9030 100644 --- a/src/i18n/compiled/en.json +++ b/src/i18n/compiled/en.json @@ -141,6 +141,12 @@ "value": "You must declare the form to be filled out truthfully before submitting" } ], + "7+0/Hz": [ + { + "type": 0, + "value": "Betalen" + } + ], "7Fk8NY": [ { "type": 1, @@ -339,6 +345,12 @@ "value": "Check and confirm" } ], + "ATXTTM": [ + { + "type": 0, + "value": "A payment is required for this product." + } + ], "Aaq2GF": [ { "type": 0, @@ -777,6 +789,16 @@ "value": "isApplicable" } ], + "LiRh7j": [ + { + "type": 0, + "value": "Confirmation: " + }, + { + "type": 1, + "value": "reference" + } + ], "LsgvKh": [ { "type": 0, @@ -807,6 +829,12 @@ "value": "m" } ], + "MJva3t": [ + { + "type": 0, + "value": "Payment" + } + ], "P194bo": [ { "type": 0, @@ -821,12 +849,6 @@ "value": "\"." } ], - "PCFPnE": [ - { - "type": 0, - "value": "Payment is required for this product" - } - ], "PCv4sQ": [ { "type": 0, @@ -1043,12 +1065,6 @@ "value": "Your payment has been received." } ], - "WVmmME": [ - { - "type": 0, - "value": "Payment overview" - } - ], "WWvqcz": [ { "type": 0, @@ -1087,12 +1103,6 @@ "value": "Choose language" } ], - "YdeY1f": [ - { - "type": 0, - "value": "Back to form start" - } - ], "YpCkBs": [ { "type": 0, @@ -1181,16 +1191,6 @@ "value": "Contact details" } ], - "bCGJi9": [ - { - "type": 0, - "value": "Confirmation: " - }, - { - "type": 1, - "value": "reference" - } - ], "bgfiRG": [ { "type": 0, @@ -1251,12 +1251,6 @@ "value": "Confirmation" } ], - "fwS7yY": [ - { - "type": 0, - "value": "Payment overview" - } - ], "g1jgtg": [ { "type": 0, @@ -1527,6 +1521,16 @@ "value": "Continue existing submission" } ], + "m1nQlh": [ + { + "type": 0, + "value": "Your reference number is: " + }, + { + "type": 1, + "value": "reference" + } + ], "mMYAWl": [ { "type": 0, @@ -1791,12 +1795,6 @@ "value": "Not co-signed" } ], - "tIN0Is": [ - { - "type": 0, - "value": "We can't display any payment information - did you maybe get here by accident?" - } - ], "tUgRfj": [ { "type": 0, diff --git a/src/i18n/compiled/nl.json b/src/i18n/compiled/nl.json index e3d0eabe7..e957f2b07 100644 --- a/src/i18n/compiled/nl.json +++ b/src/i18n/compiled/nl.json @@ -141,6 +141,12 @@ "value": "U moet verklaren dat het formulier naar waarheid ingevuld is om door te gaan" } ], + "7+0/Hz": [ + { + "type": 0, + "value": "Betalen" + } + ], "7Fk8NY": [ { "type": 1, @@ -339,6 +345,12 @@ "value": "Controleer en bevestig" } ], + "ATXTTM": [ + { + "type": 0, + "value": "Voor dit product is betaling vereist." + } + ], "Aaq2GF": [ { "type": 0, @@ -777,6 +789,16 @@ "value": "isApplicable" } ], + "LiRh7j": [ + { + "type": 0, + "value": "Bevestiging: " + }, + { + "type": 1, + "value": "reference" + } + ], "LsgvKh": [ { "type": 0, @@ -807,6 +829,12 @@ "value": "m" } ], + "MJva3t": [ + { + "type": 0, + "value": "Betalen" + } + ], "P194bo": [ { "type": 0, @@ -821,12 +849,6 @@ "value": "\"." } ], - "PCFPnE": [ - { - "type": 0, - "value": "Voor dit product is betaling vereist" - } - ], "PCv4sQ": [ { "type": 0, @@ -1043,12 +1065,6 @@ "value": "Uw betaling is ontvangen." } ], - "WVmmME": [ - { - "type": 0, - "value": "Betaaloverzicht" - } - ], "WWvqcz": [ { "type": 0, @@ -1087,12 +1103,6 @@ "value": "Taal kiezen" } ], - "YdeY1f": [ - { - "type": 0, - "value": "Afsluiten" - } - ], "YpCkBs": [ { "type": 0, @@ -1185,16 +1195,6 @@ "value": "Uw gegevens" } ], - "bCGJi9": [ - { - "type": 0, - "value": "Bevestiging: " - }, - { - "type": 1, - "value": "reference" - } - ], "bgfiRG": [ { "type": 0, @@ -1255,12 +1255,6 @@ "value": "Bevestiging" } ], - "fwS7yY": [ - { - "type": 0, - "value": "Betaaloverzicht" - } - ], "g1jgtg": [ { "type": 0, @@ -1531,6 +1525,16 @@ "value": "Doorgaan met bestaande formuliergegevens" } ], + "m1nQlh": [ + { + "type": 0, + "value": "Uw referentie nummer is: " + }, + { + "type": 1, + "value": "reference" + } + ], "mMYAWl": [ { "type": 0, @@ -1795,12 +1799,6 @@ "value": "Niet mede-ondertekend" } ], - "tIN0Is": [ - { - "type": 0, - "value": "We kunnen geen betaalinformatie tonen - bent u per ongeluk op deze pagina gekomen?" - } - ], "tUgRfj": [ { "type": 0, diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 6c8a778a0..e3db55a7a 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -84,6 +84,11 @@ "description": "Warning statement of truth not checked when submitting", "originalDefault": "You must declare the form to be filled out truthfully before submitting" }, + "7+0/Hz": { + "defaultMessage": "Betalen", + "description": "On succesful completion title but payment required", + "originalDefault": "Betalen" + }, "7Fk8NY": { "defaultMessage": "{name}: {amount}x", "description": "Product summary on appointments location and time step", @@ -149,6 +154,11 @@ "description": "Check overview and confirm", "originalDefault": "Check and confirm" }, + "ATXTTM": { + "defaultMessage": "A payment is required for this product.", + "description": "Payment request text", + "originalDefault": "A payment is required for this product." + }, "Aaq2GF": { "defaultMessage": "Sorry, there are no available dates for your appointment. Please try again later.", "description": "Appoinments: message shown for no available dates at all", @@ -354,6 +364,11 @@ "description": "Step label in progress indicator", "originalDefault": "{isApplicable, select, false {{label} (n/a)} other {{label}} }" }, + "LiRh7j": { + "defaultMessage": "Confirmation: {reference}", + "description": "Confirmation page title", + "originalDefault": "Confirmation: {reference}" + }, "LsgvKh": { "defaultMessage": "Postcode", "description": "Label for addressNL postcode input", @@ -379,16 +394,16 @@ "description": "Placeholder for month part of a date", "originalDefault": "m" }, + "MJva3t": { + "defaultMessage": "Payment", + "description": "Payment page title", + "originalDefault": "Payment" + }, "P194bo": { "defaultMessage": "Invalid input: must start with \"{startsWith}\".", "description": "ZOD 'invalid_string' error message, with startsWith", "originalDefault": "Invalid input: must start with \"{startsWith}\"." }, - "PCFPnE": { - "defaultMessage": "Payment is required for this product", - "description": "Payment required info text", - "originalDefault": "Payment is required for this product" - }, "PCv4sQ": { "defaultMessage": "House number", "description": "Label for addressNL houseNumber input", @@ -469,11 +484,6 @@ "description": "payment completed status", "originalDefault": "Your payment has been received." }, - "WVmmME": { - "defaultMessage": "Payment overview", - "description": "Payment overview title", - "originalDefault": "Payment overview" - }, "WWvqcz": { "defaultMessage": "Invalid date", "description": "ZOD 'invalid_date' error message", @@ -499,11 +509,6 @@ "description": "Language selection heading", "originalDefault": "Choose language" }, - "YdeY1f": { - "defaultMessage": "Back to form start", - "description": "return to form start button", - "originalDefault": "Back to form start" - }, "YpCkBs": { "defaultMessage": "Invalid discriminator value. Expected {expected}.", "description": "ZOD 'invalid_union_discriminator' error message", @@ -549,11 +554,6 @@ "description": "Appointments: contact details step title", "originalDefault": "Contact details" }, - "bCGJi9": { - "defaultMessage": "Confirmation: {reference}", - "description": "On succesful completion title", - "originalDefault": "Confirmation: {reference}" - }, "bgfiRG": { "defaultMessage": "You've started the payment process.", "description": "payment started status", @@ -604,11 +604,6 @@ "description": "Confirmation page title", "originalDefault": "Confirmation" }, - "fwS7yY": { - "defaultMessage": "Payment overview", - "description": "Payment overview page title", - "originalDefault": "Payment overview" - }, "g1jgtg": { "defaultMessage": "Confirm products", "description": "Appointments products step: next step text", @@ -744,6 +739,11 @@ "description": "Continue existing submission button label", "originalDefault": "Continue existing submission" }, + "m1nQlh": { + "defaultMessage": "Your reference number is: {reference}", + "description": "Submission reference text", + "originalDefault": "Your reference number is: {reference}" + }, "mMYAWl": { "defaultMessage": "Your session is about to expire {delta}. Extend your session if you wish to continue.", "description": "Session expiry warning message (in modal)", @@ -824,11 +824,6 @@ "description": "Not co-signed (summary) message", "originalDefault": "Not co-signed" }, - "tIN0Is": { - "defaultMessage": "We can't display any payment information - did you maybe get here by accident?", - "description": "Payment overview missing state error message", - "originalDefault": "We can't display any payment information - did you maybe get here by accident?" - }, "tUgRfj": { "defaultMessage": "Logging out failed, please try again later", "description": "Modal logging out failed message", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index cfec7234c..e26f2235a 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -84,6 +84,11 @@ "description": "Warning statement of truth not checked when submitting", "originalDefault": "You must declare the form to be filled out truthfully before submitting" }, + "7+0/Hz": { + "defaultMessage": "Betalen", + "description": "On succesful completion title but payment required", + "originalDefault": "Betalen" + }, "7Fk8NY": { "defaultMessage": "{name}: {amount}x", "description": "Product summary on appointments location and time step", @@ -150,6 +155,11 @@ "description": "Check overview and confirm", "originalDefault": "Check and confirm" }, + "ATXTTM": { + "defaultMessage": "Voor dit product is betaling vereist.", + "description": "Payment request text", + "originalDefault": "A payment is required for this product." + }, "Aaq2GF": { "defaultMessage": "Sorry, er zijn geen datums beschikbaar voor uw afspraak. Probeer het later opnieuw.", "description": "Appoinments: message shown for no available dates at all", @@ -357,6 +367,11 @@ "description": "Step label in progress indicator", "originalDefault": "{isApplicable, select, false {{label} (n/a)} other {{label}} }" }, + "LiRh7j": { + "defaultMessage": "Bevestiging: {reference}", + "description": "Confirmation page title", + "originalDefault": "Confirmation: {reference}" + }, "LsgvKh": { "defaultMessage": "Postcode", "description": "Label for addressNL postcode input", @@ -384,16 +399,16 @@ "isTranslated": true, "originalDefault": "m" }, + "MJva3t": { + "defaultMessage": "Betalen", + "description": "Payment page title", + "originalDefault": "Payment" + }, "P194bo": { "defaultMessage": "De ingevoerde waarde moet beginnen met \"{startsWith}\".", "description": "ZOD 'invalid_string' error message, with startsWith", "originalDefault": "Invalid input: must start with \"{startsWith}\"." }, - "PCFPnE": { - "defaultMessage": "Voor dit product is betaling vereist", - "description": "Payment required info text", - "originalDefault": "Payment is required for this product" - }, "PCv4sQ": { "defaultMessage": "Huisnummer", "description": "Label for addressNL houseNumber input", @@ -475,11 +490,6 @@ "description": "payment completed status", "originalDefault": "Your payment has been received." }, - "WVmmME": { - "defaultMessage": "Betaaloverzicht", - "description": "Payment overview title", - "originalDefault": "Payment overview" - }, "WWvqcz": { "defaultMessage": "Ongeldige datum", "description": "ZOD 'invalid_date' error message", @@ -505,11 +515,6 @@ "description": "Language selection heading", "originalDefault": "Choose language" }, - "YdeY1f": { - "defaultMessage": "Afsluiten", - "description": "return to form start button", - "originalDefault": "Back to form start" - }, "YpCkBs": { "defaultMessage": "Ongeldige discriminatorwaarde - deze moet {expected} zijn.", "description": "ZOD 'invalid_union_discriminator' error message", @@ -556,11 +561,6 @@ "description": "Appointments: contact details step title", "originalDefault": "Contact details" }, - "bCGJi9": { - "defaultMessage": "Bevestiging: {reference}", - "description": "On succesful completion title", - "originalDefault": "Confirmation: {reference}" - }, "bgfiRG": { "defaultMessage": "U hebt de betaling gestart.", "description": "payment started status", @@ -611,11 +611,6 @@ "description": "Confirmation page title", "originalDefault": "Confirmation" }, - "fwS7yY": { - "defaultMessage": "Betaaloverzicht", - "description": "Payment overview page title", - "originalDefault": "Payment overview" - }, "g1jgtg": { "defaultMessage": "Bevestig producten", "description": "Appointments products step: next step text", @@ -755,6 +750,11 @@ "description": "Continue existing submission button label", "originalDefault": "Continue existing submission" }, + "m1nQlh": { + "defaultMessage": "Uw referentie nummer is: {reference}", + "description": "Submission reference text", + "originalDefault": "Your reference number is: {reference}" + }, "mMYAWl": { "defaultMessage": "Uw sessie staat op het punt om te vervallen {delta}. Verleng uw sessie indien u door wenst te gaan.", "description": "Session expiry warning message (in modal)", @@ -835,11 +835,6 @@ "description": "Not co-signed (summary) message", "originalDefault": "Not co-signed" }, - "tIN0Is": { - "defaultMessage": "We kunnen geen betaalinformatie tonen - bent u per ongeluk op deze pagina gekomen?", - "description": "Payment overview missing state error message", - "originalDefault": "We can't display any payment information - did you maybe get here by accident?" - }, "tUgRfj": { "defaultMessage": "Het uitloggen is mislukt. Probeer het later opnieuw.", "description": "Modal logging out failed message", From 3622c591056f03c134ce95e57ae10fc6920c54b0 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Wed, 10 Jan 2024 11:35:06 +0100 Subject: [PATCH 8/8] :ok_hand: [open-formulieren/open-forms#3726] PR Feedback --- src/api-mocks/submissions.js | 4 ++-- src/components/Form.js | 2 +- .../ConfirmationView.js | 0 .../ConfirmationView.stories.js | 0 .../PaymentForm.js | 0 .../PostCompletionView.js | 0 .../PostCompletionView.stories.js | 1 + .../StartPayment.js | 0 .../StartPaymentView.js | 0 .../StartPaymentView.stories.js | 2 +- .../StatusUrlPoller.js | 0 .../{postCompletionViews => PostCompletionViews}/index.js | 0 .../index.mdx} | 8 ++++++-- .../viewsWithPolling.stories.js | 0 .../ProgressIndicator/progressIndicator.spec.js | 6 +++--- .../appointments/CreateAppointment/Confirmation.js | 2 +- src/story-utils/decorators.js | 2 +- 17 files changed, 16 insertions(+), 11 deletions(-) rename src/components/{postCompletionViews => PostCompletionViews}/ConfirmationView.js (100%) rename src/components/{postCompletionViews => PostCompletionViews}/ConfirmationView.stories.js (100%) rename src/components/{postCompletionViews => PostCompletionViews}/PaymentForm.js (100%) rename src/components/{postCompletionViews => PostCompletionViews}/PostCompletionView.js (100%) rename src/components/{postCompletionViews => PostCompletionViews}/PostCompletionView.stories.js (94%) rename src/components/{postCompletionViews => PostCompletionViews}/StartPayment.js (100%) rename src/components/{postCompletionViews => PostCompletionViews}/StartPaymentView.js (100%) rename src/components/{postCompletionViews => PostCompletionViews}/StartPaymentView.stories.js (96%) rename src/components/{postCompletionViews => PostCompletionViews}/StatusUrlPoller.js (100%) rename src/components/{postCompletionViews => PostCompletionViews}/index.js (100%) rename src/components/{postCompletionViews/postCompletionViews.mdx => PostCompletionViews/index.mdx} (74%) rename src/components/{postCompletionViews => PostCompletionViews}/viewsWithPolling.stories.js (100%) diff --git a/src/api-mocks/submissions.js b/src/api-mocks/submissions.js index 7407b744b..ae7aece9d 100644 --- a/src/api-mocks/submissions.js +++ b/src/api-mocks/submissions.js @@ -138,8 +138,8 @@ export const mockSubmissionProcessingStatusErrorGet = rest.get( ); export const mockSubmissionPaymentStartGet = rest.post( - `${BASE_URL}payment/4b0e86a8-dc5f-41cc-b812-c89857b9355b/demo/start`, + `${BASE_URL}payment/:uuid/demo/start`, (req, res, ctx) => { - return res(ctx.json({data: {method: 'get', action: 'http://example.com'}})); + return res(ctx.json({data: {method: 'get', action: 'https://example.com'}})); } ); diff --git a/src/components/Form.js b/src/components/Form.js index 9193b95ac..63211ae6d 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -10,12 +10,12 @@ import ErrorBoundary from 'components/Errors/ErrorBoundary'; import FormStart from 'components/FormStart'; import FormStep from 'components/FormStep'; import Loader from 'components/Loader'; +import {ConfirmationView, StartPaymentView} from 'components/PostCompletionViews'; import ProgressIndicator from 'components/ProgressIndicator'; import RequireSubmission from 'components/RequireSubmission'; import {RequireSession} from 'components/Sessions'; import SubmissionSummary from 'components/Summary'; import {START_FORM_QUERY_PARAM} from 'components/constants'; -import {ConfirmationView, StartPaymentView} from 'components/postCompletionViews'; import {findNextApplicableStep} from 'components/utils'; import {createSubmission, flagActiveSubmission, flagNoActiveSubmission} from 'data/submissions'; import useAutomaticRedirect from 'hooks/useAutomaticRedirect'; diff --git a/src/components/postCompletionViews/ConfirmationView.js b/src/components/PostCompletionViews/ConfirmationView.js similarity index 100% rename from src/components/postCompletionViews/ConfirmationView.js rename to src/components/PostCompletionViews/ConfirmationView.js diff --git a/src/components/postCompletionViews/ConfirmationView.stories.js b/src/components/PostCompletionViews/ConfirmationView.stories.js similarity index 100% rename from src/components/postCompletionViews/ConfirmationView.stories.js rename to src/components/PostCompletionViews/ConfirmationView.stories.js diff --git a/src/components/postCompletionViews/PaymentForm.js b/src/components/PostCompletionViews/PaymentForm.js similarity index 100% rename from src/components/postCompletionViews/PaymentForm.js rename to src/components/PostCompletionViews/PaymentForm.js diff --git a/src/components/postCompletionViews/PostCompletionView.js b/src/components/PostCompletionViews/PostCompletionView.js similarity index 100% rename from src/components/postCompletionViews/PostCompletionView.js rename to src/components/PostCompletionViews/PostCompletionView.js diff --git a/src/components/postCompletionViews/PostCompletionView.stories.js b/src/components/PostCompletionViews/PostCompletionView.stories.js similarity index 94% rename from src/components/postCompletionViews/PostCompletionView.stories.js rename to src/components/PostCompletionViews/PostCompletionView.stories.js index 0aed6f1d9..53cefb592 100644 --- a/src/components/postCompletionViews/PostCompletionView.stories.js +++ b/src/components/PostCompletionViews/PostCompletionView.stories.js @@ -4,6 +4,7 @@ import PostCompletionView from './PostCompletionView'; export default { title: 'Private API / Post completion views ', + component: PostCompletionView, render: ({body, ...args}) => {body}} />, }; diff --git a/src/components/postCompletionViews/StartPayment.js b/src/components/PostCompletionViews/StartPayment.js similarity index 100% rename from src/components/postCompletionViews/StartPayment.js rename to src/components/PostCompletionViews/StartPayment.js diff --git a/src/components/postCompletionViews/StartPaymentView.js b/src/components/PostCompletionViews/StartPaymentView.js similarity index 100% rename from src/components/postCompletionViews/StartPaymentView.js rename to src/components/PostCompletionViews/StartPaymentView.js diff --git a/src/components/postCompletionViews/StartPaymentView.stories.js b/src/components/PostCompletionViews/StartPaymentView.stories.js similarity index 96% rename from src/components/postCompletionViews/StartPaymentView.stories.js rename to src/components/PostCompletionViews/StartPaymentView.stories.js index cf4e98917..a2e960a80 100644 --- a/src/components/postCompletionViews/StartPaymentView.stories.js +++ b/src/components/PostCompletionViews/StartPaymentView.stories.js @@ -9,7 +9,7 @@ import {StartPaymentViewDisplay} from './StartPaymentView'; export default { title: 'Private API / Post completion views / Start payment', - render: StartPaymentViewDisplay, + component: StartPaymentViewDisplay, decorators: [withSubmissionPollInfo], args: { paymentUrl: `${BASE_URL}payment/4b0e86a8-dc5f-41cc-b812-c89857b9355b/demo/start`, diff --git a/src/components/postCompletionViews/StatusUrlPoller.js b/src/components/PostCompletionViews/StatusUrlPoller.js similarity index 100% rename from src/components/postCompletionViews/StatusUrlPoller.js rename to src/components/PostCompletionViews/StatusUrlPoller.js diff --git a/src/components/postCompletionViews/index.js b/src/components/PostCompletionViews/index.js similarity index 100% rename from src/components/postCompletionViews/index.js rename to src/components/PostCompletionViews/index.js diff --git a/src/components/postCompletionViews/postCompletionViews.mdx b/src/components/PostCompletionViews/index.mdx similarity index 74% rename from src/components/postCompletionViews/postCompletionViews.mdx rename to src/components/PostCompletionViews/index.mdx index fa5d15d4d..fa667df02 100644 --- a/src/components/postCompletionViews/postCompletionViews.mdx +++ b/src/components/PostCompletionViews/index.mdx @@ -22,6 +22,10 @@ When no payment is required, after submitting the form the user sees this view: ## `ConfirmationViewDisplay` with payment required -After payment, the user is redirected to this view: +After a successful payment, the user is redirected to this view: - + + +While if something went wrong or the payment was cancelled, the user sees: + + diff --git a/src/components/postCompletionViews/viewsWithPolling.stories.js b/src/components/PostCompletionViews/viewsWithPolling.stories.js similarity index 100% rename from src/components/postCompletionViews/viewsWithPolling.stories.js rename to src/components/PostCompletionViews/viewsWithPolling.stories.js diff --git a/src/components/ProgressIndicator/progressIndicator.spec.js b/src/components/ProgressIndicator/progressIndicator.spec.js index d419bc8e3..6ad2c7dd8 100644 --- a/src/components/ProgressIndicator/progressIndicator.spec.js +++ b/src/components/ProgressIndicator/progressIndicator.spec.js @@ -80,12 +80,12 @@ describe('The progress indicator component', () => { it('displays the available submission/form steps and hardcoded steps (with payment)', async () => { mswServer.use(mockSubmissionPost(buildSubmission())); const user = userEvent.setup({delay: null}); - const form = getDefaultFactory({...FORM_DEFAULTS, paymentRequired: true})(); + const form = buildForm({paymentRequired: true}); renderApp(form); const startFormLink = await screen.findByRole('link', {name: 'Start page'}); - user.click(startFormLink); + await user.click(startFormLink); const progressIndicator = await screen.findByText('Progress'); expect(progressIndicator).toBeVisible(); @@ -108,7 +108,7 @@ describe('The progress indicator component', () => { renderApp(form); const startFormLink = await screen.findByRole('link', {name: 'Start page'}); - user.click(startFormLink); + await user.click(startFormLink); const progressIndicatorSteps = screen.getAllByRole('listitem'); diff --git a/src/components/appointments/CreateAppointment/Confirmation.js b/src/components/appointments/CreateAppointment/Confirmation.js index bbf4fab6a..d4327c235 100644 --- a/src/components/appointments/CreateAppointment/Confirmation.js +++ b/src/components/appointments/CreateAppointment/Confirmation.js @@ -1,7 +1,7 @@ import React from 'react'; import {useNavigate, useSearchParams} from 'react-router-dom'; -import {ConfirmationView} from 'components/postCompletionViews'; +import {ConfirmationView} from 'components/PostCompletionViews'; import useFormContext from 'hooks/useFormContext'; import {useCreateAppointmentContext} from './CreateAppointmentState'; diff --git a/src/story-utils/decorators.js b/src/story-utils/decorators.js index ba877e741..c6246d5da 100644 --- a/src/story-utils/decorators.js +++ b/src/story-utils/decorators.js @@ -7,7 +7,7 @@ import {ConfigContext, FormContext} from 'Context'; import {BASE_URL, buildForm} from 'api-mocks'; import Card from 'components/Card'; import {LiteralsProvider} from 'components/Literal'; -import {SubmissionStatusContext} from 'components/postCompletionViews'; +import {SubmissionStatusContext} from 'components/PostCompletionViews'; export const ConfigDecorator = (Story, {parameters}) => { const defaults = {