mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
Merge branch 'main' into surveyBg
This commit is contained in:
@@ -30,6 +30,7 @@ export default function MetaInformation({
|
||||
<meta name="image" content={`https://${BASE_URL}/favicon.ico`} />
|
||||
<meta property="og:image" content={`https://${BASE_URL}/social-image.png`} />
|
||||
<link rel="icon" type="image/x-icon" href={`https://${BASE_URL}/favicon.ico`} />
|
||||
<link rel="canonical" href="https://formbricks.com/" />
|
||||
<meta name="msapplication-TileColor" content="#00C4B8" />
|
||||
<meta name="msapplication-TileImage" content={`https://${BASE_URL}/favicon.ico`} />
|
||||
<meta property="og:image:alt" content="Open Source Experience Management, Privacy-first" />
|
||||
|
||||
1
apps/web/.env
Symbolic link
1
apps/web/.env
Symbolic link
@@ -0,0 +1 @@
|
||||
../../.env
|
||||
@@ -27,6 +27,11 @@ export function EditFormbricksBranding({
|
||||
);
|
||||
const [updatingBranding, setUpdatingBranding] = useState(false);
|
||||
|
||||
const getTextFromType = (type) => {
|
||||
if (type === "linkSurvey") return "Link Surveys";
|
||||
if (type === "inAppSurvey") return "In App Surveys";
|
||||
};
|
||||
|
||||
const toggleBranding = async () => {
|
||||
try {
|
||||
setUpdatingBranding(true);
|
||||
@@ -52,8 +57,8 @@ export function EditFormbricksBranding({
|
||||
<div className="mb-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
To remove the Formbricks branding from the <span className="font-semibold">{type} surveys</span>
|
||||
, please{" "}
|
||||
To remove the Formbricks branding from the
|
||||
<span className="font-semibold">{getTextFromType(type)}</span>, please
|
||||
{type === "linkSurvey" ? (
|
||||
<span className="underline">
|
||||
<Link href={`/environments/${environmentId}/settings/billing`}>upgrade your plan.</Link>
|
||||
|
||||
@@ -96,32 +96,35 @@ export default function EmailTab({ surveyId, email }: EmailTabProps) {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
{showEmbed && (
|
||||
{showEmbed ? (
|
||||
<CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
|
||||
language="html"
|
||||
showCopyToClipboard={false}>
|
||||
{emailHtml}
|
||||
</CodeBlock>
|
||||
)}
|
||||
<div>
|
||||
<div className="mb-6 flex gap-2">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500"></div>
|
||||
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
|
||||
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="mb-2 border-b border-slate-200 pb-2 text-sm">To : {email || "user@mail.com"}</div>
|
||||
<div className="border-b border-slate-200 pb-2 text-sm">Subject : {subject}</div>
|
||||
<div className="p-4">
|
||||
{emailHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: emailHtmlPreview }}></div>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
)}
|
||||
) : (
|
||||
<div>
|
||||
<div className="mb-6 flex gap-2">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500"></div>
|
||||
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
|
||||
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="mb-2 border-b border-slate-200 pb-2 text-sm">
|
||||
To : {email || "user@mail.com"}
|
||||
</div>
|
||||
<div className="border-b border-slate-200 pb-2 text-sm">Subject : {subject}</div>
|
||||
<div className="p-4">
|
||||
{emailHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: emailHtmlPreview }}></div>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { type, productOverwrites, surveyBackground } = localSurvey;
|
||||
const { brandColor, clickOutside, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {};
|
||||
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {};
|
||||
const { bg, bgType, brightness } = surveyBackground ?? {};
|
||||
|
||||
const [inputValue, setInputValue] = useState(100);
|
||||
@@ -148,12 +148,12 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickOutside = (clickOutside: boolean) => {
|
||||
const handleClickOutsideClose = (clickOutsideClose: boolean) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
clickOutside,
|
||||
clickOutsideClose,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -278,8 +278,8 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
|
||||
setCurrentPlacement={handlePlacementChange}
|
||||
setOverlay={handleOverlay}
|
||||
overlay={darkOverlay ? "dark" : "light"}
|
||||
setClickOutside={handleClickOutside}
|
||||
clickOutside={!!clickOutside}
|
||||
setClickOutsideClose={handleClickOutsideClose}
|
||||
clickOutsideClose={!!clickOutsideClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
11
apps/web/app/api/v1/(legacy)/js/sync/lib/legacy.ts
Normal file
11
apps/web/app/api/v1/(legacy)/js/sync/lib/legacy.ts
Normal 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: {} };
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
@@ -100,18 +100,16 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center bg-red-500">
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
product={product}
|
||||
personId={personId}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={prefillAnswer}
|
||||
singleUseId={singleUseId}
|
||||
singleUseResponse={singleUseResponse}
|
||||
webAppUrl={webAppUrl}
|
||||
/>
|
||||
</div>
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
product={product}
|
||||
userId={userId}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={prefillAnswer}
|
||||
singleUseId={singleUseId}
|
||||
singleUseResponse={singleUseResponse}
|
||||
webAppUrl={webAppUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
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 SurveyBg from "@/app/s/[surveyId]/components/SurveyBg";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getEmailVerificationStatus } from "./lib/helpers";
|
||||
|
||||
|
||||
interface LinkSurveyPageProps {
|
||||
params: {
|
||||
@@ -147,9 +148,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);
|
||||
@@ -159,7 +159,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}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/s3-presigned-post": "^3.438.0",
|
||||
"@aws-sdk/s3-presigned-post": "^3.451.0",
|
||||
"@formbricks/api": "workspace:*",
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/ee": "workspace:*",
|
||||
@@ -26,42 +26,42 @@
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@react-email/components": "^0.0.9",
|
||||
"@sentry/nextjs": "^7.77.0",
|
||||
"@react-email/components": "^0.0.11",
|
||||
"@sentry/nextjs": "^7.80.1",
|
||||
"@vercel/og": "^0.5.20",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"encoding": "^0.1.13",
|
||||
"framer-motion": "10.16.4",
|
||||
"framer-motion": "10.16.5",
|
||||
"googleapis": "^128.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^10.0.1",
|
||||
"lucide-react": "^0.290.0",
|
||||
"lru-cache": "^10.0.2",
|
||||
"lucide-react": "^0.292.0",
|
||||
"mime": "^3.0.0",
|
||||
"next": "13.5.6",
|
||||
"nodemailer": "^6.9.7",
|
||||
"otplib": "^12.0.1",
|
||||
"posthog-js": "^1.87.3",
|
||||
"posthog-js": "^1.91.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "^1.9.5",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"webpack": "^5.89.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.5",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/markdown-it": "^13.0.5",
|
||||
"@types/qrcode": "^1.5.4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/lodash": "^4.14.201",
|
||||
"@types/markdown-it": "^13.0.6",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"terser": "^5.22.0",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "^3.6.0"
|
||||
"terser": "^5.24.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"predev": "pnpm generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.4.2",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"@prisma/extension-accelerate": "^0.6.2",
|
||||
"dotenv-cli": "^7.3.0"
|
||||
},
|
||||
@@ -33,9 +33,9 @@
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"prisma": "^5.4.2",
|
||||
"prisma": "^5.6.0",
|
||||
"prisma-dbml-generator": "^0.10.0",
|
||||
"prisma-json-types-generator": "^3.0.2",
|
||||
"prisma-json-types-generator": "^3.0.3",
|
||||
"zod": "^3.22.4",
|
||||
"zod-prisma": "^0.5.4"
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"stripe": "^14.0.0"
|
||||
"stripe": "^14.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"clean": "rimraf node_modules .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-next": "^14.0.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-turbo": "latest",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
|
||||
@@ -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",
|
||||
@@ -34,17 +34,17 @@
|
||||
},
|
||||
"author": "Formbricks <hola@formbricks.com>",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/preset-typescript": "^7.23.2",
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/preset-env": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@formbricks/api": "workspace:*",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/surveys": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
@@ -52,9 +52,9 @@
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"terser": "^5.22.0",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "^3.6.0"
|
||||
"terser": "^5.24.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.6.3"
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "server-only";
|
||||
import path from "path";
|
||||
import { env } from "./env.mjs";
|
||||
export const IS_FORMBRICKS_CLOUD = env.IS_FORMBRICKS_CLOUD === "1";
|
||||
export const REVALIDATION_INTERVAL = 0; //TODO: find a good way to cache and revalidate data when it changes
|
||||
@@ -59,7 +58,7 @@ export const RESPONSES_PER_PAGE = 10;
|
||||
export const TEXT_RESPONSES_PER_PAGE = 5;
|
||||
|
||||
// Storage constants
|
||||
export const UPLOADS_DIR = path.resolve("./uploads");
|
||||
export const UPLOADS_DIR = "./uploads";
|
||||
export const MAX_SIZES = {
|
||||
public: 1024 * 1024 * 10, // 10MB
|
||||
free: 1024 * 1024 * 10, // 10MB
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
import { config } from 'dotenv';
|
||||
config({ path: '../../.env' });
|
||||
/* import { config } from 'dotenv';
|
||||
config({ path: '../../.env' }); */
|
||||
|
||||
export const env = createEnv({
|
||||
/*
|
||||
|
||||
@@ -13,28 +13,28 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/api": "*",
|
||||
"@aws-sdk/client-s3": "3.433.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.433.0",
|
||||
"@aws-sdk/client-s3": "3.451.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.451.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"mime": "3.0.0",
|
||||
"@formbricks/database": "*",
|
||||
"@formbricks/types": "*",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"aws-crt": "^1.18.1",
|
||||
"aws-crt": "^1.19.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"markdown-it": "^13.0.2",
|
||||
"nanoid": "^5.0.2",
|
||||
"next-auth": "^4.23.2",
|
||||
"nodemailer": "^6.9.6",
|
||||
"posthog-node": "^3.1.2",
|
||||
"nanoid": "^5.0.3",
|
||||
"next-auth": "^4.24.5",
|
||||
"nodemailer": "^6.9.7",
|
||||
"posthog-node": "^3.1.3",
|
||||
"server-only": "^0.0.1",
|
||||
"tailwind-merge": "^1.14.0"
|
||||
"tailwind-merge": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "*",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"@types/mime": "3.0.3",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/mime": "3.0.4",
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -180,7 +181,8 @@ export const createPerson = async (environmentId: string, userId: string): Promi
|
||||
|
||||
personCache.revalidate({
|
||||
id: transformedPerson.id,
|
||||
environmentId: transformedPerson.environmentId,
|
||||
environmentId,
|
||||
userId,
|
||||
});
|
||||
|
||||
return transformedPerson;
|
||||
@@ -224,20 +226,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);
|
||||
@@ -304,7 +348,7 @@ export const getPersonByUserId = async (userId: string, environmentId: string):
|
||||
|
||||
personCache.revalidate({
|
||||
id: personWithUserIdAttribute.id,
|
||||
environmentId: personWithUserIdAttribute.environmentId,
|
||||
environmentId,
|
||||
userId,
|
||||
});
|
||||
|
||||
@@ -312,7 +356,11 @@ export const getPersonByUserId = async (userId: string, environmentId: string):
|
||||
},
|
||||
[`getPersonByUserId-${userId}-${environmentId}`],
|
||||
{
|
||||
tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
|
||||
tags: [
|
||||
personCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
|
||||
personCache.tag.byUserId(userId), // fix for caching issue on vercel
|
||||
personCache.tag.byEnvironmentId(environmentId), // fix for caching issue on vercel
|
||||
],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
@@ -348,7 +396,7 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
|
||||
|
||||
personCache.revalidate({
|
||||
id: personPrisma.id,
|
||||
environmentId: personPrisma.environmentId,
|
||||
environmentId,
|
||||
userId,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
@@ -610,12 +609,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),
|
||||
@@ -625,86 +699,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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"baseUrl": "packages/lib",
|
||||
"paths": {
|
||||
"@/*": ["../../apps/web/*"],
|
||||
"@prisma/client/*": ["@formbricks/database/client/*"]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.19",
|
||||
"@types/request-promise-native": "~1.0.19",
|
||||
"@typescript-eslint/parser": "~6.8",
|
||||
"eslint-plugin-n8n-nodes-base": "^1.16.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/request-promise-native": "~1.0.21",
|
||||
"@typescript-eslint/parser": "~6.11",
|
||||
"eslint-plugin-n8n-nodes-base": "^1.16.1",
|
||||
"gulp": "^4.0.2",
|
||||
"n8n-core": "legacy",
|
||||
"n8n-workflow": "legacy"
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"postcss": "^8.4.31",
|
||||
"preact": "^10.18.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"terser": "^5.22.0",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "^3.6.0",
|
||||
"preact": "^10.19.2",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"terser": "^5.24.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.6.3",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"clean": "rimraf node_modules .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.3"
|
||||
"tailwindcss": "^3.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"clean": "rimraf node_modules dist turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.8.6",
|
||||
"@types/react": "18.2.28",
|
||||
"@types/react-dom": "18.2.13",
|
||||
"@types/node": "20.9.0",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"lexical": "^0.12.2",
|
||||
"lucide-react": "^0.290.0",
|
||||
"lucide-react": "^0.292.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
|
||||
4142
pnpm-lock.yaml
generated
4142
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user