diff --git a/.env.example b/.env.example
index 8d66de1e91..4677a33dcc 100644
--- a/.env.example
+++ b/.env.example
@@ -117,7 +117,7 @@ IMPRINT_URL=
IMPRINT_ADDRESS=
# Configure Turnstile in signup flow
-# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
+# TURNSTILE_SITE_KEY=
# TURNSTILE_SECRET_KEY=
# Configure Github Login
diff --git a/apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx
index 90eb344a89..0861c570f7 100644
--- a/apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/components/PosthogIdentify.test.tsx
@@ -1,9 +1,7 @@
-// PosthogIdentify.test.tsx
import "@testing-library/jest-dom/vitest";
import { cleanup, render } from "@testing-library/react";
import { Session } from "next-auth";
import { usePostHog } from "posthog-js/react";
-import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { TOrganizationBilling } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx
index b31e6432d6..7b097c666b 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx
@@ -33,12 +33,16 @@ vi.mock("@formbricks/lib/constants", () => ({
WEBAPP_URL: "mock-webapp-url",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "mock-smtp-port",
- AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name",
- AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key",
- AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id",
- AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name",
- AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key",
- AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id",
+ AI_AZURE_LLM_RESSOURCE_NAME: "mock-ai-azure-llm-ressource-name",
+ AI_AZURE_LLM_API_KEY: "mock-ai",
+ AI_AZURE_LLM_DEPLOYMENT_ID: "mock-ai-azure-llm-deployment-id",
+ AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-ai-azure-embeddings-ressource-name",
+ AI_AZURE_EMBEDDINGS_API_KEY: "mock-ai-azure-embeddings-api-key",
+ AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-ai-azure-embeddings-deployment-id",
+}));
+
+vi.mock("next-auth", () => ({
+ getServerSession: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
diff --git a/apps/web/app/sentry/SentryProvider.test.tsx b/apps/web/app/sentry/SentryProvider.test.tsx
index 40b58e7165..89a44fa396 100644
--- a/apps/web/app/sentry/SentryProvider.test.tsx
+++ b/apps/web/app/sentry/SentryProvider.test.tsx
@@ -1,6 +1,6 @@
import * as Sentry from "@sentry/nextjs";
import { cleanup, render, screen } from "@testing-library/react";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
import { SentryProvider } from "./SentryProvider";
vi.mock("@sentry/nextjs", async () => {
diff --git a/apps/web/modules/auth/signup/components/signup-form.test.tsx b/apps/web/modules/auth/signup/components/signup-form.test.tsx
new file mode 100644
index 0000000000..e494668f75
--- /dev/null
+++ b/apps/web/modules/auth/signup/components/signup-form.test.tsx
@@ -0,0 +1,377 @@
+import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import { createUserAction } from "@/modules/auth/signup/actions";
+import "@testing-library/jest-dom/vitest";
+import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { useSearchParams } from "next/navigation";
+import toast from "react-hot-toast";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { createEmailTokenAction } from "../../../auth/actions";
+import { SignupForm } from "./signup-form";
+
+// Mock dependencies
+
+vi.mock("@formbricks/lib/constants", () => ({
+ IS_FORMBRICKS_CLOUD: false,
+ POSTHOG_API_KEY: "mock-posthog-api-key",
+ POSTHOG_HOST: "mock-posthog-host",
+ IS_POSTHOG_CONFIGURED: true,
+ ENCRYPTION_KEY: "mock-encryption-key",
+ ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
+ GITHUB_ID: "mock-github-id",
+ GITHUB_SECRET: "test-githubID",
+ GOOGLE_CLIENT_ID: "test-google-client-id",
+ GOOGLE_CLIENT_SECRET: "test-google-client-secret",
+ AZUREAD_CLIENT_ID: "test-azuread-client-id",
+ AZUREAD_CLIENT_SECRET: "test-azure",
+ AZUREAD_TENANT_ID: "test-azuread-tenant-id",
+ OIDC_DISPLAY_NAME: "test-oidc-display-name",
+ OIDC_CLIENT_ID: "test-oidc-client-id",
+ OIDC_ISSUER: "test-oidc-issuer",
+ OIDC_CLIENT_SECRET: "test-oidc-client-secret",
+ OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
+ WEBAPP_URL: "test-webapp-url",
+ IS_PRODUCTION: false,
+ SENTRY_DSN: "mock-sentry-dsn",
+ FB_LOGO_URL: "mock-fb-logo-url",
+ SMTP_HOST: "smtp.example.com",
+ SMTP_PORT: 587,
+ SMTP_USER: "smtp-user",
+}));
+
+// Set up a push mock for useRouter
+const pushMock = vi.fn();
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ push: pushMock,
+ }),
+ useSearchParams: vi.fn(),
+}));
+
+vi.mock("react-turnstile", () => ({
+ useTurnstile: () => ({
+ reset: vi.fn(),
+ }),
+ default: (props: any) => (
+
{
+ if (props.onSuccess) {
+ props.onSuccess("test-turnstile-token");
+ }
+ }}
+ {...props}
+ />
+ ),
+}));
+
+vi.mock("react-hot-toast", () => ({
+ default: {
+ error: vi.fn(),
+ toast: {
+ error: vi.fn(),
+ },
+ },
+}));
+
+vi.mock("@/modules/auth/signup/actions", () => ({
+ createUserAction: vi.fn(),
+}));
+
+vi.mock("../../../auth/actions", () => ({
+ createEmailTokenAction: vi.fn(),
+}));
+
+vi.mock("@/lib/utils/helper", () => ({
+ getFormattedErrorMessage: vi.fn(),
+}));
+
+// Mock components
+
+vi.mock("@/modules/ee/sso/components/sso-options", () => ({
+ SSOOptions: () =>
SSOOptions
,
+}));
+vi.mock("@/modules/auth/signup/components/terms-privacy-links", () => ({
+ TermsPrivacyLinks: () =>
TermsPrivacyLinks
,
+}));
+vi.mock("@/modules/ui/components/button", () => ({
+ Button: (props: any) =>
,
+}));
+vi.mock("@/modules/ui/components/input", () => ({
+ Input: (props: any) =>
,
+}));
+vi.mock("@/modules/ui/components/password-input", () => ({
+ PasswordInput: (props: any) =>
,
+}));
+
+const defaultProps = {
+ webAppUrl: "http://localhost",
+ privacyUrl: "http://localhost/privacy",
+ termsUrl: "http://localhost/terms",
+ emailAuthEnabled: true,
+ googleOAuthEnabled: false,
+ githubOAuthEnabled: false,
+ azureOAuthEnabled: false,
+ oidcOAuthEnabled: false,
+ userLocale: "en-US",
+ emailVerificationDisabled: false,
+ isSsoEnabled: false,
+ samlSsoEnabled: false,
+ isTurnstileConfigured: false,
+ samlTenant: "",
+ samlProduct: "",
+ defaultOrganizationId: "org1",
+ defaultOrganizationRole: "member",
+ turnstileSiteKey: "dummy", // not used since isTurnstileConfigured is false
+} as const;
+
+describe("SignupForm", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("toggles the signup form on button click", () => {
+ render(
);
+
+ // Initially, the signup form is hidden.
+ try {
+ screen.getByTestId("signup-name");
+ } catch (e) {
+ expect(e).toBeInstanceOf(Error);
+ }
+
+ // Click the button to reveal the signup form.
+ const toggleButton = screen.getByTestId("signup-show-login");
+ fireEvent.click(toggleButton);
+
+ // Now the input fields should appear.
+ expect(screen.getByTestId("signup-name")).toBeInTheDocument();
+ expect(screen.getByTestId("signup-email")).toBeInTheDocument();
+ expect(screen.getByTestId("signup-password")).toBeInTheDocument();
+ });
+
+ it("submits the form successfully", async () => {
+ // Set up mocks for the API actions.
+ vi.mocked(createUserAction).mockResolvedValue({ data: true } as any);
+ vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" });
+
+ render(
);
+
+ // Click the button to reveal the signup form.
+ const toggleButton = screen.getByTestId("signup-show-login");
+ fireEvent.click(toggleButton);
+
+ const nameInput = screen.getByTestId("signup-name");
+ const emailInput = screen.getByTestId("signup-email");
+ const passwordInput = screen.getByTestId("signup-password");
+
+ fireEvent.change(nameInput, { target: { value: "Test User" } });
+ fireEvent.change(emailInput, { target: { value: "test@example.com" } });
+ fireEvent.change(passwordInput, { target: { value: "Password123" } });
+
+ const submitButton = screen.getByTestId("signup-submit");
+ fireEvent.submit(submitButton);
+
+ await waitFor(() => {
+ expect(createUserAction).toHaveBeenCalledWith({
+ name: "Test User",
+ email: "test@example.com",
+ password: "Password123",
+ userLocale: defaultProps.userLocale,
+ inviteToken: "",
+ emailVerificationDisabled: defaultProps.emailVerificationDisabled,
+ defaultOrganizationId: defaultProps.defaultOrganizationId,
+ defaultOrganizationRole: defaultProps.defaultOrganizationRole,
+ turnstileToken: undefined,
+ });
+ });
+
+ await waitFor(() => {
+ expect(createEmailTokenAction).toHaveBeenCalledWith({ email: "test@example.com" });
+ });
+
+ // Since email verification is enabled (emailVerificationDisabled is false),
+ // router.push should be called with the verification URL.
+ expect(pushMock).toHaveBeenCalledWith("/auth/verification-requested?token=token123");
+ });
+
+ it("submits the form successfully when turnstile is configured", async () => {
+ // Override props to enable Turnstile
+ const props = {
+ ...defaultProps,
+ isTurnstileConfigured: true,
+ turnstileSiteKey: "dummy",
+ emailVerificationDisabled: true,
+ };
+
+ // Set up mocks for the API actions
+ vi.mocked(createUserAction).mockResolvedValue({ data: true } as any);
+ vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" });
+
+ render(
);
+
+ // Click the button to reveal the signup form
+ const toggleButton = screen.getByTestId("signup-show-login");
+ fireEvent.click(toggleButton);
+
+ // Fill out the form fields
+ fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } });
+ fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } });
+ fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } });
+
+ // Simulate receiving a turnstile token by clicking the Turnstile element.
+ const turnstileElement = screen.getByTestId("turnstile");
+ fireEvent.click(turnstileElement);
+
+ // Submit the form.
+ const submitButton = screen.getByTestId("signup-submit");
+ fireEvent.submit(submitButton);
+ await waitFor(() => {
+ expect(createUserAction).toHaveBeenCalledWith({
+ name: "Test User",
+ email: "test@example.com",
+ password: "Password123",
+ userLocale: props.userLocale,
+ inviteToken: "",
+ emailVerificationDisabled: true,
+ defaultOrganizationId: props.defaultOrganizationId,
+ defaultOrganizationRole: props.defaultOrganizationRole,
+ turnstileToken: "test-turnstile-token",
+ });
+ });
+
+ await waitFor(() => {
+ expect(createEmailTokenAction).toHaveBeenCalledWith({ email: "test@example.com" });
+ });
+
+ expect(pushMock).toHaveBeenCalledWith("/auth/signup-without-verification-success");
+ });
+
+ it("submits the form successfully when turnstile is configured, but createEmailTokenAction don't return data", async () => {
+ // Override props to enable Turnstile
+ const props = {
+ ...defaultProps,
+ isTurnstileConfigured: true,
+ turnstileSiteKey: "dummy",
+ emailVerificationDisabled: true,
+ };
+
+ // Set up mocks for the API actions
+ vi.mocked(createUserAction).mockResolvedValue({ data: true } as any);
+ vi.mocked(createEmailTokenAction).mockResolvedValue(undefined);
+ vi.mocked(getFormattedErrorMessage).mockReturnValue("error");
+
+ render(
);
+
+ // Click the button to reveal the signup form
+ const toggleButton = screen.getByTestId("signup-show-login");
+ fireEvent.click(toggleButton);
+
+ // Fill out the form fields
+ fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } });
+ fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } });
+ fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } });
+
+ // Simulate receiving a turnstile token by clicking the Turnstile element.
+ const turnstileElement = screen.getByTestId("turnstile");
+ fireEvent.click(turnstileElement);
+
+ // Submit the form.
+ const submitButton = screen.getByTestId("signup-submit");
+ fireEvent.submit(submitButton);
+ await waitFor(() => {
+ expect(createUserAction).toHaveBeenCalledWith({
+ name: "Test User",
+ email: "test@example.com",
+ password: "Password123",
+ userLocale: props.userLocale,
+ inviteToken: "",
+ emailVerificationDisabled: true,
+ defaultOrganizationId: props.defaultOrganizationId,
+ defaultOrganizationRole: props.defaultOrganizationRole,
+ turnstileToken: "test-turnstile-token",
+ });
+ });
+
+ // Since Turnstile is configured, but no token is received, an error message should be shown.
+ await waitFor(() => {
+ expect(toast.error).toHaveBeenCalledWith("error");
+ });
+ });
+
+ it("shows an error message if turnstile is configured, but no token is received", async () => {
+ // Override props to enable Turnstile
+ const props = {
+ ...defaultProps,
+ isTurnstileConfigured: true,
+ turnstileSiteKey: "dummy",
+ emailVerificationDisabled: true,
+ };
+
+ // Set up mocks for the API actions
+ vi.mocked(createUserAction).mockResolvedValue({ data: true } as any);
+ vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" });
+
+ render(
);
+
+ // Click the button to reveal the signup form
+ const toggleButton = screen.getByTestId("signup-show-login");
+ fireEvent.click(toggleButton);
+
+ // Fill out the form fields
+ fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } });
+ fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } });
+ fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } });
+
+ // Submit the form.
+ const submitButton = screen.getByTestId("signup-submit");
+ fireEvent.submit(submitButton);
+
+ // Since Turnstile is configured, but no token is received, an error message should be shown.
+ await waitFor(() => {
+ expect(toast.error).toHaveBeenCalledWith("auth.signup.please_verify_captcha");
+ });
+ });
+
+ it("Invite token is in the search params", async () => {
+ // Set up mocks for the API actions
+ vi.mocked(createUserAction).mockResolvedValue({ data: true } as any);
+ vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" });
+ vi.mocked(useSearchParams).mockReturnValue(new URLSearchParams("inviteToken=token123") as any);
+
+ render(
);
+
+ // Click the button to reveal the signup form
+ const toggleButton = screen.getByTestId("signup-show-login");
+ fireEvent.click(toggleButton);
+
+ // Fill out the form fields
+ fireEvent.change(screen.getByTestId("signup-name"), { target: { value: "Test User" } });
+ fireEvent.change(screen.getByTestId("signup-email"), { target: { value: "test@example.com" } });
+ fireEvent.change(screen.getByTestId("signup-password"), { target: { value: "Password123" } });
+
+ // Submit the form.
+ const submitButton = screen.getByTestId("signup-submit");
+ fireEvent.submit(submitButton);
+
+ // Check that the invite token is passed to the createUserAction
+ await waitFor(() => {
+ expect(createUserAction).toHaveBeenCalledWith({
+ name: "Test User",
+ email: "test@example.com",
+ password: "Password123",
+ userLocale: defaultProps.userLocale,
+ inviteToken: "token123",
+ emailVerificationDisabled: defaultProps.emailVerificationDisabled,
+ defaultOrganizationId: defaultProps.defaultOrganizationId,
+ defaultOrganizationRole: defaultProps.defaultOrganizationRole,
+ turnstileToken: undefined,
+ });
+ });
+
+ await waitFor(() => {
+ expect(createEmailTokenAction).toHaveBeenCalledWith({ email: "test@example.com" });
+ });
+
+ expect(pushMock).toHaveBeenCalledWith("/auth/verification-requested?token=token123");
+ });
+});
diff --git a/apps/web/modules/auth/signup/components/signup-form.tsx b/apps/web/modules/auth/signup/components/signup-form.tsx
index b2672dd5ea..08636c0f59 100644
--- a/apps/web/modules/auth/signup/components/signup-form.tsx
+++ b/apps/web/modules/auth/signup/components/signup-form.tsx
@@ -19,7 +19,6 @@ import { FormProvider, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import Turnstile, { useTurnstile } from "react-turnstile";
import { z } from "zod";
-import { env } from "@formbricks/lib/env";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TUserLocale, ZUserName, ZUserPassword } from "@formbricks/types/user";
import { createEmailTokenAction } from "../../../auth/actions";
@@ -31,8 +30,6 @@ const ZSignupInput = z.object({
password: ZUserPassword,
});
-const turnstileSiteKey = env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
-
type TSignupInput = z.infer
;
interface SignupFormProps {
@@ -55,6 +52,7 @@ interface SignupFormProps {
isTurnstileConfigured: boolean;
samlTenant: string;
samlProduct: string;
+ turnstileSiteKey?: string;
}
export const SignupForm = ({
@@ -77,6 +75,7 @@ export const SignupForm = ({
isTurnstileConfigured,
samlTenant,
samlProduct,
+ turnstileSiteKey,
}: SignupFormProps) => {
const [showLogin, setShowLogin] = useState(false);
const searchParams = useSearchParams();
@@ -171,10 +170,11 @@ export const SignupForm = ({
field.onChange(name)}
+ onChange={(e) => field.onChange(e.target.value)}
placeholder="Full name"
className="bg-white"
/>
@@ -192,9 +192,10 @@ export const SignupForm = ({
field.onChange(email)}
+ onChange={(e) => field.onChange(e.target.value)}
placeholder="work@email.com"
className="bg-white"
/>
@@ -212,10 +213,11 @@ export const SignupForm = ({
field.onChange(password)}
+ onChange={(e) => field.onChange(e.target.value)}
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
@@ -248,6 +250,7 @@ export const SignupForm = ({
{showLogin && (
diff --git a/apps/web/modules/setup/(fresh-instance)/signup/page.test.tsx b/apps/web/modules/setup/(fresh-instance)/signup/page.test.tsx
new file mode 100644
index 0000000000..8d00159231
--- /dev/null
+++ b/apps/web/modules/setup/(fresh-instance)/signup/page.test.tsx
@@ -0,0 +1,97 @@
+import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils";
+import { getTranslate } from "@/tolgee/server";
+import "@testing-library/jest-dom/vitest";
+import { render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { findMatchingLocale } from "@formbricks/lib/utils/locale";
+import { SignupPage } from "./page";
+
+// Mock dependencies
+
+vi.mock("@formbricks/lib/constants", () => ({
+ IS_FORMBRICKS_CLOUD: false,
+ POSTHOG_API_KEY: "mock-posthog-api-key",
+ POSTHOG_HOST: "mock-posthog-host",
+ IS_POSTHOG_CONFIGURED: true,
+ ENCRYPTION_KEY: "mock-encryption-key",
+ ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
+ GITHUB_ID: "mock-github-id",
+ GITHUB_SECRET: "test-githubID",
+ GOOGLE_CLIENT_ID: "test-google-client-id",
+ GOOGLE_CLIENT_SECRET: "test-google-client-secret",
+ AZUREAD_CLIENT_ID: "test-azuread-client-id",
+ AZUREAD_CLIENT_SECRET: "test-azure",
+ AZUREAD_TENANT_ID: "test-azuread-tenant-id",
+ OIDC_DISPLAY_NAME: "test-oidc-display-name",
+ OIDC_CLIENT_ID: "test-oidc-client-id",
+ OIDC_ISSUER: "test-oidc-issuer",
+ OIDC_CLIENT_SECRET: "test-oidc-client-secret",
+ OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
+ WEBAPP_URL: "test-webapp-url",
+ IS_PRODUCTION: false,
+ SENTRY_DSN: "mock-sentry-dsn",
+ FB_LOGO_URL: "mock-fb-logo-url",
+ SMTP_HOST: "smtp.example.com",
+ SMTP_PORT: 587,
+ SMTP_USER: "smtp-user",
+ SAML_AUDIENCE: "test-saml-audience",
+ SAML_PATH: "test-saml-path",
+ SAML_DATABASE_URL: "test-saml-database-url",
+ TERMS_URL: "test-terms-url",
+ SIGNUP_ENABLED: true,
+ PRIVACY_URL: "test-privacy-url",
+ EMAIL_VERIFICATION_DISABLED: false,
+ EMAIL_AUTH_ENABLED: true,
+ GOOGLE_OAUTH_ENABLED: true,
+ GITHUB_OAUTH_ENABLED: true,
+ AZURE_OAUTH_ENABLED: true,
+ OIDC_OAUTH_ENABLED: true,
+ DEFAULT_ORGANIZATION_ID: "test-default-organization-id",
+ DEFAULT_ORGANIZATION_ROLE: "test-default-organization-role",
+ IS_TURNSTILE_CONFIGURED: true,
+ SAML_TENANT: "test-saml-tenant",
+ SAML_PRODUCT: "test-saml-product",
+ TURNSTILE_SITE_KEY: "test-turnstile-site-key",
+}));
+
+vi.mock("@/modules/ee/license-check/lib/utils", () => ({
+ getisSsoEnabled: vi.fn(),
+ getIsSamlSsoEnabled: vi.fn(),
+}));
+
+vi.mock("@formbricks/lib/utils/locale", () => ({
+ findMatchingLocale: vi.fn(),
+}));
+
+vi.mock("@/tolgee/server", () => ({
+ getTranslate: vi.fn(),
+}));
+
+// Mock the SignupForm component to simplify our test assertions
+vi.mock("@/modules/auth/signup/components/signup-form", () => ({
+ SignupForm: (props) => (
+
+ SignupForm
+
+ ),
+}));
+
+describe("SignupPage", () => {
+ beforeEach(() => {
+ vi.mocked(getisSsoEnabled).mockResolvedValue(true);
+ vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(false);
+ vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
+ vi.mocked(getTranslate).mockResolvedValue((key) => key);
+ });
+
+ it("renders the signup page correctly", async () => {
+ const page = await SignupPage();
+ render(page);
+
+ expect(screen.getByTestId("signup-form")).toBeInTheDocument();
+ expect(screen.getByTestId("signup-form")).toHaveAttribute(
+ "data-turnstile-key",
+ "test-turnstile-site-key"
+ );
+ });
+});
diff --git a/apps/web/modules/setup/(fresh-instance)/signup/page.tsx b/apps/web/modules/setup/(fresh-instance)/signup/page.tsx
index 0f5c3bce31..3a15e432ee 100644
--- a/apps/web/modules/setup/(fresh-instance)/signup/page.tsx
+++ b/apps/web/modules/setup/(fresh-instance)/signup/page.tsx
@@ -18,6 +18,7 @@ import {
SAML_PRODUCT,
SAML_TENANT,
TERMS_URL,
+ TURNSTILE_SITE_KEY,
WEBAPP_URL,
} from "@formbricks/lib/constants";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
@@ -59,6 +60,7 @@ export const SignupPage = async () => {
isTurnstileConfigured={IS_TURNSTILE_CONFIGURED}
samlTenant={SAML_TENANT}
samlProduct={SAML_PRODUCT}
+ turnstileSiteKey={TURNSTILE_SITE_KEY}
/>
);
diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts
index 6fa694f853..c889d2d59a 100644
--- a/apps/web/vite.config.mts
+++ b/apps/web/vite.config.mts
@@ -1,7 +1,7 @@
// vitest.config.ts
-import react from "@vitejs/plugin-react";
import { PluginOption, loadEnv } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
+import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
@@ -19,14 +19,15 @@ export default defineConfig({
"modules/api/v2/**/*.ts",
"modules/api/v2/**/*.tsx",
"modules/auth/lib/**/*.ts",
+ "modules/signup/lib/**/*.ts",
"modules/auth/signup/lib/**/*.ts",
"modules/auth/signup/**/*.tsx",
"modules/ee/whitelabel/email-customization/components/*.tsx",
- "modules/ee/role-management/components/*.tsx",
- "modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx",
"modules/email/components/email-template.tsx",
"modules/email/emails/survey/follow-up.tsx",
"modules/ui/components/post-hog-client/*.tsx",
+ "modules/ee/role-management/components/*.tsx",
+ "modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx",
"modules/ui/components/alert/*.tsx",
"app/(app)/environments/**/layout.tsx",
"app/(app)/environments/**/settings/(organization)/general/page.tsx",
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index e6e5268159..cda136e076 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -98,7 +98,7 @@ x-environment: &environment
############################################# OPTIONAL (OAUTH CONFIGURATION) #############################################
# Set the below from Cloudflare Turnstile if you want to enable turnstile in signups
- # NEXT_PUBLIC_TURNSTILE_SITE_KEY:
+ # TURNSTILE_SITE_KEY:
# TURNSTILE_SECRET_KEY:
# Set the below from GitHub if you want to enable GitHub OAuth
diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts
index c2ca1ebb3c..aa51a417bf 100644
--- a/packages/lib/constants.ts
+++ b/packages/lib/constants.ts
@@ -261,7 +261,11 @@ export const BILLING_LIMITS = {
} as const;
export const AI_AZURE_LLM_RESSOURCE_NAME = env.AI_AZURE_LLM_RESSOURCE_NAME;
-
+export const AI_AZURE_LLM_API_KEY = env.AI_AZURE_LLM_API_KEY;
+export const AI_AZURE_LLM_DEPLOYMENT_ID = env.AI_AZURE_LLM_DEPLOYMENT_ID;
+export const AI_AZURE_EMBEDDINGS_RESSOURCE_NAME = env.AI_AZURE_EMBEDDINGS_RESSOURCE_NAME;
+export const AI_AZURE_EMBEDDINGS_API_KEY = env.AI_AZURE_EMBEDDINGS_API_KEY;
+export const AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID = env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID;
export const IS_AI_CONFIGURED = Boolean(
env.AI_AZURE_EMBEDDINGS_API_KEY &&
env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID &&
@@ -270,11 +274,6 @@ export const IS_AI_CONFIGURED = Boolean(
env.AI_AZURE_LLM_DEPLOYMENT_ID &&
env.AI_AZURE_LLM_RESSOURCE_NAME
);
-export const AI_AZURE_LLM_API_KEY = env.AI_AZURE_LLM_API_KEY;
-export const AI_AZURE_LLM_DEPLOYMENT_ID = env.AI_AZURE_LLM_DEPLOYMENT_ID;
-export const AI_AZURE_EMBEDDINGS_RESSOURCE_NAME = env.AI_AZURE_EMBEDDINGS_RESSOURCE_NAME;
-export const AI_AZURE_EMBEDDINGS_API_KEY = env.AI_AZURE_EMBEDDINGS_API_KEY;
-export const AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID = env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID;
export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY;
export const INTERCOM_APP_ID = env.INTERCOM_APP_ID;
@@ -285,8 +284,8 @@ export const POSTHOG_API_HOST = env.POSTHOG_API_HOST;
export const IS_POSTHOG_CONFIGURED = Boolean(POSTHOG_API_KEY && POSTHOG_API_HOST);
export const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY;
-
-export const IS_TURNSTILE_CONFIGURED = Boolean(env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && TURNSTILE_SECRET_KEY);
+export const TURNSTILE_SITE_KEY = env.TURNSTILE_SITE_KEY;
+export const IS_TURNSTILE_CONFIGURED = Boolean(env.TURNSTILE_SITE_KEY && TURNSTILE_SECRET_KEY);
export const IS_PRODUCTION = env.NODE_ENV === "production";
diff --git a/packages/lib/env.ts b/packages/lib/env.ts
index ff68f505db..ec58a98e5f 100644
--- a/packages/lib/env.ts
+++ b/packages/lib/env.ts
@@ -102,6 +102,7 @@ export const env = createEnv({
.optional()
.or(z.string().refine((str) => str === "")),
TURNSTILE_SECRET_KEY: z.string().optional(),
+ TURNSTILE_SITE_KEY: z.string().optional(),
UPLOADS_DIR: z.string().min(1).optional(),
VERCEL_URL: z.string().optional(),
WEBAPP_URL: z.string().url().optional(),
@@ -128,7 +129,6 @@ export const env = createEnv({
.or(z.string().refine((str) => str === "")),
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: z.string().optional(),
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: z.string().optional(),
- NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(),
},
/*
* Due to how Next.js bundles environment variables on Edge and Client,
@@ -188,7 +188,6 @@ export const env = createEnv({
SENTRY_DSN: process.env.SENTRY_DSN,
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY,
POSTHOG_API_HOST: process.env.POSTHOG_API_HOST,
- NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
@@ -226,6 +225,7 @@ export const env = createEnv({
SURVEY_URL: process.env.SURVEY_URL,
TELEMETRY_DISABLED: process.env.TELEMETRY_DISABLED,
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
+ TURNSTILE_SITE_KEY: process.env.TURNSTILE_SITE_KEY,
TERMS_URL: process.env.TERMS_URL,
UPLOADS_DIR: process.env.UPLOADS_DIR,
VERCEL_URL: process.env.VERCEL_URL,
diff --git a/turbo.json b/turbo.json
index d86852a5d0..9be6433da0 100644
--- a/turbo.json
+++ b/turbo.json
@@ -162,7 +162,6 @@
"NEXT_PUBLIC_FORMBRICKS_COM_API_HOST",
"NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID",
"NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID",
- "NEXT_PUBLIC_TURNSTILE_SITE_KEY",
"OPENTELEMETRY_LISTENER_URL",
"NEXT_RUNTIME",
"NEXTAUTH_SECRET",
@@ -208,6 +207,7 @@
"SURVEY_URL",
"TELEMETRY_DISABLED",
"TURNSTILE_SECRET_KEY",
+ "TURNSTILE_SITE_KEY",
"TERMS_URL",
"UPLOADS_DIR",
"VERCEL",