Skip to content

Commit 9fe8022

Browse files
arcoravenjnsdls
authored andcommitted
feat: dedicated support self-serve
1 parent eeb672d commit 9fe8022

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-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+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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",
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+
// Already set up.
62+
if (channelType && channelName) {
63+
return (
64+
<SettingsCard
65+
bottomText={undefined}
66+
errorText={undefined}
67+
header={{
68+
description:
69+
"Get a dedicated support channel with the thirdweb team.",
70+
title: "Dedicated Support",
71+
}}
72+
noPermissionText={undefined}
73+
>
74+
<div className="md:w-[450px]">
75+
<p className="text-muted-foreground text-sm">
76+
Your dedicated support channel: #<strong>{channelName}</strong>{" "}
77+
{CHANNEL_TYPES[channelType]}
78+
</p>
79+
</div>
80+
</SettingsCard>
81+
);
82+
}
83+
84+
return (
85+
<SettingsCard
86+
bottomText={
87+
!isFeatureEnabled ? (
88+
<>
89+
Upgrade to the <b>Scale</b> or <b>Pro</b> plan to unlock this
90+
feature.
91+
</>
92+
) : undefined
93+
}
94+
errorText={undefined}
95+
header={{
96+
description: "Get a dedicated support channel with the thirdweb team.",
97+
title: "Dedicated Support",
98+
}}
99+
noPermissionText={
100+
!props.isOwnerAccount
101+
? "Only team owners can request a dedicated support channel."
102+
: undefined
103+
}
104+
saveButton={
105+
isFeatureEnabled
106+
? {
107+
disabled: createMutation.isPending,
108+
isPending: createMutation.isPending,
109+
label: "Create Support Channel",
110+
onClick: () =>
111+
createMutation.mutate({
112+
channelType: selectedChannelType,
113+
teamId: props.team.id,
114+
}),
115+
}
116+
: {
117+
disabled: false,
118+
isPending: false,
119+
label: "Upgrade Plan",
120+
onClick: () =>
121+
router.push(
122+
`/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=scale`,
123+
),
124+
}
125+
}
126+
>
127+
<div className="md:w-[450px]">
128+
<Select
129+
disabled={!isFeatureEnabled}
130+
onValueChange={(val) => setSelectedChannelType(val as ChannelType)}
131+
value={selectedChannelType}
132+
>
133+
<SelectTrigger>
134+
<SelectValue placeholder="Select Channel Type" />
135+
</SelectTrigger>
136+
<SelectContent>
137+
{Object.entries(CHANNEL_TYPES).map(([value, name]) => (
138+
<SelectItem key={value} value={value}>
139+
{name}
140+
</SelectItem>
141+
))}
142+
</SelectContent>
143+
</Select>
144+
</div>
145+
<p className="mt-2 text-muted-foreground text-sm">
146+
All current members of this team will be sent an invite link to their
147+
email. You can invite other members later.
148+
</p>
149+
</SettingsCard>
150+
);
151+
}

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)