-
+
-
An error occurred when logging you in
-
+
An error occurred when logging you in
+
@@ -101,7 +101,7 @@ export const SignupForm = () => {
Already have an account?{" "}
-
+
Log in.
@@ -111,7 +111,7 @@ export const SignupForm = () => {
{process.env.NEXT_PUBLIC_TERMS_URL && (
@@ -121,7 +121,7 @@ export const SignupForm = () => {
{process.env.NEXT_PUBLIC_TERMS_URL && process.env.NEXT_PUBLIC_PRIVACY_URL && and }
{process.env.NEXT_PUBLIC_PRIVACY_URL && (
diff --git a/apps/web/src/components/layout/LayoutApp.tsx b/apps/web/src/components/layout/LayoutApp.tsx
index c923161956..4a64bcf1f2 100644
--- a/apps/web/src/components/layout/LayoutApp.tsx
+++ b/apps/web/src/components/layout/LayoutApp.tsx
@@ -40,7 +40,7 @@ export default function LayoutApp({ children }) {
if (!session) {
router.push(`/auth/signin?callbackUrl=${encodeURIComponent(window.location.href)}`);
- return ;
+ return ;
}
if (isLoadingMemberships) {
@@ -55,6 +55,12 @@ export default function LayoutApp({ children }) {
return Error loading ressources. Maybe you don‘t have enough access rights
;
}
+ if (session && session.user.finishedOnboarding === false && router.pathname !== "/me/onboarding") {
+ // use timeout to prevent flash of content and resulting errors
+ router.push("/me/onboarding");
+ return ;
+ }
+
return (
<>
diff --git a/apps/web/src/components/me/ProfileSettingsPage.tsx b/apps/web/src/components/me/ProfileSettingsPage.tsx
index 0cca510d69..e5e5eef022 100644
--- a/apps/web/src/components/me/ProfileSettingsPage.tsx
+++ b/apps/web/src/components/me/ProfileSettingsPage.tsx
@@ -4,7 +4,6 @@ import LoadingSpinner from "@/components/LoadingSpinner";
import Modal from "@/components/Modal";
import { createApiKey, deleteApiKey, useApiKeys } from "@/lib/apiKeys";
import { convertDateTimeString } from "@/lib/utils";
-import { Form, Submit, Text } from "@formbricks/react";
import { Button } from "@formbricks/ui";
import { useState } from "react";
@@ -127,17 +126,27 @@ export default function ProfileSettingsPage() {
Create a Personal API Key
-
-
+
)}
diff --git a/apps/web/src/components/onboarding/ForwardToApp.tsx b/apps/web/src/components/onboarding/ForwardToApp.tsx
new file mode 100644
index 0000000000..816fef3af2
--- /dev/null
+++ b/apps/web/src/components/onboarding/ForwardToApp.tsx
@@ -0,0 +1,12 @@
+const ForwardToApp = () => {
+ return (
+
+ Thanks you 🕺
+
+
+ Redirecting to app...
+
+ );
+};
+
+export default ForwardToApp;
diff --git a/apps/web/src/components/onboarding/IconRadio.tsx b/apps/web/src/components/onboarding/IconRadio.tsx
new file mode 100644
index 0000000000..f897dcd8b0
--- /dev/null
+++ b/apps/web/src/components/onboarding/IconRadio.tsx
@@ -0,0 +1,92 @@
+import { RadioGroup } from "@headlessui/react";
+import { CheckCircleIcon } from "@heroicons/react/20/solid";
+import clsx from "clsx";
+import { useEffect } from "react";
+import { Controller, useWatch } from "react-hook-form";
+
+interface IconRadioProps {
+ element: any;
+ field: any;
+ control: any;
+ onSubmit: () => void;
+ disabled: boolean;
+}
+
+export default function IconRadio({ element, control, onSubmit, disabled }: IconRadioProps) {
+ const value = useWatch({
+ control,
+ name: element.name!!,
+ });
+
+ useEffect(() => {
+ if (value && !disabled) {
+ onSubmit();
+ }
+ }, [value, onSubmit, disabled]);
+
+ return (
+
(
+
+
+ {element.label}
+
+
+ {element.help}
+
+
+
+ {element.options &&
+ element.options.map((option) => (
+
+ clsx(
+ checked ? "border-transparent" : "border-slate-200 ",
+ /* active ? "border-brand ring-brand ring-2" : "", */
+ "relative flex cursor-pointer rounded-lg border bg-slate-50 py-2 shadow-sm transition-all ease-in-out hover:scale-105 focus:outline-none"
+ )
+ }>
+ {({ checked, active }) => (
+ <>
+
+ {option.frontend?.icon && (
+
+ )}
+
+ {option.label}
+
+
+
+
+
+ >
+ )}
+
+ ))}
+
+
+ )}
+ />
+ );
+}
diff --git a/apps/web/src/components/onboarding/OnboardingPage.tsx b/apps/web/src/components/onboarding/OnboardingPage.tsx
new file mode 100644
index 0000000000..8120b3a648
--- /dev/null
+++ b/apps/web/src/components/onboarding/OnboardingPage.tsx
@@ -0,0 +1,71 @@
+import { LogoMark } from "@/components/LogoMark";
+import OnboardingSurvey from "@/components/onboarding/OnboardingSurvey";
+import { Dialog, Transition } from "@headlessui/react";
+import { Fragment, useEffect, useState } from "react";
+
+export default function OnboardingPage() {
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ setLoading(true);
+ }, 5000);
+
+ return () => {
+ clearTimeout(timeoutId);
+ };
+ }, []);
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/components/onboarding/OnboardingSurvey.tsx b/apps/web/src/components/onboarding/OnboardingSurvey.tsx
new file mode 100644
index 0000000000..79821e0330
--- /dev/null
+++ b/apps/web/src/components/onboarding/OnboardingSurvey.tsx
@@ -0,0 +1,134 @@
+import { capturePosthogEvent } from "@/lib/posthog";
+import { FormbricksEngine } from "@formbricks/engine-react";
+import { useSession } from "next-auth/react";
+import { useEffect } from "react";
+import LoadingSpinner from "../LoadingSpinner";
+import ForwardToApp from "./ForwardToApp";
+import IconRadio from "./IconRadio";
+
+const OnboardingSurvey = () => {
+ const { data: session, status } = useSession();
+
+ useEffect(() => {
+ if (session.user.finishedOnboarding) {
+ window.location.replace("/");
+ }
+ }, [session]);
+
+ if (status === "loading") return ;
+
+ const formId =
+ process.env.NODE_ENV === "production" ? "cldu60z5d0000mm0hq7k0ducf" : "cldvi1rzq0006oy0hg0ahsedi";
+
+ return (
+ {
+ console.log({
+ email: session.user.email,
+ name: session.user.name,
+ lastUserContact: submission.lastUserContact,
+ hardestPartInUserResearch: submission.hardestPartInUserResearch,
+ });
+ // send submission to formbricks
+ const res = await fetch(`https://app.formbricks.com/api/capture/forms/${formId}/submissions`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ customer: {
+ email: session.user.email,
+ name: session.user.name,
+ lastUserContact: submission.lastUserContact,
+ hardestPartInUserResearch: submission.hardestPartInUserResearch,
+ },
+ data: submission,
+ }),
+ });
+ if (!res.ok) {
+ // send event to posthog
+ capturePosthogEvent("system", "onboarding form error occured", { error: await res.text() });
+ }
+ // update user in database
+ await fetch("/api/users/me", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ finishedOnboarding: true }),
+ });
+ // redirect to app
+ window.location.replace("/");
+ }}
+ schema={{
+ config: {
+ progressBar: false,
+ },
+ pages: [
+ {
+ id: "rolePage",
+ config: {
+ autoSubmit: true,
+ },
+ elements: [
+ {
+ id: "hardestPartInUserResearch",
+ type: "radio",
+ label: "The hardest part about user research is...",
+ /* help: "Helps us focus on what you need most.", */
+ name: "hardestPartInUserResearch",
+ options: [
+ { label: "Not sure where to start", value: "notSureWhereToStart" },
+ {
+ label: "Unresponsive users",
+ value: "unresponsiveUsers",
+ },
+ { label: "Small user base", value: "smallUserBase" },
+ { label: "Doing it consistently", value: "consistency" },
+ { label: "Implementing methods", value: "Implementation" },
+ ],
+ component: IconRadio,
+ },
+ ],
+ },
+ {
+ id: "targetGroupPage",
+ config: {
+ autoSubmit: true,
+ },
+ elements: [
+ {
+ id: "lastUserContact",
+ type: "radio",
+ label: "When was the last time you talked to one of your users?",
+ /* help: "(honest answers only)", */
+ name: "lastUserContact",
+ options: [
+ { label: "Today", value: "today" },
+ {
+ label: "Yesterday",
+ value: "yesterday",
+ },
+ { label: "This week", value: "thisWeek" },
+ { label: "This month", value: "thisMonth" },
+ { label: "I should do that more often", value: "iShouldDoThatMoreOften" },
+ ],
+ component: IconRadio,
+ },
+ ],
+ },
+ {
+ id: "onboardingDone",
+ endScreen: true,
+ elements: [
+ {
+ id: "forward",
+ type: "html",
+ component: ForwardToApp,
+ },
+ ],
+ },
+ ],
+ }}
+ />
+ );
+};
+
+export default OnboardingSurvey;
diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts
index d140c2155d..0fc492e63f 100644
--- a/apps/web/src/pages/api/auth/[...nextauth].ts
+++ b/apps/web/src/pages/api/auth/[...nextauth].ts
@@ -7,6 +7,7 @@ import CredentialsProvider from "next-auth/providers/credentials";
import GitHubProvider from "next-auth/providers/github";
import { verifyPassword } from "../../../lib/auth";
import { verifyToken } from "../../../lib/jwt";
+import { type } from "os";
export const authOptions: NextAuthOptions = {
providers: [
@@ -132,6 +133,7 @@ export const authOptions: NextAuthOptions = {
select: {
id: true,
name: true,
+ finishedOnboarding: true,
},
});
@@ -140,14 +142,17 @@ export const authOptions: NextAuthOptions = {
}
return {
- ...existingUser,
...token,
+ ...existingUser,
};
},
async session({ session, token }) {
// @ts-ignore
session.user.id = token.id;
session.user.name = token.name;
+ if (typeof token.finishedOnboarding == "boolean") {
+ session.user.finishedOnboarding = token.finishedOnboarding;
+ }
return session;
},
diff --git a/apps/web/src/pages/api/capture/forms/[formId]/submissions/index.ts b/apps/web/src/pages/api/capture/forms/[formId]/submissions/index.ts
index c2bf8530b7..d9bd13aa85 100644
--- a/apps/web/src/pages/api/capture/forms/[formId]/submissions/index.ts
+++ b/apps/web/src/pages/api/capture/forms/[formId]/submissions/index.ts
@@ -44,22 +44,40 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
const customerEmail = submission.customer.email;
const customerData = { ...submission.customer };
delete customerData.email;
- // create or link customer
- event.data.customer = {
- connectOrCreate: {
+ const existingCustomer = await prisma.customer.findUnique({
+ where: {
+ email_organisationId: {
+ email: submission.customer.email,
+ organisationId: form.organisationId,
+ },
+ },
+ });
+ if (existingCustomer) {
+ // update customer
+ await prisma.customer.update({
where: {
email_organisationId: {
email: submission.customer.email,
organisationId: form.organisationId,
},
},
+ data: {
+ data: { ...existingCustomer.data, ...customerData },
+ },
+ });
+ event.data.customer = {
+ connect: { organisationId_email: { email: customerEmail, organisationId: form.organisationId } },
+ };
+ } else {
+ // create customer
+ event.data.customer = {
create: {
email: customerEmail,
organisation: { connect: { id: form.organisationId } },
data: customerData,
},
- },
- };
+ };
+ }
}
// create form in db
@@ -67,7 +85,9 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
if (submission.finished) {
pipelineEvents.push("submissionFinished");
}
+ // create submission
const submissionResult = await prisma.submission.create(event);
+ // run pipelines
await runPipelines(pipelineEvents, form, submission, submissionResult);
// tracking
capturePosthogEvent(form.organisationId, "submission received", {
diff --git a/apps/web/src/pages/api/users/me/index.ts b/apps/web/src/pages/api/users/me/index.ts
index 4b38973362..00eb5790e7 100644
--- a/apps/web/src/pages/api/users/me/index.ts
+++ b/apps/web/src/pages/api/users/me/index.ts
@@ -26,6 +26,16 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
},
});
return res.json(user);
+ } // GET /api/users/me
+ // Get the current user
+ else if (req.method === "PUT") {
+ const user = await prisma.user.update({
+ where: {
+ email: session.email,
+ },
+ data: req.body,
+ });
+ return res.json(user);
}
// Unknown HTTP Method
diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx
index dfef861c94..12a59e5572 100644
--- a/apps/web/src/pages/index.tsx
+++ b/apps/web/src/pages/index.tsx
@@ -1,35 +1,12 @@
"use client";
+import BasePathPage from "@/components/BasePathPage";
import LayoutApp from "@/components/layout/LayoutApp";
-import { useMemberships } from "@/lib/memberships";
-import { useSession } from "next-auth/react";
-import { useRouter } from "next/navigation";
-import { useEffect } from "react";
-import LoadingSpinner from "@/components/LoadingSpinner";
export default function ProjectsPage() {
- const { data: session } = useSession();
- const { memberships, isErrorMemberships } = useMemberships();
- const router = useRouter();
-
- useEffect(() => {
- if (session && memberships && memberships.length > 0) {
- const organisationId = memberships[0].organisationId;
- router.push(`/organisations/${organisationId}/forms`);
- }
- if (!session) {
- router.push(`/auth/signin?callbackUrl=${encodeURIComponent(window.location.href)}`);
- }
- }, [memberships, router, session]);
-
- if (isErrorMemberships) {
- return Something went wrong...
;
- }
return (
-
-
-
+
);
}
diff --git a/apps/web/src/pages/me/onboarding/index.tsx b/apps/web/src/pages/me/onboarding/index.tsx
new file mode 100644
index 0000000000..6c38523d12
--- /dev/null
+++ b/apps/web/src/pages/me/onboarding/index.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+import LayoutApp from "@/components/layout/LayoutApp";
+import OnboardingPage from "@/components/onboarding/OnboardingPage";
+
+export default function Verify() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/types/next-auth.d.ts b/apps/web/src/types/next-auth.d.ts
new file mode 100644
index 0000000000..f56e43bffc
--- /dev/null
+++ b/apps/web/src/types/next-auth.d.ts
@@ -0,0 +1,16 @@
+import NextAuth from "next-auth";
+
+declare module "next-auth" {
+ /**
+ * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
+ */
+ interface Session {
+ user: {
+ /** The user's postal address. */
+ email: string;
+ name: string;
+ finishedOnboarding: boolean;
+ image?: StaticImageData;
+ };
+ }
+}
diff --git a/packages/database/prisma/migrations/20230207100310_add_finished_onboarding_flag_to_user_model/migration.sql b/packages/database/prisma/migrations/20230207100310_add_finished_onboarding_flag_to_user_model/migration.sql
new file mode 100644
index 0000000000..987f216201
--- /dev/null
+++ b/packages/database/prisma/migrations/20230207100310_add_finished_onboarding_flag_to_user_model/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "finishedOnboarding" BOOLEAN NOT NULL DEFAULT false;
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 7bc282624f..248a184483 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -164,4 +164,5 @@ model User {
organisations Membership[]
accounts Account[]
apiKeys ApiKey[]
+ finishedOnboarding Boolean @default(false)
}
diff --git a/packages/engine-react/src/components/EnginePage.tsx b/packages/engine-react/src/components/EnginePage.tsx
index b6474fc04d..d94dc2d326 100644
--- a/packages/engine-react/src/components/EnginePage.tsx
+++ b/packages/engine-react/src/components/EnginePage.tsx
@@ -1,4 +1,3 @@
-import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { FormPage } from "../types";
@@ -7,13 +6,15 @@ interface FormProps {
page: FormPage;
onSkip: () => void;
onPageSubmit: (submission: any) => void;
- onFinished: ({ submission }: any) => void;
+ onFinished: ({ submission, schema }: any) => void;
submission: any;
setSubmission: (v: any) => void;
finished: boolean;
- formbricksUrl: string;
- formId: string;
+ formbricksUrl?: string;
+ formId?: string;
schema: any;
+ customer: any;
+ offline?: boolean;
}
export function EnginePage({
@@ -26,6 +27,8 @@ export function EnginePage({
formbricksUrl,
formId,
schema,
+ customer,
+ offline,
}: FormProps) {
const [submissionId, setSubmissionId] = useState();
const {
@@ -44,17 +47,34 @@ export function EnginePage({
useEffect(() => {
if (page.endScreen) {
- fetch(`${formbricksUrl}/api/capture/forms/${formId}/submissions/${submissionId}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ finished: true }),
- });
- onFinished({ submission });
+ if (!offline) {
+ if (!formbricksUrl) {
+ throw new Error("Formbricks URL not provided");
+ }
+ if (!formId) {
+ throw new Error("Form ID not provided");
+ }
+ fetch(`${formbricksUrl}/api/capture/forms/${formId}/submissions/${submissionId}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ finished: true }),
+ });
+ }
+ onFinished({ submission, schema });
}
}, [page, formId, formbricksUrl, submissionId]);
const sendToFormbricks = async (partialSubmission: any) => {
- const submissionBody: any = { data: partialSubmission };
+ if (offline) {
+ return;
+ }
+ if (!formbricksUrl) {
+ throw new Error("Formbricks URL not provided");
+ }
+ if (!formId) {
+ throw new Error("Form ID not provided");
+ }
+ const submissionBody: any = { data: partialSubmission, customer };
if (page.config?.addFieldsToCustomer && Array.isArray(page.config?.addFieldsToCustomer)) {
for (const field of page.config?.addFieldsToCustomer) {
if (field in partialSubmission) {
diff --git a/packages/engine-react/src/components/FormbricksEngine.tsx b/packages/engine-react/src/components/FormbricksEngine.tsx
index 0875f2b272..a9b17a7fe4 100644
--- a/packages/engine-react/src/components/FormbricksEngine.tsx
+++ b/packages/engine-react/src/components/FormbricksEngine.tsx
@@ -4,18 +4,22 @@ import { EnginePage } from "./EnginePage";
interface FormProps {
schema: Form;
- formbricksUrl: string;
- formId: string;
+ formbricksUrl?: string;
+ formId?: string;
+ customer?: any;
onFinished?: ({ submission }: any) => void;
onPageSubmit?: ({ page }: any) => void;
+ offline?: boolean;
}
export function FormbricksEngine({
schema,
formbricksUrl,
formId,
+ customer = {},
onFinished = () => {},
onPageSubmit = () => {},
+ offline = false,
}: FormProps) {
if (!schema) {
console.error("Formbricks Engine: No form provided");
@@ -77,6 +81,8 @@ export function FormbricksEngine({
formbricksUrl={formbricksUrl}
formId={formId}
schema={cleanedSchema}
+ customer={customer}
+ offline={offline}
/>
);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 351a235049..f7d7d0ada0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -188,7 +188,7 @@ importers:
'@formbricks/charts': workspace:*
'@formbricks/database': workspace:*
'@formbricks/ee': workspace:*
- '@formbricks/react': workspace:*
+ '@formbricks/engine-react': workspace:*
'@formbricks/tailwind-config': workspace:*
'@formbricks/tsconfig': workspace:*
'@formbricks/ui': workspace:*
@@ -224,7 +224,7 @@ importers:
dependencies:
'@formbricks/charts': link:../../packages/charts
'@formbricks/ee': link:../../packages/ee
- '@formbricks/react': link:../../packages/react
+ '@formbricks/engine-react': link:../../packages/engine-react
'@formbricks/ui': link:../../packages/ui
'@headlessui/react': 1.7.8_biqbaboplfbrettd7655fr4n2y
'@heroicons/react': 2.0.14_react@18.2.0