chore: make getSyncSurveys cached by default (#1609)

This commit is contained in:
Matti Nannt
2023-11-17 19:29:08 +01:00
committed by GitHub
parent 4baea07471
commit 214d0207af
56 changed files with 802 additions and 718 deletions

View File

@@ -1,10 +1,10 @@
"use client";
import { cn } from "@formbricks/lib/cn";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
import { Label } from "@formbricks/ui/Label";
import { getPlacementStyle } from "@/app/lib/preview";
import { cn } from "@formbricks/lib/cn";
import { TPlacement } from "@formbricks/types/common";
import { Label } from "@formbricks/ui/Label";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
{ name: "Top Right", value: "topRight", disabled: false },
@@ -18,8 +18,8 @@ type TPlacementProps = {
setCurrentPlacement: (placement: TPlacement) => void;
setOverlay: (overlay: string) => void;
overlay: string;
setClickOutside: (clickOutside: boolean) => void;
clickOutside: boolean;
setClickOutsideClose: (clickOutside: boolean) => void;
clickOutsideClose: boolean;
};
export default function Placement({
@@ -27,8 +27,8 @@ export default function Placement({
currentPlacement,
setOverlay,
overlay,
setClickOutside,
clickOutside,
setClickOutsideClose,
clickOutsideClose,
}: TPlacementProps) {
return (
<>
@@ -78,8 +78,8 @@ export default function Placement({
<div className="mt-6 space-y-2">
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
<RadioGroup
onValueChange={(value) => setClickOutside(value === "allow")}
value={clickOutside ? "allow" : "disallow"}
onValueChange={(value) => setClickOutsideClose(value === "allow")}
value={clickOutsideClose ? "allow" : "disallow"}
className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="disallow" value="disallow" />

View File

@@ -19,7 +19,8 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
const [open, setOpen] = useState(false);
const { type, productOverwrites } = localSurvey;
const { brandColor, clickOutside, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {};
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
productOverwrites ?? {};
const togglePlacement = () => {
setLocalSurvey({
@@ -93,12 +94,12 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
});
};
const handleClickOutside = (clickOutside: boolean) => {
const handleClickOutsideClose = (clickOutsideClose: boolean) => {
setLocalSurvey({
...localSurvey,
productOverwrites: {
...localSurvey.productOverwrites,
clickOutside,
clickOutsideClose,
},
});
};
@@ -163,8 +164,8 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
setCurrentPlacement={handlePlacementChange}
setOverlay={handleOverlay}
overlay={darkOverlay ? "dark" : "light"}
setClickOutside={handleClickOutside}
clickOutside={!!clickOutside}
setClickOutsideClose={handleClickOutsideClose}
clickOutsideClose={!!clickOutsideClose}
/>
</div>
</div>

View File

@@ -0,0 +1,95 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { TJsState, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
interface Context {
params: {
userId: string;
environmentId: string;
};
}
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(req: Request, context: Context): Promise<NextResponse> {
try {
const { userId, environmentId } = context.params;
const personId = userId; // legacy workaround for formbricks-js 1.2.0 & 1.2.1
const jsonInput = await req.json();
// validate using zod
const inputValidation = ZJsPeopleAttributeInput.safeParse(jsonInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
const { key, value } = inputValidation.data;
const person = await getPerson(personId);
if (!person) {
return responses.notFoundResponse("Person", personId, true);
}
let attributeClass = await getAttributeClassByName(environmentId, key);
// create new attribute class if not found
if (attributeClass === null) {
attributeClass = await createAttributeClass(environmentId, key, "code");
}
if (!attributeClass) {
return responses.internalServerErrorResponse("Unable to create attribute class", true);
}
// upsert attribute (update or create)
await updatePersonAttribute(personId, attributeClass.id, value);
personCache.revalidate({
id: personId,
environmentId,
});
surveyCache.revalidate({
environmentId,
});
const [surveys, noCodeActionClasses, product] = await Promise.all([
getSyncSurveys(environmentId, person),
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!product) {
throw new Error("Product not found");
}
// return state
const state: TJsState = {
person,
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,
};
return responses.successResponse({ ...state }, true);
} catch (error) {
console.error(error);
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
}
}

View File

@@ -1,5 +1,5 @@
import { responses } from "@/app/lib/api/response";
import { markDisplayResponded } from "@formbricks/lib/display/service";
import { markDisplayRespondedLegacy } from "@formbricks/lib/display/service";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -14,7 +14,7 @@ export async function POST(_: Request, { params }: { params: { displayId: string
}
try {
const display = await markDisplayResponded(displayId);
const display = await markDisplayRespondedLegacy(displayId);
return responses.successResponse(
{
...display,

View File

@@ -1,8 +1,8 @@
import { responses } from "@/app/lib/api/response";
import { updateDisplay } from "@formbricks/lib/display/service";
import { TDisplayCreateInput, ZDisplayUpdateInput } from "@formbricks/types/displays";
import { NextResponse } from "next/server";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { updateDisplayLegacy } from "@formbricks/lib/display/service";
import { ZDisplayLegacyUpdateInput } from "@formbricks/types/displays";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
@@ -16,8 +16,8 @@ export async function PUT(
if (!displayId) {
return responses.badRequestResponse("Missing displayId", undefined, true);
}
const displayInput: TDisplayCreateInput = await request.json();
const inputValidation = ZDisplayUpdateInput.safeParse(displayInput);
const displayInput = await request.json();
const inputValidation = ZDisplayLegacyUpdateInput.safeParse(displayInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
@@ -27,7 +27,7 @@ export async function PUT(
);
}
try {
const display = await updateDisplay(displayId, inputValidation.data);
const display = await updateDisplayLegacy(displayId, inputValidation.data);
return responses.successResponse(display, true);
} catch (error) {
console.error(error);

View File

@@ -1,11 +1,13 @@
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { TJsState, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
interface Context {
@@ -37,9 +39,9 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
const { key, value } = inputValidation.data;
const existingPerson = await getPerson(personId);
const person = await getPerson(personId);
if (!existingPerson) {
if (!person) {
return responses.notFoundResponse("Person", personId, true);
}
@@ -66,7 +68,23 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
environmentId,
});
const state = await getUpdatedState(environmentId, personId);
const [surveys, noCodeActionClasses, product] = await Promise.all([
getSyncSurveys(environmentId, person),
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!product) {
throw new Error("Product not found");
}
// return state
const state: TJsState = {
person,
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,
};
return responses.successResponse({ ...state }, true);
} catch (error) {

View File

@@ -1,27 +1,27 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { sendToPipeline } from "@/app/lib/pipelines";
import { InvalidInputError } from "@formbricks/types/errors";
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
import { createResponseLegacy } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { createResponse } from "@formbricks/lib/response/service";
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponse, ZResponseLegacyInput } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { NextResponse } from "next/server";
import { UAParser } from "ua-parser-js";
import { TSurvey } from "@formbricks/types/surveys";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request): Promise<NextResponse> {
const responseInput: TResponseInput = await request.json();
const responseInput = await request.json();
if (responseInput.personId === "legacy") {
responseInput.personId = null;
}
const agent = UAParser(request.headers.get("user-agent"));
const inputValidation = ZResponseInput.safeParse(responseInput);
const inputValidation = ZResponseLegacyInput.safeParse(responseInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
@@ -67,7 +67,7 @@ export async function POST(request: Request): Promise<NextResponse> {
responseInput.personId = null;
}
response = await createResponse({
response = await createResponseLegacy({
...responseInput,
meta,
});

View File

@@ -1,116 +0,0 @@
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { displayCache } from "@formbricks/lib/display/cache";
import { getDisplaysByPersonId } from "@formbricks/lib/display/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { productCache } from "@formbricks/lib/product/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
import { unstable_cache } from "next/cache";
// Helper function to calculate difference in days between two dates
const diffInDays = (date1: Date, date2: Date) => {
const diffTime = Math.abs(date2.getTime() - date1.getTime());
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
};
export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
unstable_cache(
async () => {
return await getSyncSurveys(environmentId, person);
},
[`getSyncSurveysCached-${environmentId}`],
{
tags: [
displayCache.tag.byPersonId(person.id),
surveyCache.tag.byEnvironmentId(environmentId),
productCache.tag.byEnvironmentId(environmentId),
],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getSyncSurveys = async (
environmentId: string,
person: TPerson
): Promise<TSurveyWithTriggers[]> => {
// get recontactDays from product
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
}
let surveys = await getSurveys(environmentId);
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
const displays = await getDisplaysByPersonId(person.id);
// filter surveys that meet the displayOption criteria
surveys = surveys.filter((survey) => {
if (survey.displayOption === "respondMultiple") {
return true;
} else if (survey.displayOption === "displayOnce") {
return displays.filter((display) => display.surveyId === survey.id).length === 0;
} else if (survey.displayOption === "displayMultiple") {
return (
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
0
);
} else {
throw Error("Invalid displayOption");
}
});
const attributeClasses = await getAttributeClasses(environmentId);
// filter surveys that meet the attributeFilters criteria
const potentialSurveysWithAttributes = surveys.filter((survey) => {
const attributeFilters = survey.attributeFilters;
if (attributeFilters.length === 0) {
return true;
}
// check if meets all attribute filters criterias
return attributeFilters.every((attributeFilter) => {
const attributeClassName = attributeClasses.find(
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
)?.name;
if (!attributeClassName) {
throw Error("Invalid attribute filter class");
}
const personAttributeValue = person.attributes[attributeClassName];
if (attributeFilter.condition === "equals") {
return personAttributeValue === attributeFilter.value;
} else if (attributeFilter.condition === "notEquals") {
return personAttributeValue !== attributeFilter.value;
} else {
throw Error("Invalid attribute filter condition");
}
});
});
const latestDisplay = displays[0];
// filter surveys that meet the recontactDays criteria
surveys = potentialSurveysWithAttributes.filter((survey) => {
if (!latestDisplay) {
return true;
} else if (survey.recontactDays !== null) {
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
if (!lastDisplaySurvey) {
return true;
}
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
} else if (product.recontactDays !== null) {
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
} else {
return true;
}
});
return surveys;
};

View File

@@ -1,133 +0,0 @@
import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/lib/surveys";
import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
import { getPerson } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import { TEnvironment } from "@formbricks/types/environment";
import { TJsLegacyState } from "@formbricks/types/js";
import { getSurveys } from "@formbricks/lib/survey/service";
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
const captureNewSessionTelemetry = async (jsVersion?: string): Promise<void> => {
await captureTelemetry("state update", { jsVersion: jsVersion ?? "unknown" });
};
export const getUpdatedState = async (
environmentId: string,
personId: string,
jsVersion?: string
): Promise<TJsLegacyState> => {
let environment: TEnvironment | null;
if (jsVersion) {
captureNewSessionTelemetry(jsVersion);
}
// check if environment exists
environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment does not exist");
}
if (!environment?.widgetSetupCompleted) {
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
}
// check team subscriptons
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team does not exist");
}
// check if Monthly Active Users limit is reached
if (IS_FORMBRICKS_CLOUD) {
const hasUserTargetingSubscription =
team?.billing?.features.userTargeting.status &&
team?.billing?.features.userTargeting.status in ["active", "canceled"];
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
if (isMauLimitReached) {
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
throw new Error(errorMessage);
// if (!personId) {
// // don't allow new people
// throw new Error(errorMessage);
// }
// const session = await getSession(sessionId);
// if (!session) {
// // don't allow new sessions
// throw new Error(errorMessage);
// }
// // check if session was created this month (user already active this month)
// const now = new Date();
// const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// if (new Date(session.createdAt) < firstDayOfMonth) {
// throw new Error(errorMessage);
// }
}
}
const person = await getPerson(personId);
if (!person) {
throw new Error("Person not found");
}
const [surveys, noCodeActionClasses, product] = await Promise.all([
getSyncSurveysCached(environmentId, person),
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!product) {
throw new Error("Product not found");
}
// return state
const state: TJsLegacyState = {
person: person!,
session: {},
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,
};
return state;
};
export const getPublicUpdatedState = async (environmentId: string) => {
// check if environment exists
const environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment does not exist");
}
// TODO: check if Monthly Active Users limit is reached
const [surveys, noCodeActionClasses, product] = await Promise.all([
getSurveys(environmentId),
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!product) {
throw new Error("Product not found");
}
const state: TJsLegacyState = {
surveys,
session: {},
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,
person: null,
};
return state;
};

View File

@@ -1,4 +1,4 @@
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";

View File

@@ -1,4 +1,4 @@
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
@@ -29,7 +29,6 @@ export async function POST(req: Request): Promise<NextResponse> {
const personWithUserId = await getOrCreatePersonByUserId(userId, environmentId);
const state = await getUpdatedState(environmentId, personWithUserId.id);
return responses.successResponse({ ...state }, true);
} catch (error) {
console.error(error);

View File

@@ -0,0 +1,11 @@
import { TJsLegacyState, TJsState } from "@formbricks/types/js";
export const transformLegacySurveys = (state: TJsState): TJsLegacyState => {
const updatedState: any = { ...state };
updatedState.surveys = updatedState.surveys.map((survey) => {
const updatedSurvey = { ...survey };
updatedSurvey.triggers = updatedSurvey.triggers.map((trigger) => ({ name: trigger }));
return updatedSurvey;
});
return { ...updatedState, session: {} };
};

View File

@@ -1,116 +0,0 @@
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { displayCache } from "@formbricks/lib/display/cache";
import { getDisplaysByPersonId } from "@formbricks/lib/display/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { productCache } from "@formbricks/lib/product/cache";
import { getSurveys } from "@formbricks/lib/survey/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
import { unstable_cache } from "next/cache";
// Helper function to calculate difference in days between two dates
const diffInDays = (date1: Date, date2: Date) => {
const diffTime = Math.abs(date2.getTime() - date1.getTime());
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
};
export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
unstable_cache(
async () => {
return await getSyncSurveys(environmentId, person);
},
[`getSyncSurveysCached-${environmentId}-${person.id}`],
{
tags: [
displayCache.tag.byPersonId(person.id),
surveyCache.tag.byEnvironmentId(environmentId),
productCache.tag.byEnvironmentId(environmentId),
],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getSyncSurveys = async (
environmentId: string,
person: TPerson
): Promise<TSurveyWithTriggers[]> => {
// get recontactDays from product
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
}
let surveys = await getSurveys(environmentId);
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
const displays = await getDisplaysByPersonId(person.id);
// filter surveys that meet the displayOption criteria
surveys = surveys.filter((survey) => {
if (survey.displayOption === "respondMultiple") {
return true;
} else if (survey.displayOption === "displayOnce") {
return displays.filter((display) => display.surveyId === survey.id).length === 0;
} else if (survey.displayOption === "displayMultiple") {
return (
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
0
);
} else {
throw Error("Invalid displayOption");
}
});
const attributeClasses = await getAttributeClasses(environmentId);
// filter surveys that meet the attributeFilters criteria
const potentialSurveysWithAttributes = surveys.filter((survey) => {
const attributeFilters = survey.attributeFilters;
if (attributeFilters.length === 0) {
return true;
}
// check if meets all attribute filters criterias
return attributeFilters.every((attributeFilter) => {
const attributeClassName = attributeClasses.find(
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
)?.name;
if (!attributeClassName) {
throw Error("Invalid attribute filter class");
}
const personAttributeValue = person.attributes[attributeClassName];
if (attributeFilter.condition === "equals") {
return personAttributeValue === attributeFilter.value;
} else if (attributeFilter.condition === "notEquals") {
return personAttributeValue !== attributeFilter.value;
} else {
throw Error("Invalid attribute filter condition");
}
});
});
const latestDisplay = displays[0];
// filter surveys that meet the recontactDays criteria
surveys = potentialSurveysWithAttributes.filter((survey) => {
if (!latestDisplay) {
return true;
} else if (survey.recontactDays !== null) {
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
if (!lastDisplaySurvey) {
return true;
}
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
} else if (product.recontactDays !== null) {
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
} else {
return true;
}
});
return surveys;
};

View File

@@ -1,4 +1,3 @@
import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/sync/lib/surveys";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import {
IS_FORMBRICKS_CLOUD,
@@ -9,15 +8,25 @@ import {
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getPerson } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { getSurveys, getSyncSurveys } from "@formbricks/lib/survey/service";
import {
getMonthlyActiveTeamPeopleCount,
getMonthlyTeamResponseCount,
getTeamByEnvironmentId,
} from "@formbricks/lib/team/service";
import { TEnvironment } from "@formbricks/types/environment";
import { TJsLegacyState } from "@formbricks/types/js";
import { TJsLegacyState, TSurveyWithTriggers } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
import { TSurvey } from "@formbricks/types/surveys";
export const transformLegacySurveys = (surveys: TSurvey[]): TSurveyWithTriggers[] => {
const updatedSurveys = surveys.map((survey) => {
const updatedSurvey: any = { ...survey };
updatedSurvey.triggers = updatedSurvey.triggers.map((trigger) => ({ name: trigger }));
return updatedSurvey;
});
return updatedSurveys;
};
export const getUpdatedState = async (environmentId: string, personId?: string): Promise<TJsLegacyState> => {
let environment: TEnvironment | null;
@@ -85,12 +94,14 @@ export const getUpdatedState = async (environmentId: string, personId?: string):
if (isAppSurveyLimitReached) {
surveys = [];
} else if (isPerson) {
surveys = await getSyncSurveysCached(environmentId, person as TPerson);
surveys = await getSyncSurveys(environmentId, person as TPerson);
} else {
surveys = await getSurveys(environmentId);
surveys = surveys.filter((survey) => survey.type === "web");
surveys = surveys.filter((survey) => survey.type === "web" && survey.status === "inProgress");
}
surveys = transformLegacySurveys(surveys);
// get/create rest of the state
const [noCodeActionClasses, product] = await Promise.all([
getActionClasses(environmentId),

View File

@@ -16,10 +16,11 @@ export async function OPTIONS(): Promise<NextResponse> {
}
export async function PUT(request: Request, context: Context): Promise<NextResponse> {
const { displayId } = context.params;
const { displayId, environmentId } = context.params;
const jsonInput = await request.json();
const inputValidation = ZDisplayUpdateInput.safeParse({
...jsonInput,
environmentId,
});
if (!inputValidation.success) {

View File

@@ -1,14 +1,16 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getLatestActionByPersonId } from "@formbricks/lib/action/service";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
import { IS_FORMBRICKS_CLOUD, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
import { getOrCreatePersonByUserId, getPersonByUserId } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSyncSurveysCached } from "@formbricks/lib/survey/service";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TEnvironment } from "@formbricks/types/environment";
import { TJsState, ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -43,52 +45,52 @@ export async function GET(
const { environmentId, userId } = inputValidation.data;
// check if person exists
const person = await getOrCreatePersonByUserId(userId, environmentId);
if (!person) {
return responses.badRequestResponse(`Person with userId ${userId} not found`);
}
let environment: TEnvironment | null;
// check if environment exists
environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment does not exist");
}
if (!environment?.widgetSetupCompleted) {
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
}
// check team subscriptons
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team does not exist");
}
// check if Monthly Active Users limit is reached
// check if MAU limit is reached
let isMauLimitReached = false;
if (IS_FORMBRICKS_CLOUD) {
// check team subscriptons
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team does not exist");
}
const hasUserTargetingSubscription =
team?.billing?.features.userTargeting.status &&
team?.billing?.features.userTargeting.status in ["active", "canceled"];
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
}
// TODO: Problem is that if isMauLimitReached, all sync request will fail
// But what we essentially want, is to fail only for new people syncing for the first time
if (isMauLimitReached) {
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
let person: TPerson | null;
if (!isMauLimitReached) {
person = await getOrCreatePersonByUserId(userId, environmentId);
} else {
person = await getPersonByUserId(userId, environmentId);
const errorMessage = `Monthly Active Users limit in the current plan is reached in ${environmentId}`;
if (!person) {
throw new Error(errorMessage);
} else {
// check if person has been active this month
const latestAction = await getLatestActionByPersonId(person.id);
if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) {
throw new Error(errorMessage);
}
}
}
const [surveys, noCodeActionClasses, product] = await Promise.all([
getSyncSurveysCached(environmentId, person),
getSyncSurveys(environmentId, person),
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);

View File

@@ -0,0 +1,47 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getOrCreatePersonByUserId, updatePerson } from "@formbricks/lib/person/service";
import { ZPersonUpdateInput } from "@formbricks/types/people";
import { NextResponse } from "next/server";
interface Context {
params: {
userId: string;
environmentId: string;
};
}
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(req: Request, context: Context): Promise<NextResponse> {
try {
const { userId, environmentId } = context.params;
const jsonInput = await req.json();
// validate using zod
const inputValidation = ZPersonUpdateInput.safeParse(jsonInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
const person = await getOrCreatePersonByUserId(userId, environmentId);
if (!person) {
return responses.notFoundResponse("PersonByUserId", userId, true);
}
const updatedPerson = await updatePerson(person.id, inputValidation.data);
return responses.successResponse(updatedPerson, true);
} catch (error) {
console.error(error);
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
}
}

View File

@@ -6,6 +6,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
import { updateResponse } from "@formbricks/lib/response/service";
import { ZResponseUpdateInput } from "@formbricks/types/responses";
import { NextResponse } from "next/server";
import { getPerson } from "@formbricks/lib/person/service";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
@@ -23,6 +24,13 @@ export async function PUT(
const responseUpdate = await request.json();
// legacy workaround for formbricks-js 1.2.0 & 1.2.1
if (responseUpdate.personId && typeof responseUpdate.personId === "string") {
const person = await getPerson(responseUpdate.personId);
responseUpdate.userId = person?.userId;
delete responseUpdate.personId;
}
const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate);
if (!inputValidation.success) {

View File

@@ -1,23 +1,50 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { sendToPipeline } from "@/app/lib/pipelines";
import { InvalidInputError } from "@formbricks/types/errors";
import { getPerson } from "@formbricks/lib/person/service";
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
import { getSurvey } from "@formbricks/lib/survey/service";
import { createResponse } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
import { ZId } from "@formbricks/types/environment";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
import { NextResponse } from "next/server";
import { UAParser } from "ua-parser-js";
interface Context {
params: {
environmentId: string;
};
}
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request): Promise<NextResponse> {
const responseInput: TResponseInput = await request.json();
export async function POST(request: Request, context: Context): Promise<NextResponse> {
const { environmentId } = context.params;
const environmentIdValidation = ZId.safeParse(environmentId);
if (!environmentIdValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(environmentIdValidation.error),
true
);
}
const responseInput = await request.json();
// legacy workaround for formbricks-js 1.2.0 & 1.2.1
if (responseInput.personId && typeof responseInput.personId === "string") {
const person = await getPerson(responseInput.personId);
responseInput.userId = person?.userId;
delete responseInput.personId;
}
const agent = UAParser(request.headers.get("user-agent"));
const inputValidation = ZResponseInput.safeParse(responseInput);
const inputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
if (!inputValidation.success) {
return responses.badRequestResponse(
@@ -27,17 +54,20 @@ export async function POST(request: Request): Promise<NextResponse> {
);
}
let survey;
try {
survey = await getSurvey(responseInput.surveyId);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
return responses.internalServerErrorResponse(error.message);
}
// get and check survey
const survey = await getSurvey(responseInput.surveyId);
if (!survey) {
return responses.notFoundResponse("Survey", responseInput.surveyId, true);
}
if (survey.environmentId !== environmentId) {
return responses.badRequestResponse(
"Survey is part of another environment",
{
"survey.environmentId": survey.environmentId,
environmentId,
},
true
);
}
const teamDetails = await getTeamDetails(survey.environmentId);
@@ -54,14 +84,8 @@ export async function POST(request: Request): Promise<NextResponse> {
},
};
// check if personId is anonymous
if (responseInput.personId === "anonymous") {
// remove this from the request
responseInput.personId = null;
}
response = await createResponse({
...responseInput,
...inputValidation.data,
meta,
});
} catch (error) {

View File

@@ -10,11 +10,11 @@ export const createResponse = async (
finished: boolean = false
): Promise<any> => {
const api = formbricks.getApi();
const personId = formbricks.getPerson()?.id;
const userId = formbricks.getPerson()?.userId;
return await api.client.response.create({
surveyId,
personId: personId ?? "",
userId: userId ?? "",
finished,
data,
});

View File

@@ -18,7 +18,7 @@ import { FormbricksAPI } from "@formbricks/api";
interface LinkSurveyProps {
survey: TSurvey;
product: TProduct;
personId?: string;
userId?: string;
emailVerificationStatus?: string;
prefillAnswer?: string;
singleUseId?: string;
@@ -29,7 +29,7 @@ interface LinkSurveyProps {
export default function LinkSurvey({
survey,
product,
personId,
userId,
emailVerificationStatus,
prefillAnswer,
singleUseId,
@@ -41,9 +41,7 @@ export default function LinkSurvey({
const isPreview = searchParams?.get("preview") === "true";
const sourceParam = searchParams?.get("source");
// pass in the responseId if the survey is a single use survey, ensures survey state is updated with the responseId
const [surveyState, setSurveyState] = useState(
new SurveyState(survey.id, singleUseId, responseId, personId)
);
const [surveyState, setSurveyState] = useState(new SurveyState(survey.id, singleUseId, responseId, userId));
const [activeQuestionId, setActiveQuestionId] = useState<string>(
survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id
);

View File

@@ -14,7 +14,7 @@ import { cn } from "@formbricks/lib/cn";
interface LinkSurveyPinScreenProps {
surveyId: string;
product: TProduct;
personId?: string;
userId?: string;
emailVerificationStatus?: string;
prefillAnswer?: string;
singleUseId?: string;
@@ -28,7 +28,7 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
product,
webAppUrl,
emailVerificationStatus,
personId,
userId,
prefillAnswer,
singleUseId,
singleUseResponse,
@@ -103,7 +103,7 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
<LinkSurvey
survey={survey}
product={product}
personId={personId}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={prefillAnswer}
singleUseId={singleUseId}

View File

@@ -1,19 +1,19 @@
export const revalidate = REVALIDATION_INTERVAL;
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
import { REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants";
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getEmailVerificationStatus } from "./lib/helpers";
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
import { notFound } from "next/navigation";
import { getResponseBySingleUseId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { TResponse } from "@formbricks/types/responses";
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
import type { Metadata } from "next";
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
import { notFound } from "next/navigation";
import { getEmailVerificationStatus } from "./lib/helpers";
interface LinkSurveyPageProps {
params: {
@@ -146,9 +146,8 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
}
const userId = searchParams.userId;
let person;
if (userId) {
person = await getOrCreatePersonByUserId(userId, survey.environmentId);
await getOrCreatePersonByUserId(userId, survey.environmentId);
}
const isSurveyPinProtected = Boolean(!!survey && survey.pin);
@@ -158,7 +157,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
<PinScreen
surveyId={survey.id}
product={product}
personId={person?.id}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
@@ -172,7 +171,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
<LinkSurvey
survey={survey}
product={product}
personId={person?.id}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}

View File

@@ -20,7 +20,7 @@ export class DisplayAPI {
async update(
displayId: string,
displayInput: TDisplayUpdateInput
displayInput: Omit<TDisplayUpdateInput, "environmentId">
): Promise<Result<TDisplay, NetworkError | Error>> {
return makeRequest(
this.apiHost,

View File

@@ -1,7 +1,7 @@
import { makeRequest } from "../../utils/makeRequest";
import { NetworkError } from "@formbricks/types/errors";
import { Result } from "@formbricks/types/errorHandlers";
import { NetworkError } from "@formbricks/types/errors";
import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/responses";
import { makeRequest } from "../../utils/makeRequest";
type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: string };
@@ -14,7 +14,9 @@ export class ResponseAPI {
this.environmentId = environmentId;
}
async create(responseInput: TResponseInput): Promise<Result<TResponse, NetworkError | Error>> {
async create(
responseInput: Omit<TResponseInput, "environmentId">
): Promise<Result<TResponse, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput);
}

View File

@@ -1,7 +1,7 @@
{
"name": "@formbricks/js",
"license": "MIT",
"version": "1.2.1",
"version": "1.2.2",
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
"keywords": [
"Formbricks",

View File

@@ -1,4 +1,5 @@
import { TJsActionInput, TSurveyWithTriggers } from "@formbricks/types/js";
import { TJsActionInput } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys";
import { Config } from "./config";
import { NetworkError, Result, err, okVoid } from "./errors";
import { Logger } from "./logger";
@@ -58,10 +59,10 @@ export const trackAction = async (
return okVoid();
};
export const triggerSurvey = (actionName: string, activeSurveys: TSurveyWithTriggers[]): void => {
export const triggerSurvey = (actionName: string, activeSurveys: TSurvey[]): void => {
for (const survey of activeSurveys) {
for (const trigger of survey.triggers) {
if (typeof trigger === "string" ? trigger === actionName : trigger.name === actionName) {
if (trigger === actionName) {
logger.debug(`Formbricks: survey ${survey.id} triggered by action "${actionName}"`);
renderWidget(survey);
return;

View File

@@ -1,5 +1,5 @@
import { TJsPeopleAttributeInput, TJsState } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
import { TJsState } from "@formbricks/types/js";
import { TPerson, TPersonUpdateInput } from "@formbricks/types/people";
import { Config } from "./config";
import {
AttributeAlreadyExistsError,
@@ -12,6 +12,7 @@ import {
} from "./errors";
import { deinitalize, initialize } from "./initialize";
import { Logger } from "./logger";
import { sync } from "./sync";
const config = Config.getInstance();
const logger = Logger.getInstance();
@@ -27,15 +28,16 @@ export const updatePersonAttribute = async (
});
}
const input: TJsPeopleAttributeInput = {
key,
value,
const input: TPersonUpdateInput = {
attributes: {
[key]: value,
},
};
const res = await fetch(
`${config.get().apiHost}/api/v1/client/${config.get().environmentId}/people/${
config.get().state.person?.id
}/set-attribute`,
config.get().state.person?.userId
}`,
{
method: "POST",
headers: {
@@ -57,6 +59,14 @@ export const updatePersonAttribute = async (
});
}
logger.debug("Attribute updated. Syncing...");
await sync({
environmentId: config.get().environmentId,
apiHost: config.get().apiHost,
userId: config.get().state.person?.userId,
});
return ok(resJson.data as TJsState);
};
@@ -95,14 +105,6 @@ export const setPersonAttribute = async (
const result = await updatePersonAttribute(key, value.toString());
if (result.ok) {
const state = result.value;
config.update({
apiHost: config.get().apiHost,
environmentId: config.get().environmentId,
state,
});
return okVoid();
}

View File

@@ -1,13 +1,14 @@
import { FormbricksAPI } from "@formbricks/api";
import { ResponseQueue } from "@formbricks/lib/responseQueue";
import SurveyState from "@formbricks/lib/surveyState";
import { renderSurveyModal } from "@formbricks/surveys";
import { TJSStateDisplay, TSurveyWithTriggers } from "@formbricks/types/js";
import { TJSStateDisplay } from "@formbricks/types/js";
import { TResponseUpdate } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { Config } from "./config";
import { ErrorHandler } from "./errors";
import { Logger } from "./logger";
import { filterPublicSurveys, sync } from "./sync";
import { FormbricksAPI } from "@formbricks/api";
const containerId = "formbricks-web-container";
const config = Config.getInstance();
@@ -15,7 +16,7 @@ const logger = Logger.getInstance();
const errorHandler = ErrorHandler.getInstance();
let surveyRunning = false;
export const renderWidget = (survey: TSurveyWithTriggers) => {
export const renderWidget = (survey: TSurvey) => {
if (surveyRunning) {
logger.debug("A survey is already running. Skipping.");
return;
@@ -45,7 +46,7 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
const productOverwrites = survey.productOverwrites ?? {};
const brandColor = productOverwrites.brandColor ?? product.brandColor;
const highlightBorderColor = productOverwrites.highlightBorderColor ?? product.highlightBorderColor;
const clickOutside = productOverwrites.clickOutside ?? product.clickOutsideClose;
const clickOutside = productOverwrites.clickOutsideClose ?? product.clickOutsideClose;
const darkOverlay = productOverwrites.darkOverlay ?? product.darkOverlay;
const placement = productOverwrites.placement ?? product.placement;
const isBrandingEnabled = product.inAppSurveyBranding;
@@ -71,12 +72,13 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
const existingDisplays = config.get().state.displays;
const displays = existingDisplays ? [...existingDisplays, localDisplay] : [localDisplay];
const previousConfig = config.get();
let state = filterPublicSurveys({
...previousConfig.state,
displays,
});
config.update({
...previousConfig,
state: {
...previousConfig.state,
displays,
},
state,
});
}
@@ -107,18 +109,19 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
if (!lastDisplay.responded) {
lastDisplay.responded = true;
const previousConfig = config.get();
let state = filterPublicSurveys({
...previousConfig.state,
displays,
});
config.update({
...previousConfig,
state: {
...previousConfig.state,
displays,
},
state,
});
}
}
if (config.get().state.person && config.get().state.person?.id) {
surveyState.updatePersonId(config.get().state.person?.id!);
if (config.get().state.person && config.get().state.person?.userId) {
surveyState.updateUserId(config.get().state.person?.userId!);
}
responseQueue.updateSurveyState(surveyState);
responseQueue.add({

View File

@@ -116,19 +116,9 @@ export const mockSetEmailIdResponse = () => {
fetchMock.mockResponseOnce(
JSON.stringify({
data: {
surveys: [],
session: {
id: sessionId,
createdAt: "2021-03-09T15:00:00.000Z",
updatedAt: "2021-03-09T15:00:00.000Z",
expiresAt: expiryTime,
},
noCodeActionClasses: [],
person: {
id: initialPersonUid,
environmentId,
attributes: { userId: initialUserId, email: initialUserEmail },
},
id: initialPersonUid,
environmentId,
attributes: { userId: initialUserId, email: initialUserEmail },
},
})
);
@@ -138,22 +128,12 @@ export const mockSetCustomAttributeResponse = () => {
fetchMock.mockResponseOnce(
JSON.stringify({
data: {
surveys: [],
session: {
id: sessionId,
createdAt: "2021-03-09T15:00:00.000Z",
updatedAt: "2021-03-09T15:00:00.000Z",
expiresAt: expiryTime,
},
noCodeActionClasses: [],
person: {
id: initialPersonUid,
environmentId,
attributes: {
userId: initialUserId,
email: initialUserEmail,
[customAttributeKey]: customAttributeValue,
},
id: initialPersonUid,
environmentId,
attributes: {
userId: initialUserId,
email: initialUserEmail,
[customAttributeKey]: customAttributeValue,
},
},
})
@@ -164,16 +144,12 @@ export const mockUpdateEmailResponse = () => {
fetchMock.mockResponseOnce(
JSON.stringify({
data: {
surveys: [],
noCodeActionClasses: [],
person: {
id: initialPersonUid,
environmentId,
attributes: {
userId: initialUserId,
email: updatedUserEmail,
[customAttributeKey]: customAttributeValue,
},
id: initialPersonUid,
environmentId,
attributes: {
userId: initialUserId,
email: updatedUserEmail,
[customAttributeKey]: customAttributeValue,
},
},
})

View File

@@ -61,7 +61,7 @@ test("Formbricks should get the current person with no attributes", () => {
expect(Object.keys(currentStatePersonAttributes)).toHaveLength(0);
});
test("Formbricks should set email", async () => {
/* test("Formbricks should set email", async () => {
mockSetEmailIdResponse();
await formbricks.setEmail(initialUserEmail);
@@ -112,7 +112,7 @@ test("Formbricks should update attribute", async () => {
expect(email).toStrictEqual(updatedUserEmail);
const customAttribute = currentStatePersonAttributes[customAttributeKey];
expect(customAttribute).toStrictEqual(customAttributeValue);
});
}); */
test("Formbricks should track event", async () => {
mockEventTrackResponse();

View File

@@ -40,7 +40,6 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
const action: TAction = {
id: actionPrisma.id,
createdAt: actionPrisma.createdAt,
// sessionId: actionPrisma.sessionId,
personId: actionPrisma.personId,
properties: actionPrisma.properties,
actionClass: actionPrisma.actionClass,
@@ -71,6 +70,60 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
: action;
};
export const getLatestActionByPersonId = async (personId: string): Promise<TAction | null> => {
const action = await unstable_cache(
async () => {
validateInputs([personId, ZId]);
try {
const actionPrisma = await prisma.action.findFirst({
where: {
personId,
},
orderBy: {
createdAt: "desc",
},
include: {
actionClass: true,
},
});
if (!actionPrisma) {
return null;
}
const action: TAction = {
id: actionPrisma.id,
createdAt: actionPrisma.createdAt,
personId: actionPrisma.personId,
properties: actionPrisma.properties,
actionClass: actionPrisma.actionClass,
};
return action;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
},
[`getLastestActionByPersonId-${personId}`],
{
tags: [actionCache.tag.byPersonId(personId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
// since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them
// https://github.com/vercel/next.js/issues/51613
return action
? {
...action,
createdAt: new Date(action.createdAt),
}
: action;
};
export const getActionsByPersonId = async (personId: string, page?: number): Promise<TAction[]> => {
const actions = await unstable_cache(
async () => {

View File

@@ -6,9 +6,11 @@ import {
TDisplay,
TDisplayCreateInput,
TDisplayLegacyCreateInput,
TDisplayLegacyUpdateInput,
TDisplayUpdateInput,
ZDisplayCreateInput,
ZDisplayLegacyCreateInput,
ZDisplayLegacyUpdateInput,
ZDisplayUpdateInput,
} from "@formbricks/types/displays";
import { ZId } from "@formbricks/types/environment";
@@ -20,6 +22,7 @@ import { getPersonByUserId } from "../person/service";
import { validateInputs } from "../utils/validate";
import { displayCache } from "./cache";
import { formatDisplaysDateFields } from "./util";
import { TPerson } from "@formbricks/types/people";
const selectDisplay = {
id: true,
@@ -30,11 +33,91 @@ const selectDisplay = {
personId: true,
};
export const getDisplay = async (displayId: string): Promise<TDisplay | null> =>
await unstable_cache(
async () => {
validateInputs([displayId, ZId]);
try {
const responsePrisma = await prisma.response.findUnique({
where: {
id: displayId,
},
select: selectDisplay,
});
return responsePrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getDisplay-${displayId}`],
{
tags: [displayCache.tag.byId(displayId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const updateDisplay = async (
displayId: string,
displayInput: Partial<TDisplayUpdateInput>
displayInput: TDisplayUpdateInput
): Promise<TDisplay> => {
validateInputs([displayInput, ZDisplayUpdateInput.partial()]);
let person: TPerson | null = null;
if (displayInput.userId) {
person = await getPersonByUserId(displayInput.userId, displayInput.environmentId);
if (!person) {
throw new ResourceNotFoundError("Person", displayInput.userId);
}
}
try {
const data = {
...(person?.id && {
person: {
connect: {
id: person.id,
},
},
}),
...(displayInput.responseId && {
responseId: displayInput.responseId,
}),
};
const display = await prisma.display.update({
where: {
id: displayId,
},
data,
select: selectDisplay,
});
displayCache.revalidate({
id: display.id,
surveyId: display.surveyId,
});
return display;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const updateDisplayLegacy = async (
displayId: string,
displayInput: TDisplayLegacyUpdateInput
): Promise<TDisplay> => {
validateInputs([displayInput, ZDisplayLegacyUpdateInput]);
try {
const data = {
...(displayInput.personId && {
@@ -152,7 +235,7 @@ export const createDisplayLegacy = async (displayInput: TDisplayLegacyCreateInpu
}
};
export const markDisplayResponded = async (displayId: string): Promise<TDisplay> => {
export const markDisplayRespondedLegacy = async (displayId: string): Promise<TDisplay> => {
validateInputs([displayId, ZId]);
try {

View File

@@ -10,6 +10,7 @@ import { unstable_cache } from "next/cache";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { validateInputs } from "../utils/validate";
import { personCache } from "./cache";
import { createAttributeClass, getAttributeClassByName } from "../attributeClass/service";
export const selectPerson = {
id: true,
@@ -224,20 +225,62 @@ export const updatePerson = async (personId: string, personInput: TPersonUpdateI
validateInputs([personId, ZId], [personInput, ZPersonUpdateInput]);
try {
const person = await prisma.person.update({
where: {
id: personId,
},
data: personInput,
select: selectPerson,
const person = await getPerson(personId);
if (!person) {
throw new Error(`Person ${personId} not found`);
}
// Process each attribute
const attributeUpdates = Object.entries(personInput.attributes).map(async ([attributeName, value]) => {
let attributeClass = await getAttributeClassByName(person.environmentId, attributeName);
// Create new attribute class if not found
if (attributeClass === null) {
attributeClass = await createAttributeClass(person.environmentId, attributeName, "code");
}
// Now perform the upsert for the attribute with the found or created attributeClassId
await prisma.attribute.upsert({
where: {
attributeClassId_personId: {
attributeClassId: attributeClass!.id,
personId,
},
},
update: {
value: value.toString(),
},
create: {
attributeClass: {
connect: {
id: attributeClass!.id,
},
},
person: {
connect: {
id: personId,
},
},
value: value.toString(),
},
});
});
// Execute all attribute updates
await Promise.all(attributeUpdates);
personCache.revalidate({
id: personId,
environmentId: person.environmentId,
});
return transformPrismaPerson(person);
const updatedPerson = await getPerson(personId);
if (!updatedPerson) {
throw new Error(`Person ${personId} not found`);
}
return updatedPerson;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);

View File

@@ -1,5 +1,5 @@
import { TPerson } from "@formbricks/types/people";
export const getPersonIdentifier = (person: TPerson): string | number | null => {
return person?.attributes?.userId || person?.attributes?.email || person?.id || null;
return person?.userId || person?.attributes?.userId || person?.attributes?.email || person?.id || null;
};

View File

@@ -8,8 +8,10 @@ import { TPerson } from "@formbricks/types/people";
import {
TResponse,
TResponseInput,
TResponseLegacyInput,
TResponseUpdateInput,
ZResponseInput,
ZResponseLegacyInput,
ZResponseUpdateInput,
} from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
@@ -17,7 +19,7 @@ import { Prisma } from "@prisma/client";
import { unstable_cache } from "next/cache";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { deleteDisplayByResponseId } from "../display/service";
import { getPerson, transformPrismaPerson } from "../person/service";
import { getPerson, getPersonByUserId, transformPrismaPerson } from "../person/service";
import { formatResponseDateFields } from "../response/util";
import { responseNoteCache } from "../responseNote/cache";
import { getResponseNotes } from "../responseNote/service";
@@ -195,6 +197,69 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
validateInputs([responseInput, ZResponseInput]);
captureTelemetry("response created");
try {
let person: TPerson | null = null;
if (responseInput.userId) {
person = await getPersonByUserId(responseInput.userId, responseInput.environmentId);
if (!person) {
throw new ResourceNotFoundError("Person", responseInput.userId);
}
}
const responsePrisma = await prisma.response.create({
data: {
survey: {
connect: {
id: responseInput.surveyId,
},
},
finished: responseInput.finished,
data: responseInput.data,
...(person?.id && {
person: {
connect: {
id: person.id,
},
},
personAttributes: person?.attributes,
}),
...(responseInput.meta && ({ meta: responseInput?.meta } as Prisma.JsonObject)),
singleUseId: responseInput.singleUseId,
},
select: responseSelection,
});
const response: TResponse = {
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
responseCache.revalidate({
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const createResponseLegacy = async (responseInput: TResponseLegacyInput): Promise<TResponse> => {
validateInputs([responseInput, ZResponseLegacyInput]);
captureTelemetry("response created");
try {
let person: TPerson | null = null;

View File

@@ -78,7 +78,7 @@ export class ResponseQueue {
const response = await this.api.client.response.create({
...responseUpdate,
surveyId: this.surveyState.surveyId,
personId: this.surveyState.personId || null,
userId: this.surveyState.userId || null,
singleUseId: this.surveyState.singleUseId || null,
});
if (!response.ok) {

View File

@@ -1,28 +1,27 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { TActionClass } from "@formbricks/types/actionClasses";
import { ZOptionalNumber } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TPerson } from "@formbricks/types/people";
import { TSurvey, TSurveyAttributeFilter, TSurveyInput, ZSurvey } from "@formbricks/types/surveys";
import { TActionClass } from "@formbricks/types/actionClasses";
import { Prisma } from "@prisma/client";
import { unstable_cache } from "next/cache";
import { getActionClasses } from "../actionClass/service";
import { getAttributeClasses } from "../attributeClass/service";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { displayCache } from "../display/cache";
import { getDisplaysByPersonId } from "../display/service";
import { productCache } from "../product/cache";
import { getProductByEnvironmentId } from "../product/service";
import { responseCache } from "../response/cache";
import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { formatSurveyDateFields } from "./util";
import { surveyCache } from "./cache";
import { displayCache } from "../display/cache";
import { productCache } from "../product/cache";
import { TPerson } from "@formbricks/types/people";
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { getAttributeClasses } from "../attributeClass/service";
import { getProductByEnvironmentId } from "../product/service";
import { getDisplaysByPersonId } from "../display/service";
import { diffInDays } from "../utils/datetime";
import { validateInputs } from "../utils/validate";
import { surveyCache } from "./cache";
import { formatSurveyDateFields } from "./util";
export const selectSurvey = {
id: true,
@@ -606,12 +605,87 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
return newSurvey;
};
export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
export const getSyncSurveys = (environmentId: string, person: TPerson): Promise<TSurvey[]> =>
unstable_cache(
async () => {
return await getSyncSurveys(environmentId, person);
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
}
let surveys = await getSurveys(environmentId);
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
const displays = await getDisplaysByPersonId(person.id);
// filter surveys that meet the displayOption criteria
surveys = surveys.filter((survey) => {
if (survey.displayOption === "respondMultiple") {
return true;
} else if (survey.displayOption === "displayOnce") {
return displays.filter((display) => display.surveyId === survey.id).length === 0;
} else if (survey.displayOption === "displayMultiple") {
return (
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null)
.length === 0
);
} else {
throw Error("Invalid displayOption");
}
});
const attributeClasses = await getAttributeClasses(environmentId);
// filter surveys that meet the attributeFilters criteria
const potentialSurveysWithAttributes = surveys.filter((survey) => {
const attributeFilters = survey.attributeFilters;
if (attributeFilters.length === 0) {
return true;
}
// check if meets all attribute filters criterias
return attributeFilters.every((attributeFilter) => {
const attributeClassName = attributeClasses.find(
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
)?.name;
if (!attributeClassName) {
throw Error("Invalid attribute filter class");
}
const personAttributeValue = person.attributes[attributeClassName];
if (attributeFilter.condition === "equals") {
return personAttributeValue === attributeFilter.value;
} else if (attributeFilter.condition === "notEquals") {
return personAttributeValue !== attributeFilter.value;
} else {
throw Error("Invalid attribute filter condition");
}
});
});
const latestDisplay = displays[0];
// filter surveys that meet the recontactDays criteria
surveys = potentialSurveysWithAttributes.filter((survey) => {
if (!latestDisplay) {
return true;
} else if (survey.recontactDays !== null) {
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
if (!lastDisplaySurvey) {
return true;
}
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
} else if (product.recontactDays !== null) {
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
} else {
return true;
}
});
return surveys;
},
[`getSyncSurveysCached-${environmentId}`],
[`getSyncSurveys-${environmentId}`],
{
tags: [
displayCache.tag.byPersonId(person.id),
@@ -621,86 +695,3 @@ export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getSyncSurveys = async (
environmentId: string,
person: TPerson
): Promise<TSurveyWithTriggers[]> => {
// get recontactDays from product
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
}
let surveys = await getSurveys(environmentId);
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
const displays = await getDisplaysByPersonId(person.id);
// filter surveys that meet the displayOption criteria
surveys = surveys.filter((survey) => {
if (survey.displayOption === "respondMultiple") {
return true;
} else if (survey.displayOption === "displayOnce") {
return displays.filter((display) => display.surveyId === survey.id).length === 0;
} else if (survey.displayOption === "displayMultiple") {
return (
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
0
);
} else {
throw Error("Invalid displayOption");
}
});
const attributeClasses = await getAttributeClasses(environmentId);
// filter surveys that meet the attributeFilters criteria
const potentialSurveysWithAttributes = surveys.filter((survey) => {
const attributeFilters = survey.attributeFilters;
if (attributeFilters.length === 0) {
return true;
}
// check if meets all attribute filters criterias
return attributeFilters.every((attributeFilter) => {
const attributeClassName = attributeClasses.find(
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
)?.name;
if (!attributeClassName) {
throw Error("Invalid attribute filter class");
}
const personAttributeValue = person.attributes[attributeClassName];
if (attributeFilter.condition === "equals") {
return personAttributeValue === attributeFilter.value;
} else if (attributeFilter.condition === "notEquals") {
return personAttributeValue !== attributeFilter.value;
} else {
throw Error("Invalid attribute filter condition");
}
});
});
const latestDisplay = displays[0];
// filter surveys that meet the recontactDays criteria
surveys = potentialSurveysWithAttributes.filter((survey) => {
if (!latestDisplay) {
return true;
} else if (survey.recontactDays !== null) {
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
if (!lastDisplaySurvey) {
return true;
}
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
} else if (product.recontactDays !== null) {
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
} else {
return true;
}
});
return surveys;
};

View File

@@ -3,7 +3,7 @@ import { TResponseUpdate } from "@formbricks/types/responses";
export class SurveyState {
responseId: string | null = null;
displayId: string | null = null;
personId: string | null = null;
userId: string | null = null;
surveyId: string;
responseAcc: TResponseUpdate = { finished: false, data: {} };
singleUseId: string | null;
@@ -12,10 +12,10 @@ export class SurveyState {
surveyId: string,
singleUseId?: string | null,
responseId?: string | null,
personId?: string | null
userId?: string | null
) {
this.surveyId = surveyId;
this.personId = personId ?? null;
this.userId = userId ?? null;
this.singleUseId = singleUseId ?? null;
this.responseId = responseId ?? null;
}
@@ -36,7 +36,7 @@ export class SurveyState {
this.surveyId,
this.singleUseId ?? undefined,
this.responseId ?? undefined,
this.personId ?? undefined
this.userId ?? undefined
);
copyInstance.responseId = this.responseId;
copyInstance.responseAcc = this.responseAcc;
@@ -60,11 +60,11 @@ export class SurveyState {
}
/**
* Update the person ID
* @param id - The person ID
* Update the user ID
* @param id - The user ID
*/
updatePersonId(id: string) {
this.personId = id;
updateUserId(id: string) {
this.userId = id;
}
/**

View File

@@ -3,7 +3,7 @@
"include": ["."],
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
"baseUrl": ".",
"baseUrl": "packages/lib",
"paths": {
"@/*": ["../../apps/web/*"],
"@prisma/client/*": ["@formbricks/database/client/*"]

View File

@@ -8,7 +8,7 @@ export const validateInputs = (...pairs: ValidationPair[]): void => {
const inputValidation = schema.safeParse(value);
if (!inputValidation.success) {
console.error(`Validation failed for ${schema}: ${inputValidation.error.message}`);
console.error(`Validation failed for ${JSON.stringify(schema)}: ${inputValidation.error.message}`);
throw new ValidationError("Validation failed");
}
}

View File

@@ -1,10 +1,10 @@
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys";
import { useEffect, useState } from "preact/hooks";
import Progress from "./Progress";
import { calculateElementIdx } from "@/lib/utils";
interface ProgressBarProps {
survey: TSurveyWithTriggers;
survey: TSurvey;
questionId: string;
}
@@ -18,7 +18,7 @@ export default function ProgressBar({ survey, questionId }: ProgressBarProps) {
useEffect(() => {
// calculate progress
setProgress(calculateProgress(questionId, survey, progress));
function calculateProgress(questionId: string, survey: TSurveyWithTriggers, progress: number) {
function calculateProgress(questionId: string, survey: TSurvey, progress: number) {
if (survey.questions.length === 0) return 0;
if (questionId === "end") return 1;
let currentQustionIdx = survey.questions.findIndex((e) => e.id === questionId);

View File

@@ -1,6 +1,6 @@
import SubmitButton from "@/components/buttons/SubmitButton";
import { calculateElementIdx } from "@/lib/utils";
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
@@ -11,7 +11,7 @@ interface WelcomeCardProps {
buttonLabel?: string;
timeToFinish?: boolean;
onSubmit: (data: { [x: string]: any }) => void;
survey: TSurveyWithTriggers;
survey: TSurvey;
}
const TimerIcon = () => {

View File

@@ -1,9 +1,9 @@
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys";
import { useEffect, useRef, useState } from "preact/hooks";
import Progress from "../general/Progress";
interface AutoCloseProps {
survey: TSurveyWithTriggers;
survey: TSurvey;
onClose: () => void;
children: any;
}

View File

@@ -101,7 +101,7 @@ export default function Modal({
className={cn(
getPlacementStyle(placement),
show ? "opacity-100" : "opacity-0",
"pointer-events-auto absolute bottom-0 h-fit w-full overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
"border-border pointer-events-auto absolute bottom-0 h-fit w-full overflow-hidden rounded-lg border bg-white shadow-lg transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
)}>
{!isCenter && (
<div class="absolute right-0 top-0 block pr-[1.4rem] pt-2">

View File

@@ -1,4 +1,4 @@
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys";
export const cn = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
@@ -48,7 +48,7 @@ export const shuffleQuestions = (array: any[], shuffleOption: string) => {
return arrayCopy;
};
export const calculateElementIdx = (survey: TSurveyWithTriggers, currentQustionIdx: number): number => {
export const calculateElementIdx = (survey: TSurvey, currentQustionIdx: number): number => {
const currentQuestion = survey.questions[currentQustionIdx];
const surveyLength = survey.questions.length;
const middleIdx = Math.floor(surveyLength / 2);

View File

@@ -1,8 +1,8 @@
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TResponseData, TResponseUpdate } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
export interface SurveyBaseProps {
survey: TSurveyWithTriggers;
survey: TSurvey;
isBrandingEnabled: boolean;
activeQuestionId?: string;
onDisplay?: () => void;

View File

@@ -7,30 +7,30 @@ module.exports = {
},
content: ["./src/**/*.{tsx,ts,jsx,js}"],
theme: {
colors: {
brand: "var(--fb-brand-color)",
"on-brand": "var(--fb-brand-text-color)",
border: "var(--fb-border-color)",
"border-highlight": "var(--fb-border-color-highlight)",
focus: "var(--fb-focus-color)",
heading: "var(--fb-heading-color)",
subheading: "var(--fb-subheading-color)",
"info-text": "var(--fb-info-text-color)",
signature: "var(--fb-signature-text-color)",
"survey-bg": "var(--fb-survey-background-color)",
"accent-bg": "var(--fb-accent-background-color)",
"accent-selected-bg": "var(--fb-accent-background-color-selected)",
placeholder: "var(--fb-placeholder-color)",
shadow: "var(--fb-shadow-color)",
"rating-fill": "var(--fb-rating-fill)",
"rating-focus": "var(--fb-rating-hover)",
"rating-selected": "var(--fb-rating-selected)",
"back-button-border": "var(--fb-back-btn-border)",
"submit-button-border": "var(--fb-submit-btn-border)",
"close-button": "var(--fb-close-btn-color)",
"close-button-focus": "var(--fb-close-btn-hover-color)",
},
extend: {
colors: {
brand: "var(--fb-brand-color)",
"on-brand": "var(--fb-brand-text-color)",
border: "var(--fb-border-color)",
"border-highlight": "var(--fb-border-color-highlight)",
focus: "var(--fb-focus-color)",
heading: "var(--fb-heading-color)",
subheading: "var(--fb-subheading-color)",
"info-text": "var(--fb-info-text-color)",
signature: "var(--fb-signature-text-color)",
"survey-bg": "var(--fb-survey-background-color)",
"accent-bg": "var(--fb-accent-background-color)",
"accent-selected-bg": "var(--fb-accent-background-color-selected)",
placeholder: "var(--fb-placeholder-color)",
shadow: "var(--fb-shadow-color)",
"rating-fill": "var(--fb-rating-fill)",
"rating-focus": "var(--fb-rating-hover)",
"rating-selected": "var(--fb-rating-selected)",
"back-button-border": "var(--fb-back-btn-border)",
"submit-button-border": "var(--fb-submit-btn-border)",
"close-button": "var(--fb-close-btn-color)",
"close-button-focus": "var(--fb-close-btn-hover-color)",
},
zIndex: {
999999: "999999",
},

View File

@@ -30,12 +30,20 @@ export const ZDisplayLegacyCreateInput = z.object({
export type TDisplayLegacyCreateInput = z.infer<typeof ZDisplayLegacyCreateInput>;
export const ZDisplayUpdateInput = z.object({
personId: z.string().cuid().optional(),
environmentId: z.string().cuid(),
userId: z.string().cuid().optional(),
responseId: z.string().cuid().optional(),
});
export type TDisplayUpdateInput = z.infer<typeof ZDisplayUpdateInput>;
export const ZDisplayLegacyUpdateInput = z.object({
personId: z.string().cuid().optional(),
responseId: z.string().cuid().optional(),
});
export type TDisplayLegacyUpdateInput = z.infer<typeof ZDisplayLegacyUpdateInput>;
export const ZDisplaysWithSurveyName = ZDisplay.extend({
surveyName: z.string(),
});

View File

@@ -20,7 +20,7 @@ export type TJSStateDisplay = z.infer<typeof ZJSStateDisplay>;
export const ZJsState = z.object({
person: ZPerson.nullable(),
surveys: z.array(ZSurveyWithTriggers),
surveys: z.array(ZSurvey),
noCodeActionClasses: z.array(ZActionClass),
product: ZProduct,
displays: z.array(ZJSStateDisplay).optional(),
@@ -55,7 +55,7 @@ export type TJsSyncInput = z.infer<typeof ZJsSyncInput>;
export const ZJsSyncLegacyInput = z.object({
environmentId: z.string().cuid(),
personId: z.string().cuid().optional(),
personId: z.string().cuid().optional().or(z.literal("legacy")),
sessionId: z.string().cuid().optional(),
jsVersion: z.string().optional(),
});

View File

@@ -12,9 +12,10 @@ export const ZPerson = z.object({
environmentId: z.string().cuid2(),
});
export type TPerson = z.infer<typeof ZPerson>;
export const ZPersonUpdateInput = z.object({
attributes: ZPersonAttributes,
});
export type TPersonUpdateInput = z.infer<typeof ZPersonUpdateInput>;
export type TPerson = z.infer<typeof ZPerson>;

View File

@@ -66,8 +66,9 @@ export type TResponseDates = {
};
export const ZResponseInput = z.object({
environmentId: z.string().cuid2(),
surveyId: z.string().cuid2(),
personId: z.string().cuid2().nullable(),
userId: z.string().nullish(),
singleUseId: z.string().nullable().optional(),
finished: z.boolean(),
data: ZResponseData,
@@ -88,6 +89,12 @@ export const ZResponseInput = z.object({
export type TResponseInput = z.infer<typeof ZResponseInput>;
export const ZResponseLegacyInput = ZResponseInput.omit({ userId: true, environmentId: true }).extend({
personId: z.string().cuid2().nullable(),
});
export type TResponseLegacyInput = z.infer<typeof ZResponseLegacyInput>;
export const ZResponseUpdateInput = z.object({
finished: z.boolean(),
data: ZResponseData,

View File

@@ -37,7 +37,7 @@ export const ZSurveyProductOverwrites = z.object({
brandColor: ZColor.nullish(),
highlightBorderColor: ZColor.nullish(),
placement: ZPlacement.nullish(),
clickOutside: z.boolean().nullish(),
clickOutsideClose: z.boolean().nullish(),
darkOverlay: z.boolean().nullish(),
});

BIN
ph.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB