Skip to content

Commit 17cc431

Browse files
committed
feat: dedicated support self-serve
1 parent 6be9459 commit 17cc431

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use server";
2+
import "server-only";
3+
4+
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
5+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
6+
7+
export async function createDedicatedSupportChannel(
8+
teamIdOrSlug: string,
9+
channelType: "slack" | "telegram",
10+
): Promise<{ error: string | null }> {
11+
const token = await getAuthToken();
12+
if (!token) {
13+
return { error: "Unauthorized" };
14+
}
15+
16+
const res = await fetch(
17+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/dedicated-support-channel`,
18+
{
19+
method: "POST",
20+
body: JSON.stringify({
21+
type: channelType,
22+
}),
23+
headers: {
24+
"Content-Type": "application/json",
25+
Authorization: `Bearer ${token}`,
26+
},
27+
},
28+
);
29+
if (!res.ok) {
30+
const json = await res.json();
31+
return {
32+
error:
33+
json.error?.message ?? "Failed to create dedicated support channel.",
34+
};
35+
}
36+
return { error: null };
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"use client";
2+
import { createDedicatedSupportChannel } from "@/api/dedicated-support";
3+
import type { Team } from "@/api/team";
4+
import { SettingsCard } from "@/components/blocks/SettingsCard";
5+
import {
6+
Select,
7+
SelectContent,
8+
SelectItem,
9+
SelectTrigger,
10+
SelectValue,
11+
} from "@/components/ui/select";
12+
import { useDashboardRouter } from "@/lib/DashboardRouter";
13+
import { useMutation } from "@tanstack/react-query";
14+
import { useState } from "react";
15+
import { toast } from "sonner";
16+
17+
const CHANNEL_TYPES = [
18+
{ name: "Slack", value: "slack" },
19+
{ name: "Telegram", value: "telegram" },
20+
] as const;
21+
type ChannelType = (typeof CHANNEL_TYPES)[number]["value"];
22+
23+
interface DedicatedSupportFormProps {
24+
teamId: string;
25+
teamSlug: string;
26+
billingPlan: Team["billingPlan"];
27+
channelType?: ChannelType;
28+
channelName?: string;
29+
}
30+
31+
export function TeamDedicatedSupportCard({
32+
teamId,
33+
teamSlug,
34+
billingPlan,
35+
channelType,
36+
channelName,
37+
}: DedicatedSupportFormProps) {
38+
const router = useDashboardRouter();
39+
const [selectedChannelType, setSelectedChannelType] = useState<ChannelType>(
40+
CHANNEL_TYPES[0].value,
41+
);
42+
43+
const isFeatureEnabled = billingPlan === "scale" || billingPlan === "pro";
44+
45+
const createMutation = useMutation({
46+
mutationFn: async (params: {
47+
teamId: string;
48+
channelType: "slack" | "telegram";
49+
}) => {
50+
const res = await createDedicatedSupportChannel(
51+
params.teamId,
52+
params.channelType,
53+
);
54+
if (res.error) {
55+
throw new Error(res.error);
56+
}
57+
},
58+
onSuccess: () => {
59+
toast.success(
60+
"Dedicated support channel requested. Please check your email for an invite link shortly.",
61+
);
62+
},
63+
onError: (error) => {
64+
toast.error(error.message);
65+
},
66+
});
67+
68+
// Already set up.
69+
if (channelType && channelName) {
70+
return (
71+
<SettingsCard
72+
header={{
73+
title: "Dedicated Support",
74+
description:
75+
"Get a dedicated support channel with the thirdweb team.",
76+
}}
77+
errorText={undefined}
78+
noPermissionText={undefined}
79+
bottomText={undefined}
80+
>
81+
<div className="md:w-[450px]">
82+
<p className="text-muted-foreground text-sm">
83+
Your dedicated support channel: #<strong>{channelName}</strong> on{" "}
84+
{CHANNEL_TYPES.find((c) => c.value === channelType)?.name}
85+
</p>
86+
</div>
87+
</SettingsCard>
88+
);
89+
}
90+
91+
const renderContent = () => {
92+
return (
93+
<>
94+
<div className="md:w-[450px]">
95+
<Select
96+
onValueChange={(val) => setSelectedChannelType(val as ChannelType)}
97+
value={selectedChannelType}
98+
disabled={!isFeatureEnabled}
99+
>
100+
<SelectTrigger>
101+
<SelectValue placeholder="Select Channel Type" />
102+
</SelectTrigger>
103+
<SelectContent>
104+
{CHANNEL_TYPES.map(({ name, value }) => (
105+
<SelectItem key={value} value={value}>
106+
{name}
107+
</SelectItem>
108+
))}
109+
</SelectContent>
110+
</Select>
111+
</div>
112+
<p className="mt-2 text-muted-foreground text-sm">
113+
All team members for this team will be sent an invite link to their
114+
email. You can invite other members later.
115+
</p>
116+
</>
117+
);
118+
};
119+
120+
return (
121+
<SettingsCard
122+
header={{
123+
title: "Dedicated Support",
124+
description: "Get a dedicated support channel with the thirdweb team.",
125+
}}
126+
errorText={undefined}
127+
noPermissionText={undefined}
128+
saveButton={
129+
isFeatureEnabled
130+
? {
131+
label: "Create Support Channel",
132+
onClick: () =>
133+
createMutation.mutate({
134+
teamId,
135+
channelType: selectedChannelType,
136+
}),
137+
disabled: createMutation.isPending,
138+
isPending: createMutation.isPending,
139+
}
140+
: {
141+
label: "Upgrade Plan",
142+
onClick: () =>
143+
router.push(
144+
`/team/${teamSlug}/~/settings/billing?showPlans=true&highlight=scale`,
145+
),
146+
disabled: false,
147+
isPending: false,
148+
}
149+
}
150+
bottomText={
151+
!isFeatureEnabled
152+
? "Upgrade to the Scale plan to enable this feature."
153+
: undefined
154+
}
155+
>
156+
{renderContent()}
157+
</SettingsCard>
158+
);
159+
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useForm } from "react-hook-form";
1717
import { toast } from "sonner";
1818
import type { ThirdwebClient } from "thirdweb";
1919
import { z } from "zod";
20+
import { TeamDedicatedSupportCard } from "../_components/settings-cards/dedicated-support";
2021
import { TeamDomainVerificationCard } from "../_components/settings-cards/domain-verification";
2122
import {
2223
maxTeamNameLength,
@@ -57,6 +58,13 @@ export function TeamGeneralSettingsPageUI(props: {
5758
initialVerification={props.initialVerification}
5859
isOwnerAccount={props.isOwnerAccount}
5960
/>
61+
<TeamDedicatedSupportCard
62+
teamId={props.team.id}
63+
teamSlug={props.team.slug}
64+
billingPlan={props.team.billingPlan}
65+
channelType={props.team.dedicatedSupportChannel?.type}
66+
channelName={props.team.dedicatedSupportChannel?.name}
67+
/>
6068

6169
<LeaveTeamCard teamName={props.team.name} leaveTeam={props.leaveTeam} />
6270
<DeleteTeamCard

0 commit comments

Comments
 (0)