Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: verify email before downloading application #4000

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
40c1f2d
Redirect download link to basic verify email page
jamdelion Nov 21, 2024
766abcc
try to download application file on email submit - no verification
jamdelion Nov 21, 2024
56f08cc
Merge branch 'main' into jh/verify-email-page
jamdelion Nov 25, 2024
aff54ca
Fix global import problem
jamdelion Nov 25, 2024
e660026
Works with permission error
jamdelion Nov 25, 2024
99a447c
Use existing endpoint to check email address
jamdelion Nov 25, 2024
1c07974
Move helper to utils file
jamdelion Nov 25, 2024
08e62da
Use react query
jamdelion Nov 25, 2024
e39f856
Lint fix
jamdelion Nov 25, 2024
2ad7978
Tidy up
jamdelion Nov 25, 2024
139c1d9
Add test todos
jamdelion Nov 26, 2024
5de9aa2
Fix typing
jamdelion Nov 26, 2024
03454a3
Display errors nicely
jamdelion Nov 27, 2024
4260930
Fix error wrapper type issue
jamdelion Nov 28, 2024
725ea12
Revert moving urlWithParams
jamdelion Dec 2, 2024
fbcca1f
Update wording to be more appropriate
jamdelion Dec 2, 2024
db050b0
Rename files to verifySubmissionEmail
jamdelion Dec 2, 2024
795497a
Rename url to download-application
jamdelion Dec 2, 2024
e7c5171
Merge branch 'main' into jh/verify-email-page
jamdelion Dec 3, 2024
ce3b34a
Replace react-query with axios
jamdelion Dec 3, 2024
810c7e4
Readd loading state
jamdelion Dec 3, 2024
6c183e4
Add application summary
jamdelion Dec 3, 2024
ea18fcf
Add one more test.todo
jamdelion Dec 3, 2024
2147de4
Remove react query
jamdelion Dec 3, 2024
a0e28f8
Remove queryProvider
jamdelion Dec 3, 2024
c58ef33
Remove rq lint plugin
jamdelion Dec 3, 2024
570df3d
Remove rq from test utils
jamdelion Dec 3, 2024
868c820
Use correct zip filename
jamdelion Dec 4, 2024
2b8c421
Move application summary and capitalise team name
jamdelion Dec 10, 2024
60cd604
Wrap verifySubmissionPage in PlanX headers
jamdelion Dec 10, 2024
d35c7da
Revert error typing
jamdelion Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api.planx.uk/modules/send/email/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ 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 = {
personalisation: {
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,
},
};
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/modules/send/email/service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const ConfirmEmail: React.FC<{
<CardHeader
title="Enter your email address"
description="We will use this to save your application so you can come back to it later. We will also email you updates about your application."
></CardHeader>
/>
<InputRow>
<InputLabel label={"Email address"} htmlFor={"email"}>
<Input
Expand All @@ -54,7 +54,7 @@ export const ConfirmEmail: React.FC<{
onChange={formik.handleChange}
type="email"
value={formik.values.email}
></Input>
/>
</InputLabel>
</InputRow>
<InputRow>
Expand All @@ -72,7 +72,7 @@ export const ConfirmEmail: React.FC<{
onChange={formik.handleChange}
type="email"
value={formik.values.confirmEmail}
></Input>
/>
</InputLabel>
</InputRow>
</Card>
Expand Down Expand Up @@ -105,7 +105,7 @@ const SaveAndReturn: React.FC<{ children: React.ReactNode }> = ({
{isEmailCaptured || isContentPage ? (
children
) : (
<ConfirmEmail handleSubmit={handleSubmit}></ConfirmEmail>
<ConfirmEmail handleSubmit={handleSubmit} />
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof VerifySubmissionEmail>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Basic = {
args: {
params: { sessionId: "1", team: "barking and dagenham" },
},
render: (args) => <VerifySubmissionEmail {...args} />,
} satisfies Story;
121 changes: 121 additions & 0 deletions editor.planx.uk/src/pages/SubmissionDownload/VerifySubmissionEmail.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Container maxWidth="contentWrap">
<Typography maxWidth="formWrap" variant="h1" pt={5} gutterBottom>
Download application
</Typography>
{loading ? (
<DelayedLoadingIndicator />
) : (
<Box width="100%">
<Card handleSubmit={formik.handleSubmit}>
<Typography maxWidth="formWrap" variant="h2" gutterBottom>
Application details
</Typography>
<SummaryListTable>
<Box component="dt">Session ID</Box>
<Box component="dd">{sessionId}</Box>
<Box component="dt">Local Authority</Box>
<Box component="dd">{startCase(team)}</Box>
</SummaryListTable>
<ErrorWrapper error={downloadApplicationError}>
<>
<CardHeader
title="Verify your submission email address"
description="We will use this to confirm that you have access to the submission email inbox that is set up for your team. Entering the correct email address will start the file download automatically."
/>

<InputRow>
<InputLabel
label={"Submission email address"}
htmlFor={"email"}
>
<Input
bordered
errorMessage={
formik.touched.email && formik.errors.email
? formik.errors.email
: undefined
}
id="email"
name="email"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
type="email"
value={formik.values.email}
jamdelion marked this conversation as resolved.
Show resolved Hide resolved
/>
</InputLabel>
</InputRow>
</>
</ErrorWrapper>
</Card>
</Box>
)}
</Container>
);
};
Original file line number Diff line number Diff line change
@@ -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);
}
};
Original file line number Diff line number Diff line change
@@ -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(<VerifySubmissionEmail params={{ sessionId: "1" }} />);

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");
});
3 changes: 3 additions & 0 deletions editor.planx.uk/src/pages/SubmissionDownload/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface VerifySubmissionEmailProps {
params: Record<string, string>;
}
8 changes: 8 additions & 0 deletions editor.planx.uk/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ const editorRoutes = mount({
),
});

const loadSendToEmailRoutes = () =>
compose(
withView(loadingView),
lazy(() => import("./sendToEmailSubmissions")),
);

const loadPayRoutes = () =>
compose(
withView(loadingView),
Expand Down Expand Up @@ -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,
});
28 changes: 28 additions & 0 deletions editor.planx.uk/src/routes/sendToEmailSubmissions.tsx
Original file line number Diff line number Diff line change
@@ -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: <VerifySubmissionEmail params={req.params} />,
});
}),
}),
);

export default routes;
Loading