mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
feat: email package
This commit is contained in:
@@ -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,6 +1,17 @@
|
||||
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 { logger } from "@formbricks/logger";
|
||||
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
@@ -23,17 +34,8 @@ import {
|
||||
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);
|
||||
|
||||
@@ -89,7 +91,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 });
|
||||
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
@@ -117,7 +119,7 @@ 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 });
|
||||
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
@@ -140,7 +142,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 });
|
||||
return await sendEmail({
|
||||
to: user.email,
|
||||
subject: t("emails.forgot_password_email_subject"),
|
||||
@@ -150,7 +152,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 });
|
||||
return await sendEmail({
|
||||
to: user.email,
|
||||
subject: t("emails.password_reset_notify_email_subject"),
|
||||
@@ -171,7 +173,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 });
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_member_email_subject"),
|
||||
@@ -185,7 +187,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 });
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_accepted_email_subject"),
|
||||
@@ -208,16 +210,19 @@ 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,
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
@@ -241,7 +246,7 @@ 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 });
|
||||
return await sendEmail({
|
||||
to,
|
||||
subject: t("emails.embed_survey_preview_email_subject"),
|
||||
@@ -255,7 +260,7 @@ 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 });
|
||||
|
||||
return await sendEmail({
|
||||
to,
|
||||
@@ -280,7 +285,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 });
|
||||
return await sendEmail({
|
||||
to: data.email,
|
||||
subject: t("emails.verified_link_survey_email_subject"),
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { render } from "@react-email/components";
|
||||
import dompurify from "isomorphic-dompurify";
|
||||
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 { 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 +36,70 @@ 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 = dompurify.sanitize(parseRecallInfo(body, response.data, response.variables), {
|
||||
ALLOWED_TAGS: ["p", "span", "b", "strong", "i", "em", "a", "br"],
|
||||
ALLOWED_ATTR: ["href", "rel", "dir", "class"],
|
||||
ALLOWED_URI_REGEXP: /^https?:\/\//, // Only allow safe URLs
|
||||
ADD_ATTR: ["target"], // Allow 'target' attribute for links
|
||||
});
|
||||
|
||||
// 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] as string | number,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
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:*",
|
||||
@@ -71,7 +72,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",
|
||||
|
||||
4
packages/email/.eslintrc.cjs
Normal file
4
packages/email/.eslintrc.cjs
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ["@formbricks/eslint-config/legacy-react.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
45
packages/email/README.md
Normal file
45
packages/email/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# @formbricks/emails
|
||||
|
||||
Email templates for Formbricks with React Email preview server.
|
||||
|
||||
## Purpose
|
||||
|
||||
This package provides email templates for visual QA and preview. It includes:
|
||||
|
||||
- Email templates (auth, invite, survey, general)
|
||||
- Shared email UI components
|
||||
- Mock translation utilities for preview
|
||||
- Example data for template rendering
|
||||
|
||||
## Development
|
||||
|
||||
Run the React Email preview server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Visit `localhost:3456` to preview all email templates with mock data.
|
||||
|
||||
## Usage in Production
|
||||
|
||||
The web app imports templates from this package:
|
||||
|
||||
```typescript
|
||||
import { VerificationEmail } from "@formbricks/emails";
|
||||
|
||||
// Pass real translation function and data
|
||||
const html = await render(
|
||||
await VerificationEmail({
|
||||
verifyLink,
|
||||
verificationRequestLink,
|
||||
t, // Real i18n function
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Preview Mode**: Templates use mock `t()` function and example data
|
||||
- **Production Mode**: Web app passes real `t()` function and actual data
|
||||
- **No Business Logic**: SMTP, i18n, JWT, and database logic remain in web app
|
||||
33
packages/email/emails/auth/forgot-password-email.tsx
Normal file
33
packages/email/emails/auth/forgot-password-email.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "@/src/components/email-button";
|
||||
import { EmailFooter } from "@/src/components/email-footer";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface ForgotPasswordEmailProps {
|
||||
verifyLink: string;
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
export function ForgotPasswordEmail({ verifyLink, t = mockT }: ForgotPasswordEmailProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function ForgotPasswordEmailPreview(): React.JSX.Element {
|
||||
return <ForgotPasswordEmail {...exampleData.forgotPasswordEmail} />;
|
||||
}
|
||||
40
packages/email/emails/auth/new-email-verification.tsx
Normal file
40
packages/email/emails/auth/new-email-verification.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import { EmailButton } from "@/src/components/email-button";
|
||||
import { EmailFooter } from "@/src/components/email-footer";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface NewEmailVerificationProps {
|
||||
readonly verifyLink: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function NewEmailVerification({
|
||||
verifyLink,
|
||||
t = mockT,
|
||||
}: NewEmailVerificationProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function NewEmailVerificationPreview(): React.JSX.Element {
|
||||
return <NewEmailVerification {...exampleData.newEmailVerification} />;
|
||||
}
|
||||
30
packages/email/emails/auth/password-reset-notify-email.tsx
Normal file
30
packages/email/emails/auth/password-reset-notify-email.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailFooter } from "@/src/components/email-footer";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface PasswordResetNotifyEmailProps {
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function PasswordResetNotifyEmail({
|
||||
t = mockT,
|
||||
}: PasswordResetNotifyEmailProps = {}): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function PasswordResetNotifyEmailPreview(): React.JSX.Element {
|
||||
return <PasswordResetNotifyEmail {...exampleData.passwordResetNotifyEmail} />;
|
||||
}
|
||||
47
packages/email/emails/auth/verification-email.tsx
Normal file
47
packages/email/emails/auth/verification-email.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Container, Heading, Link, Text } from "@react-email/components";
|
||||
import { EmailButton } from "@/src/components/email-button";
|
||||
import { EmailFooter } from "@/src/components/email-footer";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface VerificationEmailProps {
|
||||
verifyLink: string;
|
||||
verificationRequestLink: string;
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
export function VerificationEmail({
|
||||
verifyLink,
|
||||
verificationRequestLink,
|
||||
t = mockT,
|
||||
}: VerificationEmailProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function VerificationEmailPreview(): React.JSX.Element {
|
||||
return <VerificationEmail {...exampleData.verificationEmail} />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface EmailCustomizationPreviewEmailProps {
|
||||
userName: string;
|
||||
logoUrl?: string;
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
export function EmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
}: EmailCustomizationPreviewEmailProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function EmailCustomizationPreviewEmailPreview(): React.JSX.Element {
|
||||
return <EmailCustomizationPreviewEmail {...exampleData.emailCustomizationPreviewEmail} />;
|
||||
}
|
||||
39
packages/email/emails/invite/invite-accepted-email.tsx
Normal file
39
packages/email/emails/invite/invite-accepted-email.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailFooter } from "@/src/components/email-footer";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface InviteAcceptedEmailProps {
|
||||
inviterName: string;
|
||||
inviteeName: string;
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
export function InviteAcceptedEmail({
|
||||
inviterName,
|
||||
inviteeName,
|
||||
t = mockT,
|
||||
}: InviteAcceptedEmailProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function InviteAcceptedEmailPreview(): React.JSX.Element {
|
||||
return <InviteAcceptedEmail {...exampleData.inviteAcceptedEmail} />;
|
||||
}
|
||||
43
packages/email/emails/invite/invite-email.tsx
Normal file
43
packages/email/emails/invite/invite-email.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "@/src/components/email-button";
|
||||
import { EmailFooter } from "@/src/components/email-footer";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface InviteEmailProps {
|
||||
inviteeName: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
export function InviteEmail({
|
||||
inviteeName,
|
||||
inviterName,
|
||||
verifyLink,
|
||||
t = mockT,
|
||||
}: InviteEmailProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function InviteEmailPreview(): React.JSX.Element {
|
||||
return <InviteEmail {...exampleData.inviteEmail} />;
|
||||
}
|
||||
42
packages/email/emails/survey/embed-survey-preview-email.tsx
Normal file
42
packages/email/emails/survey/embed-survey-preview-email.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface EmbedSurveyPreviewEmailProps {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
logoUrl?: string;
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
export function EmbedSurveyPreviewEmail({
|
||||
html,
|
||||
environmentId,
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
}: EmbedSurveyPreviewEmailProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function EmbedSurveyPreviewEmailPreview(): React.JSX.Element {
|
||||
return <EmbedSurveyPreviewEmail {...exampleData.embedSurveyPreviewEmail} />;
|
||||
}
|
||||
135
packages/email/emails/survey/follow-up-email.tsx
Normal file
135
packages/email/emails/survey/follow-up-email.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Column, Hr, Row, Text } from "@react-email/components";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { renderEmailResponseValue } from "@/src/lib/email-utils";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
// Processed data types - web app does all the processing
|
||||
interface ProcessedResponseElement {
|
||||
element: string;
|
||||
response: string | string[];
|
||||
type: TSurveyElementTypeEnum;
|
||||
}
|
||||
|
||||
interface ProcessedVariable {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "text" | "number";
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
interface ProcessedHiddenField {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface FollowUpEmailProps {
|
||||
readonly body: string; // Already processed HTML with recall tags replaced
|
||||
readonly responseData?: ProcessedResponseElement[]; // Already mapped elements
|
||||
readonly variables?: ProcessedVariable[]; // Already filtered variables
|
||||
readonly hiddenFields?: ProcessedHiddenField[]; // Already filtered hidden fields
|
||||
readonly logoUrl?: string;
|
||||
readonly t?: TFunction;
|
||||
}
|
||||
|
||||
export function FollowUpEmail({
|
||||
body,
|
||||
responseData = [],
|
||||
variables = [],
|
||||
hiddenFields = [],
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
}: FollowUpEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{ __html: body }} />
|
||||
|
||||
{responseData.length > 0 ? (
|
||||
<>
|
||||
<Hr />
|
||||
<Text className="mb-4 text-base font-semibold text-slate-900">{t("emails.response_data")}</Text>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{responseData.map((e) => {
|
||||
if (!e.response) return null;
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
||||
{variables.map((variable) => (
|
||||
<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 whitespace-pre-wrap break-words text-sm text-slate-700">
|
||||
{variable.value}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
))}
|
||||
|
||||
{hiddenFields.map((hiddenField) => (
|
||||
<Row key={hiddenField.id}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 text-sm font-semibold text-slate-900">
|
||||
{t("emails.hidden_field")}: {hiddenField.id}
|
||||
</Text>
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words text-sm text-slate-700">
|
||||
{hiddenField.value}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
))}
|
||||
</>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server with example data
|
||||
export default function FollowUpEmailPreview(): React.JSX.Element {
|
||||
return (
|
||||
<FollowUpEmail
|
||||
body="<p>Thank you for your feedback! We've received your response and will review it shortly.</p><p>Here's a summary of what you submitted:</p>"
|
||||
responseData={[
|
||||
{
|
||||
element: "What did you like most?",
|
||||
response: "The customer service was excellent!",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
},
|
||||
{
|
||||
element: "How would you rate your experience?",
|
||||
response: "5",
|
||||
type: TSurveyElementTypeEnum.Rating,
|
||||
},
|
||||
]}
|
||||
variables={[
|
||||
{
|
||||
id: "var-1",
|
||||
name: "Customer ID",
|
||||
type: "text",
|
||||
value: "CUST-456",
|
||||
},
|
||||
]}
|
||||
hiddenFields={[
|
||||
{
|
||||
id: "userId",
|
||||
value: "user-abc-123",
|
||||
},
|
||||
]}
|
||||
logoUrl="https://app.formbricks.com/logo.png"
|
||||
/>
|
||||
);
|
||||
}
|
||||
42
packages/email/emails/survey/link-survey-email.tsx
Normal file
42
packages/email/emails/survey/link-survey-email.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "@/src/components/email-button";
|
||||
import { EmailFooter } from "@/src/components/email-footer";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
interface LinkSurveyEmailProps {
|
||||
surveyName: string;
|
||||
surveyLink: string;
|
||||
logoUrl: string;
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
export function LinkSurveyEmail({
|
||||
surveyName,
|
||||
surveyLink,
|
||||
logoUrl,
|
||||
t = mockT,
|
||||
}: LinkSurveyEmailProps): React.JSX.Element {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function LinkSurveyEmailPreview(): React.JSX.Element {
|
||||
return <LinkSurveyEmail {...exampleData.linkSurveyEmail} />;
|
||||
}
|
||||
189
packages/email/emails/survey/response-finished-email.tsx
Normal file
189
packages/email/emails/survey/response-finished-email.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
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 { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { EmailButton } from "@/src/components/email-button";
|
||||
import { EmailTemplate } from "@/src/components/email-template";
|
||||
import { renderEmailResponseValue } from "@/src/lib/email-utils";
|
||||
import { exampleData } from "@/src/lib/example-data";
|
||||
import { t as mockT } from "@/src/lib/mock-translate";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
export interface MappedElement {
|
||||
element: string;
|
||||
response: string | string[];
|
||||
type: TSurveyElementTypeEnum;
|
||||
}
|
||||
|
||||
export interface ResponseFinishedEmailProps {
|
||||
survey: TSurvey;
|
||||
responseCount: number;
|
||||
response: TResponse;
|
||||
WEBAPP_URL: string;
|
||||
environmentId: string;
|
||||
organization: TOrganization;
|
||||
elements: MappedElement[]; // Pre-processed data, not a function
|
||||
t?: TFunction;
|
||||
}
|
||||
|
||||
// Helper function to get element response mapping (simplified)
|
||||
const mockGetElementResponseMapping = (survey: TSurvey, response: TResponse) => {
|
||||
// For preview, just return the response data as elements
|
||||
return Object.entries(response.data)
|
||||
.filter(([key]) => !survey.hiddenFields.fieldIds?.includes(key))
|
||||
.map(([key, value]) => ({
|
||||
element: key,
|
||||
response: value as string | string[],
|
||||
type: TSurveyElementTypeEnum.OpenText, // Default type for preview
|
||||
}));
|
||||
};
|
||||
|
||||
export function ResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
response,
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
elements,
|
||||
t = mockT,
|
||||
}: ResponseFinishedEmailProps): React.JSX.Element {
|
||||
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 null;
|
||||
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 whitespace-pre-wrap break-words font-medium">
|
||||
{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 whitespace-pre-wrap break-words text-sm">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for preview server
|
||||
export default function ResponseFinishedEmailPreview(): React.JSX.Element {
|
||||
const { survey, response, ...rest } = exampleData.responseFinishedEmail;
|
||||
const elements = mockGetElementResponseMapping(survey, response);
|
||||
|
||||
return <ResponseFinishedEmail {...rest} survey={survey} response={response} elements={elements} />;
|
||||
}
|
||||
27
packages/email/package.json
Normal file
27
packages/email/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@formbricks/email",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Email templates for Formbricks with React Email preview server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"dev": "email dev --port 3456",
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint src --fix --ext .ts,.tsx",
|
||||
"clean": "rimraf .turbo node_modules dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "1.0.1",
|
||||
"react": "19.1.2",
|
||||
"react-dom": "19.1.2",
|
||||
"react-email": "5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@react-email/preview-server": "5.0.8"
|
||||
}
|
||||
}
|
||||
16
packages/email/src/components/email-button.tsx
Normal file
16
packages/email/src/components/email-button.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Button } from "@react-email/components";
|
||||
|
||||
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;
|
||||
29
packages/email/src/components/email-element-header.tsx
Normal file
29
packages/email/src/components/email-element-header.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Container } from "@react-email/components";
|
||||
|
||||
interface ElementHeaderProps {
|
||||
headline: string;
|
||||
subheader?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Simple cn utility for className merging
|
||||
const cn = (...classes: (string | undefined)[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ElementHeader;
|
||||
15
packages/email/src/components/email-footer.tsx
Normal file
15
packages/email/src/components/email-footer.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Text } from "@react-email/components";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
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;
|
||||
59
packages/email/src/components/email-template.tsx
Normal file
59
packages/email/src/components/email-template.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Body, Container, Html, Img, Link, Section, Tailwind } from "@react-email/components";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
const fbLogoUrl =
|
||||
"https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Formbricks-Light-transparent.png";
|
||||
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 function EmailTemplate({ children, logoUrl, t }: EmailTemplateProps): 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>
|
||||
</Section>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailTemplate;
|
||||
52
packages/email/src/index.ts
Normal file
52
packages/email/src/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export { VerificationEmail } from "@/emails/auth/verification-email";
|
||||
export { ForgotPasswordEmail } from "@/emails/auth/forgot-password-email";
|
||||
export { NewEmailVerification } from "@/emails/auth/new-email-verification";
|
||||
export { PasswordResetNotifyEmail } from "@/emails/auth/password-reset-notify-email";
|
||||
export { InviteEmail } from "@/emails/invite/invite-email";
|
||||
export { InviteAcceptedEmail } from "@/emails/invite/invite-accepted-email";
|
||||
export { LinkSurveyEmail } from "@/emails/survey/link-survey-email";
|
||||
export { EmbedSurveyPreviewEmail } from "@/emails/survey/embed-survey-preview-email";
|
||||
export { ResponseFinishedEmail } from "@/emails/survey/response-finished-email";
|
||||
export { EmailCustomizationPreviewEmail } from "@/emails/general/email-customization-preview-email";
|
||||
export { FollowUpEmail } from "@/emails/survey/follow-up-email";
|
||||
|
||||
export { EmailButton } from "./components/email-button";
|
||||
export { EmailFooter } from "./components/email-footer";
|
||||
export { EmailTemplate } from "./components/email-template";
|
||||
export { ElementHeader } from "./components/email-element-header";
|
||||
|
||||
export {
|
||||
renderVerificationEmail,
|
||||
renderForgotPasswordEmail,
|
||||
renderNewEmailVerification,
|
||||
renderPasswordResetNotifyEmail,
|
||||
renderInviteEmail,
|
||||
renderInviteAcceptedEmail,
|
||||
renderLinkSurveyEmail,
|
||||
renderEmbedSurveyPreviewEmail,
|
||||
renderResponseFinishedEmail,
|
||||
renderEmailCustomizationPreviewEmail,
|
||||
renderFollowUpEmail,
|
||||
} from "./lib/render";
|
||||
|
||||
export { render } from "@react-email/render";
|
||||
|
||||
export {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type { ProcessedResponseElement, ProcessedVariable, ProcessedHiddenField } from "./lib/render";
|
||||
82
packages/email/src/lib/email-utils.tsx
Normal file
82
packages/email/src/lib/email-utils.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Column, Container, Img, Link, Row, Text } from "@react-email/components";
|
||||
import { FileIcon } from "lucide-react";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
// Simplified version - just get the filename from URL
|
||||
const getOriginalFileNameFromUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname;
|
||||
const filename = pathname.split("/").pop() || "file";
|
||||
return decodeURIComponent(filename);
|
||||
} catch {
|
||||
return url.split("/").pop() || "file";
|
||||
}
|
||||
};
|
||||
|
||||
export const renderEmailResponseValue = (
|
||||
response: string | string[],
|
||||
questionType: TSurveyElementTypeEnum,
|
||||
t: TFunction,
|
||||
overrideFileUploadResponse = false
|
||||
): React.JSX.Element => {
|
||||
switch (questionType) {
|
||||
case TSurveyElementTypeEnum.FileUpload:
|
||||
return (
|
||||
<Container>
|
||||
{overrideFileUploadResponse ? (
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words text-sm 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 whitespace-pre-wrap break-words text-sm">{response as string}</Text>;
|
||||
}
|
||||
};
|
||||
150
packages/email/src/lib/example-data.ts
Normal file
150
packages/email/src/lib/example-data.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
// Mock data for email templates to use in React Email preview server
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const exampleData = {
|
||||
verificationEmail: {
|
||||
verifyLink: "https://app.formbricks.com/auth/verify?token=example-verification-token",
|
||||
verificationRequestLink: "https://app.formbricks.com/auth/verification-requested",
|
||||
},
|
||||
|
||||
forgotPasswordEmail: {
|
||||
verifyLink: "https://app.formbricks.com/auth/forgot-password/reset?token=example-reset-token",
|
||||
},
|
||||
|
||||
newEmailVerification: {
|
||||
verifyLink: "https://app.formbricks.com/verify-email-change?token=example-email-change-token",
|
||||
},
|
||||
|
||||
passwordResetNotifyEmail: {
|
||||
// No props needed
|
||||
},
|
||||
|
||||
inviteEmail: {
|
||||
inviteeName: "Jane Smith",
|
||||
inviterName: "John Doe",
|
||||
verifyLink: "https://app.formbricks.com/invite?token=example-invite-token",
|
||||
},
|
||||
|
||||
inviteAcceptedEmail: {
|
||||
inviterName: "John Doe",
|
||||
inviteeName: "Jane Smith",
|
||||
},
|
||||
|
||||
linkSurveyEmail: {
|
||||
surveyName: "Customer Satisfaction Survey",
|
||||
surveyLink:
|
||||
"https://app.formbricks.com/s/example-survey-id?verify=example-token&suId=example-single-use-id",
|
||||
logoUrl: "https://app.formbricks.com/logo.png",
|
||||
},
|
||||
|
||||
embedSurveyPreviewEmail: {
|
||||
html: '<div style="padding: 20px; background-color: #f3f4f6; border-radius: 8px;"><h3 style="margin-top: 0;">Example Survey Embed</h3><p>This is a preview of how your survey will look when embedded in an email.</p></div>',
|
||||
environmentId: "clxyz123456789",
|
||||
logoUrl: "https://app.formbricks.com/logo.png",
|
||||
},
|
||||
|
||||
responseFinishedEmail: {
|
||||
survey: {
|
||||
id: "survey-123",
|
||||
name: "Customer Feedback Survey",
|
||||
variables: [
|
||||
{
|
||||
id: "var-1",
|
||||
name: "Customer ID",
|
||||
type: "text" as const,
|
||||
},
|
||||
],
|
||||
hiddenFields: {
|
||||
enabled: true,
|
||||
fieldIds: ["userId"],
|
||||
},
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: "openText" as const,
|
||||
headline: { default: "What did you like most?" },
|
||||
required: true,
|
||||
inputType: "text" as const,
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: "rating" as const,
|
||||
headline: { default: "How would you rate your experience?" },
|
||||
required: true,
|
||||
scale: "number" as const,
|
||||
range: 5,
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
styling: {},
|
||||
createdBy: null,
|
||||
} as unknown as TSurvey,
|
||||
responseCount: 15,
|
||||
response: {
|
||||
id: "response-123",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey-123",
|
||||
finished: true,
|
||||
data: {
|
||||
q1: "The customer service was excellent!",
|
||||
q2: 5,
|
||||
userId: "user-abc-123",
|
||||
},
|
||||
variables: {
|
||||
"var-1": "CUST-456",
|
||||
},
|
||||
contactAttributes: {
|
||||
email: "customer@example.com",
|
||||
},
|
||||
meta: {
|
||||
userAgent: {},
|
||||
url: "https://example.com",
|
||||
},
|
||||
tags: [],
|
||||
notes: [],
|
||||
ttc: {},
|
||||
singleUseId: null,
|
||||
language: "default",
|
||||
displayId: null,
|
||||
} as unknown as TResponse,
|
||||
WEBAPP_URL: "https://app.formbricks.com",
|
||||
environmentId: "env-123",
|
||||
organization: {
|
||||
id: "org-123",
|
||||
name: "Acme Corporation",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
subscriptionStatus: null,
|
||||
features: {
|
||||
inAppSurvey: { status: "active" as const, unlimited: true },
|
||||
linkSurvey: { status: "active" as const, unlimited: true },
|
||||
userTargeting: { status: "active" as const, unlimited: true },
|
||||
},
|
||||
limits: {
|
||||
monthly: {
|
||||
responses: 1000,
|
||||
miu: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
isAIEnabled: false,
|
||||
} as unknown as TOrganization,
|
||||
},
|
||||
|
||||
emailCustomizationPreviewEmail: {
|
||||
userName: "Alex Johnson",
|
||||
logoUrl: "https://app.formbricks.com/logo.png",
|
||||
},
|
||||
};
|
||||
|
||||
// Type exports for use in templates
|
||||
export type ExampleDataKeys = keyof typeof exampleData;
|
||||
export type ExampleData<K extends ExampleDataKeys> = (typeof exampleData)[K];
|
||||
110
packages/email/src/lib/mock-translate.ts
Normal file
110
packages/email/src/lib/mock-translate.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// Mock translation function for React Email preview server
|
||||
// Returns English strings extracted from apps/web/locales/en-US.json
|
||||
|
||||
type TranslationKey = string;
|
||||
type TranslationValue = string;
|
||||
|
||||
const translations: Record<TranslationKey, TranslationValue> = {
|
||||
"emails.accept": "Accept",
|
||||
"emails.click_or_drag_to_upload_files": "Click or drag to upload files.",
|
||||
"emails.email_customization_preview_email_heading": "Hey {userName}",
|
||||
"emails.email_customization_preview_email_subject": "Formbricks Email Customization Preview",
|
||||
"emails.email_customization_preview_email_text":
|
||||
"This is an email preview to show you which logo will be rendered in the emails.",
|
||||
"emails.email_footer_text_1": "Have a great day!",
|
||||
"emails.email_footer_text_2": "The Formbricks Team",
|
||||
"emails.email_template_text_1": "This email was sent via Formbricks.",
|
||||
"emails.embed_survey_preview_email_didnt_request": "Didn't request this?",
|
||||
"emails.embed_survey_preview_email_environment_id": "Environment ID",
|
||||
"emails.embed_survey_preview_email_fight_spam":
|
||||
"Help us fight spam and forward this mail to hola@formbricks.com",
|
||||
"emails.embed_survey_preview_email_heading": "Preview Email Embed",
|
||||
"emails.embed_survey_preview_email_subject": "Formbricks Email Survey Preview",
|
||||
"emails.embed_survey_preview_email_text": "This is how the code snippet looks embedded into an email:",
|
||||
"emails.forgot_password_email_change_password": "Change password",
|
||||
"emails.forgot_password_email_did_not_request": "If you didn't request this, please ignore this email.",
|
||||
"emails.forgot_password_email_heading": "Change password",
|
||||
"emails.forgot_password_email_link_valid_for_24_hours": "The link is valid for 24 hours.",
|
||||
"emails.forgot_password_email_subject": "Reset your Formbricks password",
|
||||
"emails.forgot_password_email_text":
|
||||
"You have requested a link to change your password. You can do this by clicking the link below:",
|
||||
"emails.hidden_field": "Hidden field",
|
||||
"emails.imprint": "Imprint",
|
||||
"emails.invite_accepted_email_heading": "Hey",
|
||||
"emails.invite_accepted_email_subject": "You've got a new organization member!",
|
||||
"emails.invite_accepted_email_text_par1": "Just letting you know that",
|
||||
"emails.invite_accepted_email_text_par2": "accepted your invitation. Have fun collaborating!",
|
||||
"emails.invite_email_button_label": "Join organization",
|
||||
"emails.invite_email_heading": "Hey",
|
||||
"emails.invite_email_text_par1": "Your colleague",
|
||||
"emails.invite_email_text_par2":
|
||||
"invited you to join them at Formbricks. To accept the invitation, please click the link below:",
|
||||
"emails.invite_member_email_subject": "You're invited to collaborate on Formbricks!",
|
||||
"emails.new_email_verification_text": "To verify your new email address, please click the button below:",
|
||||
"emails.number_variable": "Number variable",
|
||||
"emails.password_changed_email_heading": "Password changed",
|
||||
"emails.password_changed_email_text": "Your password has been changed successfully.",
|
||||
"emails.password_reset_notify_email_subject": "Your Formbricks password has been changed",
|
||||
"emails.privacy_policy": "Privacy Policy",
|
||||
"emails.reject": "Reject",
|
||||
"emails.render_email_response_value_file_upload_response_link_not_included":
|
||||
"Link to uploaded file is not included for data privacy reasons",
|
||||
"emails.response_data": "Response data",
|
||||
"emails.response_finished_email_subject": "A response for {surveyName} was completed ✅",
|
||||
"emails.response_finished_email_subject_with_email":
|
||||
"{personEmail} just completed your {surveyName} survey ✅",
|
||||
"emails.schedule_your_meeting": "Schedule your meeting",
|
||||
"emails.select_a_date": "Select a date",
|
||||
"emails.survey_response_finished_email_congrats":
|
||||
"Congrats, you received a new response to your survey! Someone just completed your survey: {surveyName}",
|
||||
"emails.survey_response_finished_email_dont_want_notifications": "Don't want to get these notifications?",
|
||||
"emails.survey_response_finished_email_hey": "Hey 👋",
|
||||
"emails.survey_response_finished_email_turn_off_notifications_for_all_new_forms":
|
||||
"Turn off notifications for all newly created forms",
|
||||
"emails.survey_response_finished_email_turn_off_notifications_for_this_form":
|
||||
"Turn off notifications for this form",
|
||||
"emails.survey_response_finished_email_view_more_responses": "View {responseCount} more responses",
|
||||
"emails.survey_response_finished_email_view_survey_summary": "View survey summary",
|
||||
"emails.text_variable": "Text variable",
|
||||
"emails.verification_email_click_on_this_link": "You can also click on this link:",
|
||||
"emails.verification_email_heading": "Almost there!",
|
||||
"emails.verification_email_hey": "Hey 👋",
|
||||
"emails.verification_email_if_expired_request_new_token":
|
||||
"If it has expired please request a new token here:",
|
||||
"emails.verification_email_link_valid_for_24_hours": "The link is valid for 24 hours.",
|
||||
"emails.verification_email_request_new_verification": "Request new verification",
|
||||
"emails.verification_email_subject": "Please verify your email to use Formbricks",
|
||||
"emails.verification_email_survey_name": "Survey name",
|
||||
"emails.verification_email_take_survey": "Take survey",
|
||||
"emails.verification_email_text": "To start using Formbricks please verify your email below:",
|
||||
"emails.verification_email_thanks": "Thanks for validating your email!",
|
||||
"emails.verification_email_to_fill_survey": "To fill out the survey please click on the button below:",
|
||||
"emails.verification_email_verify_email": "Verify email",
|
||||
"emails.verification_new_email_subject": "Email change verification",
|
||||
"emails.verification_security_notice":
|
||||
"If you did not request this email change, please ignore this email or contact support immediately.",
|
||||
"emails.verified_link_survey_email_subject": "Your survey is ready to be filled out.",
|
||||
};
|
||||
|
||||
// Simple string replacement for placeholders like {userName}, {surveyName}, etc.
|
||||
const replacePlaceholders = (text: string, replacements?: Record<string, string>): string => {
|
||||
if (!replacements) return text;
|
||||
|
||||
let result = text;
|
||||
Object.entries(replacements).forEach(([key, value]) => {
|
||||
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock translation function for preview server
|
||||
* @param key - Translation key (e.g., "emails.forgot_password_email_heading")
|
||||
* @param replacements - Optional object with placeholder replacements
|
||||
* @returns Translated string with placeholders replaced
|
||||
*/
|
||||
export const t = (key: string, replacements?: Record<string, string>): string => {
|
||||
const translation = translations[key] || key;
|
||||
return replacePlaceholders(translation, replacements);
|
||||
};
|
||||
118
packages/email/src/lib/render.ts
Normal file
118
packages/email/src/lib/render.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { render } from "@react-email/render";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { FollowUpEmailProps } from "@/emails/survey/follow-up-email";
|
||||
import { ResponseFinishedEmailProps } from "@/emails/survey/response-finished-email";
|
||||
import {
|
||||
EmailCustomizationPreviewEmail,
|
||||
EmbedSurveyPreviewEmail,
|
||||
FollowUpEmail,
|
||||
ForgotPasswordEmail,
|
||||
InviteAcceptedEmail,
|
||||
InviteEmail,
|
||||
LinkSurveyEmail,
|
||||
NewEmailVerification,
|
||||
PasswordResetNotifyEmail,
|
||||
ResponseFinishedEmail,
|
||||
VerificationEmail,
|
||||
} from "@/src/index";
|
||||
|
||||
type TFunction = (key: string, replacements?: Record<string, string>) => string;
|
||||
|
||||
// Render helper functions that convert React email components to HTML strings
|
||||
// These are used by the web app to send emails without needing to import react-email
|
||||
|
||||
export async function renderVerificationEmail(props: {
|
||||
verifyLink: string;
|
||||
verificationRequestLink: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(VerificationEmail(props));
|
||||
}
|
||||
|
||||
export async function renderForgotPasswordEmail(props: {
|
||||
verifyLink: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(ForgotPasswordEmail(props));
|
||||
}
|
||||
|
||||
export async function renderNewEmailVerification(props: {
|
||||
verifyLink: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(NewEmailVerification(props));
|
||||
}
|
||||
|
||||
export async function renderPasswordResetNotifyEmail(props: { t: TFunction }): Promise<string> {
|
||||
return await render(PasswordResetNotifyEmail(props));
|
||||
}
|
||||
|
||||
export async function renderInviteEmail(props: {
|
||||
inviteeName: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(InviteEmail(props));
|
||||
}
|
||||
|
||||
export async function renderInviteAcceptedEmail(props: {
|
||||
inviterName: string;
|
||||
inviteeName: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(InviteAcceptedEmail(props));
|
||||
}
|
||||
|
||||
export async function renderLinkSurveyEmail(props: {
|
||||
surveyName: string;
|
||||
surveyLink: string;
|
||||
logoUrl: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(LinkSurveyEmail(props));
|
||||
}
|
||||
|
||||
export async function renderEmbedSurveyPreviewEmail(props: {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
logoUrl?: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(EmbedSurveyPreviewEmail(props));
|
||||
}
|
||||
|
||||
export async function renderResponseFinishedEmail(props: ResponseFinishedEmailProps): Promise<string> {
|
||||
return await render(ResponseFinishedEmail(props));
|
||||
}
|
||||
|
||||
export async function renderEmailCustomizationPreviewEmail(props: {
|
||||
userName: string;
|
||||
logoUrl?: string;
|
||||
t: TFunction;
|
||||
}): Promise<string> {
|
||||
return await render(EmailCustomizationPreviewEmail(props));
|
||||
}
|
||||
|
||||
// Follow-up email types - exported for web app use
|
||||
export interface ProcessedResponseElement {
|
||||
element: string;
|
||||
response: string | string[];
|
||||
type: TSurveyElementTypeEnum;
|
||||
}
|
||||
|
||||
export interface ProcessedVariable {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "text" | "number";
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface ProcessedHiddenField {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export async function renderFollowUpEmail(props: FollowUpEmailProps): Promise<string> {
|
||||
return await render(FollowUpEmail(props));
|
||||
}
|
||||
13
packages/email/tsconfig.json
Normal file
13
packages/email/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"rootDir": "."
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"extends": "@formbricks/config-typescript/react-library.json",
|
||||
"include": ["src/**/*", "emails/**/*"]
|
||||
}
|
||||
1603
pnpm-lock.yaml
generated
1603
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user