Compare commits

...

11 Commits

Author SHA1 Message Date
Dhruwang Jariwala
97cc6232c2 fix: page refresh issue on adding new action (#1634) 2023-11-21 08:55:02 +00:00
Matti Nannt
7331d1dd5a chore: update npm dependencies to latest version (#1651) 2023-11-21 08:32:07 +00:00
Matti Nannt
3f8bf4c34c chore: simplify getPersonByUserId by removing legacy person support (#1649) 2023-11-20 21:07:40 +00:00
Matti Nannt
91ceffba01 fix: personByUserId not cached properly (#1644) 2023-11-20 20:05:58 +00:00
Shaik_Asif
8c38495812 fix: typo in template (#1648) 2023-11-20 19:58:05 +00:00
Matti Nannt
c8c98499ed chore: Simplify person service by removing complex getOrCreatePerson function (#1643) 2023-11-20 17:22:11 +00:00
Shubham Palriwala
af181eabdc feat: formbricks/api package as per js package 1.2.2 (#1640)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-11-20 16:16:39 +00:00
Neil Chauhan
822c48ff52 fix: headline alignment issue (#1641)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-11-20 15:02:46 +00:00
Neil Chauhan
70d211a038 fix: thank you card headline issue FOR-1489 (#1639) 2023-11-20 08:25:15 +00:00
Dhruwang Jariwala
a77ce55a1d fix: Error screen on survey Editor refresh (#1635) 2023-11-19 12:47:44 +00:00
Matti Nannt
a376eb9b51 fix: caching issue by simplifying person service (#1636) 2023-11-17 19:15:17 +00:00
40 changed files with 954 additions and 1235 deletions

View File

@@ -16,10 +16,8 @@ env:
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
@@ -38,7 +36,7 @@ jobs:
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
with:
cosign-release: 'v2.1.1'
cosign-release: "v2.1.1"
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
@@ -71,6 +69,7 @@ jobs:
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
file: ./apps/web/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,152 +0,0 @@
import formbricks from "@formbricks/js";
import { useRouter } from "next/router";
import { FormEvent } from "react";
export default function SiginPage() {
const router = useRouter();
const submitAction = (e: FormEvent) => {
e.preventDefault();
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
formbricks.setEmail("matti@example.com");
formbricks.setUserId("123456");
formbricks.setAttribute("Plan", "Premium");
}
router.push("/app");
};
return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Or{" "}
<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">
start your 14-day free trial
</a>
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
<form className="space-y-6" onSubmit={submitAction}>
<div>
<label htmlFor="email" className="block text-sm font-medium leading-6 text-gray-900">
Email address
</label>
<div className="mt-2">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium leading-6 text-gray-900">
Password
</label>
<div className="mt-2">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
Remember me
</label>
</div>
<div className="text-sm">
<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">
Forgot your password?
</a>
</div>
</div>
<div>
<button
type="submit"
className="flex w-full justify-center rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Sign in
</button>
</div>
</form>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">Or continue with</span>
</div>
</div>
<div className="mt-6 grid grid-cols-3 gap-3">
<div>
<a
href="#"
className="inline-flex w-full justify-center rounded-md bg-white px-4 py-2 text-gray-500 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-offset-0">
<span className="sr-only">Sign in with Facebook</span>
<svg className="h-5 w-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M20 10c0-5.523-4.477-10-10-10S0 4.477 0 10c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V10h2.54V7.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V10h2.773l-.443 2.89h-2.33v6.988C16.343 19.128 20 14.991 20 10z"
clipRule="evenodd"
/>
</svg>
</a>
</div>
<div>
<a
href="#"
className="inline-flex w-full justify-center rounded-md bg-white px-4 py-2 text-gray-500 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-offset-0">
<span className="sr-only">Sign in with Twitter</span>
<svg className="h-5 w-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.29 18.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0020 3.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.073 4.073 0 01.8 7.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 010 16.407a11.616 11.616 0 006.29 1.84" />
</svg>
</a>
</div>
<div>
<a
href="#"
className="inline-flex w-full justify-center rounded-md bg-white px-4 py-2 text-gray-500 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-offset-0">
<span className="sr-only">Sign in with GitHub</span>
<svg className="h-5 w-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,202 +0,0 @@
import formbricks from "@formbricks/js";
import Image from "next/image";
import { useEffect, useState } from "react";
import fbsetup from "../../public/fb-setup.png";
export default function AppPage({}) {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
if (darkMode) {
document.body.classList.add("dark");
} else {
document.body.classList.remove("dark");
}
}, [darkMode]);
return (
<div className="h-full bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col justify-between md:flex-row">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks In-product Survey Demo App
</h1>
<p className="text-slate-700 dark:text-slate-300">
This app helps you test your in-app surveys. You can create and test user actions, create and
update user attributes, etc.
</p>
</div>
<button
className="mt-2 rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
onClick={() => setDarkMode(!darkMode)}>
{darkMode ? "Toggle Light Mode" : "Toggle Dark Mode"}
</button>
</div>
<div className="my-4 grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">1. Setup .env</h3>
<p className="text-slate-700 dark:text-slate-300">
Copy the environment ID of your Formbricks app to the env variable in demo/.env
</p>
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
<div className="mt-4 flex-col items-start text-sm text-slate-700 dark:text-slate-300 sm:flex sm:items-center sm:text-base">
<p className="mb-1 sm:mb-0 sm:mr-2">You&apos;re connected with env:</p>
<div className="flex items-center">
<strong className="w-32 truncate sm:w-auto">
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
</strong>
<span className="relative ml-2 flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
</span>
</div>
</div>
</div>
<div className="mt-4 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">2. Widget Logs</h3>
<p className="text-slate-700 dark:text-slate-300">
Look at the logs to understand how the widget works.{" "}
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
</p>
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
<LogsContainer />
</div> */}
</div>
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-gray-600 dark:bg-gray-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
<p className="text-slate-700 dark:text-gray-300">
On formbricks.reset() a few things happen: <strong>New person is created</strong> and{" "}
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
</p>
<button
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
formbricks.reset();
}}>
Reset
</button>
<p className="text-xs text-slate-700 dark:text-gray-300">
If you made a change in Formbricks app and it does not seem to work, hit &apos;Reset&apos; and
try again.
</p>
</div>
<div className="p-6">
<div>
<button
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
console.log("Inner Text");
}}>
Inner Text
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">Inner Text only</p>
</div>
</div>
<div className="p-6">
<div>
<button
id="css-id"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
console.log("Inner Text + CSS ID");
}}>
Inner Text
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">Inner Text + Css ID</p>
</div>
</div>
<div className="p-6">
<div>
<button
className="css-class mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
console.log("Inner Text + CSS Class");
}}>
Inner Text
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">Inner Text + CSS Class</p>
</div>
</div>
<div className="p-6">
<div>
<button
id="css-id"
className="css-class mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
console.log("ID + Class");
}}>
ID and Class
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">ID + Class</p>
</div>
</div>
<div className="p-6">
<div>
<button
id="css-id"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
console.log("ID + Class");
}}>
ID only
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">ID only</p>
</div>
</div>
<div className="p-6">
<div>
<button
className="css-class mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
console.log("Class only");
}}>
Class only
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">Class only</p>
</div>
</div>
<div className="p-6">
<div>
<button
className="css-1 css-2 mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
console.log("Class + Class");
}}>
Class + Class
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">Class + Class</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -146,7 +146,7 @@ This set of API can be used to
{
"id": "lkjaxb73ulydzeumhd51sx9g",
"type": "openText",
"headline": "What is the main benefit your receive from My Product?",
"headline": "What is the main benefit you receive from My Product?",
"required": true
},
{

View File

@@ -134,7 +134,7 @@ export const templates: TTemplate[] = [
{
id: createId(),
type: TSurveyQuestionType.OpenText,
headline: "What is the main benefit your receive from Formbricks?",
headline: "What is the main benefit you receive from Formbricks?",
inputType: "text",
longAnswer: true,
required: true,

View File

@@ -9,7 +9,11 @@ interface WidgetStatusIndicatorProps {
}
export default async function WidgetStatusIndicator({ environmentId, type }: WidgetStatusIndicatorProps) {
const [environment] = await Promise.all([getEnvironment(environmentId)]);
const environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment not found");
}
const stati = {
notImplemented: {

View File

@@ -12,8 +12,8 @@ import { TSurvey } from "@formbricks/types/surveys";
import { TProduct } from "@formbricks/types/product";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TActionClass } from "@formbricks/types/actionClasses";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { TMembershipRole } from "@formbricks/types/memberships";
import Loading from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/loading";
interface SurveyEditorProps {
survey: TSurvey;
@@ -41,6 +41,7 @@ export default function SurveyEditor({
useEffect(() => {
if (survey) {
if (localSurvey) return;
setLocalSurvey(JSON.parse(JSON.stringify(survey)));
if (survey.questions.length > 0) {
@@ -59,7 +60,7 @@ export default function SurveyEditor({
}, [localSurvey?.type]);
if (!localSurvey) {
return <ErrorComponent />;
return <Loading />;
}
return (

View File

@@ -97,6 +97,7 @@ export default function WhenToSendCard({
};
useEffect(() => {
if (isAddEventModalOpen) return;
if (activeIndex !== null) {
const newActionClass = actionClassArray[actionClassArray.length - 1].name;
const currentActionClass = localSurvey.triggers[activeIndex];

View File

@@ -406,7 +406,7 @@ export const templates: TTemplate[] = [
{
id: createId(),
type: TSurveyQuestionType.OpenText,
headline: "What is the main benefit your receive from {{productName}}?",
headline: "What is the main benefit you receive from {{productName}}?",
required: true,
inputType: "text",
},

View File

@@ -1,7 +1,7 @@
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";
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
import { ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
@@ -26,9 +26,12 @@ export async function POST(req: Request): Promise<NextResponse> {
const { environmentId, userId } = inputValidation.data;
const personWithUserId = await getOrCreatePersonByUserId(userId, environmentId);
let person = await getPersonByUserId(environmentId, userId);
if (!person) {
person = await createPerson(environmentId, userId);
}
const state = await getUpdatedState(environmentId, personWithUserId.id);
const state = await getUpdatedState(environmentId, person.id);
return responses.successResponse({ ...state }, true);
} catch (error) {
console.error(error);

View File

@@ -4,13 +4,12 @@ import { getLatestActionByPersonId } from "@formbricks/lib/action/service";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
import { getOrCreatePersonByUserId, getPersonByUserId } from "@formbricks/lib/person/service";
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/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> {
@@ -72,13 +71,15 @@ export async function GET(
isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
}
let person: TPerson | null;
let person = await getPersonByUserId(environmentId, userId);
if (!isMauLimitReached) {
person = await getOrCreatePersonByUserId(userId, environmentId);
if (!person) {
person = await createPerson(environmentId, userId);
}
} else {
person = await getPersonByUserId(userId, environmentId);
const errorMessage = `Monthly Active Users limit in the current plan is reached in ${environmentId}`;
if (!person) {
// if it's a new person and MAU limit is reached, throw an error
throw new Error(errorMessage);
} else {
// check if person has been active this month

View File

@@ -1,6 +1,6 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getOrCreatePersonByUserId, updatePerson } from "@formbricks/lib/person/service";
import { getPersonByUserId, updatePerson } from "@formbricks/lib/person/service";
import { ZPersonUpdateInput } from "@formbricks/types/people";
import { NextResponse } from "next/server";
@@ -31,7 +31,7 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
);
}
const person = await getOrCreatePersonByUserId(userId, environmentId);
const person = await getPersonByUserId(environmentId, userId);
if (!person) {
return responses.notFoundResponse("PersonByUserId", userId, true);

View File

@@ -6,7 +6,7 @@ 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 { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponseBySingleUseId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
@@ -147,7 +147,11 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
const userId = searchParams.userId;
if (userId) {
await getOrCreatePersonByUserId(userId, survey.environmentId);
// make sure the person exists or get's created
const person = await getPersonByUserId(survey.environmentId, userId);
if (!person) {
await createPerson(survey.environmentId, userId);
}
}
const isSurveyPinProtected = Boolean(!!survey && survey.pin);

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "1.2.1",
"version": "1.3.1",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
@@ -11,7 +11,6 @@
"lint": "next lint"
},
"dependencies": {
"@aws-sdk/s3-presigned-post": "^3.451.0",
"@formbricks/api": "workspace:*",
"@formbricks/database": "workspace:*",
"@formbricks/ee": "workspace:*",

View File

@@ -32,9 +32,9 @@
"@changesets/cli": "^2.26.2",
"eslint-config-formbricks": "workspace:*",
"husky": "^8.0.3",
"lint-staged": "^15.0.1",
"lint-staged": "^15.1.0",
"rimraf": "^5.0.5",
"tsx": "^3.13.0",
"tsx": "^4.2.0",
"turbo": "^1.10.16"
},
"lint-staged": {

View File

@@ -1,7 +1,7 @@
{
"name": "@formbricks/api",
"license": "MIT",
"version": "1.0.0",
"version": "1.1.0",
"description": "Formbricks-api is an api wrapper for the Formbricks client API",
"keywords": [
"Formbricks",

View File

@@ -0,0 +1,18 @@
import { Result } from "@formbricks/types/errorHandlers";
import { NetworkError } from "@formbricks/types/errors";
import { TActionInput } from "@formbricks/types/actions";
import { makeRequest } from "../../utils/makeRequest";
export class ActionAPI {
private apiHost: string;
private environmentId: string;
constructor(apiHost: string, environmentId: string) {
this.apiHost = apiHost;
this.environmentId = environmentId;
}
async create(actionInput: Omit<TActionInput, "environmentId">): Promise<Result<{}, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/actions`, "POST", actionInput);
}
}

View File

@@ -1,15 +1,21 @@
import { ResponseAPI } from "./response";
import { DisplayAPI } from "./display";
import { ApiConfig } from "../../types";
import { ActionAPI } from "./action";
import { PeopleAPI } from "./people";
export class Client {
response: ResponseAPI;
display: DisplayAPI;
action: ActionAPI;
people: PeopleAPI;
constructor(options: ApiConfig) {
const { apiHost, environmentId } = options;
this.response = new ResponseAPI(apiHost, environmentId);
this.display = new DisplayAPI(apiHost, environmentId);
this.action = new ActionAPI(apiHost, environmentId);
this.people = new PeopleAPI(apiHost, environmentId);
}
}

View File

@@ -0,0 +1,33 @@
import { Result } from "@formbricks/types/errorHandlers";
import { NetworkError } from "@formbricks/types/errors";
import { makeRequest } from "../../utils/makeRequest";
import { TPerson, TPersonUpdateInput } from "@formbricks/types/people";
export class PeopleAPI {
private apiHost: string;
private environmentId: string;
constructor(apiHost: string, environmentId: string) {
this.apiHost = apiHost;
this.environmentId = environmentId;
}
async create(userId: string): Promise<Result<TPerson, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/people`, "POST", {
environmentId: this.environmentId,
userId,
});
}
async update(
userId: string,
personInput: TPersonUpdateInput
): Promise<Result<TPerson, NetworkError | Error>> {
return makeRequest(
this.apiHost,
`/api/v1/client/${this.environmentId}/people/${userId}`,
"POST",
personInput
);
}
}

View File

@@ -18,6 +18,6 @@
},
"dependencies": {
"@formbricks/lib": "workspace:*",
"stripe": "^14.4.0"
"stripe": "^14.5.0"
}
}

View File

@@ -8,7 +8,7 @@
"clean": "rimraf node_modules .turbo"
},
"devDependencies": {
"eslint": "^8.53.0",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.3",
"eslint-config-prettier": "^9.0.0",
"eslint-config-turbo": "latest",

View File

@@ -42,9 +42,9 @@
"@formbricks/surveys": "workspace:*",
"@formbricks/tsconfig": "workspace:*",
"@formbricks/types": "workspace:*",
"@types/jest": "^29.5.8",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@types/jest": "^29.5.9",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"babel-jest": "^29.7.0",
"cross-env": "^7.0.3",
"eslint-config-formbricks": "workspace:*",

View File

@@ -4,6 +4,7 @@ import { Config } from "./config";
import { NetworkError, Result, err, okVoid } from "./errors";
import { Logger } from "./logger";
import { renderWidget } from "./widget";
import { FormbricksAPI } from "@formbricks/api";
const logger = Logger.getInstance();
const config = Config.getInstance();
@@ -23,24 +24,23 @@ export const trackAction = async (
// don't send actions to the backend if the person is not identified
if (config.get().state?.person?.userId && !intentsToNotCreateOnApp.includes(name)) {
logger.debug(`Sending action "${name}" to backend`);
const res = await fetch(`${config.get().apiHost}/api/v1/client/${config.get().environmentId}/actions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
const api = new FormbricksAPI({
apiHost: config.get().apiHost,
environmentId: config.get().environmentId,
});
const res = await api.client.action.create({
...input,
userId: config.get().state.person!.userId,
});
if (!res.ok) {
const error = await res.json();
return err({
code: "network_error",
message: `Error tracking action: ${JSON.stringify(error)}`,
status: res.status,
url: res.url,
responseMessage: error.message,
message: `Error tracking action ${name}`,
status: 500,
url: `${config.get().apiHost}/api/v1/client/${config.get().environmentId}/actions`,
responseMessage: res.error.message,
});
}
}

View File

@@ -1,18 +1,10 @@
import { TJsState } from "@formbricks/types/js";
import { TPerson, TPersonUpdateInput } from "@formbricks/types/people";
import { Config } from "./config";
import {
AttributeAlreadyExistsError,
MissingPersonError,
NetworkError,
Result,
err,
ok,
okVoid,
} from "./errors";
import { AttributeAlreadyExistsError, MissingPersonError, NetworkError, Result, err, okVoid } from "./errors";
import { deinitalize, initialize } from "./initialize";
import { Logger } from "./logger";
import { sync } from "./sync";
import { FormbricksAPI } from "@formbricks/api";
const config = Config.getInstance();
const logger = Logger.getInstance();
@@ -20,7 +12,7 @@ const logger = Logger.getInstance();
export const updatePersonAttribute = async (
key: string,
value: string
): Promise<Result<TJsState, NetworkError | MissingPersonError>> => {
): Promise<Result<void, NetworkError | MissingPersonError>> => {
if (!config.get().state.person || !config.get().state.person?.id) {
return err({
code: "missing_person",
@@ -34,28 +26,21 @@ export const updatePersonAttribute = async (
},
};
const res = await fetch(
`${config.get().apiHost}/api/v1/client/${config.get().environmentId}/people/${
config.get().state.person?.userId
}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}
);
const resJson = await res.json();
const api = new FormbricksAPI({
apiHost: config.get().apiHost,
environmentId: config.get().environmentId,
});
const res = await api.client.people.update(config.get().state.person!.userId, input);
if (!res.ok) {
return err({
code: "network_error",
status: res.status,
message: "Error updating person",
url: res.url,
responseMessage: resJson.message,
status: 500,
message: `Error updating person with userId ${config.get().state.person?.userId}`,
url: `${config.get().apiHost}/api/v1/client/${config.get().environmentId}/people/${
config.get().state.person?.userId
}`,
responseMessage: res.error.message,
});
}
@@ -67,7 +52,7 @@ export const updatePersonAttribute = async (
userId: config.get().state.person?.userId,
});
return ok(resJson.data as TJsState);
return okVoid();
};
export const hasAttributeValue = (key: string, value: string): boolean => {

View File

@@ -240,7 +240,7 @@ export const createAction = async (data: TActionInput): Promise<TAction> => {
actionType = "automatic";
}
const person = await getPersonByUserId(userId, environmentId);
const person = await getPersonByUserId(environmentId, userId);
if (!person) {
throw new Error("Person not found");

View File

@@ -70,7 +70,7 @@ export const updateDisplay = async (
let person: TPerson | null = null;
if (displayInput.userId) {
person = await getPersonByUserId(displayInput.userId, displayInput.environmentId);
person = await getPersonByUserId(displayInput.environmentId, displayInput.userId);
if (!person) {
throw new ResourceNotFoundError("Person", displayInput.userId);
}
@@ -160,7 +160,7 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
try {
let person;
if (displayInput.userId) {
person = await getPersonByUserId(displayInput.userId, displayInput.environmentId);
person = await getPersonByUserId(displayInput.environmentId, displayInput.userId);
}
const display = await prisma.display.create({
data: {

View File

@@ -22,14 +22,13 @@ import { validateInputs } from "../utils/validate";
import { environmentCache } from "./cache";
import { formatEnvironmentDateFields } from "./util";
export const getEnvironment = (environmentId: string) =>
export const getEnvironment = (environmentId: string): Promise<TEnvironment | null> =>
unstable_cache(
async (): Promise<TEnvironment> => {
async () => {
validateInputs([environmentId, ZId]);
let environmentPrisma;
try {
environmentPrisma = await prisma.environment.findUnique({
return await prisma.environment.findUnique({
where: {
id: environmentId,
},
@@ -42,16 +41,6 @@ export const getEnvironment = (environmentId: string) =>
throw error;
}
try {
const environment = ZEnvironment.parse(environmentPrisma);
return environment;
} catch (error) {
if (error instanceof z.ZodError) {
console.error(JSON.stringify(error.errors, null, 2));
}
throw new ValidationError("Data validation of environment failed");
}
},
[`getEnvironment-${environmentId}`],
{

View File

@@ -12,11 +12,12 @@
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
},
"dependencies": {
"@formbricks/api": "*",
"@aws-sdk/client-s3": "3.451.0",
"@aws-sdk/s3-request-presigner": "3.451.0",
"@aws-sdk/s3-presigned-post": "^3.454.0",
"@aws-sdk/client-s3": "3.454.0",
"@aws-sdk/s3-request-presigner": "3.454.0",
"@t3-oss/env-nextjs": "^0.7.1",
"mime": "3.0.0",
"@formbricks/api": "*",
"@formbricks/database": "*",
"@formbricks/types": "*",
"@paralleldrive/cuid2": "^2.2.2",

View File

@@ -25,6 +25,6 @@ export const canUserAccessPerson = async (userId: string, personId: string): Pro
[`canUserAccessPerson-${userId}-people-${personId}`],
{
revalidate: SERVICES_REVALIDATION_INTERVAL,
tags: [personCache.tag.byId(personId), personCache.tag.byUserId(userId)],
tags: [personCache.tag.byId(personId)],
}
)();

View File

@@ -14,11 +14,8 @@ export const personCache = {
byEnvironmentId(environmentId: string): string {
return `environments-${environmentId}-people`;
},
byUserId(userId: string): string {
return `users-${userId}-people`;
},
byEnvironmentIdAndUserId(environmentId: string, userId: string): string {
return `environments-${environmentId}-users-${userId}-people`;
return `environments-${environmentId}-personByUserId-${userId}`;
},
},
revalidate({ id, environmentId, userId }: RevalidateProps): void {
@@ -26,16 +23,12 @@ export const personCache = {
revalidateTag(this.tag.byId(id));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
if (userId) {
revalidateTag(this.tag.byUserId(userId));
}
if (environmentId && userId) {
revalidateTag(this.tag.byEnvironmentIdAndUserId(environmentId, userId));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
},
};

View File

@@ -181,7 +181,8 @@ export const createPerson = async (environmentId: string, userId: string): Promi
personCache.revalidate({
id: transformedPerson.id,
environmentId: transformedPerson.environmentId,
environmentId,
userId,
});
return transformedPerson;
@@ -290,10 +291,10 @@ export const updatePerson = async (personId: string, personInput: TPersonUpdateI
}
};
export const getPersonByUserId = async (userId: string, environmentId: string): Promise<TPerson | null> => {
const personPrisma = await unstable_cache(
export const getPersonByUserId = async (environmentId: string, userId: string): Promise<TPerson | null> =>
await unstable_cache(
async () => {
validateInputs([userId, ZString], [environmentId, ZId]);
validateInputs([environmentId, ZId], [userId, ZString]);
// check if userId exists as a column
const personWithUserId = await prisma.person.findFirst({
@@ -305,7 +306,7 @@ export const getPersonByUserId = async (userId: string, environmentId: string):
});
if (personWithUserId) {
return personWithUserId;
return transformPrismaPerson(personWithUserId);
}
// Check if a person with the userId attribute exists
@@ -347,57 +348,13 @@ export const getPersonByUserId = async (userId: string, environmentId: string):
personCache.revalidate({
id: personWithUserIdAttribute.id,
environmentId: personWithUserIdAttribute.environmentId,
environmentId,
userId,
});
return personWithUserIdAttribute;
return transformPrismaPerson(personWithUserIdAttribute);
},
[`getPersonByUserId-${userId}-${environmentId}`],
{
tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
if (!personPrisma) {
return null;
}
return transformPrismaPerson(personPrisma);
};
export const getOrCreatePersonByUserId = async (userId: string, environmentId: string): Promise<TPerson> =>
await unstable_cache(
async () => {
validateInputs([userId, ZString], [environmentId, ZId]);
let person = await getPersonByUserId(userId, environmentId);
if (person) {
return person;
}
// create a new person
const personPrisma = await prisma.person.create({
data: {
environment: {
connect: {
id: environmentId,
},
},
userId,
},
select: selectPerson,
});
personCache.revalidate({
id: personPrisma.id,
environmentId: personPrisma.environmentId,
userId,
});
return transformPrismaPerson(personPrisma);
},
[`getOrCreatePersonByUserId-${userId}-${environmentId}`],
[`getPersonByUserId-${environmentId}-${userId}`],
{
tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,

View File

@@ -201,7 +201,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
let person: TPerson | null = null;
if (responseInput.userId) {
person = await getPersonByUserId(responseInput.userId, responseInput.environmentId);
person = await getPersonByUserId(responseInput.environmentId, responseInput.userId);
if (!person) {
throw new ResourceNotFoundError("Person", responseInput.userId);
}

View File

@@ -46,7 +46,7 @@
"devDependencies": {
"@types/express": "^4.17.21",
"@types/request-promise-native": "~1.0.21",
"@typescript-eslint/parser": "~6.11",
"@typescript-eslint/parser": "~6.12",
"eslint-plugin-n8n-nodes-base": "^1.16.1",
"gulp": "^4.0.2",
"n8n-core": "legacy",

View File

@@ -25,7 +25,7 @@
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"@formbricks/types": "workspace:*",
"@preact/preset-vite": "^2.6.0",
"@preact/preset-vite": "^2.7.0",
"autoprefixer": "^10.4.16",
"eslint-config-formbricks": "workspace:*",
"postcss": "^8.4.31",

View File

@@ -1,17 +1,20 @@
interface HeadlineProps {
headline?: string;
questionId: string;
style?: any;
required?: boolean;
alignTextCenter?: boolean;
}
export default function Headline({ headline, questionId, style, required = true }: HeadlineProps) {
export default function Headline({
headline,
questionId,
required = true,
alignTextCenter = false,
}: HeadlineProps) {
return (
<label
htmlFor={questionId}
className="text-heading mb-1.5 block text-base font-semibold leading-6"
style={style}>
<div className={"mr-[3ch] flex items-center justify-between"} style={style}>
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
<div
className={`flex items-center ${alignTextCenter ? "justify-center" : "mr-[3ch] justify-between"}`}>
{headline}
{!required && (
<span className="text-info-text self-start text-sm font-normal leading-7" tabIndex={-1}>

View File

@@ -36,7 +36,7 @@ export default function ThankYouCard({
<span className="bg-shadow mb-[10px] inline-block h-1 w-16 rounded-[100%]"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" style={{ "justify-content": "center" }} />
<Headline alignTextCenter={true} headline={headline} questionId="thankYouCard" />
<Subheader subheader={subheader} questionId="thankYouCard" />
<RedirectCountDown redirectUrl={redirectUrl} isRedirectDisabled={isRedirectDisabled} />
</div>

View File

@@ -7,9 +7,9 @@
"clean": "rimraf node_modules dist turbo"
},
"devDependencies": {
"@types/node": "20.9.0",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"typescript": "^5.2.2"
"@types/node": "20.9.3",
"@types/react": "18.2.38",
"@types/react-dom": "18.2.16",
"typescript": "^5.3.2"
}
}

View File

@@ -10,6 +10,10 @@ export default async function EnvironmentNotice({ environmentId }: EnvironmentNo
const headersList = headers();
const currentUrl = headersList.get("referer") || headersList.get("x-invoke-path") || "";
const environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment not found");
}
const environments = await getEnvironments(environment.productId);
const otherEnvironmentId = environments.find((e) => e.id !== environment.id)?.id || "";

View File

@@ -19,13 +19,13 @@
"@formbricks/surveys": "workspace:*",
"@formbricks/lib": "workspace:*",
"@heroicons/react": "^2.0.18",
"@lexical/code": "^0.12.2",
"@lexical/link": "^0.12.2",
"@lexical/list": "^0.12.2",
"@lexical/markdown": "^0.12.2",
"@lexical/react": "^0.12.2",
"@lexical/rich-text": "^0.12.2",
"@lexical/table": "^0.12.2",
"@lexical/code": "^0.12.4",
"@lexical/link": "^0.12.4",
"@lexical/list": "^0.12.4",
"@lexical/markdown": "^0.12.4",
"@lexical/react": "^0.12.4",
"@lexical/rich-text": "^0.12.4",
"@lexical/table": "^0.12.4",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
@@ -40,7 +40,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"lexical": "^0.12.2",
"lexical": "^0.12.4",
"lucide-react": "^0.292.0",
"react-colorful": "^5.6.1",
"react-confetti": "^6.1.0",

1472
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff