diff --git a/apps/dashboard/src/@/api/dedicated-support.ts b/apps/dashboard/src/@/api/dedicated-support.ts new file mode 100644 index 00000000000..748ce43b406 --- /dev/null +++ b/apps/dashboard/src/@/api/dedicated-support.ts @@ -0,0 +1,40 @@ +"use server"; +import "server-only"; + +import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken"; +import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs"; + +export async function createDedicatedSupportChannel( + teamIdOrSlug: string, + channelType: "slack" | "telegram", +): Promise<{ error: string | null }> { + const token = await getAuthToken(); + if (!token) { + return { error: "Unauthorized" }; + } + + const res = await fetch( + new URL( + `/v1/teams/${teamIdOrSlug}/dedicated-support-channel`, + NEXT_PUBLIC_THIRDWEB_API_HOST, + ), + { + body: JSON.stringify({ + type: channelType, + }), + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "POST", + }, + ); + if (!res.ok) { + const json = await res.json(); + return { + error: + json.error?.message ?? "Failed to create dedicated support channel.", + }; + } + return { error: null }; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/settings-cards/dedicated-support.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/settings-cards/dedicated-support.tsx new file mode 100644 index 00000000000..dc3981b6f59 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/settings-cards/dedicated-support.tsx @@ -0,0 +1,169 @@ +"use client"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { toast } from "sonner"; +import { createDedicatedSupportChannel } from "@/api/dedicated-support"; +import type { Team } from "@/api/team"; +import { SettingsCard } from "@/components/blocks/SettingsCard"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; + +const CHANNEL_TYPES = { + slack: "Slack", + telegram: "Telegram (Coming Soon)", +} as const; + +type ChannelType = keyof typeof CHANNEL_TYPES; + +export function TeamDedicatedSupportCard(props: { + team: Team; + isOwnerAccount: boolean; +}) { + const router = useDashboardRouter(); + const [selectedChannelType, setSelectedChannelType] = + useState("slack"); + + const isFeatureEnabled = + props.team.billingPlan === "scale" || props.team.billingPlan === "pro"; + + const createMutation = useMutation({ + mutationFn: async (params: { + teamId: string; + channelType: ChannelType; + }) => { + const res = await createDedicatedSupportChannel( + params.teamId, + params.channelType, + ); + if (res.error) { + throw new Error(res.error); + } + }, + onError: (error) => { + toast.error(error.message); + }, + onSuccess: () => { + toast.success( + "Dedicated support channel requested. Please check your email for an invite link shortly.", + ); + }, + }); + + const channelType = props.team.dedicatedSupportChannel?.type; + const channelName = props.team.dedicatedSupportChannel?.name; + + const hasDefaultTeamName = props.team.name.startsWith("Your Projects"); + + // Already set up. + if (channelType && channelName) { + return ( + +
+

+ Your dedicated support channel: #{channelName}{" "} + {CHANNEL_TYPES[channelType]} +

+
+
+ ); + } + + return ( + + Upgrade to the Scale or Pro plan to unlock this + feature. + + ) : hasDefaultTeamName ? ( + "Please update your team name before requesting a dedicated support channel." + ) : undefined + } + errorText={undefined} + header={{ + description: "Get a dedicated support channel with the thirdweb team.", + title: "Dedicated Support", + }} + noPermissionText={ + !props.isOwnerAccount + ? "Only team owners can request a dedicated support channel." + : undefined + } + saveButton={ + isFeatureEnabled + ? { + disabled: createMutation.isPending, + isPending: createMutation.isPending, + label: "Create Support Channel", + onClick: () => + createMutation.mutate({ + channelType: selectedChannelType, + teamId: props.team.id, + }), + } + : hasDefaultTeamName + ? { + disabled: false, + isPending: false, + label: "Update Team Name", + onClick: () => + router.push(`/team/${props.team.slug}/~/settings`), + } + : { + disabled: false, + isPending: false, + label: "Upgrade Plan", + onClick: () => + router.push( + `/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=scale`, + ), + } + } + > +
+ +
+

+ All current members of this team will be sent an invite link to their + email. You can invite other members later. +

+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx index 23e99525eb3..d4af7099e94 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx @@ -17,6 +17,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { Input } from "@/components/ui/input"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; +import { TeamDedicatedSupportCard } from "../_components/settings-cards/dedicated-support"; import { TeamDomainVerificationCard } from "../_components/settings-cards/domain-verification"; import { maxTeamNameLength, @@ -57,6 +58,10 @@ export function TeamGeneralSettingsPageUI(props: { isOwnerAccount={props.isOwnerAccount} teamId={props.team.id} /> +