Compare commits

..

13 Commits

Author SHA1 Message Date
Piyush Gupta
65e30f3ab0 fix: improve error handling for Slack integration and update error types 2024-11-11 14:44:27 +05:30
Piyush Gupta
7f1e3502e2 Merge branch 'main' of https://github.com/formbricks/formbricks into mertergolu-fix 2024-11-11 14:07:34 +05:30
Dhruwang Jariwala
be3bbdf2e2 Merge branch 'main' into main 2024-11-08 16:32:38 +05:30
Dhruwang
b1c2f2c4cd fix reconnect 2024-11-08 16:31:50 +05:30
Dhruwang
7616133a25 tweaks 2024-11-08 15:18:33 +05:30
Dhruwang
60139afd81 merged main 2024-11-08 15:14:05 +05:30
Dhruwang
19e5865d05 fix: backward compatibility 2024-10-17 12:08:53 +05:30
Dhruwang
6c5c27f571 Merge branch 'main' of https://github.com/merteroglu/formbricks into merteroglu/main 2024-10-16 17:48:30 +05:30
Dhruwang Jariwala
c12fb1a9f8 Merge branch 'main' into main 2024-10-16 17:46:00 +05:30
Dhruwang
526439def3 Merge branch 'main' of https://github.com/merteroglu/formbricks into merteroglu/main 2024-10-04 11:45:19 +05:30
Dhruwang
4e01ac211f Merge branch 'main' of https://github.com/Dhruwang/formbricks into merteroglu/main 2024-10-04 11:44:48 +05:30
Dhruwang Jariwala
f2f3ff6d46 Merge branch 'main' into main 2024-10-04 11:44:41 +05:30
Mert Eroğlu
b332cf12ca implement new slack ep 2024-09-29 23:21:30 +03:00
41 changed files with 366 additions and 768 deletions

16
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=20
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node modules
# RUN su node -c "npm install -g <your-package-list-here>"
RUN su node -c "npm install -g pnpm"

View File

@@ -1,6 +1,28 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node-postgres
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
{
"features": {},
"image": "mcr.microsoft.com/devcontainers/universal:2",
"postAttachCommand": "pnpm go",
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev"
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["dbaeumer.vscode-eslint"]
}
},
"dockerComposeFile": "docker-compose.yml",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or with the host.
"forwardPorts": [3000, 5432, 8025],
"name": "Node.js & PostgreSQL",
"postAttachCommand": "pnpm dev --filter=@formbricks/web... --filter=@formbricks/demo...",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"service": "app",
"workspaceFolder": "/workspace"
}

View File

@@ -0,0 +1,51 @@
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
# Update 'VARIANT' to pick an LTS version of Node.js: 20, 18, 16, 14.
# Append -bullseye or -buster to pin to an OS version.
# Use -bullseye variants on local arm64/Apple Silicon.
VARIANT: "20"
volumes:
- ..:/workspace:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Uncomment the next line to use a non-root user for all processes.
# user: node
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: pgvector/pgvector:pg17
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: formbricks
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
mailhog:
image: mailhog/mailhog
network_mode: service:app
logging:
driver:
"none" # disable saving logs
# ports:
# - 8025:8025 # web ui
# 1025:1025 # smtp server
volumes:
postgres-data: null

View File

@@ -10,13 +10,6 @@ body:
description: A summary of the issue. This needs to be a clear detailed-rich summary.
validations:
required: true
- type: textarea
id: issue-expected-behavior
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
id: other-information
attributes:

View File

@@ -26,20 +26,15 @@ export const metadata = {
# n8n Setup
<Note>
The Formbricks n8n node is currently only available in the n8n self-hosted version as a community node. To
install it go to "Settings" -> "Community Nodes" and install @formbricks/n8n-nodes-formbricks.
</Note>
n8n allows you to build flexible workflows focused on deep data integration. And with sharable templates and a user-friendly UI, the less technical people on your team can collaborate on them too. Unlike other tools, complexity is not a limitation. So you can build whatever you want — without stressing over budget. Hook up Formbricks with n8n and you can send your data to 350+ other apps. Here is how to do it.
## Step 1: Setup your survey incl. `questionId` for every question
<Note>
Nailed down your survey? Any changes in the survey cause additional work in the n8n node. It makes sense to
first settle on the survey you want to run and then get to setting up n8n.
Nail down your survey? Any changes in the survey cause additional work in the n8n node. It makes
sense to first settle on the survey you want to run and then get to setting up n8n.
</Note>
## Step 1: Setup your survey incl. `questionId` for every question
When setting up the node your life will be easier when you change the `questionId`s of your survey questions. You can only do so **before** you publish your survey.
<MdxImage

View File

@@ -5,7 +5,6 @@ import "@/styles/tailwind.css";
import glob from "fast-glob";
import { type Metadata } from "next";
import { Jost } from "next/font/google";
import Script from "next/script";
export const metadata: Metadata = {
title: {
@@ -28,18 +27,6 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<head>
{process.env.NEXT_PUBLIC_LAYER_API_KEY && (
<Script
strategy="afterInteractive"
src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js"
primary-color="#00C4B8"
api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY}
walkthrough-enabled="false"
design-style="copilot"
/>
)}
</head>
<body className={`flex min-h-full bg-white antialiased dark:bg-zinc-900 ${jost.className}`}>
<Providers>
<div className="w-full">

View File

@@ -19,11 +19,7 @@ interface InviteOrganizationMemberProps {
const ZInviteOrganizationMemberDetails = z.object({
email: z.string().email(),
inviteMessage: z
.string()
.trim()
.min(1)
.refine((value) => !/https?:\/\/|<script/i.test(value), "Invite message cannot contain URLs or scripts"),
inviteMessage: z.string().trim().min(1),
});
type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMemberDetails>;

View File

@@ -48,7 +48,7 @@ const Page = async ({ params, searchParams }) => {
getProductByEnvironmentId(params.environmentId),
getEnvironment(params.environmentId),
getActionClasses(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
getAttributeClasses(params.environmentId),
getResponseCountBySurveyId(params.surveyId),
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),

View File

@@ -20,17 +20,13 @@ export const getSegmentsByAttributeClassAction = authenticatedActionClient
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "read",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
const segments = await getSegmentsByAttributeClassName(
parsedInput.environmentId,
parsedInput.attributeClass.name

View File

@@ -4,7 +4,6 @@ import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TUserLocale } from "@formbricks/types/user";
import { Label } from "@formbricks/ui/components/Label";
import { Switch } from "@formbricks/ui/components/Switch";
import { AttributeDetailModal } from "./AttributeDetailModal";
import { AttributeClassDataRow } from "./AttributeRowData";
@@ -50,15 +49,8 @@ export const AttributeClassesTable = ({
{hasArchived && (
<div className="my-4 flex items-center justify-end text-right">
<div className="flex items-center text-sm font-medium">
<Label htmlFor="showArchivedToggle" className="cursor-pointer">
{t("environments.attributes.show_archived")}
</Label>
<Switch
id="showArchivedToggle"
className="mx-3"
checked={showArchived}
onCheckedChange={toggleShowArchived}
/>
{t("environments.attributes.show_archived")}
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
</div>
</div>
)}

View File

@@ -24,7 +24,7 @@ const Page = async ({ params }) => {
const [environment, segments, attributeClasses, organization, product] = await Promise.all([
getEnvironment(params.environmentId),
getSegments(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
getAttributeClasses(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);

View File

@@ -1,16 +1,12 @@
"use client";
import { createActionClassAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { useEffect, useState } from "react";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { Button } from "@formbricks/ui/components/Button";
import { TActionClass } from "@formbricks/types/action-classes";
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
import { Label } from "@formbricks/ui/components/Label";
import { LoadingSpinner } from "@formbricks/ui/components/LoadingSpinner";
@@ -19,25 +15,15 @@ import { getActiveInactiveSurveysAction } from "../actions";
interface ActivityTabProps {
actionClass: TActionClass;
environmentId: string;
environment: TEnvironment;
otherEnvActionClasses: TActionClass[];
otherEnvironment: TEnvironment;
isReadOnly: boolean;
}
export const ActionActivityTab = ({
actionClass,
otherEnvActionClasses,
otherEnvironment,
environmentId,
environment,
isReadOnly,
}: ActivityTabProps) => {
export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabProps) => {
const t = useTranslations();
const [activeSurveys, setActiveSurveys] = useState<string[] | undefined>();
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
@@ -46,6 +32,7 @@ export const ActionActivityTab = ({
const getActiveInactiveSurveysResponse = await getActiveInactiveSurveysAction({
actionClassId: actionClass.id,
});
console.log(getActiveInactiveSurveysResponse, "randike");
if (getActiveInactiveSurveysResponse?.data) {
setActiveSurveys(getActiveInactiveSurveysResponse.data.activeSurveys);
setInactiveSurveys(getActiveInactiveSurveysResponse.data.inactiveSurveys);
@@ -59,57 +46,6 @@ export const ActionActivityTab = ({
updateState();
}, [actionClass.id, environmentId]);
const actionClassNames = useMemo(
() => otherEnvActionClasses.map((actionClass) => actionClass.name),
[otherEnvActionClasses]
);
const actionClassKeys = useMemo(() => {
const codeActionClasses: TActionClassInputCode[] = otherEnvActionClasses.filter(
(actionClass) => actionClass.type === "code"
) as TActionClassInputCode[];
return codeActionClasses.map((actionClass) => actionClass.key);
}, [otherEnvActionClasses]);
const copyAction = async (data: TActionClassInput) => {
const { type } = data;
let copyName = data.name;
try {
if (isReadOnly) {
throw new Error(t("common.you_are_not_authorised_to_perform_this_action"));
}
if (copyName && actionClassNames.includes(copyName)) {
while (actionClassNames.includes(copyName)) {
copyName += " (copy)";
}
}
if (type === "code" && data.key && actionClassKeys.includes(data.key)) {
throw new Error(t("environments.actions.action_with_key_already_exists", { key: data.key }));
}
let updatedAction = {
...data,
name: copyName.trim(),
environmentId: otherEnvironment.id,
};
const createActionClassResponse = await createActionClassAction({
action: updatedAction as TActionClassInput,
});
if (!createActionClassResponse?.data) {
throw new Error(t("environments.actions.action_copy_failed", {}));
}
toast.success(t("environments.actions.action_copied_successfully"));
} catch (e: any) {
toast.error(e.message);
}
};
if (loading) return <LoadingSpinner />;
if (error) return <ErrorComponent />;
@@ -163,22 +99,6 @@ export const ActionActivityTab = ({
<p className="text-sm text-slate-700">{capitalizeFirstLetter(actionClass.type)}</p>
</div>
</div>
<div className="">
<Label className="text-xs font-normal text-slate-500">Environment</Label>
<div className="items-center-center flex gap-2">
<p className="text-xs text-slate-700">
{environment.type === "development" ? "Development" : "Production"}
</p>
<Button
onClick={() => {
copyAction(actionClass);
}}
className="m-0 p-0 text-xs font-medium text-black underline underline-offset-4 focus:ring-0 focus:ring-offset-0"
variant="minimal">
{environment.type === "development" ? "Copy to Production" : "Copy to Development"}
</Button>
</div>
</div>
</div>
</div>
);

View File

@@ -2,27 +2,20 @@
import { useState } from "react";
import { TActionClass } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { ActionDetailModal } from "./ActionDetailModal";
interface ActionClassesTableProps {
environmentId: string;
actionClasses: TActionClass[];
environment: TEnvironment;
children: [JSX.Element, JSX.Element[]];
isReadOnly: boolean;
otherEnvironment: TEnvironment;
otherEnvActionClasses: TActionClass[];
}
export const ActionClassesTable = ({
environmentId,
actionClasses,
environment,
children: [TableHeading, actionRows],
isReadOnly,
otherEnvActionClasses,
otherEnvironment,
}: ActionClassesTableProps) => {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
@@ -55,14 +48,11 @@ export const ActionClassesTable = ({
{activeActionClass && (
<ActionDetailModal
environmentId={environmentId}
environment={environment}
open={isActionDetailModalOpen}
setOpen={setActionDetailModalOpen}
actionClasses={actionClasses}
actionClass={activeActionClass}
isReadOnly={isReadOnly}
otherEnvActionClasses={otherEnvActionClasses}
otherEnvironment={otherEnvironment}
/>
)}
</>

View File

@@ -1,21 +1,17 @@
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TActionClass } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
import { ActionActivityTab } from "./ActionActivityTab";
import { ActionSettingsTab } from "./ActionSettingsTab";
interface ActionDetailModalProps {
environmentId: string;
environment: TEnvironment;
open: boolean;
setOpen: (v: boolean) => void;
actionClass: TActionClass;
actionClasses: TActionClass[];
isReadOnly: boolean;
otherEnvironment: TEnvironment;
otherEnvActionClasses: TActionClass[];
}
export const ActionDetailModal = ({
@@ -24,25 +20,13 @@ export const ActionDetailModal = ({
setOpen,
actionClass,
actionClasses,
environment,
isReadOnly,
otherEnvActionClasses,
otherEnvironment,
}: ActionDetailModalProps) => {
const t = useTranslations();
const tabs = [
{
title: t("common.activity"),
children: (
<ActionActivityTab
otherEnvActionClasses={otherEnvActionClasses}
otherEnvironment={otherEnvironment}
isReadOnly={isReadOnly}
environment={environment}
actionClass={actionClass}
environmentId={environmentId}
/>
),
children: <ActionActivityTab actionClass={actionClass} environmentId={environmentId} />,
},
{
title: t("common.settings"),

View File

@@ -10,7 +10,6 @@ import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
@@ -45,17 +44,6 @@ const Page = async ({ params }) => {
throw new Error(t("common.product_not_found"));
}
const environments = await getEnvironments(product.id);
const currentEnvironment = environments.find((env) => env.id === params.environmentId);
if (!currentEnvironment) {
throw new Error(t("common.environment_not_found"));
}
const otherEnvironment = environments.filter((env) => env.id !== params.environmentId)[0];
const otherEnvActionClasses = await getActionClasses(otherEnvironment.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
@@ -81,9 +69,6 @@ const Page = async ({ params }) => {
<PageContentWrapper>
<PageHeader pageTitle={t("common.actions")} cta={!isReadOnly ? renderAddActionButton() : undefined} />
<ActionClassesTable
environment={currentEnvironment}
otherEnvironment={otherEnvironment}
otherEnvActionClasses={otherEnvActionClasses}
environmentId={params.environmentId}
actionClasses={actionClasses}
isReadOnly={isReadOnly}>

View File

@@ -59,13 +59,15 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const membershipRole = currentUserMembership?.role;
const { isMember } = getAccessFlags(membershipRole);
const { isOwner, isManager } = getAccessFlags(membershipRole);
const isOwnerOrManager = isOwner || isManager;
const { features, lastChecked, isPendingDowngrade, active } = await getEnterpriseLicense();
const productPermission = await getProductPermissionByUserId(session.user.id, environment.productId);
if (isMember && !productPermission) {
if (!isOwnerOrManager && !productPermission) {
throw new Error(t("common.product_permission_not_found"));
}

View File

@@ -222,7 +222,7 @@ export const MainNavigation = ({
{
label: t("common.billing"),
href: `/environments/${environment.id}/settings/billing`,
hidden: !isFormbricksCloud,
hidden: !isFormbricksCloud || isPricingDisabled,
icon: CreditCardIcon,
},
{

View File

@@ -40,7 +40,7 @@ export const OrganizationSettingsNavbar = ({
id: "billing",
label: t("common.billing"),
href: `/environments/${environmentId}/settings/billing`,
hidden: !isFormbricksCloud || loading,
hidden: !isFormbricksCloud || isPricingDisabled || loading,
current: pathname?.includes("/billing"),
},
{

View File

@@ -1,13 +1,10 @@
"use client";
import { AddMemberRole } from "@/modules/ee/role-management/components/add-member-role";
import { zodResolver } from "@hookform/resolvers/zod";
import { OrganizationRole } from "@prisma/client";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
import { ZUserName } from "@formbricks/types/user";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { Alert, AlertDescription } from "@formbricks/ui/components/Alert";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
@@ -21,7 +18,6 @@ interface IndividualInviteTabProps {
isFormbricksCloud: boolean;
environmentId: string;
}
export const IndividualInviteTab = ({
setOpen,
onSubmit,
@@ -29,13 +25,6 @@ export const IndividualInviteTab = ({
isFormbricksCloud,
environmentId,
}: IndividualInviteTabProps) => {
const ZFormSchema = z.object({
name: ZUserName,
email: z.string().email("Invalid email address"),
role: ZOrganizationRole,
});
type TFormData = z.infer<typeof ZFormSchema>;
const t = useTranslations();
const {
register,
@@ -44,13 +33,12 @@ export const IndividualInviteTab = ({
reset,
control,
watch,
formState: { isSubmitting, errors },
} = useForm<TFormData>({
resolver: zodResolver(ZFormSchema),
defaultValues: {
role: "owner",
},
});
formState: { isSubmitting },
} = useForm<{
name: string;
email: string;
role: TOrganizationRole;
}>();
const submitEventClass = async () => {
const data = getValues();
@@ -67,10 +55,9 @@ export const IndividualInviteTab = ({
<Label htmlFor="memberNameInput">{t("common.full_name")}</Label>
<Input
id="memberNameInput"
placeholder="Hans Wurst"
placeholder="e.g. Hans Wurst"
{...register("name", { required: true, validate: (value) => value.trim() !== "" })}
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name.message}</p>}
</div>
<div>
<Label htmlFor="memberEmailInput">{t("common.email")}</Label>

View File

@@ -43,7 +43,7 @@ export const EmbedView = ({
return (
<div className="h-full overflow-hidden">
{!disableBack && (
<div className="border-b border-slate-200 py-2 pl-2">
<div className="border-b border-slate-200 py-2">
<Button
variant="minimal"
className="focus:ring-0"
@@ -63,7 +63,6 @@ export const EmbedView = ({
variant="minimal"
key={tab.id}
onClick={() => setActiveId(tab.id)}
autoFocus={tab.id === activeId}
className={cn(
"rounded-md border px-4 py-2 text-slate-600",
// "focus:ring-0 focus:ring-offset-0", // enable these classes to remove the focus rings on buttons

View File

@@ -6,22 +6,8 @@ import {
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { getFormattedFilters, getTodayDate } from "@/app/lib/surveys/surveys";
import {
differenceInDays,
endOfMonth,
endOfQuarter,
endOfYear,
format,
startOfDay,
startOfMonth,
startOfQuarter,
startOfYear,
subDays,
subMonths,
subQuarters,
subYears,
} from "date-fns";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { differenceInDays, format, startOfDay, subDays } from "date-fns";
import { ArrowDownToLineIcon, ChevronDown, ChevronUp, DownloadIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
@@ -52,13 +38,6 @@ enum FilterDropDownLabels {
ALL_TIME = "environments.surveys.summary.all_time",
LAST_7_DAYS = "environments.surveys.summary.last_7_days",
LAST_30_DAYS = "environments.surveys.summary.last_30_days",
THIS_MONTH = "environments.surveys.summary.this_month",
LAST_MONTH = "environments.surveys.summary.last_month",
LAST_6_MONTHS = "environments.surveys.summary.last_6_months",
THIS_QUARTER = "environments.surveys.summary.this_quarter",
LAST_QUARTER = "environments.surveys.summary.last_quarter",
THIS_YEAR = "environments.surveys.summary.this_year",
LAST_YEAR = "environments.surveys.summary.last_year",
CUSTOM_RANGE = "environments.surveys.summary.custom_range",
}
@@ -66,62 +45,15 @@ interface CustomFilterProps {
survey: TSurvey;
}
const getDateRangeLabel = (from: Date, to: Date): FilterDropDownLabels => {
const dateRanges = [
{
label: FilterDropDownLabels.LAST_7_DAYS,
matches: () => differenceInDays(to, from) === 7,
},
{
label: FilterDropDownLabels.LAST_30_DAYS,
matches: () => differenceInDays(to, from) === 30,
},
{
label: FilterDropDownLabels.THIS_MONTH,
matches: () =>
format(from, "yyyy-MM-dd") === format(startOfMonth(new Date()), "yyyy-MM-dd") &&
format(to, "yyyy-MM-dd") === format(getTodayDate(), "yyyy-MM-dd"),
},
{
label: FilterDropDownLabels.LAST_MONTH,
matches: () =>
format(from, "yyyy-MM-dd") === format(startOfMonth(subMonths(new Date(), 1)), "yyyy-MM-dd") &&
format(to, "yyyy-MM-dd") === format(endOfMonth(subMonths(getTodayDate(), 1)), "yyyy-MM-dd"),
},
{
label: FilterDropDownLabels.LAST_6_MONTHS,
matches: () =>
format(from, "yyyy-MM-dd") === format(startOfMonth(subMonths(new Date(), 6)), "yyyy-MM-dd") &&
format(to, "yyyy-MM-dd") === format(endOfMonth(getTodayDate()), "yyyy-MM-dd"),
},
{
label: FilterDropDownLabels.THIS_QUARTER,
matches: () =>
format(from, "yyyy-MM-dd") === format(startOfQuarter(new Date()), "yyyy-MM-dd") &&
format(to, "yyyy-MM-dd") === format(endOfQuarter(getTodayDate()), "yyyy-MM-dd"),
},
{
label: FilterDropDownLabels.LAST_QUARTER,
matches: () =>
format(from, "yyyy-MM-dd") === format(startOfQuarter(subQuarters(new Date(), 1)), "yyyy-MM-dd") &&
format(to, "yyyy-MM-dd") === format(endOfQuarter(subQuarters(getTodayDate(), 1)), "yyyy-MM-dd"),
},
{
label: FilterDropDownLabels.THIS_YEAR,
matches: () =>
format(from, "yyyy-MM-dd") === format(startOfYear(new Date()), "yyyy-MM-dd") &&
format(to, "yyyy-MM-dd") === format(endOfYear(getTodayDate()), "yyyy-MM-dd"),
},
{
label: FilterDropDownLabels.LAST_YEAR,
matches: () =>
format(from, "yyyy-MM-dd") === format(startOfYear(subYears(new Date(), 1)), "yyyy-MM-dd") &&
format(to, "yyyy-MM-dd") === format(endOfYear(subYears(getTodayDate(), 1)), "yyyy-MM-dd"),
},
];
const matchedRange = dateRanges.find((range) => range.matches());
return matchedRange ? matchedRange.label : FilterDropDownLabels.CUSTOM_RANGE;
const getDifferenceOfDays = (from, to) => {
const days = differenceInDays(to, from);
if (days === 7) {
return FilterDropDownLabels.LAST_7_DAYS;
} else if (days === 30) {
return FilterDropDownLabels.LAST_30_DAYS;
} else {
return FilterDropDownLabels.CUSTOM_RANGE;
}
};
export const CustomFilter = ({ survey }: CustomFilterProps) => {
@@ -131,7 +63,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
const { selectedFilter, dateRange, setDateRange, resetState } = useResponseFilter();
const [filterRange, setFilterRange] = useState<FilterDropDownLabels>(
dateRange.from && dateRange.to
? getDateRangeLabel(dateRange.from, dateRange.to)
? getDifferenceOfDays(dateRange.from, dateRange.to)
: FilterDropDownLabels.ALL_TIME
);
const [selectingDate, setSelectingDate] = useState<DateSelected>(DateSelected.FROM);
@@ -312,67 +244,6 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.LAST_30_DAYS)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(FilterDropDownLabels.THIS_MONTH);
setDateRange({ from: startOfMonth(new Date()), to: getTodayDate() });
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.THIS_MONTH)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_MONTH);
setDateRange({
from: startOfMonth(subMonths(new Date(), 1)),
to: endOfMonth(subMonths(getTodayDate(), 1)),
});
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.LAST_MONTH)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(FilterDropDownLabels.THIS_QUARTER);
setDateRange({ from: startOfQuarter(new Date()), to: endOfQuarter(getTodayDate()) });
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.THIS_QUARTER)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_QUARTER);
setDateRange({
from: startOfQuarter(subQuarters(new Date(), 1)),
to: endOfQuarter(subQuarters(getTodayDate(), 1)),
});
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.LAST_QUARTER)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_6_MONTHS);
setDateRange({
from: startOfMonth(subMonths(new Date(), 6)),
to: endOfMonth(getTodayDate()),
});
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.LAST_6_MONTHS)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(FilterDropDownLabels.THIS_YEAR);
setDateRange({ from: startOfYear(new Date()), to: endOfYear(getTodayDate()) });
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.THIS_YEAR)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_YEAR);
setDateRange({
from: startOfYear(subYears(new Date(), 1)),
to: endOfYear(subYears(getTodayDate(), 1)),
});
}}>
<p className="text-slate-700">{t(FilterDropDownLabels.LAST_YEAR)}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setIsDatePickerOpen(true);

View File

@@ -2,19 +2,16 @@
import { TwoFactor } from "@/app/(auth)/auth/login/components/TwoFactor";
import { TwoFactorBackup } from "@/app/(auth)/auth/login/components/TwoFactorBackup";
import { zodResolver } from "@hookform/resolvers/zod";
import { XCircleIcon } from "lucide-react";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import Link from "next/dist/client/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";
import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
import { Button } from "@formbricks/ui/components/Button";
import { FormControl, FormError, FormField, FormItem } from "@formbricks/ui/components/Form";
import { PasswordInput } from "@formbricks/ui/components/PasswordInput";
import { AzureButton } from "@formbricks/ui/components/SignupOptions/components/AzureButton";
import { GithubButton } from "@formbricks/ui/components/SignupOptions/components/GithubButton";
@@ -56,23 +53,6 @@ export const SigninForm = ({
const emailRef = useRef<HTMLInputElement>(null);
const formMethods = useForm<TSigninFormState>();
const callbackUrl = searchParams?.get("callbackUrl");
const ZSignInInput = z.object({
email: z.string().email(),
password: z.string().min(8),
totpCode: z.string().optional(),
backupCode: z.string().optional(),
});
type TSignInInput = z.infer<typeof ZSignInInput>;
const form = useForm<TSignInInput>({
defaultValues: {
email: "",
password: "",
totpCode: "",
backupCode: "",
},
resolver: zodResolver(ZSignInInput),
});
const t = useTranslations();
const onSubmit: SubmitHandler<TSigninFormState> = async (data) => {
setLoggingIn(true);
@@ -157,11 +137,11 @@ export const SigninForm = ({
const TwoFactorComponent = useMemo(() => {
if (totpBackup) {
return <TwoFactorBackup form={form} />;
return <TwoFactorBackup />;
}
if (totpLogin) {
return <TwoFactor form={form} />;
return <TwoFactor />;
}
return null;
@@ -173,58 +153,53 @@ export const SigninForm = ({
<h1 className="mb-4 text-slate-700">{formLabel}</h1>
<div className="space-y-2">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<form onSubmit={formMethods.handleSubmit(onSubmit)} className="space-y-2">
{TwoFactorComponent}
{showLogin && (
<div className={cn(totpLogin && "hidden", "space-y-2")}>
<FormField
control={form.control}
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<input
id="email"
type="email"
autoComplete="email"
required
value={field.value}
onChange={(email) => field.onChange(email)}
placeholder="work@email.com"
defaultValue={searchParams?.get("email") || ""}
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<PasswordInput
id="password"
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
onFocus={() => setIsPasswordFocused(true)}
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
value={field.value}
onChange={(password) => field.onChange(password)}
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<div className={cn(totpLogin && "hidden")}>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="email" className="sr-only">
{t("common.email")}
</label>
<input
id="email"
type="email"
autoComplete="email"
required
placeholder="work@email.com"
defaultValue={searchParams?.get("email") || ""}
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
{...formMethods.register("email", {
required: true,
pattern: /\S+@\S+\.\S+/,
})}
/>
</div>
<div className="transition-all duration-500 ease-in-out">
<label htmlFor="password" className="sr-only">
{t("common.password")}
</label>
<Controller
name="password"
control={formMethods.control}
render={({ field }) => (
<PasswordInput
id="password"
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
onFocus={() => setIsPasswordFocused(true)}
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
{...field}
/>
)}
rules={{
required: true,
}}
/>
</div>
{passwordResetEnabled && isPasswordFocused && (
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
<Link

View File

@@ -2,24 +2,11 @@
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
import { FormControl, FormField, FormItem } from "@formbricks/ui/components/Form";
import { Controller, useFormContext } from "react-hook-form";
import { OTPInput } from "@formbricks/ui/components/OTPInput";
interface TwoFactorProps {
form: UseFormReturn<
{
email: string;
password: string;
totpCode?: string | undefined;
backupCode?: string | undefined;
},
any,
undefined
>;
}
export const TwoFactor = ({ form }: TwoFactorProps) => {
export const TwoFactor = () => {
const { control } = useFormContext();
const t = useTranslations();
return (
@@ -29,15 +16,11 @@ export const TwoFactor = ({ form }: TwoFactorProps) => {
{t("auth.login.enter_your_two_factor_authentication_code")}
</label>
<FormField
control={form.control}
<Controller
control={control}
name="totpCode"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<OTPInput value={field.value ?? ""} onChange={field.onChange} valueLength={6} />
</FormControl>
</FormItem>
<OTPInput value={field.value ?? ""} onChange={field.onChange} valueLength={6} />
)}
/>
</div>

View File

@@ -2,25 +2,11 @@
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
import { FormField, FormItem } from "@formbricks/ui/components/Form";
import { FormControl } from "@formbricks/ui/components/Form";
import { useFormContext } from "react-hook-form";
import { Input } from "@formbricks/ui/components/Input";
interface TwoFactorBackupProps {
form: UseFormReturn<
{
email: string;
password: string;
totpCode?: string | undefined;
backupCode?: string | undefined;
},
any,
undefined
>;
}
export const TwoFactorBackup = ({ form }: TwoFactorBackupProps) => {
export const TwoFactorBackup = () => {
const { register } = useFormContext();
const t = useTranslations();
return (
@@ -29,23 +15,12 @@ export const TwoFactorBackup = ({ form }: TwoFactorBackupProps) => {
<label htmlFor="totpBackup" className="sr-only">
{t("auth.login.backup_code")}
</label>
<FormField
control={form.control}
name="backupCode"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<Input
id="totpBackup"
required
placeholder="XXXXX-XXXXX"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
</FormItem>
)}
<Input
id="totpBackup"
required
placeholder="XXXXX-XXXXX"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
{...register("backupCode")}
/>
</div>
</>

View File

@@ -35,7 +35,6 @@ interface PricingTableProps {
SCALE: string;
ENTERPRISE: string;
};
hasBillingRights: boolean;
}
export const PricingTable = ({
@@ -45,7 +44,6 @@ export const PricingTable = ({
productFeatureKeys,
responseCount,
stripePriceLookupKeys,
hasBillingRights,
}: PricingTableProps) => {
const t = useTranslations();
const [planPeriod, setPlanPeriod] = useState<TOrganizationBillingPeriod>(
@@ -222,50 +220,48 @@ export const PricingTable = ({
</div>
</div>
{hasBillingRights && (
<div className="mx-auto mb-12">
<div className="gap-x-2">
<div className="mb-4 flex w-fit cursor-pointer overflow-hidden rounded-lg border border-slate-200 p-1 lg:mb-0">
<div
className={`flex-1 rounded-md px-4 py-0.5 text-center ${
planPeriod === "monthly" ? "bg-slate-200 font-semibold" : "bg-transparent"
}`}
onClick={() => handleMonthlyToggle("monthly")}>
{t("environments.settings.billing.monthly")}
</div>
<div
className={`flex-1 items-center whitespace-nowrap rounded-md py-0.5 pl-4 pr-2 text-center ${
planPeriod === "yearly" ? "bg-slate-200 font-semibold" : "bg-transparent"
}`}
onClick={() => handleMonthlyToggle("yearly")}>
{t("environments.settings.billing.annually")}
<span className="ml-2 inline-flex items-center rounded-full border border-green-200 bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
{t("environments.settings.billing.get_2_months_free")} 🔥
</span>
</div>
<div className="mx-auto mb-12">
<div className="flex gap-x-2">
<div className="mb-4 flex w-fit cursor-pointer overflow-hidden rounded-lg border border-slate-200 p-1 lg:mb-0">
<div
className={`flex-1 rounded-md px-4 py-0.5 text-center ${
planPeriod === "monthly" ? "bg-slate-200 font-semibold" : "bg-transparent"
}`}
onClick={() => handleMonthlyToggle("monthly")}>
{t("environments.settings.billing.monthly")}
</div>
<div className="relative mx-auto grid max-w-md grid-cols-1 gap-y-8 lg:mx-0 lg:-mb-14 lg:max-w-none lg:grid-cols-4">
<div
className="hidden lg:absolute lg:inset-x-px lg:bottom-0 lg:top-4 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
aria-hidden="true"
/>
{CLOUD_PRICING_DATA.plans.map((plan) => (
<PricingCard
planPeriod={planPeriod}
key={plan.id}
plan={plan}
onUpgrade={async () => {
await onUpgrade(plan.id);
}}
organization={organization}
productFeatureKeys={productFeatureKeys}
onManageSubscription={openCustomerPortal}
/>
))}
<div
className={`items-centerrounded-md flex-1 whitespace-nowrap py-0.5 pl-4 pr-2 text-center ${
planPeriod === "yearly" ? "bg-slate-200 font-semibold" : "bg-transparent"
}`}
onClick={() => handleMonthlyToggle("yearly")}>
{t("environments.settings.billing.annually")}
<span className="ml-2 inline-flex items-center rounded-full border border-green-200 bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
{t("environments.settings.billing.get_2_months_free")} 🔥
</span>
</div>
</div>
</div>
)}
<div className="relative mx-auto grid max-w-md grid-cols-1 gap-y-8 lg:mx-0 lg:-mb-14 lg:max-w-none lg:grid-cols-4">
<div
className="hidden lg:absolute lg:inset-x-px lg:bottom-0 lg:top-4 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
aria-hidden="true"
/>
{CLOUD_PRICING_DATA.plans.map((plan) => (
<PricingCard
planPeriod={planPeriod}
key={plan.id}
plan={plan}
onUpgrade={async () => {
await onUpgrade(plan.id);
}}
organization={organization}
productFeatureKeys={productFeatureKeys}
onManageSubscription={openCustomerPortal}
/>
))}
</div>
</div>
</div>
</main>
);

View File

@@ -4,7 +4,10 @@ import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
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 { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
export const metadata: Metadata = {
title: "Billing",
@@ -26,7 +29,10 @@ const BillingLayout = async ({ children, params }) => {
throw new Error(t("common.organization_not_found"));
}
return <>{children}</>;
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
return <>{!isMember ? <>{children}</> : <ErrorComponent />}</>;
};
export default BillingLayout;

View File

@@ -6,7 +6,6 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { PRODUCT_FEATURE_KEYS, STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
@@ -34,8 +33,6 @@ const Page = async ({ params }) => {
]);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const hasBillingRights = !isMember;
const canDoRoleManagement = await getRoleManagementPermission(organization);
@@ -58,7 +55,6 @@ const Page = async ({ params }) => {
responseCount={responseCount}
stripePriceLookupKeys={STRIPE_PRICE_LOOKUP_KEYS}
productFeatureKeys={PRODUCT_FEATURE_KEYS}
hasBillingRights={hasBillingRights}
/>
</PageContentWrapper>
);

View File

@@ -1,6 +1,6 @@
import { LegalFooter } from "@/app/s/[surveyId]/components/LegalFooter";
import { SurveyLoadingAnimation } from "@/app/s/[surveyId]/components/SurveyLoadingAnimation";
import { useState } from "react";
import React, { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TProduct, TProductStyling } from "@formbricks/types/product";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
@@ -61,9 +61,9 @@ export const LinkSurveyWrapper = ({
<div>
<SurveyLoadingAnimation survey={survey} isBackgroundLoaded={isBackgroundLoaded} />
<MediaBackground survey={survey} product={product} onBackgroundLoaded={handleBackgroundLoaded}>
<div className="flex max-h-dvh min-h-dvh items-end justify-center overflow-clip sm:items-center">
<div className="flex max-h-dvh min-h-dvh items-end justify-center overflow-clip md:items-center">
{!styling.isLogoHidden && product.logo?.url && <ClientLogo product={product} />}
<div className="h-full w-full space-y-6 p-0 sm:max-w-lg">
<div className="h-full w-full space-y-6 p-0 md:max-w-md">
{isPreview && (
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
<div />

View File

@@ -61,7 +61,7 @@ CacheHandler.onCreation(async () => {
// Fallback to LRU handler if Redis client is not available.
// The application will still work, but the cache will be in memory only and not shared.
handler = createLruHandler();
console.log("Using LRU handler for caching.");
console.warn("Falling back to LRU handler because Redis client is not available.");
}
return {

View File

@@ -54,7 +54,7 @@ const getChannelTag = (
case 2:
// Return labels for two channels concatenated with "or", removing "Survey"
return labels.map(removeSurveySuffix).join(" " + t("common.or") + " ");
return labels.map(removeSurveySuffix).join(t(" " + t("common.or") + " "));
case 3:
return t("environments.surveys.templates.all_channels");
@@ -76,14 +76,12 @@ export const TemplateTags = ({ template, selectedFilter }: TemplateTagsProps) =>
const channelTag = useMemo(() => getChannelTag(template.channels, t), [template.channels]);
const getIndustryTag = (industries: TProductConfigIndustry[] | undefined): string | undefined => {
// if user selects an industry e.g. eCommerce than the tag should not say "Multiple industries" anymore but "E-Commerce".
if (selectedFilter[1] !== null) {
const industry = industryMapping.find((industry) => industry.value === selectedFilter[1]);
if (industry) return t(industry.label);
}
if (selectedFilter[1] !== null)
return industryMapping.find((industry) => industry.value === selectedFilter[1])?.label;
if (!industries || industries.length === 0) return undefined;
return industries.length > 1
? t("environments.surveys.templates.multiple_industries")
: t(industryMapping.find((industry) => industry.value === industries[0])?.label);
: industryMapping.find((industry) => industry.value === industries[0])?.label;
};
const industryTag = useMemo(
@@ -97,7 +95,7 @@ export const TemplateTags = ({ template, selectedFilter }: TemplateTagsProps) =>
{industryTag && (
<div
className={cn("rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500")}>
{industryTag}
{t(industryTag)}
</div>
)}
{channelTag && (
@@ -105,7 +103,7 @@ export const TemplateTags = ({ template, selectedFilter }: TemplateTagsProps) =>
className={cn(
"flex-nowrap rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500"
)}>
{channelTag}
{t(channelTag)}
</div>
)}
{template.preset.questions.some((question) => question.logic && question.logic.length > 0) && (

View File

@@ -48,11 +48,7 @@ export const getAttributeClass = reactCache(
);
export const getAttributeClasses = reactCache(
async (
environmentId: string,
page?: number,
options?: { skipArchived: boolean }
): Promise<TAttributeClass[]> =>
async (environmentId: string, page?: number): Promise<TAttributeClass[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
@@ -61,7 +57,6 @@ export const getAttributeClasses = reactCache(
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
...(options?.skipArchived ? { archived: false } : {}),
},
orderBy: {
createdAt: "asc",

View File

@@ -15,7 +15,6 @@ import {
DEFAULT_ORGANIZATION_ID,
DEFAULT_ORGANIZATION_ROLE,
EMAIL_VERIFICATION_DISABLED,
ENCRYPTION_KEY,
GITHUB_ID,
GITHUB_SECRET,
GOOGLE_CLIENT_ID,
@@ -26,7 +25,6 @@ import {
OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM,
} from "./constants";
import { symmetricDecrypt, symmetricEncrypt } from "./crypto";
import { verifyToken } from "./jwt";
import { createMembership } from "./membership/service";
import { createOrganization, getOrganization } from "./organization/service";
@@ -54,8 +52,6 @@ export const authOptions: NextAuthOptions = {
type: "password",
placeholder: "Your password",
},
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
backupCode: { label: "Backup Code", type: "input", placeholder: "Two-factor backup code" },
},
async authorize(credentials, _req) {
let user;
@@ -83,54 +79,6 @@ export const authOptions: NextAuthOptions = {
throw new Error("Invalid credentials");
}
if (user.twoFactorEnabled && credentials.backupCode) {
if (!ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with backup code login.");
throw new Error("Internal Server Error");
}
if (!user.backupCodes) throw new Error("No backup codes found");
const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, ENCRYPTION_KEY));
// check if user-supplied code matches one
const index = backupCodes.indexOf(credentials.backupCode.replaceAll("-", ""));
if (index === -1) throw new Error("Invalid backup code");
// delete verified backup code and re-encrypt remaining
backupCodes[index] = null;
await prisma.user.update({
where: {
id: user.id,
},
data: {
backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), ENCRYPTION_KEY),
},
});
} else if (user.twoFactorEnabled) {
if (!credentials.totpCode) {
throw new Error("second factor required");
}
if (!user.twoFactorSecret) {
throw new Error("Internal Server Error");
}
if (!ENCRYPTION_KEY) {
throw new Error("Internal Server Error");
}
const secret = symmetricDecrypt(user.twoFactorSecret, ENCRYPTION_KEY);
if (secret.length !== 32) {
throw new Error("Internal Server Error");
}
const isValidToken = (await import("./totp")).totpAuthenticatorCheck(credentials.totpCode, secret);
if (!isValidToken) {
throw new Error("Invalid second factor code");
}
}
return {
id: user.id,
email: user.email,

View File

@@ -350,7 +350,6 @@
"status": "Status",
"step_by_step_manual": "Schritt-für-Schritt-Anleitung",
"styling": "Styling",
"submit": "Abschicken",
"summary": "Zusammenfassung",
"survey": "Umfrage",
"survey_completed": "Umfrage abgeschlossen.",
@@ -488,8 +487,6 @@
},
"environments": {
"actions": {
"action_copied_successfully": "Aktion erfolgreich kopiert",
"action_copy_failed": "Aktion konnte nicht kopiert werden",
"action_created_successfully": "Aktion erfolgreich erstellt",
"action_deleted_successfully": "Aktion erfolgreich gelöscht",
"action_type": "Aktionstyp",
@@ -1136,7 +1133,6 @@
"disable_two_factor_authentication": "Zwei-Faktor-Authentifizierung deaktivieren",
"disable_two_factor_authentication_description": "Wenn Du die Zwei-Faktor-Authentifizierung deaktivieren musst, empfehlen wir, sie so schnell wie möglich wieder zu aktivieren.",
"each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Jeder Backup-Code kann genau einmal verwendet werden, um Zugang ohne deinen Authenticator zu gewähren.",
"enable_two_factor_authentication": "Zwei-Faktor-Authentifizierung aktivieren",
"enter_the_code_from_your_authenticator_app_below": "Gib den Code aus deiner Authentifizierungs-App unten ein.",
"file_size_must_be_less_than_10mb": "Dateigröße muss weniger als 10MB sein.",
"invalid_file_type": "Ungültiger Dateityp. Nur JPEG-, PNG- und WEBP-Dateien sind erlaubt.",
@@ -1576,7 +1572,7 @@
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Warte ein paar Sekunden nach dem Auslöser, bevor Du die Umfrage anzeigst",
"waiting_period": "Wartezeit",
"welcome_message": "Willkommensnachricht",
"when": "Wenn",
"when": "Wann",
"when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Wenn die Bedingungen übereinstimmen, wird die Wartezeit ignoriert und die Umfrage angezeigt.",
"with_the_formbricks_sdk": "mit dem Formbricks SDK",
"without_a_filter_all_of_your_users_can_be_surveyed": "Ohne Filter können alle deine Nutzer befragt werden.",
@@ -1699,11 +1695,7 @@
"is_equal_to": "Ist gleich",
"is_less_than": "ist weniger als",
"last_30_days": "Letzte 30 Tage",
"last_6_months": "Letzte 6 Monate",
"last_7_days": "Letzte 7 Tage",
"last_month": "Letztes Monat",
"last_quarter": "Letztes Quartal",
"last_year": "Letztes Jahr",
"learn_how_to": "Lerne, wie man",
"link_to_public_results_copied": "Link zu öffentlichen Ergebnissen kopiert",
"make_sure_the_survey_type_is_set_to": "Stelle sicher, dass der Umfragetyp richtig eingestellt ist",
@@ -1736,9 +1728,6 @@
"static_iframe": "Statisch (iframe)",
"survey_results_are_public": "Deine Umfrageergebnisse sind öffentlich",
"survey_results_are_shared_with_anyone_who_has_the_link": "Deine Umfrageergebnisse stehen allen zur Verfügung, die den Link haben. Die Ergebnisse werden nicht von Suchmaschinen indexiert.",
"this_month": "Dieser Monat",
"this_quarter": "Dieses Quartal",
"this_year": "Dieses Jahr",
"time_to_complete": "Zeit zur Fertigstellung",
"to_connect_your_app_with_formbricks": "um deine App mit Formbricks zu verbinden",
"to_connect_your_web_app_with_formbricks": "um deine Web-App mit Formbricks zu verbinden",

View File

@@ -350,7 +350,6 @@
"status": "Status",
"step_by_step_manual": "Step by step manual",
"styling": "Styling",
"submit": "Submit",
"summary": "Summary",
"survey": "Survey",
"survey_completed": "Survey completed.",
@@ -488,8 +487,6 @@
},
"environments": {
"actions": {
"action_copied_successfully": "Action copied successfully",
"action_copy_failed": "Action copy failed",
"action_created_successfully": "Action created successfully",
"action_deleted_successfully": "Action deleted successfully",
"action_type": "Action Type",
@@ -1136,7 +1133,6 @@
"disable_two_factor_authentication": "Disable two factor authentication",
"disable_two_factor_authentication_description": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
"each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Each backup code can be used exactly once to grant access without your authenticator.",
"enable_two_factor_authentication": "Enable two factor authentication",
"enter_the_code_from_your_authenticator_app_below": "Enter the code from your authenticator app below.",
"file_size_must_be_less_than_10mb": "File size must be less than 10MB.",
"invalid_file_type": "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.",
@@ -1699,11 +1695,7 @@
"is_equal_to": "Is equal to",
"is_less_than": "Is less than",
"last_30_days": "Last 30 days",
"last_6_months": "Last 6 months",
"last_7_days": "Last 7 days",
"last_month": "Last month",
"last_quarter": "Last quarter",
"last_year": "Last year",
"learn_how_to": "Learn how to",
"link_to_public_results_copied": "Link to public results copied",
"make_sure_the_survey_type_is_set_to": "Make sure the survey type is set to",
@@ -1736,9 +1728,6 @@
"static_iframe": "Static (iframe)",
"survey_results_are_public": "Your survey results are public!",
"survey_results_are_shared_with_anyone_who_has_the_link": "Your survey results are shared with anyone who has the link. The results will not be indexed by search engines.",
"this_month": "This month",
"this_quarter": "This quarter",
"this_year": "This year",
"time_to_complete": "Time to Complete",
"to_connect_your_app_with_formbricks": "to connect your app with Formbricks",
"to_connect_your_web_app_with_formbricks": "to connect your web app with Formbricks",

View File

@@ -350,7 +350,6 @@
"status": "status",
"step_by_step_manual": "Manual passo a passo",
"styling": "estilização",
"submit": "Enviar",
"summary": "Resumo",
"survey": "Pesquisa",
"survey_completed": "Pesquisa concluída.",
@@ -488,8 +487,6 @@
},
"environments": {
"actions": {
"action_copied_successfully": "Ação copiada com sucesso",
"action_copy_failed": "Falha ao copiar a ação",
"action_created_successfully": "Ação criada com sucesso",
"action_deleted_successfully": "Ação deletada com sucesso",
"action_type": "Tipo de Ação",
@@ -1136,7 +1133,6 @@
"disable_two_factor_authentication": "Desativar a autenticação de dois fatores",
"disable_two_factor_authentication_description": "Se você precisar desativar a 2FA, recomendamos reativá-la o mais rápido possível.",
"each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Cada código de backup pode ser usado exatamente uma vez para conceder acesso sem o seu autenticador.",
"enable_two_factor_authentication": "Ativar autenticação de dois fatores",
"enter_the_code_from_your_authenticator_app_below": "Digite o código do seu app autenticador abaixo.",
"file_size_must_be_less_than_10mb": "O tamanho do arquivo deve ser menor que 10MB.",
"invalid_file_type": "Tipo de arquivo inválido. Só são permitidos arquivos JPEG, PNG e WEBP.",
@@ -1699,11 +1695,7 @@
"is_equal_to": "É igual a",
"is_less_than": "É menor que",
"last_30_days": "Últimos 30 dias",
"last_6_months": "Últimos 6 meses",
"last_7_days": "Últimos 7 dias",
"last_month": "Último mês",
"last_quarter": "Último trimestre",
"last_year": "Último ano",
"learn_how_to": "Aprenda como",
"link_to_public_results_copied": "Link pros resultados públicos copiado",
"make_sure_the_survey_type_is_set_to": "Certifique-se de que o tipo de pesquisa esteja definido como",
@@ -1736,9 +1728,6 @@
"static_iframe": "Estático (iframe)",
"survey_results_are_public": "Os resultados da sua pesquisa são públicos!",
"survey_results_are_shared_with_anyone_who_has_the_link": "Os resultados da sua pesquisa são compartilhados com quem tiver o link. Os resultados não serão indexados por motores de busca.",
"this_month": "Este mês",
"this_quarter": "Este trimestre",
"this_year": "Este ano",
"time_to_complete": "Tempo para Concluir",
"to_connect_your_app_with_formbricks": "conectar seu app com o Formbricks",
"to_connect_your_web_app_with_formbricks": "conectar seu app web com o Formbricks",

View File

@@ -409,7 +409,7 @@ export const Survey = ({
<AutoCloseWrapper survey={localSurvey} onClose={onClose} offset={offset}>
<div
className={cn(
"fb-no-scrollbar sm:fb-rounded-custom fb-rounded-t-custom fb-bg-survey-bg fb-flex fb-h-full fb-w-full fb-flex-col fb-justify-between fb-overflow-hidden fb-transition-all fb-duration-1000 fb-ease-in-out",
"fb-no-scrollbar md:fb-rounded-custom fb-rounded-t-custom fb-bg-survey-bg fb-flex fb-h-full fb-w-full fb-flex-col fb-justify-between fb-overflow-hidden fb-transition-all fb-duration-1000 fb-ease-in-out",
cardArrangement === "simple" ? "fb-survey-shadow" : "",
offset === 0 || cardArrangement === "simple" ? "fb-opacity-100" : "fb-opacity-0"
)}>
@@ -430,7 +430,7 @@ export const Survey = ({
)}>
{content()}
</div>
<div className="fb-mx-6 fb-mb-10 fb-mt-2 fb-space-y-3 sm:fb-mb-6 sm:fb-mt-6">
<div className="fb-mx-6 fb-mb-10 fb-mt-2 fb-space-y-3 md:fb-mb-6 md:fb-mt-6">
{isBrandingEnabled && <FormbricksBranding />}
{showProgressBar && <ProgressBar survey={localSurvey} questionId={questionId} />}
</div>

View File

@@ -1,6 +1,5 @@
import { z } from "zod";
import { ZOrganizationRole } from "./memberships";
import { ZUserName } from "./user";
export const ZInvite = z.object({
id: z.string(),
@@ -17,7 +16,7 @@ export type TInvite = z.infer<typeof ZInvite>;
export const ZInvitee = z.object({
email: z.string().email(),
name: ZUserName,
name: z.string(),
role: ZOrganizationRole,
});
export type TInvitee = z.infer<typeof ZInvitee>;

View File

@@ -22,17 +22,14 @@ export const ZUserNotificationSettings = z.object({
unsubscribedOrganizationIds: z.array(z.string()).optional(),
});
export const ZUserName = z
.string()
.trim()
.min(1, { message: "Name should be at least 1 character long" })
.regex(/^[a-zA-Z0-9\s]+$/, { message: "Name should only contain letters, numbers, and spaces" });
export type TUserNotificationSettings = z.infer<typeof ZUserNotificationSettings>;
export const ZUser = z.object({
id: z.string(),
name: ZUserName,
name: z
.string({ message: "Name is required" })
.trim()
.min(1, { message: "Name should be at least 1 character long" }),
email: z.string().email(),
emailVerified: z.date().nullable(),
imageUrl: z.string().url().nullable(),
@@ -49,7 +46,7 @@ export const ZUser = z.object({
export type TUser = z.infer<typeof ZUser>;
export const ZUserUpdateInput = z.object({
name: ZUserName.optional(),
name: z.string().optional(),
email: z.string().email().optional(),
emailVerified: z.date().nullish(),
role: ZRole.optional(),
@@ -62,7 +59,10 @@ export const ZUserUpdateInput = z.object({
export type TUserUpdateInput = z.infer<typeof ZUserUpdateInput>;
export const ZUserCreateInput = z.object({
name: ZUserName,
name: z
.string({ message: "Name is required" })
.trim()
.min(1, { message: "Name should be at least 1 character long" }),
email: z.string().email(),
emailVerified: z.date().optional(),
role: ZRole.optional(),

View File

@@ -385,7 +385,7 @@ export const PreviewSurvey = ({
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
)}
</div>
<div className="z-0 w-full max-w-lg rounded-lg border-transparent">
<div className="z-0 w-full max-w-md rounded-lg border-transparent">
<SurveyInline
survey={{ ...survey, type: "link" }}
isBrandingEnabled={product.linkSurveyBranding}

View File

@@ -1,16 +1,10 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useRef, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { createUser } from "@formbricks/lib/utils/users";
import { ZUserName } from "@formbricks/types/user";
import { Button } from "../Button";
import { FormControl, FormError, FormField, FormItem } from "../Form";
import { Input } from "../Input";
import { PasswordInput } from "../PasswordInput";
import { AzureButton } from "./components/AzureButton";
import { GithubButton } from "./components/GithubButton";
@@ -47,35 +41,28 @@ export const SignupOptions = ({
oidcDisplayName,
userLocale,
}: SignupOptionsProps) => {
const t = useTranslations();
const [password, setPassword] = useState<string | null>(null);
const [showLogin, setShowLogin] = useState(false);
const [isValid, setIsValid] = useState(false);
const [signingUp, setSigningUp] = useState(false);
const t = useTranslations();
const ZSignupInput = z.object({
name: ZUserName,
email: z.string().email(),
password: z
.string()
.min(8)
.regex(/^(?=.*[A-Z])(?=.*\d).*$/),
});
type TSignupInput = z.infer<typeof ZSignupInput>;
const form = useForm<TSignupInput>({
defaultValues: {
name: "",
email: emailFromSearchParams || "",
password: "",
},
resolver: zodResolver(ZSignupInput),
});
const [isButtonEnabled, setButtonEnabled] = useState(true);
const router = useRouter();
const formRef = useRef<HTMLFormElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const handleSubmit = async (data: TSignupInput) => {
const checkFormValidity = () => {
// If all fields are filled, enable the button
if (formRef.current) {
setButtonEnabled(formRef.current.checkValidity());
}
};
const handleSubmit = async (e: any) => {
e.preventDefault();
if (!isValid) {
return;
}
@@ -83,10 +70,16 @@ export const SignupOptions = ({
setSigningUp(true);
try {
await createUser(data.name, data.email, data.password, userLocale, inviteToken || "");
await createUser(
e.target.elements.name.value,
e.target.elements.email.value,
e.target.elements.password.value,
userLocale,
inviteToken || ""
);
const url = emailVerificationDisabled
? `/auth/signup-without-verification-success`
: `/auth/verification-requested?email=${encodeURIComponent(data.email)}`;
: `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`;
router.push(url);
} catch (e: any) {
@@ -100,105 +93,87 @@ export const SignupOptions = ({
return (
<div className="space-y-2">
{emailAuthEnabled && (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{showLogin && (
<div>
<div className="space-y-2">
<FormField
control={form.control}
<form onSubmit={handleSubmit} ref={formRef} className="space-y-2" onChange={checkFormValidity}>
{showLogin && (
<div>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="name" className="sr-only">
{t("common.full_name")}
</label>
<div className="mt-1">
<input
ref={nameRef}
id="name"
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<Input
value={field.value}
name="name"
autoFocus
onChange={(name) => field.onChange(name)}
placeholder="Full name"
className="bg-white"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<Input
value={field.value}
name="email"
onChange={(email) => field.onChange(email)}
defaultValue={emailFromSearchParams}
placeholder="work@email.com"
className="bg-white"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full">
<FormControl>
<div>
<PasswordInput
id="password"
name="password"
value={field.value}
onChange={(password) => field.onChange(password)}
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
type="text"
autoComplete="given-name"
placeholder={t("common.full_name")}
aria-placeholder={"Full name"}
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
<IsPasswordValid password={form.watch("password")} setIsValid={setIsValid} />
</div>
)}
{showLogin && (
<Button
type="submit"
className="h-10 w-full justify-center"
loading={signingUp}
disabled={!form.formState.isValid}>
{t("auth.continue_with_email")}
</Button>
)}
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="email" className="sr-only">
{t("common.email")}
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="work@email.com"
defaultValue={emailFromSearchParams}
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
<div className="transition-all duration-500 ease-in-out">
<label htmlFor="password" className="sr-only">
{t("common.password")}
</label>
<PasswordInput
id="password"
name="password"
value={password ? password : ""}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
required
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md shadow-sm sm:text-sm"
/>
</div>
<IsPasswordValid password={password} setIsValid={setIsValid} />
</div>
)}
{showLogin && (
<Button
size="base"
type="submit"
className="w-full justify-center"
loading={signingUp}
disabled={formRef.current ? !isButtonEnabled || !isValid : !isButtonEnabled}>
{t("auth.continue_with_email")}
</Button>
)}
{!showLogin && (
<Button
type="button"
onClick={() => {
setShowLogin(true);
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => nameRef.current?.focus(), 100);
}}
className="h-10 w-full justify-center">
{t("auth.continue_with_email")}
</Button>
)}
</form>
</FormProvider>
{!showLogin && (
<Button
size="base"
type="button"
onClick={() => {
setShowLogin(true);
setButtonEnabled(false);
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => nameRef.current?.focus(), 100);
}}
className="w-full justify-center">
{t("auth.continue_with_email")}
</Button>
)}
</form>
)}
{googleOAuthEnabled && (
<>

View File

@@ -108,7 +108,6 @@
"INVITE_DISABLED",
"IS_FORMBRICKS_CLOUD",
"MAIL_FROM",
"NEXT_PUBLIC_LAYER_API_KEY",
"NEXT_PUBLIC_DOCSEARCH_APP_ID",
"NEXT_PUBLIC_DOCSEARCH_API_KEY",
"NEXT_PUBLIC_DOCSEARCH_INDEX_NAME",