diff --git a/.env.example b/.env.example index 2441c96863..f67d8a0e6d 100644 --- a/.env.example +++ b/.env.example @@ -189,8 +189,9 @@ REDIS_URL=redis://localhost:6379 # The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this) # REDIS_HTTP_URL: -# INTERCOM_APP_ID= -# INTERCOM_SECRET_KEY= +# Chatwoot +# CHATWOOT_BASE_URL= +# CHATWOOT_WEBSITE_TOKEN= # Enable Prometheus metrics # PROMETHEUS_ENABLED= diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 65465c32a4..64b174a46f 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -1,5 +1,6 @@ import { getServerSession } from "next-auth"; -import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper"; +import { ChatwootWidget } from "@/app/chatwoot/ChatwootWidget"; +import { CHATWOOT_BASE_URL, CHATWOOT_WEBSITE_TOKEN, IS_CHATWOOT_CONFIGURED } from "@/lib/constants"; import { getUser } from "@/lib/user/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { ClientLogout } from "@/modules/ui/components/client-logout"; @@ -18,7 +19,15 @@ const AppLayout = async ({ children }) => { return ( <> - + {IS_CHATWOOT_CONFIGURED && ( + + )} {children} diff --git a/apps/web/app/(auth)/layout.tsx b/apps/web/app/(auth)/layout.tsx index ddebf022be..fe322623f6 100644 --- a/apps/web/app/(auth)/layout.tsx +++ b/apps/web/app/(auth)/layout.tsx @@ -1,11 +1,9 @@ -import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper"; import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay"; const AppLayout = async ({ children }) => { return ( <> - {children} ); diff --git a/apps/web/app/chatwoot/ChatwootWidget.tsx b/apps/web/app/chatwoot/ChatwootWidget.tsx new file mode 100644 index 0000000000..c175d373d0 --- /dev/null +++ b/apps/web/app/chatwoot/ChatwootWidget.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useCallback, useEffect, useRef } from "react"; + +interface ChatwootWidgetProps { + chatwootBaseUrl: string; + chatwootWebsiteToken?: string; + userEmail?: string | null; + userName?: string | null; + userId?: string | null; +} + +const CHATWOOT_SCRIPT_ID = "chatwoot-script"; + +export const ChatwootWidget = ({ + userEmail, + userName, + userId, + chatwootWebsiteToken, + chatwootBaseUrl, +}: ChatwootWidgetProps) => { + const userSetRef = useRef(false); + + const setUserInfo = useCallback(() => { + const $chatwoot = ( + globalThis as unknown as { + $chatwoot: { + setUser: (userId: string, userInfo: { email?: string | null; name?: string | null }) => void; + }; + } + ).$chatwoot; + if (userId && $chatwoot && !userSetRef.current) { + $chatwoot.setUser(userId, { + email: userEmail, + name: userName, + }); + userSetRef.current = true; + } + }, [userId, userEmail, userName]); + + useEffect(() => { + if (!chatwootWebsiteToken) return; + + const existingScript = document.getElementById(CHATWOOT_SCRIPT_ID); + if (existingScript) return; + + const script = document.createElement("script"); + script.src = `${chatwootBaseUrl}/packs/js/sdk.js`; + script.id = CHATWOOT_SCRIPT_ID; + script.async = true; + + script.onload = () => { + ( + globalThis as unknown as { + chatwootSDK: { run: (options: { websiteToken: string; baseUrl: string }) => void }; + } + ).chatwootSDK?.run({ + websiteToken: chatwootWebsiteToken, + baseUrl: chatwootBaseUrl, + }); + }; + + document.head.appendChild(script); + + const handleChatwootReady = () => setUserInfo(); + globalThis.addEventListener("chatwoot:ready", handleChatwootReady); + + // Check if Chatwoot is already ready + if ( + ( + globalThis as unknown as { + $chatwoot: { + setUser: (userId: string, userInfo: { email?: string | null; name?: string | null }) => void; + }; + } + ).$chatwoot + ) { + setUserInfo(); + } + + return () => { + globalThis.removeEventListener("chatwoot:ready", handleChatwootReady); + + const $chatwoot = (globalThis as unknown as { $chatwoot: { reset: () => void } }).$chatwoot; + if ($chatwoot) { + $chatwoot.reset(); + } + + const scriptElement = document.getElementById(CHATWOOT_SCRIPT_ID); + scriptElement?.remove(); + + userSetRef.current = false; + }; + }, [chatwootBaseUrl, chatwootWebsiteToken, userId, userEmail, userName, setUserInfo]); + + return null; +}; diff --git a/apps/web/app/intercom/IntercomClient.tsx b/apps/web/app/intercom/IntercomClient.tsx deleted file mode 100644 index 25581184ca..0000000000 --- a/apps/web/app/intercom/IntercomClient.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import Intercom from "@intercom/messenger-js-sdk"; -import { useCallback, useEffect } from "react"; -import { TUser } from "@formbricks/types/user"; - -interface IntercomClientProps { - isIntercomConfigured: boolean; - intercomUserHash?: string; - user?: TUser | null; - intercomAppId?: string; -} - -export const IntercomClient = ({ - user, - intercomUserHash, - isIntercomConfigured, - intercomAppId, -}: IntercomClientProps) => { - const initializeIntercom = useCallback(() => { - let initParams = {}; - - if (user && intercomUserHash) { - const { id, name, email, createdAt } = user; - - initParams = { - user_id: id, - user_hash: intercomUserHash, - name, - email, - created_at: createdAt ? Math.floor(createdAt.getTime() / 1000) : undefined, - }; - } - - Intercom({ - app_id: intercomAppId!, - ...initParams, - }); - }, [user, intercomUserHash, intercomAppId]); - - useEffect(() => { - try { - if (isIntercomConfigured) { - if (!intercomAppId) { - throw new Error("Intercom app ID is required"); - } - - if (user && !intercomUserHash) { - throw new Error("Intercom user hash is required"); - } - - initializeIntercom(); - } - - return () => { - // Shutdown Intercom when component unmounts - if (typeof window !== "undefined" && window.Intercom) { - window.Intercom("shutdown"); - } - }; - } catch (error) { - console.error("Failed to initialize Intercom:", error); - } - }, [isIntercomConfigured, initializeIntercom, intercomAppId, intercomUserHash, user]); - - return null; -}; diff --git a/apps/web/app/intercom/IntercomClientWrapper.tsx b/apps/web/app/intercom/IntercomClientWrapper.tsx deleted file mode 100644 index 488e0fe898..0000000000 --- a/apps/web/app/intercom/IntercomClientWrapper.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { createHmac } from "crypto"; -import type { TUser } from "@formbricks/types/user"; -import { INTERCOM_APP_ID, INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@/lib/constants"; -import { IntercomClient } from "./IntercomClient"; - -interface IntercomClientWrapperProps { - user?: TUser | null; -} - -export const IntercomClientWrapper = ({ user }: IntercomClientWrapperProps) => { - let intercomUserHash: string | undefined; - if (user) { - const secretKey = INTERCOM_SECRET_KEY; - if (secretKey) { - intercomUserHash = createHmac("sha256", secretKey).update(user.id).digest("hex"); - } - } - return ( - - ); -}; diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts index fb2afe4cda..3e6f93497a 100644 --- a/apps/web/lib/constants.ts +++ b/apps/web/lib/constants.ts @@ -215,9 +215,9 @@ export const BILLING_LIMITS = { }, } as const; -export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY; -export const INTERCOM_APP_ID = env.INTERCOM_APP_ID; -export const IS_INTERCOM_CONFIGURED = Boolean(env.INTERCOM_APP_ID && INTERCOM_SECRET_KEY); +export const CHATWOOT_WEBSITE_TOKEN = env.CHATWOOT_WEBSITE_TOKEN; +export const CHATWOOT_BASE_URL = env.CHATWOOT_BASE_URL || "https://app.chatwoot.com"; +export const IS_CHATWOOT_CONFIGURED = Boolean(env.CHATWOOT_WEBSITE_TOKEN); export const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY; export const TURNSTILE_SITE_KEY = env.TURNSTILE_SITE_KEY; diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index 03226736ac..f75f5709f0 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -39,8 +39,8 @@ export const env = createEnv({ .or(z.string().refine((str) => str === "")), IMPRINT_ADDRESS: z.string().optional(), INVITE_DISABLED: z.enum(["1", "0"]).optional(), - INTERCOM_SECRET_KEY: z.string().optional(), - INTERCOM_APP_ID: z.string().optional(), + CHATWOOT_WEBSITE_TOKEN: z.string().optional(), + CHATWOOT_BASE_URL: z.string().url().optional(), IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(), LOG_LEVEL: z.enum(["debug", "info", "warn", "error", "fatal"]).optional(), MAIL_FROM: z.string().email().optional(), @@ -162,7 +162,8 @@ export const env = createEnv({ IMPRINT_URL: process.env.IMPRINT_URL, IMPRINT_ADDRESS: process.env.IMPRINT_ADDRESS, INVITE_DISABLED: process.env.INVITE_DISABLED, - INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY, + CHATWOOT_WEBSITE_TOKEN: process.env.CHATWOOT_WEBSITE_TOKEN, + CHATWOOT_BASE_URL: process.env.CHATWOOT_BASE_URL, IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD, LOG_LEVEL: process.env.LOG_LEVEL, MAIL_FROM: process.env.MAIL_FROM, @@ -170,7 +171,6 @@ export const env = createEnv({ NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, SENTRY_DSN: process.env.SENTRY_DSN, OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL, - INTERCOM_APP_ID: process.env.INTERCOM_APP_ID, NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID, NOTION_OAUTH_CLIENT_SECRET: process.env.NOTION_OAUTH_CLIENT_SECRET, OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID, diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 01c090ab2e..274657c70a 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -61,10 +61,6 @@ const nextConfig = { protocol: "https", hostname: "images.unsplash.com", }, - { - protocol: "https", - hostname: "api-iam.eu.intercom.io", - }, ], }, async redirects() { @@ -168,7 +164,7 @@ const nextConfig = { }, { key: "Content-Security-Policy", - value: `default-src 'self'; script-src 'self' 'unsafe-inline'${scriptSrcUnsafeEval} https://*.intercom.io https://*.intercomcdn.com https:; style-src 'self' 'unsafe-inline' https://*.intercomcdn.com https:; img-src 'self' blob: data: http://localhost:9000 https://*.intercom.io https://*.intercomcdn.com https:; font-src 'self' data: https://*.intercomcdn.com https:; connect-src 'self' http://localhost:9000 https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com https:; frame-src 'self' https://*.intercom.io https://app.cal.com https:; media-src 'self' https:; object-src 'self' data: https:; base-uri 'self'; form-action 'self'`, + value: `default-src 'self'; script-src 'self' 'unsafe-inline'${scriptSrcUnsafeEval} https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' blob: data: http://localhost:9000 https:; font-src 'self' data: https:; connect-src 'self' http://localhost:9000 https: wss:; frame-src 'self' https://app.cal.com https:; media-src 'self' https:; object-src 'self' data: https:; base-uri 'self'; form-action 'self'`, }, { key: "Strict-Transport-Security", diff --git a/apps/web/package.json b/apps/web/package.json index eb31d6dc5c..20e05713b0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,7 +36,6 @@ "@formbricks/surveys": "workspace:*", "@formbricks/types": "workspace:*", "@hookform/resolvers": "5.0.1", - "@intercom/messenger-js-sdk": "0.0.14", "@json2csv/node": "7.0.6", "@lexical/code": "0.36.2", "@lexical/link": "0.36.2", diff --git a/packages/surveys/i18n.lock b/packages/surveys/i18n.lock index c084e632ea..9fc90ac58e 100644 --- a/packages/surveys/i18n.lock +++ b/packages/surveys/i18n.lock @@ -29,6 +29,7 @@ checksums: common/ranking_items: 463f2eb500f1b42fbce6cec17612fb9a common/respondents_will_not_see_this_card: 18c3dd44d6ff6ca2310ad196b84f30d3 common/retry: 6e44d18639560596569a1278f9c83676 + common/retrying: 0cb623dbdcbf16d3680f0180ceac734c common/select_a_date: 521e4a705800da06d091fde3e801ce02 common/select_for_ranking: e5f4e20752d1c2d852cd02dc3a0e9dd0 common/sending_responses: 184772f70cca69424eaf34f73520789f diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55ae671cb4..40d664c1b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,9 +162,6 @@ importers: '@hookform/resolvers': specifier: 5.0.1 version: 5.0.1(react-hook-form@7.56.2(react@19.1.2)) - '@intercom/messenger-js-sdk': - specifier: 0.0.14 - version: 0.0.14 '@json2csv/node': specifier: 7.0.6 version: 7.0.6 @@ -2252,9 +2249,6 @@ packages: cpu: [x64] os: [win32] - '@intercom/messenger-js-sdk@0.0.14': - resolution: {integrity: sha512-2dH4BDAh9EI90K7hUkAdZ76W79LM45Sd1OBX7t6Vzy8twpNiQ5X+7sH9G5hlJlkSGnf+vFWlFcy9TOYAyEs1hA==} - '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -12235,8 +12229,6 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@intercom/messenger-js-sdk@0.0.14': {} - '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': diff --git a/turbo.json b/turbo.json index 3178a6b024..ea4ce00866 100644 --- a/turbo.json +++ b/turbo.json @@ -146,8 +146,8 @@ "IMPRINT_ADDRESS", "INVITE_DISABLED", "IS_FORMBRICKS_CLOUD", - "INTERCOM_APP_ID", - "INTERCOM_SECRET_KEY", + "CHATWOOT_WEBSITE_TOKEN", + "CHATWOOT_BASE_URL", "LOG_LEVEL", "MAIL_FROM", "MAIL_FROM_NAME",