mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
feat: added turnstile to signup flow (#4516)
This commit is contained in:
@@ -103,6 +103,10 @@ TERMS_URL=
|
||||
IMPRINT_URL=
|
||||
IMPRINT_ADDRESS=
|
||||
|
||||
# Configure Turnstile in signup flow
|
||||
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
||||
# TURNSTILE_SECRET_KEY=
|
||||
|
||||
# Configure Github Login
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
@@ -56,6 +56,8 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to 1 else to 0. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_REJECT_UNAUTHORIZED_TLS | If set to 0, the server will accept connections without requiring authorization from the list of supplied CAs. | optional | 1 |
|
||||
| TURNSTILE_SITE_KEY | Site key for Turnstile. | optional | |
|
||||
| TURNSTILE_SECRET_KEY | Secret key for Turnstile. | optional | |
|
||||
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PHProvider } from "@/modules/ui/components/post-hog-client";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
@@ -20,7 +21,9 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
||||
<html lang={locale} translate="no">
|
||||
{process.env.VERCEL === "1" && <SpeedInsights sampleRate={0.1} />}
|
||||
<body className="flex h-dvh flex-col transition-all ease-in-out">
|
||||
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
|
||||
<PHProvider>
|
||||
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
|
||||
</PHProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
EMAIL_VERIFICATION_DISABLED,
|
||||
GITHUB_OAUTH_ENABLED,
|
||||
GOOGLE_OAUTH_ENABLED,
|
||||
IS_TURNSTILE_CONFIGURED,
|
||||
OIDC_DISPLAY_NAME,
|
||||
OIDC_OAUTH_ENABLED,
|
||||
PRIVACY_URL,
|
||||
@@ -47,6 +48,7 @@ const Page = async () => {
|
||||
defaultOrganizationId={DEFAULT_ORGANIZATION_ID}
|
||||
defaultOrganizationRole={DEFAULT_ORGANIZATION_ROLE}
|
||||
isSSOEnabled={isSSOEnabled}
|
||||
isTurnstileConfigured={IS_TURNSTILE_CONFIGURED}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
import { actionClient } from "@/lib/utils/action-client";
|
||||
import { createUser } from "@/modules/auth/lib/user";
|
||||
import { updateUser } from "@/modules/auth/lib/user";
|
||||
import { captureFailedSignup, verifyTurnstileToken } from "@/modules/auth/signup/lib/utils";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/modules/email";
|
||||
import { createTeamMembership } from "@/modules/invite/lib/team";
|
||||
import { z } from "zod";
|
||||
import { hashPassword } from "@formbricks/lib/auth";
|
||||
import { IS_TURNSTILE_CONFIGURED, TURNSTILE_SECRET_KEY } from "@formbricks/lib/constants";
|
||||
import { getInvite } from "@formbricks/lib/invite/service";
|
||||
import { deleteInvite } from "@formbricks/lib/invite/service";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization, getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { UnknownError } from "@formbricks/types/errors";
|
||||
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { ZUserLocale, ZUserName } from "@formbricks/types/user";
|
||||
|
||||
@@ -25,9 +28,29 @@ const ZCreateUserAction = z.object({
|
||||
defaultOrganizationId: z.string().optional(),
|
||||
defaultOrganizationRole: ZOrganizationRole.optional(),
|
||||
emailVerificationDisabled: z.boolean().optional(),
|
||||
turnstileToken: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(token) => !IS_TURNSTILE_CONFIGURED || (IS_TURNSTILE_CONFIGURED && token),
|
||||
"CAPTCHA verification required"
|
||||
),
|
||||
});
|
||||
|
||||
export const createUserAction = actionClient.schema(ZCreateUserAction).action(async ({ parsedInput }) => {
|
||||
if (IS_TURNSTILE_CONFIGURED) {
|
||||
if (!parsedInput.turnstileToken || !TURNSTILE_SECRET_KEY) {
|
||||
captureFailedSignup(parsedInput.email, parsedInput.name);
|
||||
throw new UnknownError("Server configuration error");
|
||||
}
|
||||
|
||||
const isHuman = await verifyTurnstileToken(TURNSTILE_SECRET_KEY, parsedInput.turnstileToken);
|
||||
if (!isHuman) {
|
||||
captureFailedSignup(parsedInput.email, parsedInput.name);
|
||||
throw new UnknownError("reCAPTCHA verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
const { inviteToken, emailVerificationDisabled } = parsedInput;
|
||||
const hashedPassword = await hashPassword(parsedInput.password);
|
||||
const user = await createUser({
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { createUserAction } from "@/modules/auth/signup/actions";
|
||||
import { TermsPrivacyLinks } from "@/modules/auth/signup/components/terms-privacy-links";
|
||||
import { captureFailedSignup } from "@/modules/auth/signup/lib/utils";
|
||||
import { SSOOptions } from "@/modules/ee/sso/components/sso-options";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
@@ -16,7 +17,9 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
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 } from "@formbricks/types/user";
|
||||
import { createEmailTokenAction } from "../../../auth/actions";
|
||||
@@ -31,6 +34,8 @@ const ZSignupInput = z.object({
|
||||
.regex(/^(?=.*[A-Z])(?=.*\d).*$/),
|
||||
});
|
||||
|
||||
const turnstileSiteKey = env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
|
||||
|
||||
type TSignupInput = z.infer<typeof ZSignupInput>;
|
||||
|
||||
interface SignupFormProps {
|
||||
@@ -49,6 +54,7 @@ interface SignupFormProps {
|
||||
defaultOrganizationId?: string;
|
||||
defaultOrganizationRole?: TOrganizationRole;
|
||||
isSSOEnabled: boolean;
|
||||
isTurnstileConfigured: boolean;
|
||||
}
|
||||
|
||||
export const SignupForm = ({
|
||||
@@ -67,12 +73,16 @@ export const SignupForm = ({
|
||||
defaultOrganizationId,
|
||||
defaultOrganizationRole,
|
||||
isSSOEnabled,
|
||||
isTurnstileConfigured,
|
||||
}: SignupFormProps) => {
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations();
|
||||
const inviteToken = searchParams?.get("inviteToken");
|
||||
const router = useRouter();
|
||||
const [turnstileToken, setTurnstileToken] = useState<string>();
|
||||
|
||||
const turnstile = useTurnstile();
|
||||
|
||||
const callbackUrl = useMemo(() => {
|
||||
if (inviteToken) {
|
||||
@@ -93,6 +103,10 @@ export const SignupForm = ({
|
||||
|
||||
const handleSubmit = async (data: TSignupInput) => {
|
||||
try {
|
||||
if (isTurnstileConfigured && !turnstileToken) {
|
||||
throw new Error(t("auth.signup.please_verify_captcha"));
|
||||
}
|
||||
|
||||
const createUserResponse = await createUserAction({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
@@ -102,6 +116,7 @@ export const SignupForm = ({
|
||||
emailVerificationDisabled,
|
||||
defaultOrganizationId,
|
||||
defaultOrganizationRole,
|
||||
turnstileToken,
|
||||
});
|
||||
|
||||
if (createUserResponse?.data) {
|
||||
@@ -114,10 +129,20 @@ export const SignupForm = ({
|
||||
|
||||
router.push(url);
|
||||
} else {
|
||||
if (isTurnstileConfigured) {
|
||||
setTurnstileToken(undefined);
|
||||
turnstile.reset();
|
||||
}
|
||||
|
||||
const errorMessage = getFormattedErrorMessage(emailTokenActionResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
if (isTurnstileConfigured) {
|
||||
setTurnstileToken(undefined);
|
||||
turnstile.reset();
|
||||
}
|
||||
|
||||
const errorMessage = getFormattedErrorMessage(createUserResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
@@ -204,6 +229,20 @@ export const SignupForm = ({
|
||||
<PasswordChecks password={form.watch("password")} />
|
||||
</div>
|
||||
)}
|
||||
{isTurnstileConfigured && showLogin && turnstileSiteKey && (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onSuccess={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
onError={() => {
|
||||
setTurnstileToken(undefined);
|
||||
toast.error(t("auth.signup.captcha_failed"));
|
||||
captureFailedSignup(form.getValues("email"), form.getValues("name"));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showLogin && (
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
38
apps/web/modules/auth/signup/lib/utils.ts
Normal file
38
apps/web/modules/auth/signup/lib/utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import posthog from "posthog-js";
|
||||
|
||||
export const verifyTurnstileToken = async (secretKey: string, token: string): Promise<boolean> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
try {
|
||||
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secret: secretKey,
|
||||
response: token,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Verification failed with status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.success === true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
export const captureFailedSignup = (email: string, name: string) => {
|
||||
posthog.capture("TELEMETRY_FAILED_SIGNUP", {
|
||||
email,
|
||||
name,
|
||||
});
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
EMAIL_VERIFICATION_DISABLED,
|
||||
GITHUB_OAUTH_ENABLED,
|
||||
GOOGLE_OAUTH_ENABLED,
|
||||
IS_TURNSTILE_CONFIGURED,
|
||||
OIDC_DISPLAY_NAME,
|
||||
OIDC_OAUTH_ENABLED,
|
||||
PRIVACY_URL,
|
||||
@@ -53,6 +54,7 @@ export const SignupPage = async ({ searchParams: searchParamsProps }) => {
|
||||
defaultOrganizationId={DEFAULT_ORGANIZATION_ID}
|
||||
defaultOrganizationRole={DEFAULT_ORGANIZATION_ROLE}
|
||||
isSSOEnabled={isSSOEnabled}
|
||||
isTurnstileConfigured={IS_TURNSTILE_CONFIGURED}
|
||||
/>
|
||||
</FormWrapper>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"react-hot-toast": "2.4.1",
|
||||
"react-icons": "5.4.0",
|
||||
"react-radio-group": "3.0.3",
|
||||
"react-turnstile": "1.1.4",
|
||||
"react-use": "17.6.0",
|
||||
"redis": "4.7.0",
|
||||
"sharp": "0.33.5",
|
||||
|
||||
@@ -89,6 +89,10 @@ 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_SECRET_KEY:
|
||||
|
||||
# Set the below from GitHub if you want to enable GitHub OAuth
|
||||
# GITHUB_ID:
|
||||
# GITHUB_SECRET:
|
||||
|
||||
@@ -247,3 +247,7 @@ export const IS_AI_CONFIGURED = Boolean(
|
||||
export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY;
|
||||
|
||||
export const IS_INTERCOM_CONFIGURED = Boolean(env.NEXT_PUBLIC_INTERCOM_APP_ID && INTERCOM_SECRET_KEY);
|
||||
|
||||
export const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY;
|
||||
|
||||
export const IS_TURNSTILE_CONFIGURED = Boolean(env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && TURNSTILE_SECRET_KEY);
|
||||
|
||||
@@ -91,6 +91,7 @@ export const env = createEnv({
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
TURNSTILE_SECRET_KEY: z.string().optional(),
|
||||
UPLOADS_DIR: z.string().min(1).optional(),
|
||||
VERCEL_URL: z.string().optional(),
|
||||
WEBAPP_URL: z.string().url().optional(),
|
||||
@@ -117,6 +118,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: z.string().optional(),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||
NEXT_PUBLIC_INTERCOM_APP_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(),
|
||||
},
|
||||
/*
|
||||
* Due to how Next.js bundles environment variables on Edge and Client,
|
||||
@@ -173,6 +175,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY: process.env.NEXT_PUBLIC_POSTHOG_API_KEY,
|
||||
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
|
||||
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
|
||||
NEXT_PUBLIC_INTERCOM_APP_ID: process.env.NEXT_PUBLIC_INTERCOM_APP_ID,
|
||||
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
|
||||
@@ -206,6 +209,7 @@ export const env = createEnv({
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
TELEMETRY_DISABLED: process.env.TELEMETRY_DISABLED,
|
||||
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
|
||||
TERMS_URL: process.env.TERMS_URL,
|
||||
UPLOADS_DIR: process.env.UPLOADS_DIR,
|
||||
VERCEL_URL: process.env.VERCEL_URL,
|
||||
|
||||
@@ -56,12 +56,14 @@
|
||||
"use_a_backup_code": "Einen Backup-Code verwenden"
|
||||
},
|
||||
"signup": {
|
||||
"captcha_failed": "reCAPTCHA fehlgeschlagen",
|
||||
"error": "Es ist ein Fehler aufgetreten, als Du Dich angemeldet hast",
|
||||
"have_an_account": "Hast Du ein Konto?",
|
||||
"log_in": "Einloggen",
|
||||
"password_validation_contain_at_least_1_number": "Enthält mindestens 1 Zahl",
|
||||
"password_validation_minimum_8_and_maximum_128_characters": "Mindestens 8 & höchstens 128 Zeichen",
|
||||
"password_validation_uppercase_and_lowercase": "Mix aus Groß- und Kleinbuchstaben",
|
||||
"please_verify_captcha": "Bitte bestätige reCAPTCHA",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"terms_of_service": "Nutzungsbedingungen",
|
||||
"title": "Erstelle dein Formbricks-Konto"
|
||||
|
||||
@@ -56,12 +56,14 @@
|
||||
"use_a_backup_code": "Use a backup code"
|
||||
},
|
||||
"signup": {
|
||||
"captcha_failed": "Captcha failed",
|
||||
"error": "An error occurred when signing you up",
|
||||
"have_an_account": "Have an account?",
|
||||
"log_in": "Log in",
|
||||
"password_validation_contain_at_least_1_number": "Contain at least 1 number",
|
||||
"password_validation_minimum_8_and_maximum_128_characters": "Minimum 8 & Maximum 128 characters",
|
||||
"password_validation_uppercase_and_lowercase": "Mix of uppercase and lowercase",
|
||||
"please_verify_captcha": "Please verify reCAPTCHA",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"terms_of_service": "Terms of Service",
|
||||
"title": "Create your Formbricks account"
|
||||
|
||||
@@ -56,12 +56,14 @@
|
||||
"use_a_backup_code": "Utiliser un code de secours"
|
||||
},
|
||||
"signup": {
|
||||
"captcha_failed": "Captcha échoué",
|
||||
"error": "Une erreur est survenue lors de votre inscription.",
|
||||
"have_an_account": "Avez-vous un compte ?",
|
||||
"log_in": "Se connecter",
|
||||
"password_validation_contain_at_least_1_number": "Contenir au moins 1 chiffre",
|
||||
"password_validation_minimum_8_and_maximum_128_characters": "Minimum 8 et Maximum 128 caractères",
|
||||
"password_validation_uppercase_and_lowercase": "Mélange de majuscules et de minuscules",
|
||||
"please_verify_captcha": "Veuillez vérifier reCAPTCHA",
|
||||
"privacy_policy": "Politique de confidentialité",
|
||||
"terms_of_service": "Conditions d'utilisation",
|
||||
"title": "Créez votre compte Formbricks"
|
||||
|
||||
@@ -56,12 +56,14 @@
|
||||
"use_a_backup_code": "Usar um código de backup"
|
||||
},
|
||||
"signup": {
|
||||
"captcha_failed": "reCAPTCHA falhou",
|
||||
"error": "Ocorreu um erro ao te cadastrar",
|
||||
"have_an_account": "Já tem uma conta?",
|
||||
"log_in": "Fazer login",
|
||||
"password_validation_contain_at_least_1_number": "Conter pelo menos 1 número",
|
||||
"password_validation_minimum_8_and_maximum_128_characters": "Mínimo 8 e Máximo 128 caracteres",
|
||||
"password_validation_uppercase_and_lowercase": "mistura de maiúsculas e minúsculas",
|
||||
"please_verify_captcha": "Por favor, verifique o reCAPTCHA",
|
||||
"privacy_policy": "Política de Privacidade",
|
||||
"terms_of_service": "Termos de Serviço",
|
||||
"title": "Crie sua conta no Formbricks"
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -622,6 +622,9 @@ importers:
|
||||
react-radio-group:
|
||||
specifier: 3.0.3
|
||||
version: 3.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react-turnstile:
|
||||
specifier: 1.1.4
|
||||
version: 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react-use:
|
||||
specifier: 17.6.0
|
||||
version: 17.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@@ -11345,6 +11348,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
|
||||
react-turnstile@1.1.4:
|
||||
resolution: {integrity: sha512-oluyRWADdsufCt5eMqacW4gfw8/csr6Tk+fmuaMx0PWMKP1SX1iCviLvD2D5w92eAzIYDHi/krUWGHhlfzxTpQ==}
|
||||
peerDependencies:
|
||||
react: '>= 16.13.1'
|
||||
react-dom: '>= 16.13.1'
|
||||
|
||||
react-universal-interface@0.6.2:
|
||||
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
|
||||
peerDependencies:
|
||||
@@ -26594,6 +26603,11 @@ snapshots:
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
|
||||
react-turnstile@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
react-universal-interface@0.6.2(react@19.0.0)(tslib@2.8.1):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@@ -28862,4 +28876,4 @@ snapshots:
|
||||
react: 19.0.0
|
||||
use-sync-external-store: 1.2.2(react@19.0.0)
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"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",
|
||||
@@ -168,6 +169,7 @@
|
||||
"SURVEYS_PACKAGE_MODE",
|
||||
"SURVEYS_PACKAGE_BUILD",
|
||||
"TELEMETRY_DISABLED",
|
||||
"TURNSTILE_SECRET_KEY",
|
||||
"TERMS_URL",
|
||||
"UPLOADS_DIR",
|
||||
"VERCEL",
|
||||
|
||||
Reference in New Issue
Block a user