Skip to content

Commit ff6e245

Browse files
authored
feat: dedicated support self-serve (#7381)
1 parent eeb672d commit ff6e245

File tree

3 files changed

+214
-0
lines changed

3 files changed

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

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
1717
import { Input } from "@/components/ui/input";
1818
import { useDashboardRouter } from "@/lib/DashboardRouter";
1919
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
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,10 @@ export function TeamGeneralSettingsPageUI(props: {
5758
isOwnerAccount={props.isOwnerAccount}
5859
teamId={props.team.id}
5960
/>
61+
<TeamDedicatedSupportCard
62+
isOwnerAccount={props.isOwnerAccount}
63+
team={props.team}
64+
/>
6065

6166
<LeaveTeamCard leaveTeam={props.leaveTeam} teamName={props.team.name} />
6267
<DeleteTeamCard

0 commit comments

Comments
 (0)