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;