chore: Streamline Onboarding (#2928)

This commit is contained in:
Johannes
2024-07-30 12:47:31 +02:00
committed by GitHub
parent ec70c6c613
commit d60dd5f281
14 changed files with 50 additions and 125 deletions

View File

@@ -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>
);

View File

@@ -103,7 +103,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
e.preventDefault();
finishOnboarding();
}}>
Skip
Not now
</Button>
<Button
id="onboarding-inapp-invite-send-invite"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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";
};

View File

@@ -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 && (

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -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"
/>
)}

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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();

View File

@@ -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/);

View File

@@ -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}>