mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-04 10:19:31 -06:00
Compare commits
1 Commits
typeerror-
...
poc-update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ce3bae78b |
@@ -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, ctx.user.locale);
|
||||
await sendVerificationNewEmail(ctx.user.id, inputEmail);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
ctx.user.email,
|
||||
emailHtml,
|
||||
survey.environmentId,
|
||||
ctx.user.locale,
|
||||
organizationLogoUrl || ""
|
||||
);
|
||||
});
|
||||
|
||||
@@ -215,14 +215,7 @@ export const POST = async (request: Request) => {
|
||||
}
|
||||
|
||||
const emailPromises = usersWithNotifications.map((user) =>
|
||||
sendResponseFinishedEmail(
|
||||
user.email,
|
||||
user.locale,
|
||||
environmentId,
|
||||
survey,
|
||||
response,
|
||||
responseCount
|
||||
).catch((error) => {
|
||||
sendResponseFinishedEmail(user.email, environmentId, survey, response, responseCount).catch((error) => {
|
||||
logger.error(
|
||||
{ error, url: request.url, userEmail: user.email },
|
||||
`Failed to send email to ${user.email}`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { getLocale } from "@/lingodotdev/language";
|
||||
import { getTranslate } from "./server";
|
||||
|
||||
@@ -11,10 +11,6 @@ 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();
|
||||
@@ -26,16 +22,4 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ 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) => {
|
||||
@@ -22,9 +21,9 @@ const initI18next = async (lng: string) => {
|
||||
return i18nInstance;
|
||||
};
|
||||
|
||||
export async function getTranslate(locale?: TUserLocale) {
|
||||
const resolvedLocale = locale ?? (await getLocale());
|
||||
export async function getTranslate() {
|
||||
const locale = await getLocale();
|
||||
|
||||
const i18nextInstance = await initI18next(resolvedLocale);
|
||||
return i18nextInstance.getFixedT(resolvedLocale);
|
||||
const i18nextInstance = await initI18next(locale);
|
||||
return i18nextInstance.getFixedT(locale);
|
||||
}
|
||||
|
||||
176
apps/web/modules/api/v2/management/responses/lib/expand.ts
Normal file
176
apps/web/modules/api/v2/management/responses/lib/expand.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { z } from "zod";
|
||||
import { TResponseData, TResponseDataValue } from "@formbricks/types/responses";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
|
||||
// Supported expansion keys
|
||||
export const ZResponseExpand = z.enum(["choiceIds", "questionHeadlines"]);
|
||||
|
||||
export type TResponseExpand = z.infer<typeof ZResponseExpand>;
|
||||
|
||||
// Schema for the expand query parameter (comma-separated list)
|
||||
export const ZExpandParam = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) return [];
|
||||
return val.split(",").map((s) => s.trim());
|
||||
})
|
||||
.pipe(z.array(ZResponseExpand));
|
||||
|
||||
export type TExpandParam = z.infer<typeof ZExpandParam>;
|
||||
|
||||
// Expanded response data structure for a single answer
|
||||
export type TExpandedValue = {
|
||||
value: TResponseDataValue;
|
||||
choiceIds?: string[];
|
||||
};
|
||||
|
||||
// Expanded response data structure
|
||||
export type TExpandedResponseData = {
|
||||
[questionId: string]: TExpandedValue;
|
||||
};
|
||||
|
||||
// Additional expansions that are added as separate fields
|
||||
export type TResponseExpansions = {
|
||||
questionHeadlines?: Record<string, string>;
|
||||
};
|
||||
|
||||
// Choice element types that support choiceIds expansion
|
||||
const CHOICE_ELEMENT_TYPES = ["multipleChoiceMulti", "multipleChoiceSingle", "ranking", "pictureSelection"];
|
||||
|
||||
/**
|
||||
* Check if an element type supports choice ID expansion
|
||||
*/
|
||||
export const isChoiceElement = (element: TSurveyElement): boolean => {
|
||||
return CHOICE_ELEMENT_TYPES.includes(element.type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check if element has choices property
|
||||
*/
|
||||
const hasChoices = (
|
||||
element: TSurveyElement
|
||||
): element is TSurveyElement & { choices: Array<{ id: string; label: Record<string, string> }> } => {
|
||||
return "choices" in element && Array.isArray(element.choices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check if element has headline property
|
||||
*/
|
||||
const hasHeadline = (
|
||||
element: TSurveyElement
|
||||
): element is TSurveyElement & { headline: Record<string, string> } => {
|
||||
return "headline" in element && typeof element.headline === "object";
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts choice IDs from a response value for choice-based questions
|
||||
* @param responseValue - The response value (string for single choice, array for multi choice)
|
||||
* @param element - The survey element containing choices
|
||||
* @param language - The language to match against (defaults to "default")
|
||||
* @returns Array of choice IDs
|
||||
*/
|
||||
export const extractChoiceIdsFromResponse = (
|
||||
responseValue: TResponseDataValue,
|
||||
element: TSurveyElement,
|
||||
language: string = "default"
|
||||
): string[] => {
|
||||
if (!isChoiceElement(element) || !responseValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Picture selection already stores IDs directly
|
||||
if (element.type === "pictureSelection") {
|
||||
if (Array.isArray(responseValue)) {
|
||||
return responseValue.filter((id): id is string => typeof id === "string");
|
||||
}
|
||||
return typeof responseValue === "string" ? [responseValue] : [];
|
||||
}
|
||||
|
||||
// For other choice types, we need to map labels to IDs
|
||||
if (!hasChoices(element)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const findChoiceByLabel = (label: string): string => {
|
||||
const choice = element.choices.find((c) => {
|
||||
// Try exact language match first
|
||||
if (c.label[language] === label) {
|
||||
return true;
|
||||
}
|
||||
// Fall back to checking all language values
|
||||
return Object.values(c.label).includes(label);
|
||||
});
|
||||
return choice?.id ?? "other";
|
||||
};
|
||||
|
||||
if (Array.isArray(responseValue)) {
|
||||
return responseValue.filter((v): v is string => typeof v === "string" && v !== "").map(findChoiceByLabel);
|
||||
}
|
||||
|
||||
if (typeof responseValue === "string") {
|
||||
return [findChoiceByLabel(responseValue)];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand response data with choice IDs
|
||||
* @param data - The response data object
|
||||
* @param survey - The survey definition
|
||||
* @param language - The language code for label matching
|
||||
* @returns Expanded response data with choice IDs
|
||||
*/
|
||||
export const expandWithChoiceIds = (
|
||||
data: TResponseData,
|
||||
survey: TSurvey,
|
||||
language: string = "default"
|
||||
): TExpandedResponseData => {
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const expandedData: TExpandedResponseData = {};
|
||||
|
||||
for (const [questionId, value] of Object.entries(data)) {
|
||||
const element = elements.find((e) => e.id === questionId);
|
||||
|
||||
if (element && isChoiceElement(element)) {
|
||||
const choiceIds = extractChoiceIdsFromResponse(value, element, language);
|
||||
expandedData[questionId] = {
|
||||
value,
|
||||
...(choiceIds.length > 0 && { choiceIds }),
|
||||
};
|
||||
} else {
|
||||
expandedData[questionId] = { value };
|
||||
}
|
||||
}
|
||||
|
||||
return expandedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate question headlines map
|
||||
* @param data - The response data object
|
||||
* @param survey - The survey definition
|
||||
* @param language - The language code for localization
|
||||
* @returns Record mapping question IDs to their headlines
|
||||
*/
|
||||
export const getQuestionHeadlines = (
|
||||
data: TResponseData,
|
||||
survey: TSurvey,
|
||||
language: string = "default"
|
||||
): Record<string, string> => {
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const headlines: Record<string, string> = {};
|
||||
|
||||
for (const questionId of Object.keys(data)) {
|
||||
const element = elements.find((e) => e.id === questionId);
|
||||
if (element && hasHeadline(element)) {
|
||||
headlines[questionId] = getLocalizedValue(element.headline, language);
|
||||
}
|
||||
}
|
||||
|
||||
return headlines;
|
||||
};
|
||||
@@ -11,7 +11,8 @@ import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/type
|
||||
export const getResponsesEndpoint: ZodOpenApiOperationObject = {
|
||||
operationId: "getResponses",
|
||||
summary: "Get responses",
|
||||
description: "Gets responses from the database.",
|
||||
description:
|
||||
"Gets responses from the database. Use the `expand` parameter to enrich response data with additional information like choice IDs (for language-agnostic processing) or question headlines.",
|
||||
requestParams: {
|
||||
query: ZGetResponsesFilter.sourceType(),
|
||||
},
|
||||
|
||||
@@ -93,4 +93,5 @@ export const responseFilter: TGetResponsesFilter = {
|
||||
skip: 0,
|
||||
sortBy: "createdAt",
|
||||
order: "asc",
|
||||
expand: [],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Response } from "@prisma/client";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TExpandParam,
|
||||
TExpandedResponseData,
|
||||
TResponseExpansions,
|
||||
expandWithChoiceIds,
|
||||
getQuestionHeadlines,
|
||||
} from "./expand";
|
||||
|
||||
export type TTransformedResponse = Omit<Response, "data"> & {
|
||||
data: TResponseData | TExpandedResponseData;
|
||||
expansions?: TResponseExpansions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform a response based on requested expansions
|
||||
* @param response - The raw response from the database
|
||||
* @param survey - The survey definition
|
||||
* @param expand - Array of expansion keys to apply
|
||||
* @returns Transformed response with requested expansions
|
||||
*/
|
||||
export const transformResponse = (
|
||||
response: Response,
|
||||
survey: TSurvey,
|
||||
expand: TExpandParam
|
||||
): TTransformedResponse => {
|
||||
const language = response.language ?? "default";
|
||||
const data = response.data as TResponseData;
|
||||
|
||||
let transformedData: TResponseData | TExpandedResponseData = data;
|
||||
const expansions: TResponseExpansions = {};
|
||||
|
||||
// Apply choiceIds expansion
|
||||
if (expand.includes("choiceIds")) {
|
||||
transformedData = expandWithChoiceIds(data, survey, language);
|
||||
}
|
||||
|
||||
// Apply questionHeadlines expansion
|
||||
if (expand.includes("questionHeadlines")) {
|
||||
expansions.questionHeadlines = getQuestionHeadlines(data, survey, language);
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
data: transformedData,
|
||||
...(Object.keys(expansions).length > 0 && { expansions }),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform multiple responses with caching of survey lookups
|
||||
* @param responses - Array of raw responses from the database
|
||||
* @param expand - Array of expansion keys to apply
|
||||
* @param getSurvey - Function to fetch survey by ID
|
||||
* @returns Array of transformed responses
|
||||
*/
|
||||
export const transformResponses = async (
|
||||
responses: Response[],
|
||||
expand: TExpandParam,
|
||||
getSurvey: (surveyId: string) => Promise<TSurvey | null>
|
||||
): Promise<TTransformedResponse[]> => {
|
||||
if (expand.length === 0) {
|
||||
// No expansion requested, return as-is
|
||||
return responses as TTransformedResponse[];
|
||||
}
|
||||
|
||||
// Cache surveys to avoid duplicate lookups
|
||||
const surveyCache = new Map<string, TSurvey | null>();
|
||||
|
||||
const transformed = await Promise.all(
|
||||
responses.map(async (response) => {
|
||||
let survey = surveyCache.get(response.surveyId);
|
||||
|
||||
if (survey === undefined) {
|
||||
survey = await getSurvey(response.surveyId);
|
||||
surveyCache.set(response.surveyId, survey);
|
||||
}
|
||||
|
||||
if (!survey) {
|
||||
// Survey not found, return response unchanged
|
||||
return response as TTransformedResponse;
|
||||
}
|
||||
|
||||
return transformResponse(response, survey, expand);
|
||||
})
|
||||
);
|
||||
|
||||
return transformed;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Response } from "@prisma/client";
|
||||
import { NextRequest } from "next/server";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { responses } from "@/modules/api/v2/lib/response";
|
||||
@@ -13,6 +13,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
import { createResponseWithQuotaEvaluation, getResponses } from "./lib/response";
|
||||
import { transformResponses } from "./lib/transform";
|
||||
|
||||
export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
@@ -34,16 +35,17 @@ export const GET = async (request: NextRequest) =>
|
||||
(permission) => permission.environmentId
|
||||
);
|
||||
|
||||
const environmentResponses: Response[] = [];
|
||||
const res = await getResponses(environmentIds, query);
|
||||
|
||||
if (!res.ok) {
|
||||
return handleApiError(request, res.error);
|
||||
}
|
||||
|
||||
environmentResponses.push(...res.data.data);
|
||||
// Transform responses if expansion is requested
|
||||
const expand = query.expand ?? [];
|
||||
const transformedResponses = await transformResponses(res.data.data, expand, getSurvey);
|
||||
|
||||
return responses.successResponse({ data: environmentResponses });
|
||||
return responses.successResponse({ data: transformedResponses, meta: res.data.meta });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { ZResponse } from "@formbricks/database/zod/responses";
|
||||
import { ZExpandParam } from "@/modules/api/v2/management/responses/lib/expand";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetResponsesFilter = ZGetFilter.extend({
|
||||
surveyId: z.string().cuid2().optional(),
|
||||
contactId: z.string().optional(),
|
||||
expand: ZExpandParam,
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.startDate && data.endDate && data.startDate > data.endDate) {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const resetPasswordAction = actionClient.schema(ZResetPasswordAction).act
|
||||
ctx.auditLoggingCtx.oldObject = oldObject;
|
||||
ctx.auditLoggingCtx.newObject = updatedUser;
|
||||
|
||||
await sendPasswordResetNotifyEmail({ email: updatedUser.email, locale: updatedUser.locale });
|
||||
await sendPasswordResetNotifyEmail(updatedUser);
|
||||
return { success: true };
|
||||
}
|
||||
)
|
||||
|
||||
@@ -69,7 +69,6 @@ describe("invite", () => {
|
||||
creator: {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
locale: "en-US",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -90,7 +89,6 @@ describe("invite", () => {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -46,7 +46,6 @@ export const getInvite = reactCache(async (inviteId: string): Promise<InviteWith
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -102,12 +102,7 @@ export const InvitePage = async (props: InvitePageProps) => {
|
||||
);
|
||||
}
|
||||
await deleteInvite(inviteId);
|
||||
await sendInviteAcceptedEmail(
|
||||
invite.creator.name ?? "",
|
||||
user?.name ?? "",
|
||||
invite.creator.email,
|
||||
invite.creator.locale
|
||||
);
|
||||
await sendInviteAcceptedEmail(invite.creator.name ?? "", user?.name ?? "", invite.creator.email);
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings: {
|
||||
...user.notificationSettings,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -127,12 +127,7 @@ async function handleInviteAcceptance(
|
||||
},
|
||||
});
|
||||
|
||||
await sendInviteAcceptedEmail(
|
||||
invite.creator.name ?? "",
|
||||
user.name,
|
||||
invite.creator.email,
|
||||
invite.creator.locale
|
||||
);
|
||||
await sendInviteAcceptedEmail(invite.creator.name ?? "", user.name, invite.creator.email);
|
||||
await deleteInvite(invite.id);
|
||||
}
|
||||
|
||||
@@ -173,7 +168,7 @@ async function handlePostUserCreation(
|
||||
}
|
||||
|
||||
if (!emailVerificationDisabled) {
|
||||
await sendVerificationEmail({ id: user.id, email: user.email, locale: user.locale });
|
||||
await sendVerificationEmail(user);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ describe("resendVerificationEmailAction", () => {
|
||||
const mockUser = {
|
||||
id: "user123",
|
||||
email: "test@example.com",
|
||||
locale: "en-US",
|
||||
emailVerified: null, // Not verified
|
||||
name: "Test User",
|
||||
};
|
||||
|
||||
const mockVerifiedUser = {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const resendVerificationEmailAction = actionClient.schema(ZResendVerifica
|
||||
};
|
||||
}
|
||||
ctx.auditLoggingCtx.userId = user.id;
|
||||
await sendVerificationEmail({ id: user.id, email: user.email, locale: user.locale });
|
||||
await sendVerificationEmail(user);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
|
||||
@@ -121,7 +121,6 @@ export const sendTestEmailAction = authenticatedActionClient
|
||||
await sendEmailCustomizationPreviewEmail(
|
||||
ctx.user.email,
|
||||
ctx.user.name,
|
||||
ctx.user.locale,
|
||||
organization?.whitelabel?.logoUrl || ""
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
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"
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${getLocalizedValue(choice.label, defaultLanguageCode)}`}
|
||||
key={choice.id}>
|
||||
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
||||
|
||||
@@ -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,13 +97,9 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
|
||||
}
|
||||
};
|
||||
|
||||
export const sendVerificationNewEmail = async (
|
||||
id: string,
|
||||
email: string,
|
||||
locale: TUserLocale
|
||||
): Promise<boolean> => {
|
||||
export const sendVerificationNewEmail = async (id: string, email: string): Promise<boolean> => {
|
||||
try {
|
||||
const t = await getTranslate(locale);
|
||||
const t = await getTranslate();
|
||||
const token = createEmailChangeToken(id, email);
|
||||
const verifyLink = `${WEBAPP_URL}/verify-email-change?token=${encodeURIComponent(token)}`;
|
||||
|
||||
@@ -123,14 +119,12 @@ export const sendVerificationNewEmail = async (
|
||||
export const sendVerificationEmail = async ({
|
||||
id,
|
||||
email,
|
||||
locale,
|
||||
}: {
|
||||
id: string;
|
||||
email: TUserEmail;
|
||||
locale: TUserLocale;
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
const t = await getTranslate(locale);
|
||||
const t = await getTranslate();
|
||||
const token = createToken(id, {
|
||||
expiresIn: "1d",
|
||||
});
|
||||
@@ -160,7 +154,7 @@ export const sendForgotPasswordEmail = async (user: {
|
||||
email: TUserEmail;
|
||||
locale: TUserLocale;
|
||||
}): Promise<boolean> => {
|
||||
const t = await getTranslate(user.locale);
|
||||
const t = await getTranslate();
|
||||
const token = createToken(user.id, {
|
||||
expiresIn: "1d",
|
||||
});
|
||||
@@ -173,11 +167,8 @@ export const sendForgotPasswordEmail = async (user: {
|
||||
});
|
||||
};
|
||||
|
||||
export const sendPasswordResetNotifyEmail = async (user: {
|
||||
email: string;
|
||||
locale: TUserLocale;
|
||||
}): Promise<boolean> => {
|
||||
const t = await getTranslate(user.locale);
|
||||
export const sendPasswordResetNotifyEmail = async (user: { email: string }): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const html = await renderPasswordResetNotifyEmail({ t, ...legalProps });
|
||||
return await sendEmail({
|
||||
to: user.email,
|
||||
@@ -210,10 +201,9 @@ export const sendInviteMemberEmail = async (
|
||||
export const sendInviteAcceptedEmail = async (
|
||||
inviterName: string,
|
||||
inviteeName: string,
|
||||
email: string,
|
||||
inviterLocale?: TUserLocale
|
||||
email: string
|
||||
): Promise<void> => {
|
||||
const t = await getTranslate(inviterLocale);
|
||||
const t = await getTranslate();
|
||||
const html = await renderInviteAcceptedEmail({ inviteeName, inviterName, t, ...legalProps });
|
||||
await sendEmail({
|
||||
to: email,
|
||||
@@ -224,13 +214,12 @@ 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(locale);
|
||||
const t = await getTranslate();
|
||||
const personEmail = response.contactAttributes?.email;
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
@@ -257,12 +246,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,
|
||||
});
|
||||
@@ -272,10 +261,9 @@ export const sendEmbedSurveyPreviewEmail = async (
|
||||
to: string,
|
||||
innerHtml: string,
|
||||
environmentId: string,
|
||||
locale: TUserLocale,
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate(locale);
|
||||
const t = await getTranslate();
|
||||
const html = await renderEmbedSurveyPreviewEmail({
|
||||
html: innerHtml,
|
||||
environmentId,
|
||||
@@ -293,10 +281,9 @@ export const sendEmbedSurveyPreviewEmail = async (
|
||||
export const sendEmailCustomizationPreviewEmail = async (
|
||||
to: string,
|
||||
userName: string,
|
||||
locale: TUserLocale,
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate(locale);
|
||||
const t = await getTranslate();
|
||||
const emailHtmlBody = await renderEmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl,
|
||||
@@ -318,7 +305,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
|
||||
const singleUseId = data.suId;
|
||||
const logoUrl = data.logoUrl || "";
|
||||
const token = createTokenForLinkSurvey(surveyId, email);
|
||||
const t = await getTranslate(data.locale);
|
||||
const t = await getTranslate();
|
||||
const getSurveyLink = (): string => {
|
||||
if (singleUseId) {
|
||||
return `${getPublicDomain()}/s/${surveyId}?verify=${encodeURIComponent(token)}&suId=${singleUseId}`;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowLeft, MailIcon } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { Toaster, toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -10,13 +10,11 @@ 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";
|
||||
@@ -28,7 +26,7 @@ interface VerifyEmailProps {
|
||||
singleUseId?: string;
|
||||
languageCode: string;
|
||||
styling: TProjectStyling;
|
||||
locale: TUserLocale;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const ZVerifyEmailInput = z.object({
|
||||
@@ -44,18 +42,7 @@ export const VerifyEmail = ({
|
||||
styling,
|
||||
locale,
|
||||
}: VerifyEmailProps) => {
|
||||
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 { t } = useTranslation();
|
||||
const form = useForm<TVerifyEmailInput>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
@@ -188,7 +175,7 @@ export const VerifyEmail = ({
|
||||
{!emailSent && showPreviewQuestions && (
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{t("s.question_preview")}</p>
|
||||
<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">
|
||||
<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">
|
||||
{questions.map((question, index) => (
|
||||
<p
|
||||
key={index}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,54 +1,2 @@
|
||||
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";
|
||||
};
|
||||
// Prefilling logic has been moved to @/modules/survey/link/lib/prefill
|
||||
// This file is kept for any future utility functions
|
||||
|
||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"lucide-react": "0.507.0",
|
||||
"markdown-it": "14.1.0",
|
||||
"mime-types": "3.0.1",
|
||||
"next": "16.1.3",
|
||||
"next": "16.1.1",
|
||||
"next-auth": "4.24.12",
|
||||
"next-safe-action": "7.10.8",
|
||||
"node-fetch": "3.3.2",
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "map-pin"
|
||||
src="https://app.formbricks.com/s/m8w91e8wi52pdao8un1f4twu"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "check"
|
||||
src="https://app.formbricks.com/s/orxp15pca6x2nfr3v8pttpwm"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "address-book"
|
||||
src="https://app.formbricks.com/s/z2zjoonfeythx5n6z5qijbsg"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "calendar"
|
||||
src="https://app.formbricks.com/s/rk844spc8ffls25vzkxzzhse"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -15,7 +15,7 @@ icon: "upload"
|
||||
src="https://app.formbricks.com/s/oo4e6vva48w0trn01ht8krwo"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -10,7 +10,7 @@ icon: "presentation-screen"
|
||||
src="https://app.formbricks.com/s/vqmpasmnt5qcpsa4enheips0"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "ranking-star"
|
||||
src="https://app.formbricks.com/s/z6s84x9wbyk0yqqtfaz238px"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
@@ -38,35 +38,8 @@ 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, 6, 7 or 10. The default is 5.
|
||||
Select the range of the rating scale. the options include: 3, 4, 5, 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>
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "calendar-check"
|
||||
src="https://app.formbricks.com/s/hx08x27c2aghywh57rroe6fi"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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: "600px",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -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 Survey Cooldown passed)?
|
||||
- Did the user see any survey recently (meaning, has Global Waiting Time passed)?
|
||||
|
||||
- Did the user see this specific survey already?
|
||||
|
||||
@@ -50,13 +50,13 @@ Available Recontact Options include:
|
||||
|
||||

|
||||
|
||||
## Project-wide Survey Cooldown
|
||||
## Project-wide Global Waiting 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.
|
||||
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.
|
||||
|
||||
<Note>The default Survey Cooldown is set to 7 days.</Note>
|
||||
<Note>The default Global Waiting Time is set to 7 days.</Note>
|
||||
|
||||
To adjust the Survey Cooldown:
|
||||
To adjust the Global Waiting Time:
|
||||
|
||||
1. Visit Formbricks Settings
|
||||
|
||||
@@ -68,9 +68,9 @@ To adjust the Survey Cooldown:
|
||||
|
||||

|
||||
|
||||
## Overriding Survey Cooldown for a Specific Survey
|
||||
## Overriding Global Waiting Time for a Specific Survey
|
||||
|
||||
For specific surveys, you may need to override the default cooldown. Below is how you can do that:
|
||||
For specific surveys, you may need to override the default waiting time. 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 cooldown. Below is ho
|
||||
|
||||
4. Set a custom recontact period:
|
||||
|
||||
- **Always Show Survey**: Displays the survey whenever triggered, ignoring the cooldown.
|
||||
- **Always Show Survey**: Displays the survey whenever triggered, ignoring the waiting time.
|
||||
|
||||
- **Wait `X` days before showing this survey again**: Sets a specific interval before the survey can be shown again.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"dependencies": {
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"next": "16.1.3"
|
||||
"next": "16.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@azure/identity": "4.13.0",
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Membership_userId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Project_organizationId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Response_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Segment_environmentId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyAttributeFilter_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyLanguage_languageId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyQuota_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyTrigger_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Tag_environmentId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."TagsOnResponses_responseId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."User_email_idx";
|
||||
@@ -173,6 +173,7 @@ model Response {
|
||||
@@index([createdAt])
|
||||
@@index([surveyId, createdAt]) // to determine monthly response count
|
||||
@@index([contactId, createdAt]) // to determine monthly identified users (persons)
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
/// Represents a label that can be applied to survey responses.
|
||||
@@ -192,6 +193,7 @@ model Tag {
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([environmentId, name])
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
/// Junction table linking tags to responses.
|
||||
@@ -206,6 +208,7 @@ model TagsOnResponses {
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([responseId, tagId])
|
||||
@@index([responseId])
|
||||
}
|
||||
|
||||
enum SurveyStatus {
|
||||
@@ -256,6 +259,7 @@ model SurveyTrigger {
|
||||
actionClassId String
|
||||
|
||||
@@unique([surveyId, actionClassId])
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
enum SurveyAttributeFilterCondition {
|
||||
@@ -293,6 +297,7 @@ model SurveyAttributeFilter {
|
||||
value String
|
||||
|
||||
@@unique([surveyId, attributeKeyId])
|
||||
@@index([surveyId])
|
||||
@@index([attributeKeyId])
|
||||
}
|
||||
|
||||
@@ -427,6 +432,7 @@ model SurveyQuota {
|
||||
countPartialSubmissions Boolean @default(false)
|
||||
|
||||
@@unique([surveyId, name])
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
/// Junction table linking responses to quotas.
|
||||
@@ -629,6 +635,7 @@ model Project {
|
||||
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
|
||||
|
||||
@@unique([organizationId, name])
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
/// Represents the top-level organizational hierarchy in Formbricks.
|
||||
@@ -683,6 +690,7 @@ model Membership {
|
||||
role OrganizationRole @default(member)
|
||||
|
||||
@@id([userId, organizationId])
|
||||
@@index([userId])
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
@@ -850,6 +858,8 @@ model User {
|
||||
teamUsers TeamUser[]
|
||||
lastLoginAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
/// Defines a segment of contacts based on attributes.
|
||||
@@ -874,6 +884,7 @@ model Segment {
|
||||
surveys Survey[]
|
||||
|
||||
@@unique([environmentId, title])
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
/// Represents a supported language in the system.
|
||||
@@ -913,6 +924,7 @@ model SurveyLanguage {
|
||||
|
||||
@@id([languageId, surveyId])
|
||||
@@index([surveyId])
|
||||
@@index([languageId])
|
||||
}
|
||||
|
||||
/// Represents a team within an organization.
|
||||
|
||||
@@ -188,6 +188,7 @@ 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,
|
||||
|
||||
@@ -9,6 +9,7 @@ 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}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"i18next": "25.5.2",
|
||||
"i18next-icu": "2.4.0",
|
||||
"isomorphic-dompurify": "2.33.0",
|
||||
"preact": "10.28.2",
|
||||
"preact": "10.26.10",
|
||||
"react-i18next": "15.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -16,41 +16,6 @@ import { ScrollableContainer } from "@/components/wrappers/scrollable-container"
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Safely calls requestSubmit on a form element with fallback for browsers
|
||||
* that don't support it (e.g., Mobile Safari 15.5)
|
||||
*/
|
||||
const safeRequestSubmit = (form: HTMLFormElement | null | undefined): void => {
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if requestSubmit is available
|
||||
if (typeof form.requestSubmit === "function") {
|
||||
try {
|
||||
form.requestSubmit();
|
||||
} catch (error) {
|
||||
// Fallback if requestSubmit throws an error
|
||||
console.warn("[Formbricks] form.requestSubmit() failed, using fallback:", error);
|
||||
dispatchSubmitEvent(form);
|
||||
}
|
||||
} else {
|
||||
// Fallback for browsers that don't support requestSubmit
|
||||
dispatchSubmitEvent(form);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fallback method to trigger form validation by dispatching a submit event
|
||||
*/
|
||||
const dispatchSubmitEvent = (form: HTMLFormElement): void => {
|
||||
const submitEvent = new Event("submit", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
form.dispatchEvent(submitEvent);
|
||||
};
|
||||
|
||||
interface BlockConditionalProps {
|
||||
block: TSurveyBlock;
|
||||
value: TResponseData;
|
||||
@@ -176,7 +141,7 @@ export function BlockConditional({
|
||||
response.length < rankingElement.choices.length);
|
||||
|
||||
if (hasIncompleteRanking) {
|
||||
safeRequestSubmit(form);
|
||||
form.requestSubmit();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -209,7 +174,7 @@ export function BlockConditional({
|
||||
element.type === TSurveyElementTypeEnum.ContactInfo
|
||||
) {
|
||||
if (!form.checkValidity()) {
|
||||
safeRequestSubmit(form);
|
||||
form.requestSubmit();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -226,14 +191,14 @@ export function BlockConditional({
|
||||
response &&
|
||||
hasUnansweredRows(response, element)
|
||||
) {
|
||||
safeRequestSubmit(form);
|
||||
form.requestSubmit();
|
||||
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)) {
|
||||
safeRequestSubmit(form);
|
||||
form.requestSubmit();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -264,7 +229,9 @@ export function BlockConditional({
|
||||
// Call each form's submit method to trigger TTC calculation
|
||||
block.elements.forEach((element) => {
|
||||
const form = elementFormRefs.current.get(element.id);
|
||||
safeRequestSubmit(form);
|
||||
if (form) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
// Collect TTC from the ref (populated synchronously by form submissions)
|
||||
|
||||
@@ -96,9 +96,7 @@ 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}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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: ZUserLocale,
|
||||
locale: z.string(),
|
||||
logoUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
141
pnpm-lock.yaml
generated
141
pnpm-lock.yaml
generated
@@ -21,8 +21,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
next:
|
||||
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)
|
||||
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)
|
||||
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.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))
|
||||
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))
|
||||
'@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.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)
|
||||
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)
|
||||
next-auth:
|
||||
specifier: 4.24.12
|
||||
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)
|
||||
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)
|
||||
next-safe-action:
|
||||
specifier: 7.10.8
|
||||
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)
|
||||
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)
|
||||
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.28.2
|
||||
version: 10.28.2
|
||||
specifier: 10.26.10
|
||||
version: 10.26.10
|
||||
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.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))
|
||||
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))
|
||||
'@tailwindcss/postcss':
|
||||
specifier: 4.1.17
|
||||
version: 4.1.17
|
||||
'@testing-library/preact':
|
||||
specifier: 3.2.4
|
||||
version: 3.2.4(preact@10.28.2)
|
||||
version: 3.2.4(preact@10.26.10)
|
||||
'@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.3':
|
||||
resolution: {integrity: sha512-BLP14oBOvZWXgfdJf9ao+VD8O30uE+x7PaV++QtACLX329WcRSJRO5YJ+Bcvu0Q+c/lei41TjSiFf6pXqnpbQA==}
|
||||
'@next/env@16.1.1':
|
||||
resolution: {integrity: sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==}
|
||||
|
||||
'@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.3':
|
||||
resolution: {integrity: sha512-CpOD3lmig6VflihVoGxiR/l5Jkjfi4uLaOR4ziriMv0YMDoF6cclI+p5t2nstM8TmaFiY6PCTBgRWB57/+LiBA==}
|
||||
'@next/swc-darwin-arm64@16.1.1':
|
||||
resolution: {integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
@@ -2813,8 +2813,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.1.3':
|
||||
resolution: {integrity: sha512-aF4us2JXh0zn3hNxvL1Bx3BOuh8Lcw3p3Xnurlvca/iptrDH1BrpObwkw9WZra7L7/0qB9kjlREq3hN/4x4x+Q==}
|
||||
'@next/swc-darwin-x64@16.1.1':
|
||||
resolution: {integrity: sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
@@ -2825,8 +2825,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.1.3':
|
||||
resolution: {integrity: sha512-8VRkcpcfBtYvhGgXAF7U3MBx6+G1lACM1XCo1JyaUr4KmAkTNP8Dv2wdMq7BI+jqRBw3zQE7c57+lmp7jCFfKA==}
|
||||
'@next/swc-linux-arm64-gnu@16.1.1':
|
||||
resolution: {integrity: sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@@ -2837,8 +2837,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.3':
|
||||
resolution: {integrity: sha512-UbFx69E2UP7MhzogJRMFvV9KdEn4sLGPicClwgqnLht2TEi204B71HuVfps3ymGAh0c44QRAF+ZmvZZhLLmhNg==}
|
||||
'@next/swc-linux-arm64-musl@16.1.1':
|
||||
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@@ -2849,8 +2849,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.3':
|
||||
resolution: {integrity: sha512-SzGTfTjR5e9T+sZh5zXqG/oeRQufExxBF6MssXS7HPeZFE98JDhCRZXpSyCfWrWrYrzmnw/RVhlP2AxQm+wkRQ==}
|
||||
'@next/swc-linux-x64-gnu@16.1.1':
|
||||
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@@ -2861,8 +2861,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.3':
|
||||
resolution: {integrity: sha512-HlrDpj0v+JBIvQex1mXHq93Mht5qQmfyci+ZNwGClnAQldSfxI6h0Vupte1dSR4ueNv4q7qp5kTnmLOBIQnGow==}
|
||||
'@next/swc-linux-x64-musl@16.1.1':
|
||||
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@@ -2873,8 +2873,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.3':
|
||||
resolution: {integrity: sha512-3gFCp83/LSduZMSIa+lBREP7+5e7FxpdBoc9QrCdmp+dapmTK9I+SLpY60Z39GDmTXSZA4huGg9WwmYbr6+WRw==}
|
||||
'@next/swc-win32-arm64-msvc@16.1.1':
|
||||
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
@@ -2885,8 +2885,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.1.3':
|
||||
resolution: {integrity: sha512-1SZVfFT8zmMB+Oblrh5OKDvUo5mYQOkX2We6VGzpg7JUVZlqe4DYOFGKYZKTweSx1gbMixyO1jnFT4thU+nNHQ==}
|
||||
'@next/swc-win32-x64-msvc@16.1.1':
|
||||
resolution: {integrity: sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -8703,8 +8703,8 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
next@16.1.3:
|
||||
resolution: {integrity: sha512-gthG3TRD+E3/mA0uDQb9lqBmx1zVosq5kIwxNN6+MRNd085GzD+9VXMPUs+GGZCbZ+GDZdODUq4Pm7CTXK6ipw==}
|
||||
next@16.1.1:
|
||||
resolution: {integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==}
|
||||
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'}
|
||||
@@ -10249,7 +10249,6 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
|
||||
|
||||
tarn@3.0.2:
|
||||
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
|
||||
@@ -13792,7 +13791,7 @@ snapshots:
|
||||
|
||||
'@next/env@16.0.9': {}
|
||||
|
||||
'@next/env@16.1.3': {}
|
||||
'@next/env@16.1.1': {}
|
||||
|
||||
'@next/eslint-plugin-next@15.3.2':
|
||||
dependencies:
|
||||
@@ -13801,49 +13800,49 @@ snapshots:
|
||||
'@next/swc-darwin-arm64@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-arm64@16.1.3':
|
||||
'@next/swc-darwin-arm64@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.1.3':
|
||||
'@next/swc-darwin-x64@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.1.3':
|
||||
'@next/swc-linux-arm64-gnu@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.3':
|
||||
'@next/swc-linux-arm64-musl@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.3':
|
||||
'@next/swc-linux-x64-gnu@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.3':
|
||||
'@next/swc-linux-x64-musl@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.3':
|
||||
'@next/swc-win32-arm64-msvc@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.0.9':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.1.3':
|
||||
'@next/swc-win32-x64-msvc@16.1.1':
|
||||
optional: true
|
||||
|
||||
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
|
||||
@@ -14313,12 +14312,12 @@ snapshots:
|
||||
dependencies:
|
||||
playwright: 1.56.1
|
||||
|
||||
'@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))':
|
||||
'@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))':
|
||||
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.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))
|
||||
'@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))
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5)
|
||||
debug: 4.4.3
|
||||
@@ -14333,20 +14332,20 @@ snapshots:
|
||||
|
||||
'@prefresh/babel-plugin@0.5.2': {}
|
||||
|
||||
'@prefresh/core@1.5.9(preact@10.28.2)':
|
||||
'@prefresh/core@1.5.9(preact@10.26.10)':
|
||||
dependencies:
|
||||
preact: 10.28.2
|
||||
preact: 10.26.10
|
||||
|
||||
'@prefresh/utils@1.2.1': {}
|
||||
|
||||
'@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))':
|
||||
'@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))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@prefresh/babel-plugin': 0.5.2
|
||||
'@prefresh/core': 1.5.9(preact@10.28.2)
|
||||
'@prefresh/core': 1.5.9(preact@10.26.10)
|
||||
'@prefresh/utils': 1.2.1
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
preact: 10.28.2
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -15701,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.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))':
|
||||
'@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))':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.38.0
|
||||
@@ -15714,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.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: 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)
|
||||
resolve: 1.22.8
|
||||
rollup: 4.54.0
|
||||
stacktrace-parser: 0.1.11
|
||||
@@ -16562,10 +16561,10 @@ snapshots:
|
||||
lodash: 4.17.21
|
||||
redent: 3.0.0
|
||||
|
||||
'@testing-library/preact@3.2.4(preact@10.28.2)':
|
||||
'@testing-library/preact@3.2.4(preact@10.26.10)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 8.20.1
|
||||
preact: 10.28.2
|
||||
preact: 10.26.10
|
||||
|
||||
'@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:
|
||||
@@ -20621,13 +20620,13 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
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):
|
||||
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):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@panva/hkdf': 1.2.1
|
||||
cookie: 0.7.2
|
||||
jose: 4.15.9
|
||||
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)
|
||||
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)
|
||||
oauth: 0.9.15
|
||||
openid-client: 5.7.1
|
||||
preact: 10.26.6
|
||||
@@ -20638,9 +20637,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
nodemailer: 7.0.11
|
||||
|
||||
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):
|
||||
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):
|
||||
dependencies:
|
||||
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)
|
||||
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
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
@@ -20671,9 +20670,9 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
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):
|
||||
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):
|
||||
dependencies:
|
||||
'@next/env': 16.1.3
|
||||
'@next/env': 16.1.1
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.9.11
|
||||
caniuse-lite: 1.0.30001762
|
||||
@@ -20682,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.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
|
||||
'@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
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@playwright/test': 1.56.1
|
||||
sharp: 0.34.5
|
||||
@@ -21198,9 +21197,9 @@ snapshots:
|
||||
preact: 10.26.6
|
||||
pretty-format: 3.8.0
|
||||
|
||||
preact@10.26.6: {}
|
||||
preact@10.26.10: {}
|
||||
|
||||
preact@10.28.2: {}
|
||||
preact@10.26.6: {}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user