Compare commits

...

7 Commits

Author SHA1 Message Date
Cursor Agent
520c337748 test: add unit tests for safeFormRequestSubmit utility
- Test requestSubmit method when available
- Test fallback behavior for iOS Safari 15.5
- Test validation failure prevents form submission
- Ensures proper event dispatching with correct properties
2026-01-21 02:54:03 +00:00
Cursor Agent
7f5c93b629 fix: add iOS Safari 15.5 compatibility for form.requestSubmit()
- Created safeFormRequestSubmit utility function with fallback for browsers
  that don't support requestSubmit() method (iOS Safari < 16.0)
- Updated block-conditional.tsx to use safe form submission
- Updated login-form.tsx to use safe form submission
- Fallback uses reportValidity() for validation UI and dispatches submit event

Fixes TypeError: n.requestSubmit is not a function on iOS Safari 15.5
2026-01-21 02:53:04 +00:00
Matti Nannt
8f6d27c1ef fix: upgrade next.js and preact to fix high-severity vulnerabilities (#7134) 2026-01-20 11:22:01 +00:00
Dhruwang Jariwala
a37815b831 fix: breaking email embed preview for single select question (#7133) 2026-01-20 06:42:15 +00:00
Dhruwang Jariwala
2b526a87ca fix: email locale in invite accepted email (#7124) 2026-01-19 13:32:01 +00:00
Dhruwang Jariwala
047750967c fix: console warnings in survey ui package (#7130) 2026-01-19 07:19:13 +00:00
Johannes
a54356c3b0 docs: add CSAT and update Survey Cooldown (#7128)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-01-19 07:06:16 +00:00
49 changed files with 488 additions and 156 deletions

View File

@@ -58,7 +58,7 @@ async function handleEmailUpdate({
payload.email = inputEmail;
await updateBrevoCustomer({ id: ctx.user.id, email: inputEmail });
} else {
await sendVerificationNewEmail(ctx.user.id, inputEmail);
await sendVerificationNewEmail(ctx.user.id, inputEmail, ctx.user.locale);
}
return payload;
}

View File

@@ -58,6 +58,7 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
ctx.user.email,
emailHtml,
survey.environmentId,
ctx.user.locale,
organizationLogoUrl || ""
);
});

View File

@@ -215,7 +215,14 @@ export const POST = async (request: Request) => {
}
const emailPromises = usersWithNotifications.map((user) =>
sendResponseFinishedEmail(user.email, environmentId, survey, response, responseCount).catch((error) => {
sendResponseFinishedEmail(
user.email,
user.locale,
environmentId,
survey,
response,
responseCount
).catch((error) => {
logger.error(
{ error, url: request.url, userEmail: user.email },
`Failed to send email to ${user.email}`

View File

@@ -1,4 +1,4 @@
import { describe, expect, test, vi } from "vitest";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { getLocale } from "@/lingodotdev/language";
import { getTranslate } from "./server";
@@ -11,6 +11,10 @@ vi.mock("@/lingodotdev/shared", () => ({
}));
describe("lingodotdev server", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should get translate", async () => {
vi.mocked(getLocale).mockResolvedValue("en-US");
const translate = await getTranslate();
@@ -22,4 +26,16 @@ describe("lingodotdev server", () => {
const translate = await getTranslate();
expect(translate).toBeDefined();
});
test("should use provided locale instead of calling getLocale", async () => {
const translate = await getTranslate("de-DE");
expect(getLocale).not.toHaveBeenCalled();
expect(translate).toBeDefined();
});
test("should call getLocale when locale is not provided", async () => {
vi.mocked(getLocale).mockResolvedValue("fr-FR");
await getTranslate();
expect(getLocale).toHaveBeenCalled();
});
});

View File

@@ -3,6 +3,7 @@ import ICU from "i18next-icu";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { DEFAULT_LOCALE } from "@/lib/constants";
import { TUserLocale } from "@formbricks/types/user";
import { getLocale } from "@/lingodotdev/language";
const initI18next = async (lng: string) => {
@@ -21,9 +22,9 @@ const initI18next = async (lng: string) => {
return i18nInstance;
};
export async function getTranslate() {
const locale = await getLocale();
export async function getTranslate(locale?: TUserLocale) {
const resolvedLocale = locale ?? (await getLocale());
const i18nextInstance = await initI18next(locale);
return i18nextInstance.getFixedT(locale);
const i18nextInstance = await initI18next(resolvedLocale);
return i18nextInstance.getFixedT(resolvedLocale);
}

View File

@@ -33,7 +33,7 @@ export const resetPasswordAction = actionClient.schema(ZResetPasswordAction).act
ctx.auditLoggingCtx.oldObject = oldObject;
ctx.auditLoggingCtx.newObject = updatedUser;
await sendPasswordResetNotifyEmail(updatedUser);
await sendPasswordResetNotifyEmail({ email: updatedUser.email, locale: updatedUser.locale });
return { success: true };
}
)

View File

@@ -69,6 +69,7 @@ describe("invite", () => {
creator: {
name: "Test User",
email: "test@example.com",
locale: "en-US",
},
};
@@ -89,6 +90,7 @@ describe("invite", () => {
select: {
name: true,
email: true,
locale: true,
},
},
},

View File

@@ -46,6 +46,7 @@ export const getInvite = reactCache(async (inviteId: string): Promise<InviteWith
select: {
name: true,
email: true,
locale: true,
},
},
},

View File

@@ -102,7 +102,12 @@ export const InvitePage = async (props: InvitePageProps) => {
);
}
await deleteInvite(inviteId);
await sendInviteAcceptedEmail(invite.creator.name ?? "", user?.name ?? "", invite.creator.email);
await sendInviteAcceptedEmail(
invite.creator.name ?? "",
user?.name ?? "",
invite.creator.email,
invite.creator.locale
);
await updateUser(session.user.id, {
notificationSettings: {
...user.notificationSettings,

View File

@@ -1,10 +1,12 @@
import { Invite } from "@prisma/client";
import { TUserLocale } from "@formbricks/types/user";
export interface InviteWithCreator
extends Pick<Invite, "id" | "expiresAt" | "organizationId" | "role" | "teamIds"> {
creator: {
name: string | null;
email: string;
locale: TUserLocale;
};
}

View File

@@ -19,6 +19,7 @@ import { TwoFactorBackup } from "@/modules/ee/two-factor-auth/components/two-fac
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { PasswordInput } from "@/modules/ui/components/password-input";
import { safeFormRequestSubmit } from "@/modules/ui/lib/utils";
const ZLoginForm = z.object({
email: z.string().email(),
@@ -236,7 +237,7 @@ export const LoginForm = ({
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => emailRef.current?.focus(), 100);
} else if (formRef.current) {
formRef.current.requestSubmit();
safeFormRequestSubmit(formRef.current);
}
}}
className="relative w-full justify-center"

View File

@@ -127,7 +127,12 @@ async function handleInviteAcceptance(
},
});
await sendInviteAcceptedEmail(invite.creator.name ?? "", user.name, invite.creator.email);
await sendInviteAcceptedEmail(
invite.creator.name ?? "",
user.name,
invite.creator.email,
invite.creator.locale
);
await deleteInvite(invite.id);
}
@@ -168,7 +173,7 @@ async function handlePostUserCreation(
}
if (!emailVerificationDisabled) {
await sendVerificationEmail(user);
await sendVerificationEmail({ id: user.id, email: user.email, locale: user.locale });
}
}

View File

@@ -48,8 +48,7 @@ describe("resendVerificationEmailAction", () => {
const mockUser = {
id: "user123",
email: "test@example.com",
emailVerified: null, // Not verified
name: "Test User",
locale: "en-US",
};
const mockVerifiedUser = {

View File

@@ -32,7 +32,7 @@ export const resendVerificationEmailAction = actionClient.schema(ZResendVerifica
};
}
ctx.auditLoggingCtx.userId = user.id;
await sendVerificationEmail(user);
await sendVerificationEmail({ id: user.id, email: user.email, locale: user.locale });
return {
success: true,
};

View File

@@ -121,6 +121,7 @@ export const sendTestEmailAction = authenticatedActionClient
await sendEmailCustomizationPreviewEmail(
ctx.user.email,
ctx.user.name,
ctx.user.locale,
organization?.whitelabel?.logoUrl || ""
);

View File

@@ -213,8 +213,8 @@ export async function PreviewEmailTemplate({
{ "rounded-l-lg border-l": i === 0 },
{ "rounded-r-lg": i === firstQuestion.range - 1 },
firstQuestion.isColorCodingEnabled &&
firstQuestion.scale === "number" &&
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
firstQuestion.scale === "number" &&
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
firstQuestion.scale === "star" && "border-transparent"
)}
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
@@ -288,7 +288,7 @@ export async function PreviewEmailTemplate({
<Container className="mx-0 max-w-none">
{firstQuestion.choices.map((choice) => (
<Link
className="border-input-border-color bg-input-color text-question-color rounded-custom mt-2 block border border-solid p-4 hover:bg-slate-100"
className="border-input-border-color bg-input-color text-question-color rounded-custom mt-2 block border border-solid p-4"
href={`${urlWithPrefilling}${firstQuestion.id}=${getLocalizedValue(choice.label, defaultLanguageCode)}`}
key={choice.id}>
{getLocalizedValue(choice.label, defaultLanguageCode)}

View File

@@ -71,12 +71,12 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
...(SMTP_AUTHENTICATED
? {
auth: {
type: "LOGIN",
user: SMTP_USER,
pass: SMTP_PASSWORD,
},
}
auth: {
type: "LOGIN",
user: SMTP_USER,
pass: SMTP_PASSWORD,
},
}
: {}),
tls: {
rejectUnauthorized: SMTP_REJECT_UNAUTHORIZED_TLS,
@@ -97,9 +97,13 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
}
};
export const sendVerificationNewEmail = async (id: string, email: string): Promise<boolean> => {
export const sendVerificationNewEmail = async (
id: string,
email: string,
locale: TUserLocale
): Promise<boolean> => {
try {
const t = await getTranslate();
const t = await getTranslate(locale);
const token = createEmailChangeToken(id, email);
const verifyLink = `${WEBAPP_URL}/verify-email-change?token=${encodeURIComponent(token)}`;
@@ -119,12 +123,14 @@ export const sendVerificationNewEmail = async (id: string, email: string): Promi
export const sendVerificationEmail = async ({
id,
email,
locale,
}: {
id: string;
email: TUserEmail;
locale: TUserLocale;
}): Promise<boolean> => {
try {
const t = await getTranslate();
const t = await getTranslate(locale);
const token = createToken(id, {
expiresIn: "1d",
});
@@ -154,7 +160,7 @@ export const sendForgotPasswordEmail = async (user: {
email: TUserEmail;
locale: TUserLocale;
}): Promise<boolean> => {
const t = await getTranslate();
const t = await getTranslate(user.locale);
const token = createToken(user.id, {
expiresIn: "1d",
});
@@ -167,8 +173,11 @@ export const sendForgotPasswordEmail = async (user: {
});
};
export const sendPasswordResetNotifyEmail = async (user: { email: string }): Promise<boolean> => {
const t = await getTranslate();
export const sendPasswordResetNotifyEmail = async (user: {
email: string;
locale: TUserLocale;
}): Promise<boolean> => {
const t = await getTranslate(user.locale);
const html = await renderPasswordResetNotifyEmail({ t, ...legalProps });
return await sendEmail({
to: user.email,
@@ -201,9 +210,10 @@ export const sendInviteMemberEmail = async (
export const sendInviteAcceptedEmail = async (
inviterName: string,
inviteeName: string,
email: string
email: string,
inviterLocale?: TUserLocale
): Promise<void> => {
const t = await getTranslate();
const t = await getTranslate(inviterLocale);
const html = await renderInviteAcceptedEmail({ inviteeName, inviterName, t, ...legalProps });
await sendEmail({
to: email,
@@ -214,12 +224,13 @@ export const sendInviteAcceptedEmail = async (
export const sendResponseFinishedEmail = async (
email: string,
locale: TUserLocale,
environmentId: string,
survey: TSurvey,
response: TResponse,
responseCount: number
): Promise<void> => {
const t = await getTranslate();
const t = await getTranslate(locale);
const personEmail = response.contactAttributes?.email;
const organization = await getOrganizationByEnvironmentId(environmentId);
@@ -246,12 +257,12 @@ export const sendResponseFinishedEmail = async (
to: email,
subject: personEmail
? t("emails.response_finished_email_subject_with_email", {
personEmail,
surveyName: survey.name,
})
personEmail,
surveyName: survey.name,
})
: t("emails.response_finished_email_subject", {
surveyName: survey.name,
}),
surveyName: survey.name,
}),
replyTo: personEmail?.toString() ?? MAIL_FROM,
html,
});
@@ -261,9 +272,10 @@ export const sendEmbedSurveyPreviewEmail = async (
to: string,
innerHtml: string,
environmentId: string,
locale: TUserLocale,
logoUrl?: string
): Promise<boolean> => {
const t = await getTranslate();
const t = await getTranslate(locale);
const html = await renderEmbedSurveyPreviewEmail({
html: innerHtml,
environmentId,
@@ -281,9 +293,10 @@ export const sendEmbedSurveyPreviewEmail = async (
export const sendEmailCustomizationPreviewEmail = async (
to: string,
userName: string,
locale: TUserLocale,
logoUrl?: string
): Promise<boolean> => {
const t = await getTranslate();
const t = await getTranslate(locale);
const emailHtmlBody = await renderEmailCustomizationPreviewEmail({
userName,
logoUrl,
@@ -305,7 +318,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
const singleUseId = data.suId;
const logoUrl = data.logoUrl || "";
const token = createTokenForLinkSurvey(surveyId, email);
const t = await getTranslate();
const t = await getTranslate(data.locale);
const getSurveyLink = (): string => {
if (singleUseId) {
return `${getPublicDomain()}/s/${surveyId}?verify=${encodeURIComponent(token)}&suId=${singleUseId}`;

View File

@@ -2,7 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeft, MailIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Toaster, toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -10,11 +10,13 @@ import { z } from "zod";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { TUserLocale } from "@formbricks/types/user";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { replaceHeadlineRecall } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { isSurveyResponsePresentAction, sendLinkSurveyEmailAction } from "@/modules/survey/link/actions";
import { getWebAppLocale } from "@/modules/survey/link/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
@@ -26,7 +28,7 @@ interface VerifyEmailProps {
singleUseId?: string;
languageCode: string;
styling: TProjectStyling;
locale: string;
locale: TUserLocale;
}
const ZVerifyEmailInput = z.object({
@@ -42,7 +44,18 @@ export const VerifyEmail = ({
styling,
locale,
}: VerifyEmailProps) => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
// Set i18n language based on survey language
useEffect(() => {
const webAppLocale = getWebAppLocale(languageCode, survey);
if (i18n.language !== webAppLocale) {
i18n.changeLanguage(webAppLocale).catch(() => {
// If changeLanguage fails, fallback to default locale
i18n.changeLanguage("en-US");
});
}
}, [languageCode, survey, i18n]);
const form = useForm<TVerifyEmailInput>({
defaultValues: {
email: "",
@@ -175,7 +188,7 @@ export const VerifyEmail = ({
{!emailSent && showPreviewQuestions && (
<div>
<p className="text-2xl font-bold">{t("s.question_preview")}</p>
<div className="mt-4 flex max-h-[50vh] w-full flex-col overflow-y-auto rounded-lg border border-slate-200 bg-slate-50 bg-opacity-20 p-4 text-slate-700">
<div className="bg-opacity-20 mt-4 flex max-h-[50vh] w-full flex-col overflow-y-auto rounded-lg border border-slate-200 bg-slate-50 p-4 text-slate-700">
{questions.map((question, index) => (
<p
key={index}

View File

@@ -0,0 +1,85 @@
import { describe, expect, test } from "vitest";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getWebAppLocale } from "./utils";
describe("getWebAppLocale", () => {
const createMockSurvey = (languages: TSurvey["languages"] = []): TSurvey => {
return {
id: "survey-1",
createdAt: new Date(),
updatedAt: new Date(),
name: "Test Survey",
type: "link",
environmentId: "env-1",
createdBy: null,
status: "draft",
displayOption: "displayOnce",
autoClose: null,
triggers: [],
recontactDays: null,
displayLimit: null,
welcomeCard: {
enabled: false,
headline: { default: "Welcome" },
timeToFinish: false,
showResponseCount: false,
},
questions: [],
blocks: [],
endings: [],
hiddenFields: { enabled: false, fieldIds: [] },
variables: [],
styling: null,
segment: null,
languages,
displayPercentage: null,
isVerifyEmailEnabled: false,
isSingleResponsePerEmailEnabled: false,
singleUse: null,
pin: null,
projectOverwrites: null,
surveyClosedMessage: null,
followUps: [],
delay: 0,
autoComplete: null,
showLanguageSwitch: null,
recaptcha: null,
isBackButtonHidden: false,
isCaptureIpEnabled: false,
slug: null,
metadata: {},
} as TSurvey;
};
test("maps language codes to web app locales", () => {
const survey = createMockSurvey();
expect(getWebAppLocale("en", survey)).toBe("en-US");
expect(getWebAppLocale("de", survey)).toBe("de-DE");
expect(getWebAppLocale("pt-BR", survey)).toBe("pt-BR");
});
test("handles 'default' languageCode by finding default language in survey", () => {
const survey = createMockSurvey([
{
language: {
id: "lang1",
code: "de",
alias: null,
createdAt: new Date(),
updatedAt: new Date(),
projectId: "proj1",
},
default: true,
enabled: true,
},
]);
expect(getWebAppLocale("default", survey)).toBe("de-DE");
});
test("falls back to en-US when language is not supported", () => {
const survey = createMockSurvey();
expect(getWebAppLocale("default", survey)).toBe("en-US");
expect(getWebAppLocale("xx", survey)).toBe("en-US");
});
});

View File

@@ -1,2 +1,54 @@
// Prefilling logic has been moved to @/modules/survey/link/lib/prefill
// This file is kept for any future utility functions
import { TSurvey } from "@formbricks/types/surveys/types";
/**
* Maps survey language codes to web app locale codes.
* Falls back to "en-US" if the language is not available in web app locales.
*/
export const getWebAppLocale = (languageCode: string, survey: TSurvey): string => {
// Map of common 2-letter language codes to web app locale codes
const languageToLocaleMap: Record<string, string> = {
en: "en-US",
de: "de-DE",
pt: "pt-BR", // Default to Brazilian Portuguese
"pt-BR": "pt-BR",
"pt-PT": "pt-PT",
fr: "fr-FR",
nl: "nl-NL",
zh: "zh-Hans-CN", // Default to Simplified Chinese
"zh-Hans": "zh-Hans-CN",
"zh-Hans-CN": "zh-Hans-CN",
"zh-Hant": "zh-Hant-TW",
"zh-Hant-TW": "zh-Hant-TW",
ro: "ro-RO",
ja: "ja-JP",
es: "es-ES",
sv: "sv-SE",
ru: "ru-RU",
};
let codeToMap = languageCode;
// If languageCode is "default", get the default language from survey
if (languageCode === "default") {
const defaultLanguage = survey.languages?.find((lang) => lang.default);
if (defaultLanguage) {
codeToMap = defaultLanguage.language.code;
} else {
return "en-US";
}
}
// Check if it's already a web app locale code
if (languageToLocaleMap[codeToMap]) {
return languageToLocaleMap[codeToMap];
}
// Try to find a match by base language code (e.g., "pt-BR" -> "pt")
const baseCode = codeToMap.split("-")[0].toLowerCase();
if (languageToLocaleMap[baseCode]) {
return languageToLocaleMap[baseCode];
}
// Fallback to English if language is not supported
return "en-US";
};

View File

@@ -4,3 +4,27 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Safely requests form submission with validation.
* Provides a fallback for browsers that don't support requestSubmit() (iOS Safari < 16.0).
* @param form The form element to submit
*/
export function safeFormRequestSubmit(form: HTMLFormElement): void {
// Check if requestSubmit is supported (iOS Safari 16.0+, all modern browsers)
if (typeof form.requestSubmit === "function") {
form.requestSubmit();
} else {
// Fallback for older browsers (iOS Safari < 16.0)
// reportValidity() triggers native validation UI
if (!form.reportValidity()) {
return;
}
// Dispatch submit event manually to trigger form submission handlers
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true,
});
form.dispatchEvent(submitEvent);
}
}

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -101,7 +101,7 @@
"lucide-react": "0.507.0",
"markdown-it": "14.1.0",
"mime-types": "3.0.1",
"next": "16.1.1",
"next": "16.1.3",
"next-auth": "4.24.12",
"next-safe-action": "7.10.8",
"node-fetch": "3.3.2",

View File

@@ -9,7 +9,7 @@ icon: "map-pin"
src="https://app.formbricks.com/s/m8w91e8wi52pdao8un1f4twu"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -9,7 +9,7 @@ icon: "check"
src="https://app.formbricks.com/s/orxp15pca6x2nfr3v8pttpwm"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -9,7 +9,7 @@ icon: "address-book"
src="https://app.formbricks.com/s/z2zjoonfeythx5n6z5qijbsg"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -9,7 +9,7 @@ icon: "calendar"
src="https://app.formbricks.com/s/rk844spc8ffls25vzkxzzhse"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -15,7 +15,7 @@ icon: "upload"
src="https://app.formbricks.com/s/oo4e6vva48w0trn01ht8krwo"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -12,7 +12,7 @@ Free text questions allow respondents to enter a custom answer. Displays a title
src="https://app.formbricks.com/s/cm2b2eftv000012b0l3htbu0a"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -11,7 +11,7 @@ The values range from 0 to a user-defined maximum (e.g., 0 to X). The selection
src="https://app.formbricks.com/s/obqeey0574jig4lo2gqyv51e"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -10,7 +10,7 @@ icon: "presentation-screen"
src="https://app.formbricks.com/s/vqmpasmnt5qcpsa4enheips0"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -9,7 +9,7 @@ icon: "ranking-star"
src="https://app.formbricks.com/s/z6s84x9wbyk0yqqtfaz238px"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -11,7 +11,7 @@ Rating questions allow respondents to rate questions on a scale. Displays a titl
src="https://app.formbricks.com/s/cx7u4n6hwvc3nztuk4vdezl9"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,
@@ -38,8 +38,35 @@ Select the icon to be used for the rating scale. The options include: stars, num
### Range
Select the range of the rating scale. the options include: 3, 4, 5, 7 or 10. The default is 5.
Select the range of the rating scale. the options include: 3, 4, 5, 6, 7 or 10. The default is 5.
### Labels
Add labels for the lower and upper bounds of the rating scale. The default is "Not good" and "Very good".
## CSAT Summary
After collecting responses, rating questions display a CSAT (Customer Satisfaction) score with a visual traffic light indicator to help you quickly assess satisfaction levels:
- 🟢 **Green** (> 80%): High satisfaction - your users are very satisfied
- 🟠 **Orange** (55-80%): Moderate satisfaction - there's room for improvement
- 🔴 **Red** (< 55%): Low satisfaction - immediate attention needed
<Note>The traffic light indicator appears automatically in the survey summary view, giving you instant feedback on user satisfaction without needing to dig into the data.</Note>
### How CSAT is Calculated
The CSAT percentage represents the proportion of respondents who gave a "satisfied" rating. What counts as "satisfied" depends on your selected range:
| Range | Satisfied Ratings | Examples |
|-------|------------------|----------|
| 3 | Highest rating only | ⭐⭐⭐ |
| 4 | Top 2 ratings | ⭐⭐⭐ or ⭐⭐⭐⭐ |
| 5 | Top 2 ratings | ⭐⭐⭐⭐ or ⭐⭐⭐⭐⭐ |
| 6 | Top 2 ratings | 5 or 6 |
| 7 | Top 2 ratings | 6 or 7 |
| 10 | Top 3 ratings | 8, 9, or 10 |
<Note>
**Pro Tip:** For most use cases, a 5-point scale with star or smiley icons provides the best balance between granularity and user experience. Users find it easy to understand and quick to complete.
</Note>

View File

@@ -9,7 +9,7 @@ icon: "calendar-check"
src="https://app.formbricks.com/s/hx08x27c2aghywh57rroe6fi"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -12,7 +12,7 @@ Multi select questions allow respondents to select several answers from a list.
src="https://app.formbricks.com/s/jhyo6lwzf6eh3fyplhlp7h5f"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -17,7 +17,7 @@ Picture selection questions allow respondents to select one or more images from
src="https://app.formbricks.com/s/xtgmwxlk7jxxr4oi6ym7odki"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -12,7 +12,7 @@ Single select questions allow respondents to select one answer from a list. Disp
src="https://app.formbricks.com/s/wybd3v3cxpdfve4472fu3lhi"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -11,7 +11,7 @@ It consists of a title (can be Question or Short Note) and a description, which
src="https://app.formbricks.com/s/k3p7r7riyy504u4zziqat8zj"
style={{
position: "relative",
height: "90vh",
height: "600px",
maxHeight: "100vh",
width: "100%",
border: 0,

View File

@@ -14,7 +14,7 @@ Recontact options are the last layer of the logic that determines if a survey is
3. **Recontact Options:** Should the survey be shown (again) to this user? That's dependent on:
- Did the user see any survey recently (meaning, has Global Waiting Time passed)?
- Did the user see any survey recently (meaning, has Survey Cooldown passed)?
- Did the user see this specific survey already?
@@ -50,13 +50,13 @@ Available Recontact Options include:
![Choose Recontanct Options for the Survey](/images/xm-and-surveys/surveys/website-app-surveys/recontact/survey-recontact.webp)
## Project-wide Global Waiting Time
## Project-wide Survey Cooldown
The Global Waiting Time is a universal blocker to make sure that no user sees too many surveys. This is particularly helpful when several teams of large organisations use Formbricks at the same time.
The Survey Cooldown is a universal blocker to make sure that no user sees too many surveys. This is particularly helpful when several teams of large organisations use Formbricks at the same time.
<Note>The default Global Waiting Time is set to 7 days.</Note>
<Note>The default Survey Cooldown is set to 7 days.</Note>
To adjust the Global Waiting Time:
To adjust the Survey Cooldown:
1. Visit Formbricks Settings
@@ -68,9 +68,9 @@ To adjust the Global Waiting Time:
![Formbricks Project-Wide Wait Time](/images/xm-and-surveys/surveys/website-app-surveys/recontact/global-wait-time.webp)
## Overriding Global Waiting Time for a Specific Survey
## Overriding Survey Cooldown for a Specific Survey
For specific surveys, you may need to override the default waiting time. Below is how you can do that:
For specific surveys, you may need to override the default cooldown. Below is how you can do that:
1. In the Survey Editor, access the Settings Tab.
@@ -80,11 +80,11 @@ For specific surveys, you may need to override the default waiting time. Below i
4. Set a custom recontact period:
- **Always Show Survey**: Displays the survey whenever triggered, ignoring the waiting time.
- **Always Show Survey**: Displays the survey whenever triggered, ignoring the cooldown.
- **Wait `X` days before showing this survey again**: Sets a specific interval before the survey can be shown again.
![Ignore Global Waiting Time for a Specific Survey](/images/xm-and-surveys/surveys/website-app-surveys/recontact/ignore-wait-time.webp)
![Ignore Survey Cooldown for a Specific Survey](/images/xm-and-surveys/surveys/website-app-surveys/recontact/ignore-wait-time.webp)
---

View File

@@ -46,7 +46,7 @@
"dependencies": {
"react": "19.2.3",
"react-dom": "19.2.3",
"next": "16.1.1"
"next": "16.1.3"
},
"devDependencies": {
"@azure/identity": "4.13.0",

View File

@@ -188,7 +188,6 @@ function Calendar({
...classNames,
}}
components={{
// @ts-expect-error - React types version mismatch - the project uses React 19 types, but some Radix UI packages (react-day-picker) bundle their own older React types, creating incompatible Ref type definitions
Root: CalendarRoot,
Chevron: CalendarChevron,
DayButton: CalendarDayButton,

View File

@@ -9,7 +9,6 @@ export interface ProgressProps extends Omit<React.ComponentProps<"div">, "childr
function Progress({ className, value, ...props }: Readonly<ProgressProps>): React.JSX.Element {
const progressValue: number = typeof value === "number" ? value : 0;
return (
// @ts-expect-error - React types version mismatch - the project uses React 19 types, but some Radix UI packages (@radix-ui/react-progress) bundle their own older React types, creating incompatible Ref type definitions
<ProgressPrimitive.Root
data-slot="progress"
value={progressValue}

View File

@@ -44,7 +44,7 @@
"i18next": "25.5.2",
"i18next-icu": "2.4.0",
"isomorphic-dompurify": "2.33.0",
"preact": "10.26.10",
"preact": "10.28.2",
"react-i18next": "15.7.3"
},
"devDependencies": {

View File

@@ -14,7 +14,7 @@ import { SubmitButton } from "@/components/buttons/submit-button";
import { ElementConditional } from "@/components/general/element-conditional";
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
import { getLocalizedValue } from "@/lib/i18n";
import { cn } from "@/lib/utils";
import { cn, safeFormRequestSubmit } from "@/lib/utils";
interface BlockConditionalProps {
block: TSurveyBlock;
@@ -141,7 +141,7 @@ export function BlockConditional({
response.length < rankingElement.choices.length);
if (hasIncompleteRanking) {
form.requestSubmit();
safeFormRequestSubmit(form);
return false;
}
return true;
@@ -174,7 +174,7 @@ export function BlockConditional({
element.type === TSurveyElementTypeEnum.ContactInfo
) {
if (!form.checkValidity()) {
form.requestSubmit();
safeFormRequestSubmit(form);
return false;
}
return true;
@@ -191,14 +191,14 @@ export function BlockConditional({
response &&
hasUnansweredRows(response, element)
) {
form.requestSubmit();
safeFormRequestSubmit(form);
return false;
}
// For other element types, check if required fields are empty
// CTA elements should not block navigation even if marked required (as they are informational)
if (element.type !== TSurveyElementTypeEnum.CTA && element.required && isEmptyResponse(response)) {
form.requestSubmit();
safeFormRequestSubmit(form);
return false;
}
@@ -230,7 +230,7 @@ export function BlockConditional({
block.elements.forEach((element) => {
const form = elementFormRefs.current.get(element.id);
if (form) {
form.requestSubmit();
safeFormRequestSubmit(form);
}
});

View File

@@ -96,7 +96,9 @@ export const StackedCard = ({
return (
<div
ref={(el) => (cardRefs.current[dynamicQuestionIndex] = el)}
ref={(el) => {
cardRefs.current[dynamicQuestionIndex] = el;
}}
id={`questionCard-${dynamicQuestionIndex}`}
data-testid={`questionCard-${dynamicQuestionIndex}`}
key={dynamicQuestionIndex}

View File

@@ -10,6 +10,7 @@ import {
getMimeType,
getShuffledChoicesIds,
getShuffledRowIndices,
safeFormRequestSubmit,
} from "./utils";
// Mock crypto.getRandomValues for deterministic shuffle tests
@@ -327,3 +328,54 @@ describe("findBlockByElementId", () => {
expect(block).toBeUndefined();
});
});
describe("safeFormRequestSubmit", () => {
let mockForm: HTMLFormElement;
beforeEach(() => {
// Create a mock form element
mockForm = document.createElement("form");
});
test("should call requestSubmit when it's supported", () => {
// Mock requestSubmit as a function
const requestSubmitSpy = vi.fn();
mockForm.requestSubmit = requestSubmitSpy;
safeFormRequestSubmit(mockForm);
expect(requestSubmitSpy).toHaveBeenCalled();
});
test("should use fallback when requestSubmit is not supported", () => {
// Remove requestSubmit to simulate iOS Safari 15.5
mockForm.requestSubmit = undefined as unknown as typeof mockForm.requestSubmit;
const reportValiditySpy = vi.spyOn(mockForm, "reportValidity").mockReturnValue(true);
const dispatchEventSpy = vi.spyOn(mockForm, "dispatchEvent");
safeFormRequestSubmit(mockForm);
expect(reportValiditySpy).toHaveBeenCalled();
expect(dispatchEventSpy).toHaveBeenCalled();
// Verify the submit event was dispatched with correct properties
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchedEvent.type).toBe("submit");
expect(dispatchedEvent.bubbles).toBe(true);
expect(dispatchedEvent.cancelable).toBe(true);
});
test("should not dispatch event when reportValidity returns false", () => {
// Remove requestSubmit to simulate iOS Safari 15.5
mockForm.requestSubmit = undefined as unknown as typeof mockForm.requestSubmit;
const reportValiditySpy = vi.spyOn(mockForm, "reportValidity").mockReturnValue(false);
const dispatchEventSpy = vi.spyOn(mockForm, "dispatchEvent");
safeFormRequestSubmit(mockForm);
expect(reportValiditySpy).toHaveBeenCalled();
expect(dispatchEventSpy).not.toHaveBeenCalled();
});
});

View File

@@ -275,3 +275,27 @@ export const getFirstElementIdInBlock = (
const block = survey.blocks.find((b) => b.id === blockId);
return block?.elements[0]?.id;
};
/**
* Safely requests form submission with validation.
* Provides a fallback for browsers that don't support requestSubmit() (iOS Safari < 16.0).
* @param form The form element to submit
*/
export const safeFormRequestSubmit = (form: HTMLFormElement): void => {
// Check if requestSubmit is supported (iOS Safari 16.0+, all modern browsers)
if (typeof form.requestSubmit === "function") {
form.requestSubmit();
} else {
// Fallback for older browsers (iOS Safari < 16.0)
// reportValidity() triggers native validation UI
if (!form.reportValidity()) {
return;
}
// Dispatch submit event manually to trigger form submission handlers
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true,
});
form.dispatchEvent(submitEvent);
}
};

View File

@@ -1,11 +1,12 @@
import { z } from "zod";
import { ZUserLocale } from "./user";
export const ZLinkSurveyEmailData = z.object({
surveyId: z.string(),
email: z.string(),
suId: z.string().optional(),
surveyName: z.string(),
locale: z.string(),
locale: ZUserLocale,
logoUrl: z.string().optional(),
});

140
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
.:
dependencies:
next:
specifier: 16.1.1
version: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
specifier: 16.1.3
version: 16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react:
specifier: 19.2.3
version: 19.2.3
@@ -279,7 +279,7 @@ importers:
version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@sentry/nextjs':
specifier: 10.5.0
version: 10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.11))
version: 10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.11))
'@t3-oss/env-nextjs':
specifier: 0.13.4
version: 0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.24.4)
@@ -368,14 +368,14 @@ importers:
specifier: 3.0.1
version: 3.0.1
next:
specifier: 16.1.1
version: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
specifier: 16.1.3
version: 16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-auth:
specifier: 4.24.12
version: 4.24.12(patch_hash=43pqaaqjvqhdw6jmcjbeq3fjse)(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 4.24.12(patch_hash=43pqaaqjvqhdw6jmcjbeq3fjse)(next@16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-safe-action:
specifier: 7.10.8
version: 7.10.8(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4)
version: 7.10.8(next@16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4)
node-fetch:
specifier: 3.3.2
version: 3.3.2
@@ -982,8 +982,8 @@ importers:
specifier: 2.33.0
version: 2.33.0
preact:
specifier: 10.26.10
version: 10.26.10
specifier: 10.28.2
version: 10.28.2
react-i18next:
specifier: 15.7.3
version: 15.7.3(i18next@25.5.2(typescript@5.8.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.8.3)
@@ -1002,13 +1002,13 @@ importers:
version: link:../types
'@preact/preset-vite':
specifier: 2.10.1
version: 2.10.1(@babel/core@7.28.5)(preact@10.26.10)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))
version: 2.10.1(@babel/core@7.28.5)(preact@10.28.2)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))
'@tailwindcss/postcss':
specifier: 4.1.17
version: 4.1.17
'@testing-library/preact':
specifier: 3.2.4
version: 3.2.4(preact@10.26.10)
version: 3.2.4(preact@10.28.2)
'@types/react':
specifier: 19.1.4
version: 19.1.4
@@ -2789,8 +2789,8 @@ packages:
'@next/env@16.0.9':
resolution: {integrity: sha512-6284pl8c8n9PQidN63qjPVEu1uXXKjnmbmaLebOzIfTrSXdGiAPsIMRi4pk/+v/ezqweE1/B8bFqiAAfC6lMXg==}
'@next/env@16.1.1':
resolution: {integrity: sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==}
'@next/env@16.1.3':
resolution: {integrity: sha512-BLP14oBOvZWXgfdJf9ao+VD8O30uE+x7PaV++QtACLX329WcRSJRO5YJ+Bcvu0Q+c/lei41TjSiFf6pXqnpbQA==}
'@next/eslint-plugin-next@15.3.2':
resolution: {integrity: sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==}
@@ -2801,8 +2801,8 @@ packages:
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-arm64@16.1.1':
resolution: {integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==}
'@next/swc-darwin-arm64@16.1.3':
resolution: {integrity: sha512-CpOD3lmig6VflihVoGxiR/l5Jkjfi4uLaOR4ziriMv0YMDoF6cclI+p5t2nstM8TmaFiY6PCTBgRWB57/+LiBA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -2813,8 +2813,8 @@ packages:
cpu: [x64]
os: [darwin]
'@next/swc-darwin-x64@16.1.1':
resolution: {integrity: sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==}
'@next/swc-darwin-x64@16.1.3':
resolution: {integrity: sha512-aF4us2JXh0zn3hNxvL1Bx3BOuh8Lcw3p3Xnurlvca/iptrDH1BrpObwkw9WZra7L7/0qB9kjlREq3hN/4x4x+Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -2825,8 +2825,8 @@ packages:
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-gnu@16.1.1':
resolution: {integrity: sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==}
'@next/swc-linux-arm64-gnu@16.1.3':
resolution: {integrity: sha512-8VRkcpcfBtYvhGgXAF7U3MBx6+G1lACM1XCo1JyaUr4KmAkTNP8Dv2wdMq7BI+jqRBw3zQE7c57+lmp7jCFfKA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -2837,8 +2837,8 @@ packages:
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@16.1.1':
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
'@next/swc-linux-arm64-musl@16.1.3':
resolution: {integrity: sha512-UbFx69E2UP7MhzogJRMFvV9KdEn4sLGPicClwgqnLht2TEi204B71HuVfps3ymGAh0c44QRAF+ZmvZZhLLmhNg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -2849,8 +2849,8 @@ packages:
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-gnu@16.1.1':
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
'@next/swc-linux-x64-gnu@16.1.3':
resolution: {integrity: sha512-SzGTfTjR5e9T+sZh5zXqG/oeRQufExxBF6MssXS7HPeZFE98JDhCRZXpSyCfWrWrYrzmnw/RVhlP2AxQm+wkRQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -2861,8 +2861,8 @@ packages:
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@16.1.1':
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
'@next/swc-linux-x64-musl@16.1.3':
resolution: {integrity: sha512-HlrDpj0v+JBIvQex1mXHq93Mht5qQmfyci+ZNwGClnAQldSfxI6h0Vupte1dSR4ueNv4q7qp5kTnmLOBIQnGow==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -2873,8 +2873,8 @@ packages:
cpu: [arm64]
os: [win32]
'@next/swc-win32-arm64-msvc@16.1.1':
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
'@next/swc-win32-arm64-msvc@16.1.3':
resolution: {integrity: sha512-3gFCp83/LSduZMSIa+lBREP7+5e7FxpdBoc9QrCdmp+dapmTK9I+SLpY60Z39GDmTXSZA4huGg9WwmYbr6+WRw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -2885,8 +2885,8 @@ packages:
cpu: [x64]
os: [win32]
'@next/swc-win32-x64-msvc@16.1.1':
resolution: {integrity: sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==}
'@next/swc-win32-x64-msvc@16.1.3':
resolution: {integrity: sha512-1SZVfFT8zmMB+Oblrh5OKDvUo5mYQOkX2We6VGzpg7JUVZlqe4DYOFGKYZKTweSx1gbMixyO1jnFT4thU+nNHQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -8703,8 +8703,8 @@ packages:
sass:
optional: true
next@16.1.1:
resolution: {integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==}
next@16.1.3:
resolution: {integrity: sha512-gthG3TRD+E3/mA0uDQb9lqBmx1zVosq5kIwxNN6+MRNd085GzD+9VXMPUs+GGZCbZ+GDZdODUq4Pm7CTXK6ipw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -9190,12 +9190,12 @@ packages:
peerDependencies:
preact: '>=10'
preact@10.26.10:
resolution: {integrity: sha512-sqdfdSa8AZeJ+wfMYjFImIRTnhfyPSLCH+LEb1+BoRUDKLnE6AnvZeClx3Bkj2Q9nn44GFAefOKIx5oc54q93A==}
preact@10.26.6:
resolution: {integrity: sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==}
preact@10.28.2:
resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@@ -13791,7 +13791,7 @@ snapshots:
'@next/env@16.0.9': {}
'@next/env@16.1.1': {}
'@next/env@16.1.3': {}
'@next/eslint-plugin-next@15.3.2':
dependencies:
@@ -13800,49 +13800,49 @@ snapshots:
'@next/swc-darwin-arm64@16.0.9':
optional: true
'@next/swc-darwin-arm64@16.1.1':
'@next/swc-darwin-arm64@16.1.3':
optional: true
'@next/swc-darwin-x64@16.0.9':
optional: true
'@next/swc-darwin-x64@16.1.1':
'@next/swc-darwin-x64@16.1.3':
optional: true
'@next/swc-linux-arm64-gnu@16.0.9':
optional: true
'@next/swc-linux-arm64-gnu@16.1.1':
'@next/swc-linux-arm64-gnu@16.1.3':
optional: true
'@next/swc-linux-arm64-musl@16.0.9':
optional: true
'@next/swc-linux-arm64-musl@16.1.1':
'@next/swc-linux-arm64-musl@16.1.3':
optional: true
'@next/swc-linux-x64-gnu@16.0.9':
optional: true
'@next/swc-linux-x64-gnu@16.1.1':
'@next/swc-linux-x64-gnu@16.1.3':
optional: true
'@next/swc-linux-x64-musl@16.0.9':
optional: true
'@next/swc-linux-x64-musl@16.1.1':
'@next/swc-linux-x64-musl@16.1.3':
optional: true
'@next/swc-win32-arm64-msvc@16.0.9':
optional: true
'@next/swc-win32-arm64-msvc@16.1.1':
'@next/swc-win32-arm64-msvc@16.1.3':
optional: true
'@next/swc-win32-x64-msvc@16.0.9':
optional: true
'@next/swc-win32-x64-msvc@16.1.1':
'@next/swc-win32-x64-msvc@16.1.3':
optional: true
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
@@ -14312,12 +14312,12 @@ snapshots:
dependencies:
playwright: 1.56.1
'@preact/preset-vite@2.10.1(@babel/core@7.28.5)(preact@10.26.10)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))':
'@preact/preset-vite@2.10.1(@babel/core@7.28.5)(preact@10.28.2)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5)
'@prefresh/vite': 2.4.11(preact@10.26.10)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))
'@prefresh/vite': 2.4.11(preact@10.28.2)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))
'@rollup/pluginutils': 4.2.1
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5)
debug: 4.4.3
@@ -14332,20 +14332,20 @@ snapshots:
'@prefresh/babel-plugin@0.5.2': {}
'@prefresh/core@1.5.9(preact@10.26.10)':
'@prefresh/core@1.5.9(preact@10.28.2)':
dependencies:
preact: 10.26.10
preact: 10.28.2
'@prefresh/utils@1.2.1': {}
'@prefresh/vite@2.4.11(preact@10.26.10)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))':
'@prefresh/vite@2.4.11(preact@10.28.2)(vite@6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@babel/core': 7.28.5
'@prefresh/babel-plugin': 0.5.2
'@prefresh/core': 1.5.9(preact@10.26.10)
'@prefresh/core': 1.5.9(preact@10.28.2)
'@prefresh/utils': 1.2.1
'@rollup/pluginutils': 4.2.1
preact: 10.26.10
preact: 10.28.2
vite: 6.4.1(@types/node@22.15.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -15700,7 +15700,7 @@ snapshots:
'@sentry/core@10.5.0': {}
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.11))':
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.11))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.38.0
@@ -15713,7 +15713,7 @@ snapshots:
'@sentry/vercel-edge': 10.5.0
'@sentry/webpack-plugin': 4.6.1(encoding@0.1.13)(webpack@5.99.8(esbuild@0.25.11))
chalk: 3.0.0
next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
resolve: 1.22.8
rollup: 4.54.0
stacktrace-parser: 0.1.11
@@ -16561,10 +16561,10 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
'@testing-library/preact@3.2.4(preact@10.26.10)':
'@testing-library/preact@3.2.4(preact@10.28.2)':
dependencies:
'@testing-library/dom': 8.20.1
preact: 10.26.10
preact: 10.28.2
'@testing-library/react@16.3.0(@testing-library/dom@8.20.1)(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
@@ -20620,13 +20620,13 @@ snapshots:
neo-async@2.6.2: {}
next-auth@4.24.12(patch_hash=43pqaaqjvqhdw6jmcjbeq3fjse)(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next-auth@4.24.12(patch_hash=43pqaaqjvqhdw6jmcjbeq3fjse)(next@16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@babel/runtime': 7.28.4
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.26.6
@@ -20637,9 +20637,9 @@ snapshots:
optionalDependencies:
nodemailer: 7.0.11
next-safe-action@7.10.8(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4):
next-safe-action@7.10.8(next@16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4):
dependencies:
next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
optionalDependencies:
@@ -20670,9 +20670,9 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@16.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 16.1.1
'@next/env': 16.1.3
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.9.11
caniuse-lite: 1.0.30001762
@@ -20681,14 +20681,14 @@ snapshots:
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.1.1
'@next/swc-darwin-x64': 16.1.1
'@next/swc-linux-arm64-gnu': 16.1.1
'@next/swc-linux-arm64-musl': 16.1.1
'@next/swc-linux-x64-gnu': 16.1.1
'@next/swc-linux-x64-musl': 16.1.1
'@next/swc-win32-arm64-msvc': 16.1.1
'@next/swc-win32-x64-msvc': 16.1.1
'@next/swc-darwin-arm64': 16.1.3
'@next/swc-darwin-x64': 16.1.3
'@next/swc-linux-arm64-gnu': 16.1.3
'@next/swc-linux-arm64-musl': 16.1.3
'@next/swc-linux-x64-gnu': 16.1.3
'@next/swc-linux-x64-musl': 16.1.3
'@next/swc-win32-arm64-msvc': 16.1.3
'@next/swc-win32-x64-msvc': 16.1.3
'@opentelemetry/api': 1.9.0
'@playwright/test': 1.56.1
sharp: 0.34.5
@@ -21197,10 +21197,10 @@ snapshots:
preact: 10.26.6
pretty-format: 3.8.0
preact@10.26.10: {}
preact@10.26.6: {}
preact@10.28.2: {}
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2