Skip to content

Commit 31d7173

Browse files
committed
Dashboard: Show upsell card on support page for free plan (#7648)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR enhances the support functionality within the dashboard, particularly for teams on a free billing plan. It adds checks for the billing plan, modifies the `SupportHeader`, and integrates upsell content to encourage users to upgrade their plans. ### Detailed summary - `SupportHeader` now accepts `isFreePlan` prop to conditionally render buttons. - Redirects free plan users from creating support cases. - Displays upsell content for free plan users in the support page. - Added functionality to show/hide support forms based on billing plan. - Updated `CreateSupportCase` component to handle dialog state for support case creation. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Free plan users now see an upsell prompt encouraging upgrade to access the Support Center, with a clear list of benefits. * The Support Center title has been updated for improved clarity. * Support case creation is disabled for free plan users, showing an informative message with a billing link instead. * Conditional support action buttons are shown only for paid plans, adapting to the user's context. * **Improvements** * Enhanced visual styling and layout for benefit descriptions in upsell prompts. * Error messages in the Support Center now display detailed information for easier troubleshooting. * Dialogs for creating support cases now open and close with explicit user control for better interaction. * The chat button is hidden on the Support Center page to reduce UI clutter. * **Bug Fixes** * Free plan users are prevented from accessing the support case creation page. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a48329b commit 31d7173

File tree

9 files changed

+141
-54
lines changed

9 files changed

+141
-54
lines changed

apps/dashboard/src/@/components/blocks/upsell-wrapper.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { CrownIcon, LockIcon, SparklesIcon } from "lucide-react";
3+
import { CrownIcon, LockIcon } from "lucide-react";
44
import Link from "next/link";
55
import type React from "react";
66
import type { Team } from "@/api/team";
@@ -111,18 +111,15 @@ export function UpsellContent(props: {
111111
<CardContent className="space-y-6">
112112
{props.benefits && props.benefits.length > 0 && (
113113
<div className="space-y-3">
114-
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide">
115-
What you'll get:
114+
<h4 className="font-semibold text-foreground text-sm capitalize text-center">
115+
What you'll get
116116
</h4>
117-
<div className="grid gap-2">
117+
<div className="grid gap-1.5">
118118
{props.benefits.map((benefit) => (
119119
<div
120-
className="flex items-center gap-3"
120+
className="flex items-center justify-center gap-3 text-center text-balance"
121121
key={benefit.description}
122122
>
123-
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-accent">
124-
<SparklesIcon className="h-3 w-3 text-success-text" />
125-
</div>
126123
<span className="text-sm">{benefit.description}</span>
127124
{benefit.status === "soon" && (
128125
<Badge className="text-xs" variant="secondary">

apps/dashboard/src/@/components/chat/CustomChatButton.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { MessageCircleIcon, XIcon } from "lucide-react";
4+
import { useSelectedLayoutSegments } from "next/navigation";
45
import { useCallback, useRef, useState } from "react";
56
import { createThirdwebClient } from "thirdweb";
67
import type { Team } from "@/api/team";
@@ -21,11 +22,16 @@ export function CustomChatButton(props: {
2122
team: Team;
2223
clientId: string | undefined;
2324
}) {
25+
const layoutSegments = useSelectedLayoutSegments();
2426
const [isOpen, setIsOpen] = useState(false);
2527
const [hasBeenOpened, setHasBeenOpened] = useState(false);
2628
const closeModal = useCallback(() => setIsOpen(false), []);
2729
const ref = useRef<HTMLDivElement>(null);
2830

31+
if (layoutSegments[0] === "~" && layoutSegments[1] === "support") {
32+
return null;
33+
}
34+
2935
return (
3036
<>
3137
{/* Inline Button (not floating) */}

apps/dashboard/src/@/components/chat/CustomChats.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
DialogContent,
1515
DialogHeader,
1616
DialogTitle,
17-
DialogTrigger,
1817
} from "@/components/ui/dialog";
1918
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
2019
import { cn } from "@/lib/utils";
@@ -105,6 +104,8 @@ export function CustomChats(props: {
105104
chatScrollContainer.addEventListener("wheel", disableScroll);
106105
}, [setEnableAutoScroll, enableAutoScroll]);
107106

107+
const [showSupportFormDialog, setShowSupportFormDialog] = useState(false);
108+
108109
return (
109110
<div
110111
className="relative flex max-h-full flex-1 flex-col overflow-hidden"
@@ -149,18 +150,19 @@ export function CustomChats(props: {
149150
{/* Only show button/form if ticket not created */}
150151
{!supportTicketCreated && (
151152
<div className="mt-3 pl-12">
152-
<Dialog>
153-
<DialogTrigger asChild>
154-
<Button
155-
onClick={() => props.setShowSupportForm(true)}
156-
size="sm"
157-
variant="outline"
158-
className="rounded-full bg-card"
159-
>
160-
Create Support Case
161-
<ArrowRightIcon className="w-4 h-4 ml-2" />
162-
</Button>
163-
</DialogTrigger>
153+
<Dialog
154+
open={showSupportFormDialog}
155+
onOpenChange={setShowSupportFormDialog}
156+
>
157+
<Button
158+
onClick={() => setShowSupportFormDialog(true)}
159+
size="sm"
160+
variant="outline"
161+
className="rounded-full bg-card"
162+
>
163+
Create Support Case
164+
<ArrowRightIcon className="w-4 h-4 ml-2" />
165+
</Button>
164166

165167
<DialogContent className="p-0 bg-card">
166168
<DynamicHeight>
@@ -178,6 +180,9 @@ export function CustomChats(props: {
178180
productLabel={props.productLabel}
179181
setProductLabel={props.setProductLabel}
180182
conversationId={props.sessionId}
183+
closeForm={() => {
184+
setShowSupportFormDialog(false);
185+
}}
181186
onSuccess={() => {
182187
props.setShowSupportForm(false);
183188
props.setProductLabel("");

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/CreateSupportCase.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
DialogContent,
1313
DialogHeader,
1414
DialogTitle,
15-
DialogTrigger,
1615
} from "@/components/ui/dialog";
1716
import { TextShimmer } from "@/components/ui/text-shimmer";
1817
import { AutoResizeTextarea } from "@/components/ui/textarea";
@@ -153,6 +152,8 @@ export function CreateSupportCase(props: { team: Team; authToken: string }) {
153152
</div>
154153
);
155154

155+
const [showCreateCaseDialog, setShowCreateCaseDialog] = useState(false);
156+
156157
return (
157158
<div className="flex flex-col border bg-card rounded-xl mt-4">
158159
<div className="px-4 lg:px-12 py-4 lg:py-10 border-b border-dashed">
@@ -203,17 +204,19 @@ export function CreateSupportCase(props: { team: Team; authToken: string }) {
203204
!message.isSuccessMessage &&
204205
chatMessages.length > 2 && (
205206
<div className="mt-5">
206-
<Dialog>
207-
<DialogTrigger asChild>
208-
<Button
209-
size="sm"
210-
variant="outline"
211-
className="gap-2 rounded-full bg-background"
212-
>
213-
Create Support Case
214-
<ArrowRightIcon className="w-4 h-4" />
215-
</Button>
216-
</DialogTrigger>
207+
<Dialog
208+
open={showCreateCaseDialog}
209+
onOpenChange={setShowCreateCaseDialog}
210+
>
211+
<Button
212+
size="sm"
213+
variant="outline"
214+
className="gap-2 rounded-full bg-background"
215+
onClick={() => setShowCreateCaseDialog(true)}
216+
>
217+
Create Support Case
218+
<ArrowRightIcon className="w-4 h-4" />
219+
</Button>
217220
<DialogContent className="p-0 bg-card !gap-0">
218221
<DynamicHeight>
219222
<DialogHeader className="p-4 lg:p-6 border-b border-dashed space-y-1">
@@ -227,6 +230,9 @@ export function CreateSupportCase(props: { team: Team; authToken: string }) {
227230
</DialogHeader>
228231

229232
<SupportTicketForm
233+
closeForm={() => {
234+
setShowCreateCaseDialog(false);
235+
}}
230236
team={team}
231237
productLabel={productLabel}
232238
setProductLabel={setProductLabel}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportHeader.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,41 @@ import Link from "next/link";
33
import { useSelectedLayoutSegment } from "next/navigation";
44
import { Button } from "@/components/ui/button";
55

6-
export function SupportHeader(props: { teamSlug: string }) {
6+
export function SupportHeader(props: {
7+
teamSlug: string;
8+
isFreePlan: boolean;
9+
}) {
710
const layoutSegment = useSelectedLayoutSegment();
811

912
return (
1013
<div className="border-border border-b">
1114
<div className="container flex max-w-5xl flex-col items-start gap-3 py-9 md:flex-row md:items-center">
1215
<div className="flex-1">
1316
<h1 className="font-semibold text-3xl tracking-tight mb-1">
14-
Support
17+
Support Center
1518
</h1>
1619
<p className="text-sm text-muted-foreground">
1720
Create and view support cases for your projects
1821
</p>
1922
</div>
20-
{layoutSegment === null ? (
21-
<Button asChild className="rounded-full gap-2">
22-
<Link href={`/team/${props.teamSlug}/~/support/create`}>
23-
Create Case
24-
</Link>
25-
</Button>
26-
) : (
27-
<Button
28-
asChild
29-
className="rounded-full gap-2 bg-card"
30-
variant="outline"
31-
>
32-
<Link href={`/team/${props.teamSlug}/~/support`}>
33-
View All Cases
34-
</Link>
35-
</Button>
36-
)}
23+
{!props.isFreePlan &&
24+
(layoutSegment === null ? (
25+
<Button asChild className="rounded-full gap-2">
26+
<Link href={`/team/${props.teamSlug}/~/support/create`}>
27+
Create Case
28+
</Link>
29+
</Button>
30+
) : (
31+
<Button
32+
asChild
33+
className="rounded-full gap-2 bg-card"
34+
variant="outline"
35+
>
36+
<Link href={`/team/${props.teamSlug}/~/support`}>
37+
View All Cases
38+
</Link>
39+
</Button>
40+
))}
3741
</div>
3842
</div>
3943
);

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { LockIcon } from "lucide-react";
12
import dynamic from "next/dynamic";
3+
import Link from "next/link";
24
import { useRef, useState } from "react";
35
import { toast } from "sonner";
46
import { revalidatePathAction } from "@/actions/revalidate";
@@ -105,6 +107,7 @@ interface SupportTicketFormProps {
105107
setProductLabel: (val: string) => void;
106108
onSuccess?: () => void;
107109
conversationId?: string;
110+
closeForm: () => void;
108111
}
109112

110113
export function SupportTicketForm({
@@ -113,6 +116,7 @@ export function SupportTicketForm({
113116
setProductLabel,
114117
onSuccess,
115118
conversationId,
119+
closeForm,
116120
}: SupportTicketFormProps) {
117121
const [isSubmittingForm, setIsSubmittingForm] = useState(false);
118122
const formRef = useRef<HTMLFormElement>(null);
@@ -190,6 +194,37 @@ export function SupportTicketForm({
190194
}
191195
};
192196

197+
if (team.billingPlan === "free") {
198+
return (
199+
<div className="relative">
200+
<div className="px-4 lg:px-6 py-16 z-10 relative">
201+
<div className="flex justify-center">
202+
<div className="mb-4 border rounded-full p-3 bg-background">
203+
<LockIcon className="size-5 text-foreground" />
204+
</div>
205+
</div>
206+
<p className="text-sm text-foreground text-center mb-4">
207+
You are currently on the free plan. <br /> Please upgrade to a paid
208+
plan to create a support case.
209+
</p>
210+
211+
<div className="flex justify-center">
212+
<Button asChild size="sm">
213+
<Link
214+
href={`/team/${team.slug}/~/billing?showPlans=true`}
215+
onClick={() => {
216+
closeForm();
217+
}}
218+
>
219+
Upgrade Plan
220+
</Link>
221+
</Button>
222+
</div>
223+
</div>
224+
</div>
225+
);
226+
}
227+
193228
return (
194229
<form onSubmit={handleFormSubmit} ref={formRef}>
195230
<div className="p-4 lg:p-6 max-h-[60vh] overflow-y-auto">

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/create/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { notFound } from "next/navigation";
1+
import { notFound, redirect } from "next/navigation";
22
import { getAuthToken } from "@/api/auth-token";
33
import { getTeamBySlug } from "@/api/team";
44
import { CreateSupportCase } from "../_components/CreateSupportCase";
@@ -19,5 +19,9 @@ export default async function CreatePage(props: {
1919
notFound();
2020
}
2121

22+
if (team.billingPlan === "free") {
23+
redirect(`/team/${params.team_slug}/~/support`);
24+
}
25+
2226
return <CreateSupportCase authToken={token} team={team} />;
2327
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ export default async function Layout(props: {
1515

1616
return (
1717
<div className="flex grow flex-col">
18-
<SupportHeader teamSlug={team.slug} />
18+
<SupportHeader
19+
teamSlug={team.slug}
20+
isFreePlan={team.billingPlan === "free"}
21+
/>
1922
<div className="container max-w-5xl pt-6 pb-10 flex flex-col grow">
2023
{props.children}
2124
</div>

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { XIcon } from "lucide-react";
22
import { notFound } from "next/navigation";
33
import { getAuthToken } from "@/api/auth-token";
44
import { getTeamBySlug } from "@/api/team";
5+
import { UpsellContent } from "@/components/blocks/upsell-wrapper";
56
import { tryCatch } from "@/utils/try-catch";
67
import { SupportsCaseList } from "./_components/case-list";
78
import { getSupportTicketsByTeam } from "./apis/tickets";
@@ -22,6 +23,31 @@ export default async function Page(props: {
2223
notFound();
2324
}
2425

26+
if (team.billingPlan === "free") {
27+
return (
28+
<div className="flex flex-col grow justify-center items-center">
29+
<UpsellContent
30+
currentPlan={team.billingPlan}
31+
featureDescription="Create support cases for your projects"
32+
featureName="Support Center"
33+
requiredPlan="starter"
34+
teamSlug={params.team_slug}
35+
benefits={[
36+
{
37+
description:
38+
"Create & Manage all your support requests in one place",
39+
status: "available",
40+
},
41+
{
42+
description: "Personalized Assistance tailored to your use case",
43+
status: "available",
44+
},
45+
]}
46+
/>
47+
</div>
48+
);
49+
}
50+
2551
const supportedTicketsResult = await tryCatch(
2652
getSupportTicketsByTeam({
2753
authToken: token,
@@ -37,6 +63,7 @@ export default async function Page(props: {
3763
<XIcon className="size-5 text-muted-foreground" />
3864
</div>
3965
<p className="text-sm">Failed to load support tickets</p>
66+
<p>{supportedTicketsResult.error.message}</p>
4067
</div>
4168
</div>
4269
);

0 commit comments

Comments
 (0)