diff --git a/app/(gcforms)/[locale]/layout.tsx b/app/(gcforms)/[locale]/layout.tsx index e975844ba1..0fb8b344e9 100644 --- a/app/(gcforms)/[locale]/layout.tsx +++ b/app/(gcforms)/[locale]/layout.tsx @@ -1,6 +1,7 @@ import { auth } from "@lib/auth"; import { ClientContexts } from "@clientComponents/globals/ClientContexts"; import { ReactHydrationCheck } from "@clientComponents/globals"; +import { NotifyCatcher } from "@lib/notifyCatcher/NotifyCatcher"; import { checkAll } from "@lib/cache/flags"; export default async function Layout({ children }: { children: React.ReactNode }) { @@ -13,6 +14,7 @@ export default async function Layout({ children }: { children: React.ReactNode } {children} + {process.env.APP_ENV === "local" && } ); } diff --git a/lib/integration/notifyConnector.ts b/lib/integration/notifyConnector.ts index 459ee1c4fd..64832301be 100644 --- a/lib/integration/notifyConnector.ts +++ b/lib/integration/notifyConnector.ts @@ -1,5 +1,6 @@ import { GCNotifyConnector, type Personalisation } from "@gcforms/connectors"; import { logMessage } from "@lib/logger"; +import { notifyCatcher } from "@lib/notifyCatcher"; const gcNotifyConnector = GCNotifyConnector.default(process.env.NOTIFY_API_KEY ?? ""); @@ -10,6 +11,21 @@ export const sendEmail = async (email: string, personalisation: Personalisation, return; } + if (process.env.APP_ENV === "local") { + try { + notifyCatcher(email, personalisation); + } catch (e) { + logMessage.error("NotifyCatcher failed to catch the email", e); + + // just log it out + logMessage.info("Development Notify email sending:", "", { email, personalisation }); + logMessage.info("To: " + email); + logMessage.info("Subject: " + personalisation.subject); + logMessage.info("Body: " + personalisation.formResponse); + } + return; + } + const templateId = process.env.TEMPLATE_ID; if (!templateId) { throw new Error("No Notify template ID configured."); diff --git a/lib/notifyCatcher/NotifyCatcher.tsx b/lib/notifyCatcher/NotifyCatcher.tsx new file mode 100644 index 0000000000..37234a1ca3 --- /dev/null +++ b/lib/notifyCatcher/NotifyCatcher.tsx @@ -0,0 +1,175 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +import { fetchMessages, resetMessages } from "./actions"; +import Markdown from "markdown-to-jsx"; + +interface Message { + email: string; + personalisation: { + subject: string; + formResponse: string; + }; +} + +export const NotifyCatcher = () => { + const ref = useRef(null); + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + const [visible, setVisible] = useState(false); + + const getMessages = async () => { + setLoading(true); + const messages = await fetchMessages(); + setMessages(messages); + setLoading(false); + }; + + const open = () => { + getMessages(); + setVisible(true); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setVisible(false); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setVisible(false); + } + }; + + const clearMessages = async () => { + resetMessages(); + setMessages([]); + }; + + useEffect(() => { + getMessages(); + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + return ( + <> + {!visible && ( +
open()} + > + + + + Notify Intercept +
+ )} + {visible && ( +
+ {loading ? ( +
Loading...
+ ) : ( + <> +
+
+ + + +

Notify Intercept

+
+
+ {messages.length > 0 && ( + + )} + + +
+
+
+ {messages.length === 0 && ( +
+ No messages to show +
+ )} + {messages && + messages.map((message, index) => { + return ( +
+
+
To:
+
{message.email}
+
Subject:
+
{message.personalisation.subject}
+
+
+
+ + {message.personalisation.formResponse} + +
+
+
+ ); + })} +
+ + )} +
+ )} + + ); +}; diff --git a/lib/notifyCatcher/actions.ts b/lib/notifyCatcher/actions.ts new file mode 100644 index 0000000000..c4016c0c7e --- /dev/null +++ b/lib/notifyCatcher/actions.ts @@ -0,0 +1,13 @@ +"use server"; +import { getRedisInstance } from "@lib/integration/redisConnector"; + +export const fetchMessages = async () => { + const redis = await getRedisInstance(); + const emails = await redis.lrange("notifyEmails", 0, -1); + return emails.map((email) => JSON.parse(email)); +}; + +export const resetMessages = async () => { + const redis = await getRedisInstance(); + await redis.del("notifyEmails"); +}; diff --git a/lib/notifyCatcher/index.ts b/lib/notifyCatcher/index.ts new file mode 100644 index 0000000000..aa61d4948e --- /dev/null +++ b/lib/notifyCatcher/index.ts @@ -0,0 +1,15 @@ +import { type Personalisation } from "@gcforms/connectors"; +import { getRedisInstance } from "@lib/integration/redisConnector"; +import { logMessage } from "@lib/logger"; + +export const notifyCatcher = async (email: string, personalisation: Personalisation) => { + try { + const redis = await getRedisInstance(); + await redis.lpush("notifyEmails", JSON.stringify({ email, personalisation })); + } catch (e) { + logMessage.info("Development Notify email sending:", "", { email, personalisation }); + logMessage.info("To: " + email); + logMessage.info("Subject: " + personalisation.subject); + logMessage.info("Body: " + personalisation.formResponse); + } +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 2e270ee948..2f8c803f25 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -217,6 +217,10 @@ module.exports = { xxs: { max: "290px" }, }, }, - content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}"], + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./lib/notifyCatcher/**/*.{ts,tsx}", + ], plugins: [], } satisfies Config;