mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-30 03:33:48 -05:00
feat: email package for client side email components (#6986)
This commit is contained in:
@@ -474,7 +474,7 @@
|
||||
"forgot_password_email_subject": "Réinitialise ton mot de passe Formbricks",
|
||||
"forgot_password_email_text": "Vous avez demandé un lien pour changer votre mot de passe. Vous pouvez le faire en cliquant sur le lien ci-dessous :",
|
||||
"hidden_field": "Champ caché",
|
||||
"imprint": "Impressum",
|
||||
"imprint": "Empreinte",
|
||||
"invite_accepted_email_heading": "Salut",
|
||||
"invite_accepted_email_subject": "Vous avez un nouveau membre dans votre organisation !",
|
||||
"invite_accepted_email_text_par1": "Je te fais savoir que",
|
||||
|
||||
@@ -474,7 +474,7 @@
|
||||
"forgot_password_email_subject": "Redefinir sua senha Formbricks",
|
||||
"forgot_password_email_text": "Você pediu um link pra trocar sua senha. Você pode fazer isso clicando no link abaixo:",
|
||||
"hidden_field": "Campo oculto",
|
||||
"imprint": "Impressum",
|
||||
"imprint": "impressão",
|
||||
"invite_accepted_email_heading": "E aí",
|
||||
"invite_accepted_email_subject": "Você tem um novo membro na sua organização!",
|
||||
"invite_accepted_email_text_par1": "Só pra te avisar que",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Button } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
interface EmailButtonProps {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Element {
|
||||
return (
|
||||
<Button className="rounded-md bg-black px-6 py-3 text-sm text-white" href={href}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailButton;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Container } from "@react-email/components";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
interface ElementHeaderProps {
|
||||
headline: string;
|
||||
subheader?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ElementHeader({ headline, subheader, className }: ElementHeaderProps): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Container className={cn("text-question-color m-0 block text-base font-semibold leading-6", className)}>
|
||||
<div dangerouslySetInnerHTML={{ __html: headline }} />
|
||||
</Container>
|
||||
{subheader && (
|
||||
<Container className="text-question-color m-0 mt-2 block p-0 text-sm font-normal leading-6">
|
||||
<div dangerouslySetInnerHTML={{ __html: subheader }} />
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Text } from "@react-email/components";
|
||||
import { TFunction } from "i18next";
|
||||
import React from "react";
|
||||
|
||||
export function EmailFooter({ t }: { t: TFunction }): React.JSX.Element {
|
||||
return (
|
||||
<Text className="text-sm">
|
||||
{t("emails.email_footer_text_1")}
|
||||
<br />
|
||||
{t("emails.email_footer_text_2")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailFooter;
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components";
|
||||
import { TFunction } from "i18next";
|
||||
import React from "react";
|
||||
import { FB_LOGO_URL, IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@/lib/constants";
|
||||
|
||||
const fbLogoUrl = FB_LOGO_URL;
|
||||
const logoLink = "https://formbricks.com?utm_source=email_header&utm_medium=email";
|
||||
|
||||
interface EmailTemplateProps {
|
||||
readonly children: React.ReactNode;
|
||||
readonly logoUrl?: string;
|
||||
readonly t: TFunction;
|
||||
}
|
||||
|
||||
export async function EmailTemplate({
|
||||
children,
|
||||
logoUrl,
|
||||
t,
|
||||
}: EmailTemplateProps): Promise<React.JSX.Element> {
|
||||
const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Tailwind>
|
||||
<Body
|
||||
className="m-0 h-full w-full justify-center bg-slate-50 p-6 text-center text-sm text-slate-800"
|
||||
style={{
|
||||
fontFamily: "'Jost', 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'sans-serif'",
|
||||
}}>
|
||||
<Section>
|
||||
{isDefaultLogo ? (
|
||||
<Link href={logoLink} target="_blank">
|
||||
<Img data-testid="default-logo-image" alt="Logo" className="mx-auto w-60" src={fbLogoUrl} />
|
||||
</Link>
|
||||
) : (
|
||||
<Img
|
||||
data-testid="logo-image"
|
||||
alt="Logo"
|
||||
className="mx-auto max-h-[100px] w-80 object-contain"
|
||||
src={logoUrl}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
<Container className="mx-auto my-8 max-w-xl rounded-md bg-white p-4 text-left">
|
||||
{children}
|
||||
</Container>
|
||||
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Link
|
||||
className="m-0 text-sm font-normal text-slate-500"
|
||||
href="https://formbricks.com/?utm_source=email_header&utm_medium=email"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("emails.email_template_text_1")}
|
||||
</Link>
|
||||
{IMPRINT_ADDRESS && (
|
||||
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
)}
|
||||
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">
|
||||
{IMPRINT_URL && (
|
||||
<Link
|
||||
href={IMPRINT_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-slate-500">
|
||||
{t("emails.imprint")}
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && " • "}
|
||||
{PRIVACY_URL && (
|
||||
<Link
|
||||
href={PRIVACY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-slate-500">
|
||||
{t("emails.privacy_policy")}
|
||||
</Link>
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { TFunction } from "i18next";
|
||||
import { CalendarDaysIcon, ExternalLinkIcon, UploadIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import {
|
||||
Column,
|
||||
Container,
|
||||
ElementHeader,
|
||||
Button as EmailButton,
|
||||
Img,
|
||||
Link,
|
||||
@@ -8,11 +12,8 @@ import {
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import { render } from "@react-email/render";
|
||||
import { TFunction } from "i18next";
|
||||
import { CalendarDaysIcon, ExternalLinkIcon, UploadIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
render,
|
||||
} from "@formbricks/email";
|
||||
import { TSurveyCTAElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { type TSurvey, type TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
@@ -24,7 +25,6 @@ import { isLight, mixColor } from "@/lib/utils/colors";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { RatingSmiley } from "@/modules/analysis/components/RatingSmiley";
|
||||
import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
|
||||
import { ElementHeader } from "./email-element-header";
|
||||
|
||||
interface PreviewEmailTemplateProps {
|
||||
survey: TSurvey;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface ForgotPasswordEmailProps {
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export async function ForgotPasswordEmail({
|
||||
verifyLink,
|
||||
}: ForgotPasswordEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.forgot_password_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.forgot_password_email_text")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.forgot_password_email_change_password")} />
|
||||
<Text className="text-sm font-bold">{t("emails.forgot_password_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="mb-0 text-sm">{t("emails.forgot_password_email_did_not_request")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgotPasswordEmail;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface VerificationEmailProps {
|
||||
readonly verifyLink: string;
|
||||
}
|
||||
|
||||
export async function NewEmailVerification({
|
||||
verifyLink,
|
||||
}: VerificationEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.new_email_verification_text")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_security_notice")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
|
||||
<Text className="text-sm">{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-sm text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewEmailVerification;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
export async function PasswordResetNotifyEmail(): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.password_changed_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.password_changed_email_text")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default PasswordResetNotifyEmail;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface VerificationEmailProps {
|
||||
verifyLink: string;
|
||||
verificationRequestLink: string;
|
||||
}
|
||||
|
||||
export async function VerificationEmail({
|
||||
verifyLink,
|
||||
verificationRequestLink,
|
||||
}: VerificationEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.verification_email_text")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
|
||||
<Text className="text-sm">{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-sm text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="text-sm">
|
||||
{t("emails.verification_email_if_expired_request_new_token")}
|
||||
<Link className="text-sm text-black underline" href={verificationRequestLink}>
|
||||
{t("emails.verification_email_request_new_verification")}
|
||||
</Link>
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default VerificationEmail;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface EmailCustomizationPreviewEmailProps {
|
||||
userName: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export async function EmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl,
|
||||
}: EmailCustomizationPreviewEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.email_customization_preview_email_heading", { userName })}</Heading>
|
||||
<Text className="text-sm">{t("emails.email_customization_preview_email_text")}</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailCustomizationPreviewEmail;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface InviteAcceptedEmailProps {
|
||||
inviterName: string;
|
||||
inviteeName: string;
|
||||
}
|
||||
|
||||
export async function InviteAcceptedEmail({
|
||||
inviterName,
|
||||
inviteeName,
|
||||
}: InviteAcceptedEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>
|
||||
{t("emails.invite_accepted_email_heading", { inviterName })} {inviterName}
|
||||
</Heading>
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_accepted_email_text_par1", { inviteeName })} {inviteeName}{" "}
|
||||
{t("emails.invite_accepted_email_text_par2")}
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteAcceptedEmail;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface InviteEmailProps {
|
||||
inviteeName: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
}
|
||||
|
||||
export async function InviteEmail({
|
||||
inviteeName,
|
||||
inviterName,
|
||||
verifyLink,
|
||||
}: InviteEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>
|
||||
{t("emails.invite_email_heading", { inviteeName })} {inviteeName}
|
||||
</Heading>
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_email_text_par1", { inviterName })} {inviterName}{" "}
|
||||
{t("emails.invite_email_text_par2")}
|
||||
</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.invite_email_button_label")} />
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteEmail;
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Column, Container, Img, Link, Row, Text } from "@react-email/components";
|
||||
import { TFunction } from "i18next";
|
||||
import { FileIcon } from "lucide-react";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { getOriginalFileNameFromUrl } from "@/modules/storage/utils";
|
||||
|
||||
export const renderEmailResponseValue = async (
|
||||
response: string | string[],
|
||||
questionType: TSurveyElementTypeEnum,
|
||||
t: TFunction,
|
||||
overrideFileUploadResponse = false
|
||||
): Promise<React.JSX.Element> => {
|
||||
switch (questionType) {
|
||||
case TSurveyElementTypeEnum.FileUpload:
|
||||
return (
|
||||
<Container>
|
||||
{overrideFileUploadResponse ? (
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap italic">
|
||||
{t("emails.render_email_response_value_file_upload_response_link_not_included")}
|
||||
</Text>
|
||||
) : (
|
||||
Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Link
|
||||
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-slate-200 p-2 text-sm text-black shadow-sm"
|
||||
href={responseItem}
|
||||
key={responseItem}>
|
||||
<FileIcon className="h-4 w-4" />
|
||||
<Text className="mx-auto mb-0 truncate text-sm">
|
||||
{getOriginalFileNameFromUrl(responseItem)}
|
||||
</Text>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyElementTypeEnum.PictureSelection:
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
{Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Column key={responseItem}>
|
||||
<Img alt={responseItem.split("/").pop()} className="m-2 h-28" src={responseItem} />
|
||||
</Column>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
case TSurveyElementTypeEnum.Ranking:
|
||||
return (
|
||||
<Container>
|
||||
<Row className="mb-2 text-sm text-slate-700" dir="auto">
|
||||
{Array.isArray(response) &&
|
||||
response.filter(Boolean).map((item, index) => (
|
||||
<Row key={item} className="mb-1 flex items-center">
|
||||
<Column className="w-6 text-slate-400">#{index + 1}</Column>
|
||||
<Column className="rounded bg-slate-100 px-2 py-1">{item}</Column>
|
||||
</Row>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
default:
|
||||
return <Text className="mt-0 text-sm break-words whitespace-pre-wrap">{response}</Text>;
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface EmbedSurveyPreviewEmailProps {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export async function EmbedSurveyPreviewEmail({
|
||||
html,
|
||||
environmentId,
|
||||
logoUrl,
|
||||
}: EmbedSurveyPreviewEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.embed_survey_preview_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.embed_survey_preview_email_text")}</Text>
|
||||
<Text className="text-sm">
|
||||
<b>{t("emails.embed_survey_preview_email_didnt_request")}</b>{" "}
|
||||
{t("emails.embed_survey_preview_email_fight_spam")}
|
||||
</Text>
|
||||
<div className="text-sm" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<Text className="text-center text-sm text-slate-700">
|
||||
{t("emails.embed_survey_preview_email_environment_id")}: {environmentId}
|
||||
</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmbedSurveyPreviewEmail;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface LinkSurveyEmailProps {
|
||||
surveyName: string;
|
||||
surveyLink: string;
|
||||
logoUrl: string;
|
||||
}
|
||||
|
||||
export async function LinkSurveyEmail({
|
||||
surveyName,
|
||||
surveyLink,
|
||||
logoUrl,
|
||||
}: LinkSurveyEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_hey")}</Heading>
|
||||
<Text className="text-sm">{t("emails.verification_email_thanks")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_email_to_fill_survey")}</Text>
|
||||
<EmailButton href={surveyLink} label={t("emails.verification_email_take_survey")} />
|
||||
<Text className="text-sm text-slate-400">
|
||||
{t("emails.verification_email_survey_name")}: {surveyName}
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkSurveyEmail;
|
||||
@@ -1,160 +0,0 @@
|
||||
import { Column, Container, Heading, Hr, Link, Row, Section, Text } from "@react-email/components";
|
||||
import { FileDigitIcon, FileType2Icon } from "lucide-react";
|
||||
import type { TOrganization } from "@formbricks/types/organizations";
|
||||
import type { TResponse } from "@formbricks/types/responses";
|
||||
import { type TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface ResponseFinishedEmailProps {
|
||||
survey: TSurvey;
|
||||
responseCount: number;
|
||||
response: TResponse;
|
||||
WEBAPP_URL: string;
|
||||
environmentId: string;
|
||||
organization: TOrganization;
|
||||
}
|
||||
|
||||
export async function ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
}: ResponseFinishedEmailProps): Promise<React.JSX.Element> {
|
||||
const elements = getElementResponseMapping(survey, response);
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Column>
|
||||
<Heading> {t("emails.survey_response_finished_email_hey")}</Heading>
|
||||
<Text className="mb-4 text-sm">
|
||||
{t("emails.survey_response_finished_email_congrats", {
|
||||
surveyName: survey.name,
|
||||
})}
|
||||
</Text>
|
||||
<Hr />
|
||||
{elements.map((e) => {
|
||||
if (!e.response) return;
|
||||
return (
|
||||
<Row key={e.element}>
|
||||
<Column className="w-full font-medium">
|
||||
<Text className="mb-2 text-sm">{e.element}</Text>
|
||||
{renderEmailResponseValue(e.response, e.type, t)}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
{survey.variables
|
||||
.filter((variable) => {
|
||||
const variableResponse = response.variables[variable.id];
|
||||
if (typeof variableResponse !== "string" && typeof variableResponse !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return variableResponse !== undefined;
|
||||
})
|
||||
.map((variable) => {
|
||||
const variableResponse = response.variables[variable.id];
|
||||
return (
|
||||
<Row key={variable.id}>
|
||||
<Column className="w-full text-sm font-medium">
|
||||
<Text className="mb-1 flex items-center gap-2">
|
||||
{variable.type === "number" ? (
|
||||
<FileDigitIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<FileType2Icon className="h-4 w-4" />
|
||||
)}
|
||||
{variable.name}
|
||||
</Text>
|
||||
<Text className="mt-0 font-medium break-words whitespace-pre-wrap">
|
||||
{variableResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
{survey.hiddenFields.fieldIds
|
||||
?.filter((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = response.data[hiddenFieldId];
|
||||
return hiddenFieldResponse && typeof hiddenFieldResponse === "string";
|
||||
})
|
||||
.map((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = response.data[hiddenFieldId] as string;
|
||||
return (
|
||||
<Row key={hiddenFieldId}>
|
||||
<Column className="w-full font-medium">
|
||||
<Text className="mb-2 flex items-center gap-2 text-sm">
|
||||
{hiddenFieldId} <EyeOffIcon />
|
||||
</Text>
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap">
|
||||
{hiddenFieldResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
<EmailButton
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=email_notification&utm_medium=email&utm_content=view_responses_CTA`}
|
||||
label={
|
||||
responseCount > 1
|
||||
? t("emails.survey_response_finished_email_view_more_responses", {
|
||||
responseCount: String(responseCount - 1),
|
||||
})
|
||||
: t("emails.survey_response_finished_email_view_survey_summary")
|
||||
}
|
||||
/>
|
||||
<Hr />
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Text className="text-sm font-medium">
|
||||
{t("emails.survey_response_finished_email_dont_want_notifications")}
|
||||
</Text>
|
||||
<Text className="mb-0">
|
||||
<Link
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=alert&elementId=${survey.id}`}>
|
||||
{t("emails.survey_response_finished_email_turn_off_notifications_for_this_form")}
|
||||
</Link>
|
||||
</Text>
|
||||
<Text className="mt-0">
|
||||
<Link
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=unsubscribedOrganizationIds&elementId=${organization.id}`}>
|
||||
{t("emails.survey_response_finished_email_turn_off_notifications_for_all_new_forms")}
|
||||
</Link>
|
||||
</Text>
|
||||
</Section>
|
||||
</Column>
|
||||
</Row>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function EyeOffIcon(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-eye-off h-4 w-4 rounded-lg bg-slate-200 p-1">
|
||||
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" />
|
||||
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" />
|
||||
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" />
|
||||
<line x1="2" x2="22" y1="2" y2="22" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
import { render } from "@react-email/render";
|
||||
import { createTransport } from "nodemailer";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import {
|
||||
renderEmailCustomizationPreviewEmail,
|
||||
renderEmbedSurveyPreviewEmail,
|
||||
renderForgotPasswordEmail,
|
||||
renderInviteAcceptedEmail,
|
||||
renderInviteEmail,
|
||||
renderLinkSurveyEmail,
|
||||
renderNewEmailVerification,
|
||||
renderPasswordResetNotifyEmail,
|
||||
renderResponseFinishedEmail,
|
||||
renderVerificationEmail,
|
||||
} from "@formbricks/email";
|
||||
import { TEmailTemplateLegalProps } from "@formbricks/email/src/types/email";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
@@ -9,8 +21,11 @@ import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserEmail, TUserLocale } from "@formbricks/types/user";
|
||||
import {
|
||||
DEBUG,
|
||||
IMPRINT_ADDRESS,
|
||||
IMPRINT_URL,
|
||||
MAIL_FROM,
|
||||
MAIL_FROM_NAME,
|
||||
PRIVACY_URL,
|
||||
SMTP_AUTHENTICATED,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
@@ -18,25 +33,24 @@ import {
|
||||
SMTP_REJECT_UNAUTHORIZED_TLS,
|
||||
SMTP_SECURE_ENABLED,
|
||||
SMTP_USER,
|
||||
TERMS_URL,
|
||||
WEBAPP_URL,
|
||||
} from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { createEmailChangeToken, createInviteToken, createToken, createTokenForLinkSurvey } from "@/lib/jwt";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import NewEmailVerification from "@/modules/email/emails/auth/new-email-verification";
|
||||
import { EmailCustomizationPreviewEmail } from "@/modules/email/emails/general/email-customization-preview-email";
|
||||
import { ForgotPasswordEmail } from "./emails/auth/forgot-password-email";
|
||||
import { PasswordResetNotifyEmail } from "./emails/auth/password-reset-notify-email";
|
||||
import { VerificationEmail } from "./emails/auth/verification-email";
|
||||
import { InviteAcceptedEmail } from "./emails/invite/invite-accepted-email";
|
||||
import { InviteEmail } from "./emails/invite/invite-email";
|
||||
import { EmbedSurveyPreviewEmail } from "./emails/survey/embed-survey-preview-email";
|
||||
import { LinkSurveyEmail } from "./emails/survey/link-survey-email";
|
||||
import { ResponseFinishedEmail } from "./emails/survey/response-finished-email";
|
||||
|
||||
export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT);
|
||||
|
||||
const legalProps: TEmailTemplateLegalProps = {
|
||||
privacyUrl: PRIVACY_URL || undefined,
|
||||
termsUrl: TERMS_URL || undefined,
|
||||
imprintUrl: IMPRINT_URL || undefined,
|
||||
imprintAddress: IMPRINT_ADDRESS || undefined,
|
||||
};
|
||||
|
||||
interface SendEmailDataProps {
|
||||
to: string;
|
||||
replyTo?: string;
|
||||
@@ -89,7 +103,7 @@ export const sendVerificationNewEmail = async (id: string, email: string): Promi
|
||||
const token = createEmailChangeToken(id, email);
|
||||
const verifyLink = `${WEBAPP_URL}/verify-email-change?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const html = await render(await NewEmailVerification({ verifyLink }));
|
||||
const html = await renderNewEmailVerification({ verifyLink, t, ...legalProps });
|
||||
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
@@ -117,7 +131,12 @@ export const sendVerificationEmail = async ({
|
||||
const verifyLink = `${WEBAPP_URL}/auth/verify?token=${encodeURIComponent(token)}`;
|
||||
const verificationRequestLink = `${WEBAPP_URL}/auth/verification-requested?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const html = await render(await VerificationEmail({ verificationRequestLink, verifyLink }));
|
||||
const html = await renderVerificationEmail({
|
||||
verificationRequestLink,
|
||||
verifyLink,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
@@ -140,7 +159,7 @@ export const sendForgotPasswordEmail = async (user: {
|
||||
expiresIn: "1d",
|
||||
});
|
||||
const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`;
|
||||
const html = await render(await ForgotPasswordEmail({ verifyLink }));
|
||||
const html = await renderForgotPasswordEmail({ verifyLink, t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: user.email,
|
||||
subject: t("emails.forgot_password_email_subject"),
|
||||
@@ -150,7 +169,7 @@ export const sendForgotPasswordEmail = async (user: {
|
||||
|
||||
export const sendPasswordResetNotifyEmail = async (user: { email: string }): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const html = await render(await PasswordResetNotifyEmail());
|
||||
const html = await renderPasswordResetNotifyEmail({ t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: user.email,
|
||||
subject: t("emails.password_reset_notify_email_subject"),
|
||||
@@ -171,7 +190,7 @@ export const sendInviteMemberEmail = async (
|
||||
|
||||
const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const html = await render(await InviteEmail({ inviteeName, inviterName, verifyLink }));
|
||||
const html = await renderInviteEmail({ inviteeName, inviterName, verifyLink, t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_member_email_subject"),
|
||||
@@ -185,7 +204,7 @@ export const sendInviteAcceptedEmail = async (
|
||||
email: string
|
||||
): Promise<void> => {
|
||||
const t = await getTranslate();
|
||||
const html = await render(await InviteAcceptedEmail({ inviteeName, inviterName }));
|
||||
const html = await renderInviteAcceptedEmail({ inviteeName, inviterName, t, ...legalProps });
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_accepted_email_subject"),
|
||||
@@ -208,16 +227,20 @@ export const sendResponseFinishedEmail = async (
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const html = await render(
|
||||
await ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
})
|
||||
);
|
||||
// Pre-process the element response mapping before passing to email
|
||||
const elements = getElementResponseMapping(survey, response);
|
||||
|
||||
const html = await renderResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
elements,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
@@ -241,7 +264,13 @@ export const sendEmbedSurveyPreviewEmail = async (
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const html = await render(await EmbedSurveyPreviewEmail({ html: innerHtml, environmentId, logoUrl }));
|
||||
const html = await renderEmbedSurveyPreviewEmail({
|
||||
html: innerHtml,
|
||||
environmentId,
|
||||
logoUrl,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
return await sendEmail({
|
||||
to,
|
||||
subject: t("emails.embed_survey_preview_email_subject"),
|
||||
@@ -255,7 +284,12 @@ export const sendEmailCustomizationPreviewEmail = async (
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const emailHtmlBody = await render(await EmailCustomizationPreviewEmail({ userName, logoUrl }));
|
||||
const emailHtmlBody = await renderEmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
|
||||
return await sendEmail({
|
||||
to,
|
||||
@@ -280,7 +314,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
|
||||
};
|
||||
const surveyLink = getSurveyLink();
|
||||
|
||||
const html = await render(await LinkSurveyEmail({ surveyName, surveyLink, logoUrl }));
|
||||
const html = await renderLinkSurveyEmail({ surveyName, surveyLink, logoUrl, t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: data.email,
|
||||
subject: t("emails.verified_link_survey_email_subject"),
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Column, Hr, Row, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { TSurveyFollowUp } from "@formbricks/database/types/survey-follow-up";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { EmailTemplate } from "@/modules/email/components/email-template";
|
||||
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
|
||||
|
||||
interface FollowUpEmailProps {
|
||||
readonly followUp: TSurveyFollowUp;
|
||||
readonly logoUrl?: string;
|
||||
readonly attachResponseData: boolean;
|
||||
readonly includeVariables: boolean;
|
||||
readonly includeHiddenFields: boolean;
|
||||
readonly survey: TSurvey;
|
||||
readonly response: TResponse;
|
||||
}
|
||||
|
||||
export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JSX.Element> {
|
||||
const { properties } = props.followUp.action;
|
||||
let { body } = properties;
|
||||
|
||||
// Parse recall tags and replace with actual response values
|
||||
body = parseRecallInfo(body, props.response.data, props.response.variables);
|
||||
|
||||
const elements = props.attachResponseData ? getElementResponseMapping(props.survey, props.response) : [];
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<EmailTemplate logoUrl={props.logoUrl} t={t}>
|
||||
<>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtml(body, {
|
||||
allowedTags: ["p", "span", "b", "strong", "i", "em", "a", "br"],
|
||||
allowedAttributes: {
|
||||
a: ["href", "rel", "target"],
|
||||
"*": ["dir", "class"],
|
||||
},
|
||||
allowedSchemes: ["http", "https"],
|
||||
allowedSchemesByTag: {
|
||||
a: ["http", "https"],
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
{elements.length > 0 ? (
|
||||
<>
|
||||
<Hr />
|
||||
<Text className="mb-4 text-base font-semibold text-slate-900">{t("emails.response_data")}</Text>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{elements.map((e) => {
|
||||
if (!e.response) return;
|
||||
return (
|
||||
<Row key={e.element}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">{e.element}</Text>
|
||||
{renderEmailResponseValue(e.response, e.type, t, true)}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
{props.attachResponseData &&
|
||||
props.includeVariables &&
|
||||
props.survey.variables
|
||||
.filter((variable) => {
|
||||
const variableResponse = props.response.variables[variable.id];
|
||||
if (typeof variableResponse !== "string" && typeof variableResponse !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return variableResponse !== undefined;
|
||||
})
|
||||
.map((variable) => {
|
||||
const variableResponse = props.response.variables[variable.id];
|
||||
return (
|
||||
<Row key={variable.id}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">
|
||||
{variable.type === "number"
|
||||
? `${t("emails.number_variable")}: ${variable.name}`
|
||||
: `${t("emails.text_variable")}: ${variable.name}`}
|
||||
</Text>
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap text-slate-700">
|
||||
{variableResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
{props.attachResponseData &&
|
||||
props.includeHiddenFields &&
|
||||
props.survey.hiddenFields.fieldIds
|
||||
?.filter((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = props.response.data[hiddenFieldId];
|
||||
return hiddenFieldResponse && typeof hiddenFieldResponse === "string";
|
||||
})
|
||||
.map((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = props.response.data[hiddenFieldId] as string;
|
||||
return (
|
||||
<Row key={hiddenFieldId}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">
|
||||
{t("emails.hidden_field")}: {hiddenFieldId}
|
||||
</Text>
|
||||
<Text className="mt-0 text-sm break-words whitespace-pre-wrap text-slate-700">
|
||||
{hiddenFieldResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import { render } from "@react-email/components";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { TSurveyFollowUp } from "@formbricks/database/types/survey-follow-up";
|
||||
import {
|
||||
ProcessedHiddenField,
|
||||
ProcessedResponseElement,
|
||||
ProcessedVariable,
|
||||
renderFollowUpEmail,
|
||||
} from "@formbricks/email";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL, TERMS_URL } from "@/lib/constants";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { sendEmail } from "@/modules/email";
|
||||
import { FollowUpEmail } from "@/modules/survey/follow-ups/components/follow-up-email";
|
||||
|
||||
export const sendFollowUpEmail = async ({
|
||||
followUp,
|
||||
@@ -28,21 +37,79 @@ export const sendFollowUpEmail = async ({
|
||||
}): Promise<void> => {
|
||||
const {
|
||||
action: {
|
||||
properties: { subject },
|
||||
properties: { subject, body },
|
||||
},
|
||||
} = followUp;
|
||||
|
||||
const emailHtmlBody = await render(
|
||||
await FollowUpEmail({
|
||||
followUp,
|
||||
logoUrl,
|
||||
attachResponseData,
|
||||
includeVariables,
|
||||
includeHiddenFields,
|
||||
survey,
|
||||
response,
|
||||
})
|
||||
);
|
||||
const t = await getTranslate();
|
||||
|
||||
// Process body: parse recall tags and sanitize HTML
|
||||
const processedBody = sanitizeHtml(parseRecallInfo(body, response.data, response.variables), {
|
||||
allowedTags: ["p", "span", "b", "strong", "i", "em", "a", "br"],
|
||||
allowedAttributes: {
|
||||
a: ["href", "rel", "target"],
|
||||
"*": ["dir", "class"],
|
||||
},
|
||||
allowedSchemes: ["http", "https"],
|
||||
allowedSchemesByTag: {
|
||||
a: ["http", "https"],
|
||||
},
|
||||
});
|
||||
|
||||
// Process response data
|
||||
const responseData: ProcessedResponseElement[] = attachResponseData
|
||||
? getElementResponseMapping(survey, response).map((e) => ({
|
||||
element: e.element,
|
||||
response: e.response,
|
||||
type: e.type,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Process variables
|
||||
const variables: ProcessedVariable[] =
|
||||
attachResponseData && includeVariables
|
||||
? survey.variables
|
||||
.filter((variable) => {
|
||||
const variableResponse = response.variables[variable.id];
|
||||
return (
|
||||
(typeof variableResponse === "string" || typeof variableResponse === "number") &&
|
||||
variableResponse !== undefined
|
||||
);
|
||||
})
|
||||
.map((variable) => ({
|
||||
id: variable.id,
|
||||
name: variable.name,
|
||||
type: variable.type,
|
||||
value: response.variables[variable.id],
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Process hidden fields
|
||||
const hiddenFields: ProcessedHiddenField[] =
|
||||
attachResponseData && includeHiddenFields
|
||||
? (survey.hiddenFields.fieldIds
|
||||
?.filter((hiddenFieldId) => {
|
||||
const hiddenFieldResponse = response.data[hiddenFieldId];
|
||||
return hiddenFieldResponse && typeof hiddenFieldResponse === "string";
|
||||
})
|
||||
.map((hiddenFieldId) => ({
|
||||
id: hiddenFieldId,
|
||||
value: response.data[hiddenFieldId] as string,
|
||||
})) ?? [])
|
||||
: [];
|
||||
|
||||
const emailHtmlBody = await renderFollowUpEmail({
|
||||
body: processedBody,
|
||||
responseData,
|
||||
variables,
|
||||
hiddenFields,
|
||||
logoUrl,
|
||||
t,
|
||||
privacyUrl: PRIVACY_URL || undefined,
|
||||
termsUrl: TERMS_URL || undefined,
|
||||
imprintUrl: IMPRINT_URL || undefined,
|
||||
imprintAddress: IMPRINT_ADDRESS || undefined,
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
to,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@formbricks/cache": "workspace:*",
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/email": "workspace:*",
|
||||
"@formbricks/i18n-utils": "workspace:*",
|
||||
"@formbricks/js-core": "workspace:*",
|
||||
"@formbricks/logger": "workspace:*",
|
||||
@@ -70,7 +71,6 @@
|
||||
"@radix-ui/react-toggle": "1.1.8",
|
||||
"@radix-ui/react-toggle-group": "1.1.9",
|
||||
"@radix-ui/react-tooltip": "1.2.6",
|
||||
"@react-email/components": "0.0.38",
|
||||
"@sentry/nextjs": "10.5.0",
|
||||
"@t3-oss/env-nextjs": "0.13.4",
|
||||
"@tailwindcss/forms": "0.5.10",
|
||||
|
||||
Reference in New Issue
Block a user