mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
feat: whitelabel 2 | Email customization (#4546)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -101,6 +101,7 @@ PASSWORD_RESET_DISABLED=1
|
||||
PRIVACY_URL=
|
||||
TERMS_URL=
|
||||
IMPRINT_URL=
|
||||
IMPRINT_ADDRESS=
|
||||
|
||||
# Configure Github Login
|
||||
GITHUB_ID=
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,49 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import EmailCustomizationSettings from "./email-customization-card.webp";
|
||||
import EmailSample from "./email-sample.webp";
|
||||
import UpdatedLogo from "./updated-logo.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Email Customization",
|
||||
description: "Customize the email that is sent to your users!",
|
||||
};
|
||||
|
||||
# Email Customization
|
||||
|
||||
Email customization is a white-label feature that allows you to customize the email that is sent to your users. You can upload a logo of your company and use it in the email.
|
||||
|
||||
<Note>
|
||||
This feature is a white-label feature. It is only available for users on paid plans or have an enterprise
|
||||
license.
|
||||
</Note>
|
||||
|
||||
## How to Upload a Logo
|
||||
|
||||
1. Go to the Organization Settings page.
|
||||
2. You will see a card called **Email Customization** under the **General** section.
|
||||
|
||||
<MdxImage
|
||||
src={EmailCustomizationSettings}
|
||||
alt="Email Customization Settings"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Upload a logo of your company.
|
||||
4. Click on the **Save** button.
|
||||
|
||||
<MdxImage src={UpdatedLogo} alt="Updated Logo" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
|
||||
## Viewing the Logo in the Email
|
||||
|
||||
You can click on the **Send test email** button to get a test email with the logo.
|
||||
|
||||
<MdxImage src={EmailSample} alt="Email Sample" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
|
||||
<Note>Only the owner and managers of the organization can modify the logo.</Note>
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **White-labeling**: You can use this feature to white-label your emails to your users.
|
||||
- **Branding**: You can use this feature to add your logo to your emails to increase brand recognition.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -43,6 +43,7 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| TERMS_URL | URL for terms of service. | optional | |
|
||||
| IMPRINT_URL | URL for imprint. | optional | |
|
||||
|IMPRINT_ADDRESS | Address for imprint. | optional | |
|
||||
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to 1. | optional | |
|
||||
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to 1. | optional | |
|
||||
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to 1. | optional | |
|
||||
|
||||
@@ -129,6 +129,7 @@ export const navigation: Array<NavGroup> = [
|
||||
},
|
||||
{ title: "User Management", href: "/core-features/global/access-roles" },
|
||||
{ title: "Styling Theme", href: "/core-features/global/styling-theme" },
|
||||
{ title: "Email Customization", href: "/core-features/global/email-customization" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getIsMultiOrgEnabled, getIsOrganizationAIReady } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsOrganizationAIReady,
|
||||
getWhiteLabelPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||
@@ -11,6 +16,7 @@ import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
||||
@@ -22,6 +28,8 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
const user = session?.user?.id ? await getUser(session.user.id) : null;
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
|
||||
if (!organization) {
|
||||
@@ -31,6 +39,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role);
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization);
|
||||
|
||||
const isDeleteDisabled = !isOwner || !isMultiOrgEnabled;
|
||||
const currentUserRole = currentUserMembership?.role;
|
||||
@@ -69,6 +78,14 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
/>
|
||||
</SettingsCard>
|
||||
)}
|
||||
<EmailCustomizationSettings
|
||||
organization={organization}
|
||||
hasWhiteLabelPermission={hasWhiteLabelPermission}
|
||||
environmentId={params.environmentId}
|
||||
isReadOnly={!isOwnerOrManager}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
user={user}
|
||||
/>
|
||||
{isMultiOrgEnabled && (
|
||||
<SettingsCard
|
||||
title={t("environments.settings.general.delete_organization")}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/s
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getOrganizationLogoUrl } from "@/modules/ee/whitelabel/email-customization/lib/organization";
|
||||
import { sendEmbedSurveyPreviewEmail } from "@/modules/email";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -18,9 +19,12 @@ const ZSendEmbedSurveyPreviewEmailAction = z.object({
|
||||
export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
.schema(ZSendEmbedSurveyPreviewEmailAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const organizationLogoUrl = await getOrganizationLogoUrl(organizationId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -50,7 +54,8 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
"Formbricks Email Survey Preview",
|
||||
emailHtml,
|
||||
survey.environmentId,
|
||||
ctx.user.locale
|
||||
ctx.user.locale,
|
||||
organizationLogoUrl || ""
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { sendFollowUpEmail } from "@/modules/email";
|
||||
import { z } from "zod";
|
||||
import { TSurveyFollowUpAction } from "@formbricks/database/types/survey-follow-up";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -13,11 +14,13 @@ type FollowUpResult = {
|
||||
const evaluateFollowUp = async (
|
||||
followUpId: string,
|
||||
followUpAction: TSurveyFollowUpAction,
|
||||
response: TResponse
|
||||
response: TResponse,
|
||||
organization: TOrganization
|
||||
): Promise<void> => {
|
||||
const { properties } = followUpAction;
|
||||
const { to, subject, body, replyTo } = properties;
|
||||
const toValueFromResponse = response.data[to];
|
||||
const logoUrl = organization.whitelabel?.logoUrl || "";
|
||||
if (!toValueFromResponse) {
|
||||
throw new Error(`"To" value not found in response data for followup: ${followUpId}`);
|
||||
}
|
||||
@@ -27,7 +30,7 @@ const evaluateFollowUp = async (
|
||||
const parsedResult = z.string().email().safeParse(toValueFromResponse);
|
||||
if (parsedResult.data) {
|
||||
// send email to this email address
|
||||
await sendFollowUpEmail(body, subject, parsedResult.data, replyTo);
|
||||
await sendFollowUpEmail(body, subject, parsedResult.data, replyTo, logoUrl);
|
||||
} else {
|
||||
throw new Error(`Email address is not valid for followup: ${followUpId}`);
|
||||
}
|
||||
@@ -38,14 +41,18 @@ const evaluateFollowUp = async (
|
||||
}
|
||||
const parsedResult = z.string().email().safeParse(emailAddress);
|
||||
if (parsedResult.data) {
|
||||
await sendFollowUpEmail(body, subject, parsedResult.data, replyTo);
|
||||
await sendFollowUpEmail(body, subject, parsedResult.data, replyTo, logoUrl);
|
||||
} else {
|
||||
throw new Error(`Email address is not valid for followup: ${followUpId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sendSurveyFollowUps = async (survey: TSurvey, response: TResponse) => {
|
||||
export const sendSurveyFollowUps = async (
|
||||
survey: TSurvey,
|
||||
response: TResponse,
|
||||
organization: TOrganization
|
||||
) => {
|
||||
const followUpPromises = survey.followUps.map(async (followUp): Promise<FollowUpResult> => {
|
||||
const { trigger } = followUp;
|
||||
|
||||
@@ -62,7 +69,7 @@ export const sendSurveyFollowUps = async (survey: TSurvey, response: TResponse)
|
||||
}
|
||||
}
|
||||
|
||||
return evaluateFollowUp(followUp.id, followUp.action, response)
|
||||
return evaluateFollowUp(followUp.id, followUp.action, response, organization)
|
||||
.then(() => ({
|
||||
followUpId: followUp.id,
|
||||
status: "success" as const,
|
||||
|
||||
@@ -169,7 +169,7 @@ export const POST = async (request: Request) => {
|
||||
const surveyFollowUpsPermission = await getSurveyFollowUpsPermission(organization);
|
||||
|
||||
if (surveyFollowUpsPermission) {
|
||||
await sendSurveyFollowUps(survey, response);
|
||||
await sendSurveyFollowUps(survey, response, organization);
|
||||
}
|
||||
|
||||
const emailPromises = usersWithNotifications.map((user) =>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { actionClient } from "@/lib/utils/action-client";
|
||||
import { getOrganizationIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getOrganizationLogoUrl } from "@/modules/ee/whitelabel/email-customization/lib/organization";
|
||||
import { sendLinkSurveyToVerifiedEmail } from "@/modules/email";
|
||||
import { z } from "zod";
|
||||
import { verifyTokenForLinkSurvey } from "@formbricks/lib/jwt";
|
||||
@@ -13,7 +15,10 @@ import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/erro
|
||||
export const sendLinkSurveyEmailAction = actionClient
|
||||
.schema(ZLinkSurveyEmailData)
|
||||
.action(async ({ parsedInput }) => {
|
||||
await sendLinkSurveyToVerifiedEmail(parsedInput);
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const organizationLogoUrl = await getOrganizationLogoUrl(organizationId);
|
||||
|
||||
await sendLinkSurveyToVerifiedEmail({ ...parsedInput, logoUrl: organizationLogoUrl || "" });
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ export const VerifyEmail = ({
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
surveyId: localSurvey.id,
|
||||
email: email,
|
||||
|
||||
@@ -277,6 +277,22 @@ export const getRemoveBrandingPermission = async (organization: TOrganization):
|
||||
}
|
||||
};
|
||||
|
||||
export const getWhiteLabelPermission = async (organization: TOrganization): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult?.features?.whitelabel ?? false;
|
||||
}
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) {
|
||||
return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE;
|
||||
} else {
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
|
||||
return licenseFeatures.whitelabel;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRoleManagementPermission = async (organization: TOrganization): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
|
||||
100
apps/web/modules/ee/whitelabel/email-customization/actions.ts
Normal file
100
apps/web/modules/ee/whitelabel/email-customization/actions.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
"use server";
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
removeOrganizationEmailLogoUrl,
|
||||
updateOrganizationEmailLogoUrl,
|
||||
} from "@/modules/ee/whitelabel/email-customization/lib/organization";
|
||||
import { sendEmailCustomizationPreviewEmail } from "@/modules/email";
|
||||
import { z } from "zod";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const checkWhiteLabelPermission = async (organizationId: string) => {
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
const isWhiteLabelAllowed = await getWhiteLabelPermission(organization);
|
||||
|
||||
if (!isWhiteLabelAllowed) {
|
||||
throw new OperationNotAllowedError("White label is not allowed for this organization");
|
||||
}
|
||||
};
|
||||
|
||||
const ZUpdateOrganizationEmailLogoUrlAction = z.object({
|
||||
organizationId: ZId,
|
||||
logoUrl: z.string(),
|
||||
});
|
||||
|
||||
export const updateOrganizationEmailLogoUrlAction = authenticatedActionClient
|
||||
.schema(ZUpdateOrganizationEmailLogoUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await checkWhiteLabelPermission(parsedInput.organizationId);
|
||||
return await updateOrganizationEmailLogoUrl(parsedInput.organizationId, parsedInput.logoUrl);
|
||||
});
|
||||
|
||||
const ZRemoveOrganizationEmailLogoUrlAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const removeOrganizationEmailLogoUrlAction = authenticatedActionClient
|
||||
.schema(ZRemoveOrganizationEmailLogoUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [{ type: "organization", roles: ["owner", "manager"] }],
|
||||
});
|
||||
|
||||
await checkWhiteLabelPermission(parsedInput.organizationId);
|
||||
return await removeOrganizationEmailLogoUrl(parsedInput.organizationId);
|
||||
});
|
||||
|
||||
const ZSendTestEmailAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const sendTestEmailAction = authenticatedActionClient
|
||||
.schema(ZSendTestEmailAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organization = await getOrganization(parsedInput.organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", parsedInput.organizationId);
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: organization.id,
|
||||
access: [{ type: "organization", roles: ["owner", "manager"] }],
|
||||
});
|
||||
|
||||
await checkWhiteLabelPermission(organization.id);
|
||||
|
||||
await sendEmailCustomizationPreviewEmail(
|
||||
ctx.user.email,
|
||||
"Formbricks Email Customization Preview",
|
||||
ctx.user.name,
|
||||
ctx.user.locale,
|
||||
organization?.whitelabel?.logoUrl || ""
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
"use client";
|
||||
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import {
|
||||
removeOrganizationEmailLogoUrlAction,
|
||||
sendTestEmailAction,
|
||||
updateOrganizationEmailLogoUrlAction,
|
||||
} from "@/modules/ee/whitelabel/email-customization/actions";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Uploader } from "@/modules/ui/components/file-input/components/uploader";
|
||||
import { uploadFile } from "@/modules/ui/components/file-input/lib/utils";
|
||||
import { Muted, P, Small } from "@/modules/ui/components/typography";
|
||||
import { ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
import { RepeatIcon, Trash2Icon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
const allowedFileExtensions: TAllowedFileExtension[] = ["jpeg", "png", "jpg", "webp"];
|
||||
const DEFAULT_LOGO_URL =
|
||||
"https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Formbricks-Light-transparent.png";
|
||||
|
||||
interface EmailCustomizationSettingsProps {
|
||||
organization: TOrganization;
|
||||
hasWhiteLabelPermission: boolean;
|
||||
environmentId: string;
|
||||
isReadOnly: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
user: TUser | null;
|
||||
}
|
||||
|
||||
export const EmailCustomizationSettings = ({
|
||||
organization,
|
||||
hasWhiteLabelPermission,
|
||||
environmentId,
|
||||
isReadOnly,
|
||||
isFormbricksCloud,
|
||||
user,
|
||||
}: EmailCustomizationSettingsProps) => {
|
||||
const t = useTranslations();
|
||||
|
||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||
const [logoUrl, setLogoUrl] = useState<string>(organization.whitelabel?.logoUrl || DEFAULT_LOGO_URL);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isDefaultLogo = logoUrl === DEFAULT_LOGO_URL;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const onFileInputChange = (files: File[]) => {
|
||||
const file = files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Revoke any previous object URL so we don’t leak memory
|
||||
if (logoUrl) {
|
||||
URL.revokeObjectURL(logoUrl);
|
||||
}
|
||||
|
||||
setLogoFile(file);
|
||||
setLogoUrl(URL.createObjectURL(file));
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
const file = files[0];
|
||||
if (!file) return;
|
||||
|
||||
const extension = file.name.split(".").pop()! as TAllowedFileExtension;
|
||||
if (!allowedFileExtensions.includes(extension)) {
|
||||
toast.error(t("common.invalid_file_type"));
|
||||
return;
|
||||
}
|
||||
onFileInputChange(files);
|
||||
};
|
||||
|
||||
const removeLogo = async () => {
|
||||
if (logoUrl) {
|
||||
URL.revokeObjectURL(logoUrl);
|
||||
}
|
||||
setLogoFile(null);
|
||||
setLogoUrl("");
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
|
||||
if (isDefaultLogo || !organization.whitelabel?.logoUrl) return;
|
||||
|
||||
const removeLogoResponse = await removeOrganizationEmailLogoUrlAction({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
|
||||
if (removeLogoResponse?.data) {
|
||||
toast.success(t("environments.settings.general.logo_removed_successfully"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(removeLogoResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!logoFile) return;
|
||||
setIsSaving(true);
|
||||
const { url } = await uploadFile(logoFile, allowedFileExtensions, environmentId);
|
||||
|
||||
const updateLogoResponse = await updateOrganizationEmailLogoUrlAction({
|
||||
organizationId: organization.id,
|
||||
logoUrl: url,
|
||||
});
|
||||
|
||||
if (updateLogoResponse?.data) {
|
||||
toast.success(t("environments.settings.general.logo_saved_successfully"));
|
||||
setLogoUrl(url);
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updateLogoResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const sendTestEmail = async () => {
|
||||
if (!logoUrl) {
|
||||
toast.error(t("environments.settings.general.please_add_a_logo"));
|
||||
return;
|
||||
}
|
||||
if (logoUrl !== organization.whitelabel?.logoUrl && !isDefaultLogo) {
|
||||
toast.error(t("environments.settings.general.please_save_logo_before_sending_test_email"));
|
||||
return;
|
||||
}
|
||||
const sendTestEmailResponse = await sendTestEmailAction({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
|
||||
if (sendTestEmailResponse?.data) {
|
||||
toast.success(t("environments.settings.general.test_email_sent_successfully"));
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(sendTestEmailResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const buttons: [ModalButton, ModalButton] = [
|
||||
{
|
||||
text: t("common.start_free_trial"),
|
||||
href: isFormbricksCloud
|
||||
? `/environments/${environmentId}/settings/billing`
|
||||
: "https://formbricks.com/upgrade-self-hosting-license",
|
||||
},
|
||||
{
|
||||
text: t("common.learn_more"),
|
||||
href: isFormbricksCloud
|
||||
? `/environments/${environmentId}/settings/billing`
|
||||
: "https://formbricks.com/learn-more-self-hosting-license",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
className="overflow-hidden pb-0"
|
||||
title={t("environments.project.look.email_customization")}
|
||||
description={t("environments.project.look.email_customization_description")}
|
||||
noPadding>
|
||||
<div className="px-6 pt-6">
|
||||
{hasWhiteLabelPermission ? (
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div className="mb-10">
|
||||
<Small>{t("environments.settings.general.logo_in_email_header")}</Small>
|
||||
|
||||
<div className="mb-6 mt-2 flex items-center gap-4">
|
||||
{logoUrl && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex w-max items-center justify-center rounded-lg border border-slate-200 px-4 py-2">
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt="Logo"
|
||||
className="max-h-24 max-w-full object-contain"
|
||||
width={192}
|
||||
height={192}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
disabled={isReadOnly}>
|
||||
<RepeatIcon className="h-4 w-4" />
|
||||
{t("environments.settings.general.replace_logo")}
|
||||
</Button>
|
||||
<Button onClick={removeLogo} variant="outline" disabled={isReadOnly}>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
{t("environments.settings.general.remove_logo")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Uploader
|
||||
ref={inputRef}
|
||||
allowedFileExtensions={allowedFileExtensions}
|
||||
id="email-customization"
|
||||
name="email-customization"
|
||||
handleDragOver={handleDragOver}
|
||||
uploaderClassName={cn(
|
||||
"h-20 w-96 border border-slate-200 bg-white",
|
||||
logoUrl ? "hidden" : "block"
|
||||
)}
|
||||
handleDrop={handleDrop}
|
||||
multiple={false}
|
||||
handleUpload={onFileInputChange}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button variant="secondary" disabled={isReadOnly} onClick={sendTestEmail}>
|
||||
{t("common.send_test_email")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!logoFile || isReadOnly} loading={isSaving}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shadow-card-xl min-h-52 w-[446px] rounded-t-lg border border-slate-100 px-10 pb-4 pt-10">
|
||||
<Image
|
||||
src={logoUrl || DEFAULT_LOGO_URL}
|
||||
alt="Logo"
|
||||
className="mx-auto max-h-[100px] max-w-full object-contain"
|
||||
width={192}
|
||||
height={192}
|
||||
/>
|
||||
<P className="font-bold">
|
||||
{t("environments.settings.general.email_customization_preview_email_heading", {
|
||||
userName: user?.name,
|
||||
})}
|
||||
</P>
|
||||
<Muted className="text-slate-500">
|
||||
{t("environments.settings.general.email_customization_preview_email_text")}
|
||||
</Muted>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<UpgradePrompt
|
||||
title={t("environments.settings.general.customize_email_with_a_higher_plan")}
|
||||
description={t("environments.settings.general.eliminate_branding_with_whitelabel")}
|
||||
buttons={buttons}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasWhiteLabelPermission && isReadOnly && (
|
||||
<Alert variant="warning" className="mb-6 mt-4">
|
||||
<AlertDescription>
|
||||
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { organizationCache } from "@formbricks/lib/organization/cache";
|
||||
import { projectCache } from "@formbricks/lib/project/cache";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId, ZString } from "@formbricks/types/common";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const updateOrganizationEmailLogoUrl = async (
|
||||
organizationId: string,
|
||||
logoUrl: string
|
||||
): Promise<boolean> => {
|
||||
validateInputs([organizationId, ZId], [logoUrl, ZString]);
|
||||
|
||||
try {
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
const updatedOrganization = await prisma.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: {
|
||||
whitelabel: {
|
||||
...organization.whitelabel,
|
||||
logoUrl,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
projects: {
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
organizationCache.revalidate({
|
||||
id: organizationId,
|
||||
});
|
||||
|
||||
for (const project of updatedOrganization.projects) {
|
||||
for (const environment of project.environments) {
|
||||
organizationCache.revalidate({
|
||||
environmentId: environment.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
projectCache.revalidate({
|
||||
organizationId: organizationId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeOrganizationEmailLogoUrl = async (organizationId: string): Promise<boolean> => {
|
||||
validateInputs([organizationId, ZId]);
|
||||
|
||||
try {
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
select: {
|
||||
whitelabel: true,
|
||||
projects: {
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
await prisma.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: {
|
||||
whitelabel: {
|
||||
...organization.whitelabel,
|
||||
logoUrl: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
organizationCache.revalidate({
|
||||
id: organizationId,
|
||||
});
|
||||
|
||||
for (const project of organization.projects) {
|
||||
for (const environment of project.environments) {
|
||||
organizationCache.revalidate({
|
||||
environmentId: environment.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
projectCache.revalidate({
|
||||
organizationId: organizationId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrganizationLogoUrl = reactCache(
|
||||
async (organizationId: string): Promise<string | null> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([organizationId, ZId]);
|
||||
try {
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
select: {
|
||||
whitelabel: true,
|
||||
},
|
||||
});
|
||||
return organization?.whitelabel?.logoUrl ?? null;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getOrganizationLogoUrl-${organizationId}`],
|
||||
{
|
||||
tags: [organizationCache.tag.byId(organizationId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -1,7 +1,18 @@
|
||||
import { Body, Column, Container, Html, Img, Link, Section, Tailwind } from "@react-email/components";
|
||||
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||
import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components";
|
||||
import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||
|
||||
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 {
|
||||
children: React.ReactNode;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export function EmailTemplate({ children, logoUrl }: EmailTemplateProps): React.JSX.Element {
|
||||
const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl;
|
||||
|
||||
export function EmailTemplate({ children }): React.JSX.Element {
|
||||
return (
|
||||
<Html>
|
||||
<Tailwind>
|
||||
@@ -11,53 +22,37 @@ export function EmailTemplate({ children }): React.JSX.Element {
|
||||
fontFamily: "'Jost', 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'sans-serif'",
|
||||
}}>
|
||||
<Section>
|
||||
<Link href="https://formbricks.com?utm_source=email_header&utm_medium=email" target="_blank">
|
||||
<Img
|
||||
alt="Formbricks Logo"
|
||||
className="mx-auto w-80"
|
||||
src="https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Formbricks-Light-transparent.png"
|
||||
/>
|
||||
</Link>
|
||||
{isDefaultLogo ? (
|
||||
<Link href={logoLink} target="_blank">
|
||||
<Img alt="Logo" className="mx-auto w-80" src={fbLogoUrl} />
|
||||
</Link>
|
||||
) : (
|
||||
<Img alt="Logo" className="mx-auto max-h-[100px] w-80 object-contain" src={logoUrl} />
|
||||
)}
|
||||
</Section>
|
||||
<Container className="mx-auto my-8 max-w-xl bg-white p-4 text-left">{children}</Container>
|
||||
<Container className="mx-auto my-8 max-w-xl rounded-md bg-white p-4 text-left">
|
||||
{children}
|
||||
</Container>
|
||||
|
||||
<Section className="flex justify-center">
|
||||
<Column align="center" className="px-2" key="twitter">
|
||||
<Link href="https://twitter.com/formbricks" target="_blank" className="w-fit">
|
||||
<Img
|
||||
alt="Tw"
|
||||
src="https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Twitter-transp.png"
|
||||
title="Twitter"
|
||||
width="32"
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
<Column align="center" className="px-2" key="github">
|
||||
<Link href="https://formbricks.com/github" target="_blank" className="w-fit">
|
||||
<Img
|
||||
alt="GitHub"
|
||||
src="https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Github-transp.png"
|
||||
title="GitHub"
|
||||
width="32"
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
</Section>
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
Formbricks {new Date().getFullYear()}. All rights reserved.
|
||||
<br />
|
||||
{IMPRINT_URL && (
|
||||
<Link href={IMPRINT_URL} target="_blank" rel="noopener noreferrer">
|
||||
Imprint{" "}
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && "|"}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank" rel="noopener noreferrer">
|
||||
{" "}
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Text className="m-0 font-normal text-slate-500">This email was sent via Formbricks.</Text>
|
||||
{IMPRINT_ADDRESS && (
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
)}
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">
|
||||
{IMPRINT_URL && (
|
||||
<Link href={IMPRINT_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
|
||||
Imprint{" "}
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && "•"}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
|
||||
{" "}
|
||||
Privacy Policy
|
||||
</Link>
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
import { translateEmailText } from "../../lib/utils";
|
||||
|
||||
interface EmailCustomizationPreviewEmailProps {
|
||||
userName: string;
|
||||
locale: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export function EmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
locale,
|
||||
logoUrl,
|
||||
}: EmailCustomizationPreviewEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl}>
|
||||
<Container>
|
||||
<Heading>
|
||||
{translateEmailText("email_customization_preview_email_heading", locale, {
|
||||
userName,
|
||||
})}
|
||||
</Heading>
|
||||
<Text>{translateEmailText("email_customization_preview_email_text", locale)}</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailCustomizationPreviewEmail;
|
||||
@@ -7,15 +7,17 @@ interface EmbedSurveyPreviewEmailProps {
|
||||
html: string;
|
||||
environmentId: string;
|
||||
locale: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export function EmbedSurveyPreviewEmail({
|
||||
html,
|
||||
environmentId,
|
||||
locale,
|
||||
logoUrl,
|
||||
}: EmbedSurveyPreviewEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<EmailTemplate logoUrl={logoUrl}>
|
||||
<Container>
|
||||
<Heading>{translateEmailText("embed_survey_preview_email_heading", locale)}</Heading>
|
||||
<Text>{translateEmailText("embed_survey_preview_email_text", locale)}</Text>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Body, Container, Html, Link, Section, Tailwind } from "@react-email/components";
|
||||
import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components";
|
||||
import dompurify from "isomorphic-dompurify";
|
||||
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||
import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||
|
||||
interface FollowUpEmailProps {
|
||||
html: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export function FollowUpEmail({ html }: FollowUpEmailProps): React.JSX.Element {
|
||||
export function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<Html>
|
||||
<Tailwind>
|
||||
@@ -15,7 +16,12 @@ export function FollowUpEmail({ html }: FollowUpEmailProps): React.JSX.Element {
|
||||
style={{
|
||||
fontFamily: "'Jost', 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'sans-serif'",
|
||||
}}>
|
||||
<Container className="mx-auto my-8 max-w-xl bg-white p-4 text-left">
|
||||
{logoUrl && (
|
||||
<Section>
|
||||
<Img 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">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: dompurify.sanitize(html, {
|
||||
@@ -29,15 +35,25 @@ export function FollowUpEmail({ html }: FollowUpEmailProps): React.JSX.Element {
|
||||
</Container>
|
||||
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
powered by Formbricks
|
||||
<br />
|
||||
<Link href={IMPRINT_URL} target="_blank" rel="noopener noreferrer">
|
||||
Imprint
|
||||
</Link>{" "}
|
||||
|{" "}
|
||||
<Link href={PRIVACY_URL} target="_blank" rel="noopener noreferrer">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Text className="m-0 font-normal text-slate-500">powered by Formbricks</Text>
|
||||
|
||||
{IMPRINT_ADDRESS && (
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
)}
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">
|
||||
{IMPRINT_URL && (
|
||||
<Link href={IMPRINT_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
|
||||
Imprint{" "}
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && "•"}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
|
||||
{" "}
|
||||
Privacy Policy
|
||||
</Link>
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
|
||||
@@ -9,11 +9,17 @@ interface LinkSurveyEmailProps {
|
||||
surveyName: string;
|
||||
surveyLink: string;
|
||||
locale: string;
|
||||
logoUrl: string;
|
||||
}
|
||||
|
||||
export function LinkSurveyEmail({ surveyName, surveyLink, locale }: LinkSurveyEmailProps): React.JSX.Element {
|
||||
export function LinkSurveyEmail({
|
||||
surveyName,
|
||||
surveyLink,
|
||||
locale,
|
||||
logoUrl,
|
||||
}: LinkSurveyEmailProps): React.JSX.Element {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<EmailTemplate logoUrl={logoUrl}>
|
||||
<Container>
|
||||
<Heading>{translateEmailText("verification_email_hey", locale)}</Heading>
|
||||
<Text>{translateEmailText("verification_email_thanks", locale)}</Text>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EmailCustomizationPreviewEmail } from "@/modules/email/emails/general/email-customization-preview-email";
|
||||
import { render } from "@react-email/render";
|
||||
import { createTransport } from "nodemailer";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
@@ -211,9 +212,10 @@ export const sendEmbedSurveyPreviewEmail = async (
|
||||
subject: string,
|
||||
innerHtml: string,
|
||||
environmentId: string,
|
||||
locale: string
|
||||
locale: string,
|
||||
logoUrl?: string
|
||||
): Promise<void> => {
|
||||
const html = await render(EmbedSurveyPreviewEmail({ html: innerHtml, environmentId, locale }));
|
||||
const html = await render(EmbedSurveyPreviewEmail({ html: innerHtml, environmentId, locale, logoUrl }));
|
||||
await sendEmail({
|
||||
to,
|
||||
subject,
|
||||
@@ -221,12 +223,29 @@ export const sendEmbedSurveyPreviewEmail = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const sendEmailCustomizationPreviewEmail = async (
|
||||
to: string,
|
||||
subject: string,
|
||||
userName: string,
|
||||
locale: string,
|
||||
logoUrl?: string
|
||||
): Promise<void> => {
|
||||
const emailHtmlBody = await render(EmailCustomizationPreviewEmail({ userName, locale, logoUrl }));
|
||||
|
||||
await sendEmail({
|
||||
to,
|
||||
subject,
|
||||
html: emailHtmlBody,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData): Promise<void> => {
|
||||
const surveyId = data.surveyId;
|
||||
const email = data.email;
|
||||
const surveyName = data.surveyName;
|
||||
const singleUseId = data.suId;
|
||||
const locale = data.locale;
|
||||
const logoUrl = data.logoUrl || "";
|
||||
const token = createTokenForLinkSurvey(surveyId, email);
|
||||
const getSurveyLink = (): string => {
|
||||
if (singleUseId) {
|
||||
@@ -236,7 +255,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
|
||||
};
|
||||
const surveyLink = getSurveyLink();
|
||||
|
||||
const html = await render(LinkSurveyEmail({ surveyName, surveyLink, locale }));
|
||||
const html = await render(LinkSurveyEmail({ surveyName, surveyLink, locale, logoUrl }));
|
||||
await sendEmail({
|
||||
to: data.email,
|
||||
subject: "Your survey is ready to be filled out.",
|
||||
@@ -312,11 +331,13 @@ export const sendFollowUpEmail = async (
|
||||
html: string,
|
||||
subject: string,
|
||||
to: string,
|
||||
replyTo: string[]
|
||||
replyTo: string[],
|
||||
logoUrl?: string
|
||||
): Promise<void> => {
|
||||
const emailHtmlBody = await render(
|
||||
FollowUpEmail({
|
||||
html,
|
||||
logoUrl,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import {
|
||||
getMultiLanguagePermission,
|
||||
getRemoveBrandingPermission,
|
||||
getRoleManagementPermission,
|
||||
getWhiteLabelPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
@@ -43,7 +43,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
const locale = session?.user.id ? await getUserLocale(session.user.id) : undefined;
|
||||
const canRemoveBranding = await getRemoveBrandingPermission(organization);
|
||||
const canRemoveBranding = await getWhiteLabelPermission(organization);
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
@@ -89,6 +89,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
|
||||
description={t("environments.project.look.app_survey_placement_settings_description")}>
|
||||
<EditPlacementForm project={project} environmentId={params.environmentId} isReadOnly={isReadOnly} />
|
||||
</SettingsCard>
|
||||
|
||||
<BrandingSettingsCard
|
||||
canRemoveBranding={canRemoveBranding}
|
||||
project={project}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TAllowedFileExtension } from "@formbricks/types/common";
|
||||
interface UploaderProps {
|
||||
id: string;
|
||||
name: string;
|
||||
ref?: React.RefObject<HTMLInputElement>;
|
||||
handleDragOver: (e: React.DragEvent<HTMLLabelElement>) => void;
|
||||
uploaderClassName: string;
|
||||
handleDrop: (e: React.DragEvent<HTMLLabelElement>) => void;
|
||||
@@ -18,6 +19,7 @@ interface UploaderProps {
|
||||
export const Uploader = ({
|
||||
id,
|
||||
name,
|
||||
ref,
|
||||
handleDragOver,
|
||||
uploaderClassName,
|
||||
handleDrop,
|
||||
@@ -52,6 +54,7 @@ export const Uploader = ({
|
||||
className="hidden"
|
||||
multiple={multiple}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
onChange={async (e) => {
|
||||
let selectedFiles = Array.from(e.target?.files || []);
|
||||
handleUpload(selectedFiles);
|
||||
|
||||
@@ -40,45 +40,50 @@ export const SecondaryNavigation = ({ navigation, activeId, loading, ...props }:
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: navigation.map((navElem) => (
|
||||
<div className="group flex h-full flex-col" key={navElem.id}>
|
||||
{navElem.href ? (
|
||||
<Link
|
||||
href={navElem.href}
|
||||
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
|
||||
className={cn(
|
||||
navElem.id === activeId
|
||||
? "font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"flex h-full items-center px-3 text-sm font-medium",
|
||||
navElem.hidden && "hidden"
|
||||
: navigation.map(
|
||||
(navElem) =>
|
||||
!navElem.hidden && (
|
||||
<div className="group flex h-full flex-col" key={navElem.id}>
|
||||
{navElem.href ? (
|
||||
<Link
|
||||
href={navElem.href}
|
||||
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
|
||||
className={cn(
|
||||
navElem.id === activeId
|
||||
? "font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"flex h-full items-center px-3 text-sm font-medium",
|
||||
navElem.hidden && "hidden"
|
||||
)}
|
||||
aria-current={navElem.id === activeId ? "page" : undefined}>
|
||||
{navElem.label}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
|
||||
className={cn(
|
||||
navElem.id === activeId
|
||||
? "font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"grow items-center px-3 text-sm font-medium transition-all duration-150 ease-in-out",
|
||||
navElem.hidden && "hidden"
|
||||
)}
|
||||
aria-current={navElem.id === activeId ? "page" : undefined}>
|
||||
{navElem.label}
|
||||
</button>
|
||||
)}
|
||||
aria-current={navElem.id === activeId ? "page" : undefined}>
|
||||
{navElem.label}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
|
||||
className={cn(
|
||||
navElem.id === activeId
|
||||
? "font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"grow items-center px-3 text-sm font-medium transition-all duration-150 ease-in-out",
|
||||
navElem.hidden && "hidden"
|
||||
)}
|
||||
aria-current={navElem.id === activeId ? "page" : undefined}>
|
||||
{navElem.label}
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"bottom-0 mt-auto h-[2px] w-full rounded-t-lg transition-all duration-150 ease-in-out",
|
||||
navElem.id === activeId ? "bg-brand-dark" : "bg-transparent group-hover:bg-slate-300",
|
||||
navElem.hidden && "hidden"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
"bottom-0 mt-auto h-[2px] w-full rounded-t-lg transition-all duration-150 ease-in-out",
|
||||
navElem.id === activeId
|
||||
? "bg-brand-dark"
|
||||
: "bg-transparent group-hover:bg-slate-300",
|
||||
navElem.hidden && "hidden"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ module.exports = {
|
||||
"card-sm": "0px 0.5px 12px -5px rgba(30,41,59,0.20)",
|
||||
"card-md": "0px 1px 25px -10px rgba(30,41,59,0.30)",
|
||||
"card-lg": "0px 2px 51px -19px rgba(30,41,59,0.40)",
|
||||
"card-xl": "0px 20px 25px -5px rgba(0, 0, 0, 0.10), 0px 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
|
||||
@@ -138,6 +138,9 @@ x-environment: &environment
|
||||
# Set the below to have your own Imprint Page URL on auth & link survey page
|
||||
# IMPRINT_URL:
|
||||
|
||||
# Set the below to have your own Address on email footer
|
||||
# IMPRINT_ADDRESS:
|
||||
|
||||
########################################## OPTIONAL (SERVER CONFIGURATION) ###########################################
|
||||
|
||||
# Set the below to 1 to disable Rate Limiting across Formbricks
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Organization" ADD COLUMN "whitelabel" JSONB NOT NULL DEFAULT '{}';
|
||||
@@ -493,6 +493,9 @@ model Organization {
|
||||
/// @zod.custom(imports.ZOrganizationBilling)
|
||||
/// [OrganizationBilling]
|
||||
billing Json
|
||||
/// @zod.custom(imports.ZOrganizationWhitelabel)
|
||||
/// [OrganizationWhitelabel]
|
||||
whitelabel Json @default("{}")
|
||||
invites Invite[]
|
||||
isAIEnabled Boolean @default(false)
|
||||
teams Team[]
|
||||
|
||||
@@ -23,5 +23,5 @@ export {
|
||||
export { ZSurveyFollowUpAction, ZSurveyFollowUpTrigger } from "./types/survey-follow-up";
|
||||
|
||||
export { ZSegmentFilters } from "../types/segment";
|
||||
export { ZOrganizationBilling } from "../types/organizations";
|
||||
export { ZOrganizationBilling, ZOrganizationWhitelabel } from "../types/organizations";
|
||||
export { ZUserNotificationSettings } from "../types/user";
|
||||
|
||||
@@ -21,6 +21,7 @@ export const DEFAULT_BRAND_COLOR = "#64748b";
|
||||
export const PRIVACY_URL = env.PRIVACY_URL;
|
||||
export const TERMS_URL = env.TERMS_URL;
|
||||
export const IMPRINT_URL = env.IMPRINT_URL;
|
||||
export const IMPRINT_ADDRESS = env.IMPRINT_ADDRESS;
|
||||
|
||||
export const PASSWORD_RESET_DISABLED = env.PASSWORD_RESET_DISABLED === "1";
|
||||
export const EMAIL_VERIFICATION_DISABLED = env.EMAIL_VERIFICATION_DISABLED === "1";
|
||||
|
||||
@@ -44,6 +44,7 @@ export const env = createEnv({
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
IMPRINT_ADDRESS: z.string().optional(),
|
||||
INVITE_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
INTERCOM_SECRET_KEY: z.string().optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
@@ -160,6 +161,7 @@ export const env = createEnv({
|
||||
HTTP_PROXY: process.env.HTTP_PROXY,
|
||||
HTTPS_PROXY: process.env.HTTPS_PROXY,
|
||||
IMPRINT_URL: process.env.IMPRINT_URL,
|
||||
IMPRINT_ADDRESS: process.env.IMPRINT_ADDRESS,
|
||||
INVITE_DISABLED: process.env.INVITE_DISABLED,
|
||||
INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY,
|
||||
IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD,
|
||||
|
||||
@@ -325,6 +325,7 @@
|
||||
"questions": "Fragen",
|
||||
"read_docs": "Dokumentation lesen",
|
||||
"remove": "Entfernen",
|
||||
"removed": "Entfernt",
|
||||
"reorder_and_hide_columns": "Spalten neu anordnen und ausblenden",
|
||||
"report_survey": "Umfrage melden",
|
||||
"request_an_enterprise_license": "eine Enterprise Lizenz anfordern.",
|
||||
@@ -339,6 +340,7 @@
|
||||
"sales": "Vertrieb",
|
||||
"save": "Speichern",
|
||||
"save_changes": "Änderungen speichern",
|
||||
"saved": "Gespeichert",
|
||||
"scheduled": "Geplant",
|
||||
"search": "Suchen",
|
||||
"security": "Sicherheit",
|
||||
@@ -352,6 +354,8 @@
|
||||
"selection": "Auswahl",
|
||||
"selections": "Auswahlen",
|
||||
"send": "Senden",
|
||||
"send_test_email": "Test-E-Mail senden",
|
||||
"sent": "Gesendet",
|
||||
"session_not_found": "Sitzung nicht gefunden",
|
||||
"settings": "Einstellungen",
|
||||
"share_feedback": "Feedback geben",
|
||||
@@ -437,6 +441,8 @@
|
||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Du wirst am {Datum} auf die Community Edition herabgestuft."
|
||||
},
|
||||
"emails": {
|
||||
"email_customization_preview_email_heading": "Hey {userName}",
|
||||
"email_customization_preview_email_text": "Dies ist eine E-Mail-Vorschau, um dir zu zeigen, welches Logo in den E-Mails gerendert wird.",
|
||||
"embed_survey_preview_email_didnt_request": "Kein Interesse?",
|
||||
"embed_survey_preview_email_environment_id": "Umgebungs-ID",
|
||||
"embed_survey_preview_email_fight_spam": "Hilf uns, Spam zu bekämpfen, und leite diese Mail an hola@formbricks.com weiter.",
|
||||
@@ -897,7 +903,8 @@
|
||||
"app_survey_placement": "Platzierung der Umfragen",
|
||||
"app_survey_placement_settings_description": "Ändere, wo Umfragen in deiner App oder Website angezeigt werden.",
|
||||
"centered_modal_overlay_color": "Hintergrundfarbe bei Modal-Ansicht",
|
||||
"eliminate_branding_with_whitelabel": "Entferne Formbricks Branding und aktiviere zusätzliche White-Label-Anpassungsoptionen.",
|
||||
"email_customization": "E-Mail-Anpassung",
|
||||
"email_customization_description": "Ändere das Aussehen und das Gefühl der E-Mails, die Formbricks in deinem Namen sendet.",
|
||||
"enable_custom_styling": "Eigenes Styling aktivieren",
|
||||
"enable_custom_styling_description": "Erlaube Nutzern, dieses Styling im Umfrageditor zu überschreiben.",
|
||||
"failed_to_remove_logo": "Logo konnte nicht entfernt werden",
|
||||
@@ -1117,12 +1124,16 @@
|
||||
"copy_invite_link_to_clipboard": "Einladungslink in die Zwischenablage kopieren",
|
||||
"create_new_organization": "Neue Organisation erstellen",
|
||||
"create_new_organization_description": "Erstelle eine neue Organisation, um weitere Projekte zu verwalten.",
|
||||
"customize_email_with_a_higher_plan": "E-Mail-Anpassung mit einem höheren Plan",
|
||||
"delete_organization": "Organisation löschen",
|
||||
"delete_organization_description": "Organisation mit allen Projekten einschließlich aller Umfragen, Antworten, Personen, Aktionen und Attribute löschen",
|
||||
"delete_organization_warning": "Bevor Du mit dem Löschen dieser Organisation fortfährst, sei dir bitte der folgenden Konsequenzen bewusst:",
|
||||
"delete_organization_warning_1": "Dauerhafte Entfernung aller Projekte, die mit dieser Organisation verbunden sind.",
|
||||
"delete_organization_warning_2": "Diese Aktion kann nicht rückgängig gemacht werden. Wenn es weg ist, ist es weg.",
|
||||
"delete_organization_warning_3": "Bitte gib {organizationName} in das folgende Feld ein, um die endgültige Löschung dieser Organisation zu bestätigen:",
|
||||
"eliminate_branding_with_whitelabel": "Entferne Formbricks Branding und aktiviere zusätzliche White-Label-Anpassungsoptionen.",
|
||||
"email_customization_preview_email_heading": "Hey {userName}",
|
||||
"email_customization_preview_email_text": "Dies ist eine E-Mail-Vorschau, um dir zu zeigen, welches Logo in den E-Mails gerendert wird.",
|
||||
"error_deleting_organization_please_try_again": "Fehler beim Löschen der Organisation. Bitte versuche es erneut.",
|
||||
"formbricks_ai": "Formbricks KI",
|
||||
"formbricks_ai_description": "Erhalte personalisierte Einblicke aus deinen Umfrageantworten mit Formbricks KI",
|
||||
@@ -1139,6 +1150,9 @@
|
||||
"leave_organization_description": "Du wirst diese Organisation verlassen und den Zugriff auf alle Umfragen und Antworten verlieren. Du kannst nur wieder beitreten, wenn Du erneut eingeladen wirst.",
|
||||
"leave_organization_ok_btn_text": "Ja, Organisation verlassen",
|
||||
"leave_organization_title": "Bist Du sicher?",
|
||||
"logo_in_email_header": "Logo in der E-Mail-Kopfzeile",
|
||||
"logo_removed_successfully": "Logo erfolgreich entfernt",
|
||||
"logo_saved_successfully": "Logo erfolgreich gespeichert",
|
||||
"manage_members": "Mitglieder verwalten",
|
||||
"manage_members_description": "Mitglieder in deiner Organisation hinzufügen oder entfernen.",
|
||||
"member_deleted_successfully": "Mitglied erfolgreich gelöscht",
|
||||
@@ -1155,11 +1169,16 @@
|
||||
"organization_name_updated_successfully": "Organisationsname erfolgreich aktualisiert",
|
||||
"organization_settings": "Organisationseinstellungen",
|
||||
"ownership_transferred_successfully": "Eigentumsrechte erfolgreich übertragen",
|
||||
"please_add_a_logo": "Bitte füge ein Logo hinzu",
|
||||
"please_check_csv_file": "Bitte überprüfe die CSV-Datei und stelle sicher, dass sie unserem Format entspricht",
|
||||
"please_save_logo_before_sending_test_email": "Bitte speichere das Logo, bevor Du einen Test-E-Mail sendest.",
|
||||
"remove_logo": "Logo entfernen",
|
||||
"replace_logo": "Logo ersetzen",
|
||||
"resend_invitation_email": "Einladungsemail erneut senden",
|
||||
"send_invitation": "Einladung senden",
|
||||
"share_invite_link": "Einladungslink teilen",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:",
|
||||
"test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet",
|
||||
"there_can_only_be_one_owner_of_each_organization": "Es kann nur einen Besitzer jeder Organisation geben. Wenn Du deine Eigentümerschaft überträgst an",
|
||||
"to_confirm": "bestätigen:",
|
||||
"type_in": "Tippe ein",
|
||||
|
||||
@@ -325,6 +325,7 @@
|
||||
"questions": "Questions",
|
||||
"read_docs": "Read Docs",
|
||||
"remove": "Remove",
|
||||
"removed": "Removed",
|
||||
"reorder_and_hide_columns": "Reorder and hide columns",
|
||||
"report_survey": "Report Survey",
|
||||
"request_an_enterprise_license": "request an Enterprise license.",
|
||||
@@ -339,6 +340,7 @@
|
||||
"sales": "Sales",
|
||||
"save": "Save",
|
||||
"save_changes": "Save changes",
|
||||
"saved": "Saved",
|
||||
"scheduled": "Scheduled",
|
||||
"search": "Search",
|
||||
"security": "Security",
|
||||
@@ -352,6 +354,8 @@
|
||||
"selection": "Selection",
|
||||
"selections": "Selections",
|
||||
"send": "Send",
|
||||
"send_test_email": "Send test email",
|
||||
"sent": "Sent",
|
||||
"session_not_found": "Session not found",
|
||||
"settings": "Settings",
|
||||
"share_feedback": "Share feedback",
|
||||
@@ -437,6 +441,8 @@
|
||||
"you_will_be_downgraded_to_the_community_edition_on_date": "You will be downgraded to the Community Edition on {date}."
|
||||
},
|
||||
"emails": {
|
||||
"email_customization_preview_email_heading": "Hey {userName}",
|
||||
"email_customization_preview_email_text": "This is an email preview to show you which logo will be rendered in the emails.",
|
||||
"embed_survey_preview_email_didnt_request": "Didn't request this?",
|
||||
"embed_survey_preview_email_environment_id": "Environment ID",
|
||||
"embed_survey_preview_email_fight_spam": "Help us fight spam and forward this mail to hola@formbricks.com",
|
||||
@@ -897,7 +903,8 @@
|
||||
"app_survey_placement": "App Survey Placement",
|
||||
"app_survey_placement_settings_description": "Change where surveys will be shown in your web app or website.",
|
||||
"centered_modal_overlay_color": "Centered modal overlay color",
|
||||
"eliminate_branding_with_whitelabel": "Eliminate Formbricks branding and enable additional white-label customization options.",
|
||||
"email_customization": "Email Customization",
|
||||
"email_customization_description": "Change the look and feel of emails Formbricks sends out on your behalf.",
|
||||
"enable_custom_styling": "Enable custom styling",
|
||||
"enable_custom_styling_description": "Allow users to override this theme in the survey editor.",
|
||||
"failed_to_remove_logo": "Failed to remove the logo",
|
||||
@@ -1117,12 +1124,16 @@
|
||||
"copy_invite_link_to_clipboard": "Copy invite link to clipboard",
|
||||
"create_new_organization": "Create new organization",
|
||||
"create_new_organization_description": "Create a new organization to handle a different set of projects.",
|
||||
"customize_email_with_a_higher_plan": "Customize email with a higher plan",
|
||||
"delete_organization": "Delete Organization",
|
||||
"delete_organization_description": "Delete organization with all its projects including all surveys, responses, people, actions and attributes",
|
||||
"delete_organization_warning": "Before you proceed with deleting this organization, please be aware of the following consequences:",
|
||||
"delete_organization_warning_1": "Permanent removal of all projects linked to this organization.",
|
||||
"delete_organization_warning_2": "This action cannot be undone. If it's gone, it's gone.",
|
||||
"delete_organization_warning_3": "Please enter {organizationName} in the following field to confirm the definitive deletion of this organization:",
|
||||
"eliminate_branding_with_whitelabel": "Eliminate Formbricks branding and enable additional white-label customization options.",
|
||||
"email_customization_preview_email_heading": "Hey {userName}",
|
||||
"email_customization_preview_email_text": "This is an email preview to show you which logo will be rendered in the emails.",
|
||||
"error_deleting_organization_please_try_again": "Error deleting organization. Please try again.",
|
||||
"formbricks_ai": "Formbricks AI",
|
||||
"formbricks_ai_description": "Get personalised insights from your survey responses with Formbricks AI",
|
||||
@@ -1139,6 +1150,9 @@
|
||||
"leave_organization_description": "You wil leave this organization and loose access to all surveys and responses. You can only rejoin if you are invited again.",
|
||||
"leave_organization_ok_btn_text": "Yes, leave organization",
|
||||
"leave_organization_title": "Are you sure?",
|
||||
"logo_in_email_header": "Logo in email header",
|
||||
"logo_removed_successfully": "Logo removed successfully",
|
||||
"logo_saved_successfully": "Logo saved successfully",
|
||||
"manage_members": "Manage members",
|
||||
"manage_members_description": "Add or remove members in your organization.",
|
||||
"member_deleted_successfully": "Member deleted successfully",
|
||||
@@ -1155,11 +1169,16 @@
|
||||
"organization_name_updated_successfully": "Organization name updated successfully",
|
||||
"organization_settings": "Organization Settings",
|
||||
"ownership_transferred_successfully": "Ownership transferred successfully",
|
||||
"please_add_a_logo": "Please add a logo",
|
||||
"please_check_csv_file": "Please check the CSV file and make sure it is according to our format",
|
||||
"please_save_logo_before_sending_test_email": "Please save the logo before sending a test email.",
|
||||
"remove_logo": "Remove logo",
|
||||
"replace_logo": "Replace logo",
|
||||
"resend_invitation_email": "Resend Invitation Email",
|
||||
"send_invitation": "Send Invitation",
|
||||
"share_invite_link": "Share Invite Link",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:",
|
||||
"test_email_sent_successfully": "Test email sent successfully",
|
||||
"there_can_only_be_one_owner_of_each_organization": "There can only be one owner of each organization. If you transfer your ownership to",
|
||||
"to_confirm": "to confirm:",
|
||||
"type_in": "Type in",
|
||||
|
||||
@@ -325,6 +325,7 @@
|
||||
"questions": "Questions",
|
||||
"read_docs": "Lire les documents",
|
||||
"remove": "Retirer",
|
||||
"removed": "Retiré",
|
||||
"reorder_and_hide_columns": "Réorganiser et masquer des colonnes",
|
||||
"report_survey": "Rapport d'enquête",
|
||||
"request_an_enterprise_license": "demander une licence Entreprise.",
|
||||
@@ -339,6 +340,7 @@
|
||||
"sales": "Ventes",
|
||||
"save": "Enregistrer",
|
||||
"save_changes": "Enregistrer les modifications",
|
||||
"saved": "Enregistré",
|
||||
"scheduled": "Programmé",
|
||||
"search": "Recherche",
|
||||
"security": "Sécurité",
|
||||
@@ -352,6 +354,8 @@
|
||||
"selection": "Sélection",
|
||||
"selections": "Sélections",
|
||||
"send": "Envoyer",
|
||||
"send_test_email": "Envoyer un e-mail de test",
|
||||
"sent": "Envoyé",
|
||||
"session_not_found": "Session non trouvée",
|
||||
"settings": "Paramètres",
|
||||
"share_feedback": "Partager des retours",
|
||||
@@ -437,6 +441,8 @@
|
||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Vous serez rétrogradé à l'édition communautaire le {date}."
|
||||
},
|
||||
"emails": {
|
||||
"email_customization_preview_email_heading": "Salut {userName}",
|
||||
"email_customization_preview_email_text": "C'est une prévisualisation d'e-mail pour vous montrer quel logo sera rendu dans les e-mails.",
|
||||
"embed_survey_preview_email_didnt_request": "Vous n'avez pas demandé cela ?",
|
||||
"embed_survey_preview_email_environment_id": "ID d'environnement",
|
||||
"embed_survey_preview_email_fight_spam": "Aidez-nous à lutter contre le spam et transférez ce mail à hola@formbricks.com.",
|
||||
@@ -897,7 +903,9 @@
|
||||
"app_survey_placement": "Placement de l'enquête dans l'application",
|
||||
"app_survey_placement_settings_description": "Changez l'emplacement où les enquêtes seront affichées dans votre application web ou votre site web.",
|
||||
"centered_modal_overlay_color": "Couleur de superposition modale centrée",
|
||||
"eliminate_branding_with_whitelabel": "Éliminez la marque Formbricks et activez des options de personnalisation supplémentaires.",
|
||||
|
||||
"email_customization": "Personnalisation des e-mails",
|
||||
"email_customization_description": "Modifiez l'apparence des e-mails envoyés par Formbricks en votre nom.",
|
||||
"enable_custom_styling": "Activer le style personnalisé",
|
||||
"enable_custom_styling_description": "Permettre aux utilisateurs de remplacer ce thème dans l'éditeur d'enquête.",
|
||||
"failed_to_remove_logo": "Échec de la suppression du logo",
|
||||
@@ -1117,12 +1125,16 @@
|
||||
"copy_invite_link_to_clipboard": "Copier le lien d'invitation dans le presse-papiers",
|
||||
"create_new_organization": "Créer une nouvelle organisation",
|
||||
"create_new_organization_description": "Créer une nouvelle organisation pour gérer un ensemble différent de projets.",
|
||||
"customize_email_with_a_higher_plan": "Personnalisez l'e-mail avec un plan supérieur",
|
||||
"delete_organization": "Supprimer l'organisation",
|
||||
"delete_organization_description": "Supprimer l'organisation avec tous ses projets, y compris toutes les enquêtes, réponses, personnes, actions et attributs.",
|
||||
"delete_organization_warning": "Avant de procéder à la suppression de cette organisation, veuillez prendre connaissance des conséquences suivantes :",
|
||||
"delete_organization_warning_1": "Suppression permanente de tous les projets liés à cette organisation.",
|
||||
"delete_organization_warning_2": "Cette action ne peut pas être annulée. Si c'est parti, c'est parti.",
|
||||
"delete_organization_warning_3": "Veuillez entrer {organizationName} dans le champ suivant pour confirmer la suppression définitive de cette organisation :",
|
||||
"eliminate_branding_with_whitelabel": "Éliminez la marque Formbricks et activez des options de personnalisation supplémentaires.",
|
||||
"email_customization_preview_email_heading": "Hey {userName}",
|
||||
"email_customization_preview_email_text": "Cette est une prévisualisation d'e-mail pour vous montrer quel logo sera rendu dans les e-mails.",
|
||||
"error_deleting_organization_please_try_again": "Erreur lors de la suppression de l'organisation. Veuillez réessayer.",
|
||||
"formbricks_ai": "Formbricks IA",
|
||||
"formbricks_ai_description": "Obtenez des insights personnalisés à partir de vos réponses au sondage avec Formbricks AI.",
|
||||
@@ -1139,6 +1151,9 @@
|
||||
"leave_organization_description": "Vous quitterez cette organisation et perdrez l'accès à toutes les enquêtes et réponses. Vous ne pourrez revenir que si vous êtes de nouveau invité.",
|
||||
"leave_organization_ok_btn_text": "Oui, quitter l'organisation",
|
||||
"leave_organization_title": "Es-tu sûr ?",
|
||||
"logo_in_email_header": "Logo dans l'en-tête de l'e-mail",
|
||||
"logo_removed_successfully": "Logo supprimé avec succès",
|
||||
"logo_saved_successfully": "Logo enregistré avec succès",
|
||||
"manage_members": "Gérer les membres",
|
||||
"manage_members_description": "Ajouter ou supprimer des membres dans votre organisation.",
|
||||
"member_deleted_successfully": "Membre supprimé avec succès",
|
||||
@@ -1155,11 +1170,16 @@
|
||||
"organization_name_updated_successfully": "Nom de l'organisation mis à jour avec succès",
|
||||
"organization_settings": "Paramètres de l'organisation",
|
||||
"ownership_transferred_successfully": "Propriété transférée avec succès",
|
||||
"please_add_a_logo": "Veuillez ajouter un logo",
|
||||
"please_check_csv_file": "Veuillez vérifier le fichier CSV et vous assurer qu'il est conforme à notre format.",
|
||||
"please_save_logo_before_sending_test_email": "Veuillez enregistrer le logo avant d'envoyer un e-mail de test.",
|
||||
"remove_logo": "Supprimer le logo",
|
||||
"replace_logo": "Remplacer le logo",
|
||||
"resend_invitation_email": "Renvoyer l'e-mail d'invitation",
|
||||
"send_invitation": "Envoyer l'invitation",
|
||||
"share_invite_link": "Partager le lien d'invitation",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :",
|
||||
"test_email_sent_successfully": "E-mail de test envoyé avec succès",
|
||||
"there_can_only_be_one_owner_of_each_organization": "Il ne peut y avoir qu'un seul propriétaire pour chaque organisation. Si vous transférez votre propriété à",
|
||||
"to_confirm": "pour confirmer :",
|
||||
"type_in": "Tapez",
|
||||
|
||||
@@ -325,6 +325,7 @@
|
||||
"questions": "Perguntas",
|
||||
"read_docs": "Ler Documentos",
|
||||
"remove": "remover",
|
||||
"removed": "Removido",
|
||||
"reorder_and_hide_columns": "Reordenar e ocultar colunas",
|
||||
"report_survey": "Relatório de Pesquisa",
|
||||
"request_an_enterprise_license": "pedir uma licença Enterprise",
|
||||
@@ -339,6 +340,7 @@
|
||||
"sales": "vendas",
|
||||
"save": "Salvar",
|
||||
"save_changes": "Salvar alterações",
|
||||
"saved": "Salvo",
|
||||
"scheduled": "agendado",
|
||||
"search": "Buscar",
|
||||
"security": "Segurança",
|
||||
@@ -352,6 +354,8 @@
|
||||
"selection": "seleção",
|
||||
"selections": "seleções",
|
||||
"send": "Enviar",
|
||||
"send_test_email": "Enviar e-mail de teste",
|
||||
"sent": "Enviado",
|
||||
"session_not_found": "Sessão não encontrada",
|
||||
"settings": "Configurações",
|
||||
"share_feedback": "Compartilhar feedback",
|
||||
@@ -437,6 +441,8 @@
|
||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Você será rebaixado para a Edição Comunitária em {data}."
|
||||
},
|
||||
"emails": {
|
||||
"email_customization_preview_email_heading": "Oi {userName}",
|
||||
"email_customization_preview_email_text": "Esta é uma pré-visualização de e-mail para mostrar qual logo será renderizado nos e-mails.",
|
||||
"embed_survey_preview_email_didnt_request": "Não pediu isso?",
|
||||
"embed_survey_preview_email_environment_id": "ID do Ambiente",
|
||||
"embed_survey_preview_email_fight_spam": "Ajude a gente a combater spam e encaminhe este e-mail para hola@formbricks.com",
|
||||
@@ -897,7 +903,8 @@
|
||||
"app_survey_placement": "Posicionamento da Pesquisa no App",
|
||||
"app_survey_placement_settings_description": "Mude onde as pesquisas serão exibidas no seu app ou site.",
|
||||
"centered_modal_overlay_color": "Cor de sobreposição modal centralizada",
|
||||
"eliminate_branding_with_whitelabel": "Elimine a marca Formbricks e ative opções adicionais de personalização de marca branca.",
|
||||
"email_customization": "Personalização de Email",
|
||||
"email_customization_description": "Mude a aparência e o estilo dos e-mails que o Formbricks envia em seu nome.",
|
||||
"enable_custom_styling": "Ativar estilo personalizado",
|
||||
"enable_custom_styling_description": "Permitir que os usuários alterem esse tema no editor de pesquisa.",
|
||||
"failed_to_remove_logo": "Falha ao remover o logo",
|
||||
@@ -1117,12 +1124,16 @@
|
||||
"copy_invite_link_to_clipboard": "Copiar link do convite para a área de transferência",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
"create_new_organization_description": "Criar uma nova organização para lidar com um conjunto diferente de projetos.",
|
||||
"customize_email_with_a_higher_plan": "Personalize o email com um plano superior",
|
||||
"delete_organization": "Excluir Organização",
|
||||
"delete_organization_description": "Excluir organização com todos os seus projetos, incluindo todas as pesquisas, respostas, pessoas, ações e atributos",
|
||||
"delete_organization_warning": "Antes de continuar com a exclusão desta organização, esteja ciente das seguintes consequências:",
|
||||
"delete_organization_warning_1": "Remoção permanente de todos os projetos ligados a essa organização.",
|
||||
"delete_organization_warning_2": "Essa ação não pode ser desfeita. Se foi, foi.",
|
||||
"delete_organization_warning_3": "Por favor, digite {organizationName} no campo abaixo para confirmar a exclusão definitiva desta organização:",
|
||||
"eliminate_branding_with_whitelabel": "Elimine a marca Formbricks e ative opções adicionais de personalização de marca branca.",
|
||||
"email_customization_preview_email_heading": "Olá {userName}",
|
||||
"email_customization_preview_email_text": "Esta é uma pré-visualização de e-mail para mostrar qual logo será renderizado nos e-mails.",
|
||||
"error_deleting_organization_please_try_again": "Erro ao deletar a organização. Por favor, tente novamente.",
|
||||
"formbricks_ai": "Formbricks IA",
|
||||
"formbricks_ai_description": "Obtenha insights personalizados das suas respostas de pesquisa com o Formbricks AI",
|
||||
@@ -1139,6 +1150,9 @@
|
||||
"leave_organization_description": "Você vai sair dessa organização e perder acesso a todas as pesquisas e respostas. Você só pode voltar se for convidado de novo.",
|
||||
"leave_organization_ok_btn_text": "Sim, sair da organização",
|
||||
"leave_organization_title": "Você tem certeza?",
|
||||
"logo_in_email_header": "Logo na cabeçalho do e-mail",
|
||||
"logo_removed_successfully": "Logo removido com sucesso",
|
||||
"logo_saved_successfully": "Logo salvo com sucesso",
|
||||
"manage_members": "Gerenciar membros",
|
||||
"manage_members_description": "Adicionar ou remover membros na sua organização.",
|
||||
"member_deleted_successfully": "Membro deletado com sucesso",
|
||||
@@ -1155,11 +1169,16 @@
|
||||
"organization_name_updated_successfully": "Nome da organização atualizado com sucesso",
|
||||
"organization_settings": "Configurações da Organização",
|
||||
"ownership_transferred_successfully": "Propriedade transferida com sucesso",
|
||||
"please_add_a_logo": "Por favor, adicione um logo",
|
||||
"please_check_csv_file": "Por favor, verifique o arquivo CSV e certifique-se de que está de acordo com o nosso formato",
|
||||
"please_save_logo_before_sending_test_email": "Por favor, salve o logo antes de enviar um e-mail de teste.",
|
||||
"remove_logo": "Remover logo",
|
||||
"replace_logo": "Substituir logo",
|
||||
"resend_invitation_email": "Reenviar E-mail de Convite",
|
||||
"send_invitation": "Enviar Convite",
|
||||
"share_invite_link": "Compartilhar Link de Convite",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:",
|
||||
"test_email_sent_successfully": "E-mail de teste enviado com sucesso",
|
||||
"there_can_only_be_one_owner_of_each_organization": "Só pode ter um dono de cada organização. Se você transferir sua propriedade para",
|
||||
"to_confirm": "pra confirmar:",
|
||||
"type_in": "Digita aí",
|
||||
|
||||
@@ -27,6 +27,7 @@ export const select: Prisma.OrganizationSelect = {
|
||||
name: true,
|
||||
billing: true,
|
||||
isAIEnabled: true,
|
||||
whitelabel: true,
|
||||
};
|
||||
|
||||
export const getOrganizationsTag = (organizationId: string) => `organizations-${organizationId}`;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const ZLinkSurveyEmailData = z.object({
|
||||
suId: z.string().optional(),
|
||||
surveyName: z.string(),
|
||||
locale: z.string(),
|
||||
logoUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TLinkSurveyEmailData = z.infer<typeof ZLinkSurveyEmailData>;
|
||||
|
||||
@@ -33,6 +33,12 @@ export const ZOrganizationBilling = z.object({
|
||||
|
||||
export type TOrganizationBilling = z.infer<typeof ZOrganizationBilling>;
|
||||
|
||||
export const ZOrganizationWhitelabel = z.object({
|
||||
logoUrl: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type TOrganizationWhitelabel = z.infer<typeof ZOrganizationWhitelabel>;
|
||||
|
||||
export const ZOrganization = z.object({
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.date(),
|
||||
@@ -40,6 +46,7 @@ export const ZOrganization = z.object({
|
||||
name: z.string({ message: "Organization name is required" }).trim().min(1, {
|
||||
message: "Organization name must be at least 1 character long",
|
||||
}),
|
||||
whitelabel: ZOrganizationWhitelabel.optional(),
|
||||
billing: ZOrganizationBilling,
|
||||
isAIEnabled: z.boolean().default(false),
|
||||
});
|
||||
@@ -53,6 +60,7 @@ export type TOrganizationCreateInput = z.infer<typeof ZOrganizationCreateInput>;
|
||||
|
||||
export const ZOrganizationUpdateInput = z.object({
|
||||
name: z.string(),
|
||||
whitelabel: ZOrganizationWhitelabel.optional(),
|
||||
billing: ZOrganizationBilling.optional(),
|
||||
isAIEnabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"IMPRINT_URL",
|
||||
"IMPRINT_ADDRESS",
|
||||
"INVITE_DISABLED",
|
||||
"IS_FORMBRICKS_CLOUD",
|
||||
"INTERCOM_SECRET_KEY",
|
||||
|
||||
Reference in New Issue
Block a user