mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 21:32:02 -06:00
chore: Streamline Onboarding (#2928)
This commit is contained in:
@@ -73,17 +73,20 @@ export const ConnectWithFormbricks = ({
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Image src={Lost} alt="lost" height={250} />
|
||||
<p className="pt-4 text-slate-400">Waiting for your signal...</p>
|
||||
<p className="animate-pulse pt-4 text-sm font-semibold text-slate-700">
|
||||
Waiting for your signal...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
id="finishOnboarding"
|
||||
className="text-slate-400 hover:text-slate-700"
|
||||
variant={widgetSetupCompleted ? "darkCTA" : "minimal"}
|
||||
onClick={handleFinishOnboarding}
|
||||
EndIcon={ArrowRight}>
|
||||
{widgetSetupCompleted ? "Finish Onboarding" : "Skip"}
|
||||
{widgetSetupCompleted ? "Finish Onboarding" : "I don't know how to do it"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -103,7 +103,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
e.preventDefault();
|
||||
finishOnboarding();
|
||||
}}>
|
||||
Skip
|
||||
Not now
|
||||
</Button>
|
||||
<Button
|
||||
id="onboarding-inapp-invite-send-invite"
|
||||
|
||||
@@ -32,8 +32,8 @@ const Page = async ({ params }: InvitePageProps) => {
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center">
|
||||
<Header
|
||||
title="Invite your organization to help out"
|
||||
subtitle="Ask your tech-savvy co-worker to finish the setup:"
|
||||
title="Who is your favorite engineer?"
|
||||
subtitle="Invite your tech-savvy co-worker to help with the setup 🤓"
|
||||
/>
|
||||
<div className="space-y-4 text-center">
|
||||
<p className="text-4xl font-medium text-slate-800"></p>
|
||||
|
||||
@@ -38,7 +38,7 @@ const Page = async ({ params }: ConnectPageProps) => {
|
||||
<div className="flex min-h-full flex-col items-center justify-center py-10">
|
||||
<Header
|
||||
title={`Let's connect your ${customHeadline} with Formbricks`}
|
||||
subtitle="If you don't do it now, chances are low that you will ever do it!"
|
||||
subtitle="It takes less than 4 minutes, pinky promise!"
|
||||
/>
|
||||
<div className="space-y-4 text-center">
|
||||
<p className="text-4xl font-medium text-slate-800"></p>
|
||||
|
||||
@@ -4,8 +4,10 @@ export const getCustomHeadline = (channel: TProductConfigChannel, industry: TPro
|
||||
const combinations = {
|
||||
"website+eCommerce": "web shop",
|
||||
"website+saas": "landing page",
|
||||
"website+other": "website",
|
||||
"app+eCommerce": "shopping app",
|
||||
"app+saas": "SaaS app",
|
||||
"app+other": "app",
|
||||
};
|
||||
return combinations[`${channel}+${industry}`] || "app";
|
||||
return combinations[`${channel}+${industry}`] || "product";
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { CircleUserRoundIcon, EarthIcon, SendHorizonalIcon, XIcon } from "lucide-react";
|
||||
import { CircleUserRoundIcon, EarthIcon, LinkIcon, XIcon } from "lucide-react";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
@@ -14,24 +14,24 @@ const Page = async ({ params }: ChannelPageProps) => {
|
||||
const channelOptions = [
|
||||
{
|
||||
title: "Public website",
|
||||
description: "Display surveys on public websites, well timed and targeted.",
|
||||
description: "Run well-timed pop-up surveys.",
|
||||
icon: EarthIcon,
|
||||
iconText: "Built for scale",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=website`,
|
||||
},
|
||||
{
|
||||
title: "App with sign up",
|
||||
description: "Run highly targeted surveys with any user cohort.",
|
||||
description: "Run highly-targeted micro-surveys.",
|
||||
icon: CircleUserRoundIcon,
|
||||
iconText: "Enrich user profiles",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=app`,
|
||||
},
|
||||
{
|
||||
channel: "link",
|
||||
title: "Anywhere online",
|
||||
description: "Create link and email surveys, reach your people anywhere.",
|
||||
icon: SendHorizonalIcon,
|
||||
iconText: "100% custom branding",
|
||||
title: "Link & email surveys",
|
||||
description: "Reach people anywhere online.",
|
||||
icon: LinkIcon,
|
||||
iconText: "Anywhere online",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=link`,
|
||||
},
|
||||
];
|
||||
@@ -42,7 +42,7 @@ const Page = async ({ params }: ChannelPageProps) => {
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header
|
||||
title="Where do you want to survey people?"
|
||||
subtitle="Get started with proven best practices 🚀"
|
||||
subtitle="Run surveys on public websites, in your app, or with shareable links & emails."
|
||||
/>
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{products.length >= 1 && (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { HeartIcon, MonitorIcon, ShoppingCart, XIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -27,32 +26,33 @@ const Page = async ({ params, searchParams }: IndustryPageProps) => {
|
||||
const industryOptions = [
|
||||
{
|
||||
title: "E-Commerce",
|
||||
description: "Implement proven best practices to understand why people buy.",
|
||||
description: "Understand why people buy.",
|
||||
icon: ShoppingCart,
|
||||
iconText: "B2B and B2C",
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=eCommerce`,
|
||||
},
|
||||
{
|
||||
title: "SaaS",
|
||||
description: "Gather contextualized feedback to improve product-market fit.",
|
||||
description: "Improve product-market fit.",
|
||||
icon: MonitorIcon,
|
||||
iconText: "Proven methods",
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=saas`,
|
||||
},
|
||||
{
|
||||
title: "Other",
|
||||
description: "Universal Formbricks experience with features for every industry.",
|
||||
description: "Listen to your customers.",
|
||||
icon: HeartIcon,
|
||||
iconText: "Customer insights",
|
||||
href: IS_FORMBRICKS_CLOUD
|
||||
? `/organizations/${params.organizationId}/products/new/survey?channel=${channel}&industry=other`
|
||||
: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=other`,
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=other`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title="Which industry do you work for?" subtitle="Get started with proven best practices 🚀" />
|
||||
<Header
|
||||
title="Which industry do you work for?"
|
||||
subtitle="Get started with battle-tested best practices."
|
||||
/>
|
||||
<OnboardingOptionsContainer options={industryOptions} />
|
||||
{products.length >= 1 && (
|
||||
<Button
|
||||
|
||||
@@ -97,7 +97,7 @@ export const ProductSettings = ({
|
||||
<FormItem className="w-full space-y-4">
|
||||
<div>
|
||||
<FormLabel>Brand color</FormLabel>
|
||||
<FormDescription>Change the brand color of the survey.</FormDescription>
|
||||
<FormDescription>Match the main color of surveys with your brand.</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<div>
|
||||
@@ -118,9 +118,9 @@ export const ProductSettings = ({
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem className="w-full space-y-4">
|
||||
<div>
|
||||
<FormLabel>Product Name</FormLabel>
|
||||
<FormLabel>Product name</FormLabel>
|
||||
<FormDescription>
|
||||
What is your {getCustomHeadline(channel, industry)} called ?
|
||||
What is your {getCustomHeadline(channel, industry)} called?
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -128,7 +128,7 @@ export const ProductSettings = ({
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(name) => field.onChange(name)}
|
||||
placeholder="Formbricks Merch Store"
|
||||
placeholder="e.g. Formbricks"
|
||||
className="bg-white"
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,7 @@ const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
|
||||
/>
|
||||
) : (
|
||||
<Header
|
||||
title={`You run ${startsWithVowel(customHeadline) ? "an " + customHeadline : "a " + customHeadline}, how exciting!`}
|
||||
title={`You maintain ${startsWithVowel(customHeadline) ? "an " + customHeadline : "a " + customHeadline}, how exciting!`}
|
||||
subtitle="Get 2x more responses matching surveys with your brand and UI"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
|
||||
interface OnboardingSurveyProps {
|
||||
organizationId: string;
|
||||
channel: TProductConfigChannel;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const OnboardingSurvey = ({ organizationId, channel, userId }: OnboardingSurveyProps) => {
|
||||
const [isIFrameVisible, setIsIFrameVisible] = useState(false);
|
||||
const [fadeout, setFadeout] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleMessageEvent = (event: MessageEvent) => {
|
||||
if (event.data === "formbricksSurveyCompleted") {
|
||||
setFadeout(true); // Start fade-out
|
||||
setTimeout(() => {
|
||||
router.push(
|
||||
`/organizations/${organizationId}/products/new/settings?channel=${channel}&industry=other`
|
||||
);
|
||||
}, 800); // Delay the navigation until fade-out completes
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isIFrameVisible) {
|
||||
window.addEventListener("message", handleMessageEvent, false);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessageEvent, false);
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isIFrameVisible]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`overflow relative flex h-[100vh] flex-col items-center justify-center ${fadeout ? "opacity-0 transition-opacity duration-1000" : "opacity-100"}`}>
|
||||
<iframe
|
||||
onLoad={() => setIsIFrameVisible(true)}
|
||||
src={`https://app.formbricks.com/s/clxcwr22p0cwlpvgekzdab2x5?userId=${userId}`}
|
||||
className="absolute left-0 top-0 h-full w-full overflow-visible border-0"></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { OnboardingSurvey } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/survey/components/OnboardingSurvey";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
|
||||
interface OnboardingSurveyPageProps {
|
||||
params: {
|
||||
organizationId: string;
|
||||
};
|
||||
searchParams: {
|
||||
channel?: TProductConfigChannel;
|
||||
industry?: TProductConfigIndustry;
|
||||
};
|
||||
}
|
||||
|
||||
const Page = async ({ params, searchParams }: OnboardingSurveyPageProps) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
const channel = searchParams.channel;
|
||||
const industry = searchParams.industry;
|
||||
if (!channel || !industry) return notFound();
|
||||
|
||||
return (
|
||||
<OnboardingSurvey organizationId={params.organizationId} channel={channel} userId={session.user.id} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -11,11 +11,11 @@ test.describe("Onboarding Flow Test", async () => {
|
||||
|
||||
await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/channel/);
|
||||
|
||||
await page.getByRole("button", { name: "100% custom branding Anywhere" }).click();
|
||||
await page.getByRole("button", { name: "Anywhere online Link" }).click();
|
||||
await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click();
|
||||
await page.getByPlaceholder("Formbricks Merch Store").click();
|
||||
await page.getByPlaceholder("Formbricks Merch Store").fill(productName);
|
||||
await page.locator("form").filter({ hasText: "Brand colorChange the brand" }).getByRole("button").click();
|
||||
await page.getByPlaceholder("e.g. Formbricks").click();
|
||||
await page.getByPlaceholder("e.g. Formbricks").fill(productName);
|
||||
await page.locator("form").filter({ hasText: "Brand colorMatch the main" }).getByRole("button").click();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await expect(page.getByText(productName)).toBeVisible();
|
||||
@@ -29,12 +29,12 @@ test.describe("Onboarding Flow Test", async () => {
|
||||
|
||||
await page.getByRole("button", { name: "Enrich user profiles App with" }).click();
|
||||
await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click();
|
||||
await page.getByPlaceholder("Formbricks Merch Store").click();
|
||||
await page.getByPlaceholder("Formbricks Merch Store").fill(productName);
|
||||
await page.locator("form").filter({ hasText: "Brand colorChange the brand" }).getByRole("button").click();
|
||||
await page.getByRole("button", { name: "Skip" }).click();
|
||||
await page.getByPlaceholder("e.g. Formbricks").click();
|
||||
await page.getByPlaceholder("e.g. Formbricks").fill(productName);
|
||||
await page.locator("form").filter({ hasText: "Brand colorMatch the main" }).getByRole("button").click();
|
||||
await page.getByRole("button", { name: "I don't know how to do it" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/connect\/invite/);
|
||||
await page.getByRole("button", { name: "Skip" }).click();
|
||||
await page.getByRole("button", { name: "Not now" }).click();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await expect(page.getByText(productName)).toBeVisible();
|
||||
|
||||
@@ -86,18 +86,18 @@ export const finishOnboarding = async (
|
||||
} else if (ProductChannel === "app") {
|
||||
await page.getByRole("button", { name: "Enrich user profiles App with" }).click();
|
||||
} else {
|
||||
await page.getByRole("button", { name: "100% custom branding Anywhere" }).click();
|
||||
await page.getByRole("button", { name: "Anywhere online Link" }).click();
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "Proven methods SaaS" }).click();
|
||||
await page.getByPlaceholder("Formbricks Merch Store").click();
|
||||
await page.getByPlaceholder("Formbricks Merch Store").fill("My Product");
|
||||
await page.locator("form").filter({ hasText: "Brand colorChange the brand" }).getByRole("button").click();
|
||||
await page.getByPlaceholder("e.g. Formbricks").click();
|
||||
await page.getByPlaceholder("e.g. Formbricks").fill("My Product");
|
||||
await page.locator("form").filter({ hasText: "Brand colorMatch the main" }).getByRole("button").click();
|
||||
|
||||
if (ProductChannel !== "link") {
|
||||
await page.getByRole("button", { name: "Skip" }).click();
|
||||
await page.getByRole("button", { name: "I don't know how to do it" }).click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByRole("button", { name: "Skip" }).click();
|
||||
await page.getByRole("button", { name: "Not now" }).click();
|
||||
}
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
@@ -26,10 +26,10 @@ export const OptionCard: React.FC<PathwayOptionProps> = ({
|
||||
loading,
|
||||
cssId,
|
||||
}) => (
|
||||
<div className="relative">
|
||||
<div className="relative h-full">
|
||||
<div
|
||||
id={cssId}
|
||||
className={`flex cursor-pointer flex-col items-center justify-center bg-white p-6 hover:scale-105 hover:border-slate-300 ${sizeClasses[size]}`}
|
||||
className={`flex h-full cursor-pointer flex-col items-center justify-center bg-white p-6 hover:scale-105 hover:border-slate-300 ${sizeClasses[size]}`}
|
||||
onClick={onSelect}
|
||||
role="button"
|
||||
tabIndex={0}>
|
||||
|
||||
Reference in New Issue
Block a user