Skip to content

Commit b41e8c9

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

File tree

5 files changed

+191
-0
lines changed

5 files changed

+191
-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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 { 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+
billingPlan: Team["billingPlan"];
26+
channelType?: ChannelType;
27+
channelName?: string;
28+
}
29+
30+
export function TeamDedicatedSupportCard({
31+
teamId,
32+
billingPlan,
33+
channelType,
34+
channelName,
35+
}: DedicatedSupportFormProps) {
36+
const [selectedChannelType, setSelectedChannelType] = useState<ChannelType>(
37+
CHANNEL_TYPES[0].value,
38+
);
39+
40+
const isFeatureEnabled = billingPlan === "scale" || billingPlan === "pro";
41+
42+
const createMutation = useMutation({
43+
mutationFn: async (params: {
44+
teamId: string;
45+
channelType: "slack" | "telegram";
46+
}) => {
47+
const res = await createDedicatedSupportChannel(
48+
params.teamId,
49+
params.channelType,
50+
);
51+
if (res.error) {
52+
throw new Error(res.error);
53+
}
54+
},
55+
onSuccess: () => {
56+
toast.success(
57+
"Dedicated support channel requested. Please check your email for an invite link shortly.",
58+
);
59+
},
60+
onError: (error) => {
61+
toast.error(error.message);
62+
},
63+
});
64+
65+
// Already set up.
66+
if (channelType && channelName) {
67+
return (
68+
<SettingsCard
69+
header={{
70+
title: "Dedicated Support",
71+
description:
72+
"Get a dedicated support channel with the thirdweb team.",
73+
}}
74+
errorText={undefined}
75+
noPermissionText={undefined}
76+
bottomText={undefined}
77+
>
78+
<div className="md:w-[450px]">
79+
<p className="text-muted-foreground text-sm">
80+
Your dedicated support channel: <strong>{channelName}</strong> on{" "}
81+
{CHANNEL_TYPES.find((c) => c.value === channelType)?.name}
82+
</p>
83+
</div>
84+
</SettingsCard>
85+
);
86+
}
87+
88+
const renderContent = () => {
89+
return (
90+
<>
91+
<div className="md:w-[450px]">
92+
<Select
93+
onValueChange={(val) => setSelectedChannelType(val as ChannelType)}
94+
value={selectedChannelType}
95+
disabled={!isFeatureEnabled}
96+
>
97+
<SelectTrigger>
98+
<SelectValue placeholder="Select Channel Type" />
99+
</SelectTrigger>
100+
<SelectContent>
101+
{CHANNEL_TYPES.map(({ name, value }) => (
102+
<SelectItem key={value} value={value}>
103+
{name}
104+
</SelectItem>
105+
))}
106+
</SelectContent>
107+
</Select>
108+
</div>
109+
<p className="mt-2 text-muted-foreground text-sm">
110+
All team members for this team will be sent an invite link to their
111+
email. You can invite other members later.
112+
</p>
113+
</>
114+
);
115+
};
116+
117+
return (
118+
<SettingsCard
119+
header={{
120+
title: "Dedicated Support",
121+
description: "Get a dedicated support channel with the thirdweb team.",
122+
}}
123+
errorText={undefined}
124+
noPermissionText={
125+
!isFeatureEnabled
126+
? "Upgrade your plan to enable this feature."
127+
: undefined
128+
}
129+
saveButton={{
130+
onClick: () =>
131+
createMutation.mutate({ teamId, channelType: selectedChannelType }),
132+
disabled: !isFeatureEnabled || createMutation.isPending,
133+
isPending: createMutation.isPending,
134+
label: "Create Support Channel",
135+
}}
136+
bottomText=""
137+
>
138+
{renderContent()}
139+
</SettingsCard>
140+
);
141+
}

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

Lines changed: 7 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,12 @@ export function TeamGeneralSettingsPageUI(props: {
5758
initialVerification={props.initialVerification}
5859
isOwnerAccount={props.isOwnerAccount}
5960
/>
61+
<TeamDedicatedSupportCard
62+
teamId={props.team.id}
63+
billingPlan={props.team.billingPlan}
64+
channelType={props.team.dedicatedSupportChannel?.type}
65+
channelName={props.team.dedicatedSupportChannel?.name}
66+
/>
6067

6168
<LeaveTeamCard teamName={props.team.name} leaveTeam={props.leaveTeam} />
6269
<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)