diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-1.webp b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-1.webp new file mode 100644 index 0000000000..6169f86383 Binary files /dev/null and b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-1.webp differ diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-2.webp b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-2.webp new file mode 100644 index 0000000000..f17d540c93 Binary files /dev/null and b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-2.webp differ diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-3.webp b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-3.webp new file mode 100644 index 0000000000..ea4e6217e4 Binary files /dev/null and b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-3.webp differ diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-4.webp b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-4.webp new file mode 100644 index 0000000000..f070e360ac Binary files /dev/null and b/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-4.webp differ diff --git a/apps/docs/app/developer-docs/integrations/slack/page.mdx b/apps/docs/app/developer-docs/integrations/slack/page.mdx index f0124d6d8f..8e9c4fe133 100644 --- a/apps/docs/app/developer-docs/integrations/slack/page.mdx +++ b/apps/docs/app/developer-docs/integrations/slack/page.mdx @@ -8,7 +8,10 @@ import LinkWithQuestions from "./images/link-with-questions.webp"; import ListLinkedSurveys from "./images/list-linked-surveys.webp"; import SlackAuth from "./images/slack-auth.webp"; import SlackConnected from "./images/slack-connected.webp"; - +import AddSlackBot1 from "./images/add-slack-bot-1.webp"; +import AddSlackBot2 from "./images/add-slack-bot-2.webp"; +import AddSlackBot3 from "./images/add-slack-bot-3.webp"; +import AddSlackBot4 from "./images/add-slack-bot-4.webp"; export const metadata = { title: "Slack", description: @@ -69,7 +72,34 @@ The slack integration allows you to automatically send responses to a Slack chan channel in the Slack workspace you integrated. -5. Now click on the "Link channel" button to link a Slack channel with Formbricks and a modal will open up. +5. In order to make your channel available in channel dropdown, you need to add formbricks integration bot to the channel you want to link. You can do this by going to channel settings -> Integrations -> Add apps -> Search for "Formbricks" -> Select the bot -> Add. + + + + + + +6. Now click on the "Link channel" button to link a Slack channel with Formbricks and a modal will open up. -6. Select the channel you want to link with Formbricks and the Survey. On doing so, you will be asked to select the questions' responses you want to feed in the Slack channel. Select the questions and click on the "Link Channel" button. +7. Select the channel you want to link with Formbricks and the Survey. On doing so, you will be asked to select the questions' responses you want to feed in the Slack channel. Select the questions and click on the "Link Channel" button. -7. On submitting, the modal will close and you will see the linked Slack channel in the list of linked Slack channels. +8. On submitting, the modal will close and you will see the linked Slack channel in the list of linked Slack channels. {responseCount > 0 && ( -
+
diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts index 69a53a2bb0..b3188cbc3d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts @@ -7,12 +7,12 @@ import { z } from "zod"; import { getSlackChannels } from "@formbricks/lib/slack/service"; import { ZId } from "@formbricks/types/common"; -const ZRefreshChannelsAction = z.object({ +const ZGetSlackChannelsAction = z.object({ environmentId: ZId, }); -export const refreshChannelsAction = authenticatedActionClient - .schema(ZRefreshChannelsAction) +export const getSlackChannelsAction = authenticatedActionClient + .schema(ZGetSlackChannelsAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx index 14047741e4..39907f4243 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx @@ -23,6 +23,8 @@ interface ManageIntegrationProps { React.SetStateAction<(TIntegrationSlackConfigData & { index: number }) | null> >; refreshChannels: () => void; + showReconnectButton: boolean; + handleSlackAuthorization: () => void; locale: TUserLocale; } @@ -33,6 +35,8 @@ export const ManageIntegration = ({ setIsConnected, setSelectedIntegration, refreshChannels, + showReconnectButton, + handleSlackAuthorization, locale, }: ManageIntegrationProps) => { const t = useTranslations(); @@ -70,7 +74,19 @@ export const ManageIntegration = ({ return (
-
+ {showReconnectButton && ( +
+

+ {t.rich("environments.integrations.slack.slack_reconnect_button_description", { + b: (chunks) => {chunks}, + })} +

+ +
+ )} +
diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx index b207f3faa8..cbbaab6649 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx @@ -1,11 +1,11 @@ "use client"; -import { refreshChannelsAction } from "@/app/(app)/environments/[environmentId]/integrations/slack/actions"; +import { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/integrations/slack/actions"; import { AddChannelMappingModal } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal"; import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration"; import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/slack/lib/slack"; import slackLogo from "@/images/slacklogo.png"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TEnvironment } from "@formbricks/types/environment"; import { TIntegrationItem } from "@formbricks/types/integration"; @@ -18,7 +18,6 @@ interface SlackWrapperProps { isEnabled: boolean; environment: TEnvironment; surveys: TSurvey[]; - channelsArray: TIntegrationItem[]; slackIntegration?: TIntegrationSlack; webAppUrl: string; attributeClasses: TAttributeClass[]; @@ -29,27 +28,38 @@ export const SlackWrapper = ({ isEnabled, environment, surveys, - channelsArray, slackIntegration, webAppUrl, attributeClasses, locale, }: SlackWrapperProps) => { const [isConnected, setIsConnected] = useState(slackIntegration ? slackIntegration.config?.key : false); - const [slackChannels, setSlackChannels] = useState(channelsArray); + const [slackChannels, setSlackChannels] = useState([]); const [isModalOpen, setModalOpen] = useState(false); + const [showReconnectButton, setShowReconnectButton] = useState(false); const [selectedIntegration, setSelectedIntegration] = useState< (TIntegrationSlackConfigData & { index: number }) | null >(null); - const refreshChannels = async () => { - const refreshChannelsResponse = await refreshChannelsAction({ environmentId: environment.id }); + const getSlackChannels = async () => { + const getSlackChannelsResponse = await getSlackChannelsAction({ environmentId: environment.id }); - if (refreshChannelsResponse?.data) { - setSlackChannels(refreshChannelsResponse.data); + if ( + getSlackChannelsResponse?.serverError && + getSlackChannelsResponse.serverError.includes("missing_scope") + ) { + setShowReconnectButton(true); + } + + if (getSlackChannelsResponse?.data) { + setSlackChannels(getSlackChannelsResponse.data); } }; + useEffect(() => { + getSlackChannels(); + }, []); + const handleSlackAuthorization = async () => { authorize(environment.id, webAppUrl).then((url: string) => { if (url) { @@ -76,7 +86,9 @@ export const SlackWrapper = ({ setOpenAddIntegrationModal={setModalOpen} setIsConnected={setIsConnected} setSelectedIntegration={setSelectedIntegration} - refreshChannels={refreshChannels} + refreshChannels={getSlackChannels} + showReconnectButton={showReconnectButton} + handleSlackAuthorization={handleSlackAuthorization} locale={locale} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx index 10687c64c8..2fb8ee989b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx @@ -12,10 +12,8 @@ import { getIntegrationByType } from "@formbricks/lib/integration/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; -import { getSlackChannels } from "@formbricks/lib/slack/service"; import { getSurveys } from "@formbricks/lib/survey/service"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; -import { TIntegrationItem } from "@formbricks/types/integration"; import { TIntegrationSlack } from "@formbricks/types/integration/slack"; import { GoBackButton } from "@formbricks/ui/components/GoBackButton"; import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper"; @@ -45,10 +43,6 @@ const Page = async ({ params }) => { throw new Error(t("common.product_not_found")); } - let channelsArray: TIntegrationItem[] = []; - if (slackIntegration && slackIntegration.config.key) { - channelsArray = await getSlackChannels(params.environmentId); - } const locale = await findMatchingLocale(); const currentUserMembership = await getMembershipByUserIdOrganizationId( @@ -75,7 +69,6 @@ const Page = async ({ params }) => { { const url = req.url; @@ -58,18 +62,20 @@ export const GET = async (req: NextRequest) => { team: data.team, }; + const slackIntegration = await getIntegrationByType(environmentId, "slack"); + const slackConfiguration: TIntegrationSlackConfig = { - data: [], + data: (slackIntegration?.config.data as TIntegrationSlackConfigData[]) ?? [], key: slackCredentials, }; - const slackIntegration = { + const integration = { type: "slack" as "slack", environment: environmentId, config: slackConfiguration, }; - const result = await createOrUpdateIntegration(environmentId, slackIntegration); + const result = await createOrUpdateIntegration(environmentId, integration); if (result) { return Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/slack`); diff --git a/apps/web/lib/utils/action-client.ts b/apps/web/lib/utils/action-client.ts index 495f11562d..67c94f7774 100644 --- a/apps/web/lib/utils/action-client.ts +++ b/apps/web/lib/utils/action-client.ts @@ -22,7 +22,6 @@ export const actionClient = createSafeActionClient({ } console.error("SERVER ERROR: ", e); - return DEFAULT_SERVER_ERROR_MESSAGE; }, }); diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index fc557b7904..da1b192fc7 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -53,7 +53,7 @@ export const INVITE_DISABLED = env.INVITE_DISABLED === "1"; export const SLACK_CLIENT_SECRET = env.SLACK_CLIENT_SECRET; export const SLACK_CLIENT_ID = env.SLACK_CLIENT_ID; -export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize`; +export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read`; export const GOOGLE_SHEETS_CLIENT_ID = env.GOOGLE_SHEETS_CLIENT_ID; export const GOOGLE_SHEETS_CLIENT_SECRET = env.GOOGLE_SHEETS_CLIENT_SECRET; diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index 9f419483be..9c0b5759a9 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -685,7 +685,9 @@ "select_channel": "Kanal auswählen", "slack_integration": "Slack Integration", "slack_integration_description": "Sende Antworten direkt an Slack.", - "slack_integration_is_not_configured": "Slack Integration ist in deiner Instanz von Formbricks nicht konfiguriert." + "slack_integration_is_not_configured": "Slack Integration ist in deiner Instanz von Formbricks nicht konfiguriert.", + "slack_reconnect_button": "Erneut verbinden", + "slack_reconnect_button_description": "Hinweis: Wir haben kürzlich unsere Slack-Integration geändert, um auch private Kanäle zu unterstützen. Bitte verbinden Sie Ihren Slack-Workspace erneut." }, "slack_integration_description": "Verbinde deinen Slack Arbeitsbereich sofort mit Formbricks", "to_configure_it": "es zu konfigurieren.", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 5d937a2409..6dfb1a5449 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -685,7 +685,9 @@ "select_channel": "Select Channel", "slack_integration": "Slack Integration", "slack_integration_description": "Send responses directly to Slack.", - "slack_integration_is_not_configured": "Slack Integration is not configured in your instance of Formbricks." + "slack_integration_is_not_configured": "Slack Integration is not configured in your instance of Formbricks.", + "slack_reconnect_button": "Reconnect", + "slack_reconnect_button_description": "Note: We recently changed our Slack integration to also support private channels. Please reconnect your Slack workspace." }, "slack_integration_description": "Instantly connect your Slack Workspace with Formbricks", "to_configure_it": "to configure it.", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index 4c9ebf7374..1dcd7d4c57 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -685,7 +685,9 @@ "select_channel": "Selecionar Canal", "slack_integration": "Integração com o Slack", "slack_integration_description": "Manda as respostas direto pro Slack.", - "slack_integration_is_not_configured": "A integração do Slack não está configurada na sua instância do Formbricks." + "slack_integration_is_not_configured": "A integração do Slack não está configurada na sua instância do Formbricks.", + "slack_reconnect_button": "Reconectar", + "slack_reconnect_button_description": "Observação: Recentemente, alteramos nossa integração com o Slack para também suportar canais privados. Por favor, reconecte seu workspace do Slack." }, "slack_integration_description": "Conecte instantaneamente seu Workspace do Slack com o Formbricks", "to_configure_it": "configurar isso.", diff --git a/packages/lib/slack/service.ts b/packages/lib/slack/service.ts index 5adbef16dd..f76f893998 100644 --- a/packages/lib/slack/service.ts +++ b/packages/lib/slack/service.ts @@ -1,5 +1,5 @@ import { Prisma } from "@prisma/client"; -import { DatabaseError } from "@formbricks/types/errors"; +import { DatabaseError, UnknownError } from "@formbricks/types/errors"; import { TIntegration, TIntegrationItem } from "@formbricks/types/integration"; import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack"; import { deleteIntegration, getIntegrationByType } from "../integration/service"; @@ -11,8 +11,9 @@ export const fetchChannels = async (slackIntegration: TIntegration): Promise