Skip to content

Commit feb34be

Browse files
committed
feat: dedicated support self-serve
1 parent 5492331 commit feb34be

File tree

5 files changed

+211
-0
lines changed

5 files changed

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

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

apps/dashboard/src/stories/stubs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
110110
planCancellationDate: null,
111111
unthreadCustomerId: null,
112112
verifiedDomain: null,
113+
dedicatedSupportChannel: null,
113114
};
114115

115116
return team;

packages/service-utils/src/core/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ export type TeamResponse = {
147147
unthreadCustomerId: string | null;
148148
planCancellationDate: string | null;
149149
verifiedDomain: string | null;
150+
dedicatedSupportChannel: {
151+
type: "slack" | "telegram";
152+
name: string;
153+
} | null;
150154
};
151155

152156
export type ProjectSecretKey = {

0 commit comments

Comments
 (0)