diff --git a/api.planx.uk/modules/send/email/index.ts b/api.planx.uk/modules/send/email/index.ts index 5e5e08f990..c23df87e13 100644 --- a/api.planx.uk/modules/send/email/index.ts +++ b/api.planx.uk/modules/send/email/index.ts @@ -33,6 +33,7 @@ export const sendToEmail: SendIntegrationController = async ( // Get the applicant email and flow slug associated with the session const { email, flow } = await getSessionEmailDetailsById(sessionId); const flowName = flow.name; + const serviceURL = `${process.env.EDITOR_URL_EXT}/${localAuthority}/${flow.slug}/${sessionId}`; // Prepare email template const config: EmailSubmissionNotifyConfig = { @@ -40,7 +41,7 @@ export const sendToEmail: SendIntegrationController = async ( serviceName: flowName, sessionId, applicantEmail: email, - downloadLink: `${process.env.API_URL_EXT}/download-application-files/${sessionId}?email=${teamSettings.submissionEmail}&localAuthority=${localAuthority}`, + downloadLink: `${serviceURL}/download-application`, ...teamSettings, }, }; diff --git a/api.planx.uk/modules/send/email/service.ts b/api.planx.uk/modules/send/email/service.ts index 67425a1509..7148d4cfb3 100644 --- a/api.planx.uk/modules/send/email/service.ts +++ b/api.planx.uk/modules/send/email/service.ts @@ -1,9 +1,9 @@ -import { gql } from "graphql-request"; -import { $api } from "../../../client/index.js"; import type { Session, TeamContactSettings, } from "@opensystemslab/planx-core/types"; +import { gql } from "graphql-request"; +import { $api } from "../../../client/index.js"; import type { EmailSubmissionNotifyConfig } from "../../../types.js"; interface GetTeamEmailSettings { diff --git a/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx b/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx index 06b2a3c9c1..a79823f63f 100644 --- a/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx +++ b/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx @@ -38,7 +38,7 @@ export const ConfirmEmail: React.FC<{ + /> + /> @@ -72,7 +72,7 @@ export const ConfirmEmail: React.FC<{ onChange={formik.handleChange} type="email" value={formik.values.confirmEmail} - > + /> @@ -105,7 +105,7 @@ const SaveAndReturn: React.FC<{ children: React.ReactNode }> = ({ {isEmailCaptured || isContentPage ? ( children ) : ( - + )} ); diff --git a/editor.planx.uk/src/pages/SubmissionDownload/VerifySubmissionEmail.stories.tsx b/editor.planx.uk/src/pages/SubmissionDownload/VerifySubmissionEmail.stories.tsx new file mode 100644 index 0000000000..77993fd961 --- /dev/null +++ b/editor.planx.uk/src/pages/SubmissionDownload/VerifySubmissionEmail.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from "@storybook/react"; +import React from "react"; + +import { VerifySubmissionEmail } from "./VerifySubmissionEmail"; + +const meta = { + title: "Design System/Pages/VerifySubmissionEmail", + component: VerifySubmissionEmail, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Basic = { + args: { + params: { sessionId: "1", team: "barking and dagenham" }, + }, + render: (args) => , +} satisfies Story; diff --git a/editor.planx.uk/src/pages/SubmissionDownload/VerifySubmissionEmail.tsx b/editor.planx.uk/src/pages/SubmissionDownload/VerifySubmissionEmail.tsx new file mode 100644 index 0000000000..58ec46febc --- /dev/null +++ b/editor.planx.uk/src/pages/SubmissionDownload/VerifySubmissionEmail.tsx @@ -0,0 +1,121 @@ +import Box from "@mui/material/Box"; +import Container from "@mui/material/Container"; +import Typography from "@mui/material/Typography"; +import Card from "@planx/components/shared/Preview/Card"; +import { CardHeader } from "@planx/components/shared/Preview/CardHeader/CardHeader"; +import { SummaryListTable } from "@planx/components/shared/Preview/SummaryList"; +import axios, { isAxiosError } from "axios"; +import DelayedLoadingIndicator from "components/DelayedLoadingIndicator/DelayedLoadingIndicator"; +import { useFormik } from "formik"; +import startCase from "lodash/startCase.js"; +import React, { useState } from "react"; +import InputLabel from "ui/public/InputLabel"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; +import Input from "ui/shared/Input/Input"; +import InputRow from "ui/shared/InputRow"; +import { object, string } from "yup"; + +import { downloadZipFile } from "./helpers/downloadZip"; +import { VerifySubmissionEmailProps } from "./types"; + +export const DOWNLOAD_APPLICATION_FILE_URL = `${ + import.meta.env.VITE_APP_API_URL +}/download-application-files`; + +const verifySubmissionEmailSchema = object({ + email: string().email("Invalid email").required("Email address required"), +}); +export const VerifySubmissionEmail = ({ + params, +}: VerifySubmissionEmailProps): JSX.Element => { + const { sessionId, team, flow } = params; + const [downloadApplicationError, setDownloadApplicationError] = useState(""); + const [loading, setLoading] = useState(false); + + const formik = useFormik({ + initialValues: { + email: "", + }, + onSubmit: async (values, { resetForm }) => { + setDownloadApplicationError(""); + setLoading(true); + const url = `${DOWNLOAD_APPLICATION_FILE_URL}/${sessionId}/?email=${encodeURIComponent( + values.email, + )}&localAuthority=${team}`; + try { + const { data } = await axios.get(url, { + responseType: "arraybuffer", + }); + downloadZipFile(data, { filename: `${flow}-${sessionId}.zip` }); + resetForm(); + setLoading(false); + } catch (error) { + setLoading(false); + if (isAxiosError(error)) { + setDownloadApplicationError( + "Sorry, something went wrong. Please try again.", + ); + resetForm(); + } + console.error(error); + } + }, + validateOnChange: false, + validateOnBlur: false, + validationSchema: verifySubmissionEmailSchema, + }); + return ( + + + Download application + + {loading ? ( + + ) : ( + + + + Application details + + + Session ID + {sessionId} + Local Authority + {startCase(team)} + + + <> + + + + + + + + + + + + )} + + ); +}; diff --git a/editor.planx.uk/src/pages/SubmissionDownload/helpers/downloadZip.tsx b/editor.planx.uk/src/pages/SubmissionDownload/helpers/downloadZip.tsx new file mode 100644 index 0000000000..0be785b652 --- /dev/null +++ b/editor.planx.uk/src/pages/SubmissionDownload/helpers/downloadZip.tsx @@ -0,0 +1,24 @@ +type ZipFileName = `${string}.zip`; + +export const downloadZipFile = ( + data: string, + options: { filename: ZipFileName }, +) => { + if (!data) { + console.error("No data to download"); + return; + } + const blobData = new Blob([data], { type: "application/zip" }); + try { + const href = URL.createObjectURL(blobData); + const link = document.createElement("a"); + link.href = href; + link.setAttribute("download", options.filename); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(href); + } catch (error) { + console.error("Error creating object URL:", error); + } +}; diff --git a/editor.planx.uk/src/pages/SubmissionDownload/tests/VerifySubmissionEmail.test.tsx b/editor.planx.uk/src/pages/SubmissionDownload/tests/VerifySubmissionEmail.test.tsx new file mode 100644 index 0000000000..6ec9f62bf1 --- /dev/null +++ b/editor.planx.uk/src/pages/SubmissionDownload/tests/VerifySubmissionEmail.test.tsx @@ -0,0 +1,35 @@ +import { screen } from "@testing-library/react"; +import React from "react"; +import { setup } from "testUtils"; + +import { VerifySubmissionEmail } from "../VerifySubmissionEmail"; + +describe("when the VerifySubmissionEmail component renders", () => { + it("displays the email address input", () => { + setup(); + + expect( + screen.queryByText("Verify your submission email address"), + ).toBeInTheDocument(); + expect( + screen.queryByLabelText("Submission email address"), + ).toBeInTheDocument(); + }); + it.todo("should not display an error message"); + it.todo( + "shows sessionId and local authority in the application details table", + ); +}); + +describe("when the user submits a correct email address", () => { + it.todo("displays visual feedback to the user"); + it.todo("downloads the application file"); +}); + +describe("when the user submits an incorrect email address", () => { + it.todo("displays a suitable error message"); +}); + +describe("when user submits an email address and there is a server-side issue", () => { + it.todo("displays a suitable error message"); +}); diff --git a/editor.planx.uk/src/pages/SubmissionDownload/types.ts b/editor.planx.uk/src/pages/SubmissionDownload/types.ts new file mode 100644 index 0000000000..23c2e41687 --- /dev/null +++ b/editor.planx.uk/src/pages/SubmissionDownload/types.ts @@ -0,0 +1,3 @@ +export interface VerifySubmissionEmailProps { + params: Record; +} diff --git a/editor.planx.uk/src/routes/index.tsx b/editor.planx.uk/src/routes/index.tsx index 73c77abf78..425e30f4b3 100644 --- a/editor.planx.uk/src/routes/index.tsx +++ b/editor.planx.uk/src/routes/index.tsx @@ -57,6 +57,12 @@ const editorRoutes = mount({ ), }); +const loadSendToEmailRoutes = () => + compose( + withView(loadingView), + lazy(() => import("./sendToEmailSubmissions")), + ); + const loadPayRoutes = () => compose( withView(loadingView), @@ -100,5 +106,7 @@ export default isPreviewOnlyDomain "/:team/:flow/preview": loadPreviewRoutes(), // loads current draft flow and latest published external portals, or throws Not Found if any external portal is unpublished "/:team/:flow/draft": loadDraftRoutes(), // loads current draft flow and draft external portals "/:team/:flow/pay": loadPayRoutes(), + "/:team/:flow/:sessionId/download-application": loadSendToEmailRoutes(), + "*": editorRoutes, }); diff --git a/editor.planx.uk/src/routes/sendToEmailSubmissions.tsx b/editor.planx.uk/src/routes/sendToEmailSubmissions.tsx new file mode 100644 index 0000000000..82f0d51a4f --- /dev/null +++ b/editor.planx.uk/src/routes/sendToEmailSubmissions.tsx @@ -0,0 +1,28 @@ +import { compose, map, mount, route, withData, withView } from "navi"; +import { VerifySubmissionEmail } from "pages/SubmissionDownload/VerifySubmissionEmail"; +import React from "react"; + +import { makeTitle, validateTeamRoute } from "./utils"; +import standaloneView from "./views/standalone"; + +const routes = compose( + withData(async (req) => ({ + mountpath: req.mountpath, + })), + + withView(async (req) => { + await validateTeamRoute(req); + return await standaloneView(req); + }), + + mount({ + "/": map((req) => { + return route({ + title: makeTitle("Download application"), + view: , + }); + }), + }), +); + +export default routes;