[WEB-2103]: chore: Intercom integration (#5295)

* fix: intecom sdk integration

* dev: integrated intercom in god-mode

* dev: intercom default value true

* dev: updated intercom keys in intercom provider

* chore: added restriction values

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
guru_sainath 2024-08-05 13:37:11 +05:30 committed by GitHub
parent 34820eec7a
commit 0619f1b6d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 221 additions and 40 deletions

View file

@ -9,6 +9,7 @@ import { IInstance, IInstanceAdmin } from "@plane/types";
import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
// components
import { ControllerInput } from "@/components/common";
import { IntercomConfig } from "./intercom";
// hooks
import { useInstance } from "@/hooks/store";
@ -20,11 +21,13 @@ export interface IGeneralConfigurationForm {
export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer((props) => {
const { instance, instanceAdmins } = props;
// hooks
const { updateInstanceInfo } = useInstance();
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
// form data
const {
handleSubmit,
control,
watch,
formState: { errors, isSubmitting },
} = useForm<Partial<IInstance>>({
defaultValues: {
@ -36,7 +39,16 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
const onSubmit = async (formData: Partial<IInstance>) => {
const payload: Partial<IInstance> = { ...formData };
console.log("payload", payload);
// update the intercom configuration
const isIntercomEnabled =
instanceConfigurations?.find((config) => config.key === "IS_INTERCOM_ENABLED")?.value === "1";
if (!payload.is_telemetry_enabled && isIntercomEnabled) {
try {
await updateInstanceConfigurations({ IS_INTERCOM_ENABLED: "0" });
} catch (error) {
console.error(error);
}
}
await updateInstanceInfo(payload)
.then(() =>
@ -93,7 +105,8 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
</div>
<div className="space-y-3">
<div className="text-lg font-medium">Telemetry</div>
<div className="text-lg font-medium">Chat + telemetry</div>
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
<div className="grow flex items-center gap-4">
<div className="shrink-0">

View file

@ -0,0 +1,82 @@
"use client";
import { FC, FormEvent, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { MessageSquare } from "lucide-react";
import useSWR from "swr";
import { IFormattedInstanceConfiguration } from "@plane/types";
import { Button, Input, ToggleSwitch } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
type TIntercomConfig = {
isTelemetryEnabled: boolean;
};
export const IntercomConfig: FC<TIntercomConfig> = observer((props) => {
const { isTelemetryEnabled } = props;
// hooks
const { instanceConfigurations, instance, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// derived values
const isIntercomEnabled = isTelemetryEnabled
? instanceConfigurations
? instanceConfigurations?.find((config) => config.key === "IS_INTERCOM_ENABLED")?.value === "1"
? true
: false
: undefined
: false;
const { isLoading } = useSWR(isTelemetryEnabled ? "INSTANCE_CONFIGURATIONS" : null, () =>
isTelemetryEnabled ? fetchInstanceConfigurations() : null
);
const initialLoader = isLoading && isIntercomEnabled === undefined;
const submitInstanceConfigurations = async (payload: Partial<IFormattedInstanceConfiguration>) => {
try {
await updateInstanceConfigurations(payload);
} catch (error) {
console.error(error);
} finally {
setIsSubmitting(false);
}
};
const enableIntercomConfig = () => {
submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
};
return (
<>
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
<div className="grow flex items-center gap-4">
<div className="shrink-0">
<div className="flex items-center justify-center w-10 h-10 bg-custom-background-80 rounded-full">
<MessageSquare className="w-6 h-6 text-custom-text-300/80 p-0.5" />
</div>
</div>
<div className="grow">
<div className="text-sm font-medium text-custom-text-100 leading-5">Talk to Plane</div>
<div className="text-xs font-normal text-custom-text-300 leading-5">
Let your members chat with us via Intercom or another service. Toggling Telemetry off turns this off
automatically.
</div>
</div>
<div className="ml-auto">
<ToggleSwitch
value={isIntercomEnabled ? true : false}
onChange={enableIntercomConfig}
size="sm"
disabled={!isTelemetryEnabled || isSubmitting || initialLoader}
/>
</div>
</div>
</div>
</>
);
});

View file

@ -7,7 +7,7 @@ import { GeneralConfigurationForm } from "./form";
function GeneralPage() {
const { instance, instanceAdmins } = useInstance();
console.log("instance", instance);
return (
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">

View file

@ -57,8 +57,6 @@ export const InstanceSignInForm: FC = (props) => {
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value }));
console.log("csrfToken", csrfToken);
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));

View file

@ -66,6 +66,8 @@ class InstanceEndpoint(BaseAPIView):
POSTHOG_HOST,
UNSPLASH_ACCESS_KEY,
OPENAI_API_KEY,
IS_INTERCOM_ENABLED,
INTERCOM_APP_ID,
) = get_configuration_value(
[
{
@ -116,6 +118,15 @@ class InstanceEndpoint(BaseAPIView):
"key": "OPENAI_API_KEY",
"default": os.environ.get("OPENAI_API_KEY", ""),
},
# Intercom settings
{
"key": "IS_INTERCOM_ENABLED",
"default": os.environ.get("IS_INTERCOM_ENABLED", "1"),
},
{
"key": "INTERCOM_APP_ID",
"default": os.environ.get("INTERCOM_APP_ID", ""),
},
]
)
@ -151,6 +162,10 @@ class InstanceEndpoint(BaseAPIView):
# is smtp configured
data["is_smtp_configured"] = bool(EMAIL_HOST)
# Intercom settings
data["is_intercom_enabled"] = IS_INTERCOM_ENABLED == "1"
data["intercom_app_id"] = INTERCOM_APP_ID
# Base URL
data["admin_base_url"] = settings.ADMIN_BASE_URL
data["space_base_url"] = settings.SPACE_BASE_URL

View file

@ -143,6 +143,19 @@ class Command(BaseCommand):
"category": "UNSPLASH",
"is_encrypted": True,
},
# intercom settings
{
"key": "IS_INTERCOM_ENABLED",
"value": os.environ.get("IS_INTERCOM_ENABLED", "1"),
"category": "INTERCOM",
"is_encrypted": False,
},
{
"key": "INTERCOM_APP_ID",
"value": os.environ.get("INTERCOM_APP_ID", ""),
"category": "INTERCOM",
"is_encrypted": False,
},
]
for item in config_keys:
@ -265,7 +278,11 @@ class Command(BaseCommand):
]
)
)
if bool(GITLAB_HOST) and bool(GITLAB_CLIENT_ID) and bool(GITLAB_CLIENT_SECRET):
if (
bool(GITLAB_HOST)
and bool(GITLAB_CLIENT_ID)
and bool(GITLAB_CLIENT_SECRET)
):
value = "1"
else:
value = "0"

View file

@ -27,4 +27,9 @@ RESTRICTED_WORKSPACE_SLUGS = [
"channels",
"upgrade",
"billing",
"sign-in",
"sign-up",
"signin",
"signup",
"config",
]

View file

@ -52,6 +52,9 @@ export interface IInstanceConfig {
app_base_url: string | undefined;
space_base_url: string | undefined;
admin_base_url: string | undefined;
// intercom
is_intercom_enabled: boolean;
intercom_app_id: string | undefined;
}
export interface IInstanceAdmin {
@ -66,11 +69,16 @@ export interface IInstanceAdmin {
user_detail: IUserLite;
}
export type TInstanceIntercomConfigurationKeys =
| "IS_INTERCOM_ENABLED"
| "INTERCOM_APP_ID";
export type TInstanceConfigurationKeys =
| TInstanceAIConfigurationKeys
| TInstanceEmailConfigurationKeys
| TInstanceImageConfigurationKeys
| TInstanceAuthenticationKeys;
| TInstanceAuthenticationKeys
| TInstanceIntercomConfigurationKeys;
export interface IInstanceConfiguration {
id: string;

View file

@ -46,7 +46,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<meta name="apple-mobile-web-app-title" content={SITE_NAME} />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="/icons/touch-icon-iphone.png" />
<link rel="apple-touch-icon" href="/icons/icon-512x512.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180x180.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png" />

View file

@ -19,7 +19,7 @@ import { InstanceWrapper } from "@/lib/wrappers";
// dynamic imports
const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { ssr: false });
const PostHogProvider = dynamic(() => import("@/lib/posthog-provider"), { ssr: false });
const CrispWrapper = dynamic(() => import("@/lib/wrappers/crisp-wrapper"), { ssr: false });
const IntercomProvider = dynamic(() => import("@/lib/intercom-provider"), { ssr: false });
export interface IAppProvider {
children: ReactNode;
@ -39,15 +39,15 @@ export const AppProvider: FC<IAppProvider> = (props) => {
<StoreProvider>
<ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system">
<ToastWithTheme />
<InstanceWrapper>
<StoreWrapper>
<CrispWrapper>
<InstanceWrapper>
<IntercomProvider>
<PostHogProvider>
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider>
</CrispWrapper>
</StoreWrapper>
</IntercomProvider>
</InstanceWrapper>
</StoreWrapper>
</ThemeProvider>
</StoreProvider>
</>

View file

@ -10,7 +10,7 @@ import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useCommandPalette } from "@/hooks/store";
import { useAppTheme, useCommandPalette, useInstance } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
// components
@ -44,6 +44,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { toggleShortcutModal } = useCommandPalette();
const { isMobile } = usePlatformOS();
const { config } = useInstance();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// refs
@ -148,7 +149,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
</span>
</Link>
))}
{process.env.NEXT_PUBLIC_CRISP_ID && (
{config?.intercom_app_id && config?.is_intercom_enabled && (
<button
type="button"
onClick={handleCrispWindowShow}

View file

@ -1,36 +1,32 @@
export * from "./estimates";
export * from "./notifications";
export * from "./pages";
export * from "./use-app-theme";
export * from "./use-calendar-view";
export * from "./use-cycle-filter";
export * from "./use-command-palette";
export * from "./use-cycle";
export * from "./use-event-tracker";
export * from "./use-cycle-filter";
export * from "./use-dashboard";
export * from "./use-event-tracker";
export * from "./use-global-view";
export * from "./use-inbox-issues";
export * from "./use-instance";
export * from "./use-issue-detail";
export * from "./use-issues";
export * from "./use-kanban-view";
export * from "./use-label";
export * from "./use-member";
export * from "./use-mention";
export * from "./use-module";
export * from "./use-multiple-select-store";
export * from "./pages/use-project-page";
export * from "./pages/use-page";
export * from "./use-module-filter";
export * from "./use-multiple-select-store";
export * from "./use-project";
export * from "./use-project-filter";
export * from "./use-project-inbox";
export * from "./use-project-publish";
export * from "./use-project-state";
export * from "./use-project-view";
export * from "./use-project";
export * from "./use-router-params";
export * from "./use-webhook";
export * from "./use-workspace";
export * from "./use-issues";
export * from "./use-kanban-view";
export * from "./use-issue-detail";
// project inbox
export * from "./use-project-inbox";
export * from "./use-inbox-issues";
export * from "./user";
export * from "./use-instance";
export * from "./use-app-theme";
export * from "./use-command-palette";
export * from "./use-router-params";
export * from "./estimates";
export * from "./notifications";

View file

@ -0,0 +1,2 @@
export * from "./use-page";
export * from "./use-project-page";

View file

@ -0,0 +1,33 @@
"use client";
import React, { FC, useEffect } from "react";
import Intercom from "@intercom/messenger-js-sdk";
import { observer } from "mobx-react";
// store hooks
import { useUser, useInstance } from "@/hooks/store";
export type IntercomProviderProps = {
children: React.ReactNode;
};
const IntercomProvider: FC<IntercomProviderProps> = observer((props) => {
const { children } = props;
// hooks
const { data: user } = useUser();
const { config } = useInstance();
useEffect(() => {
if (user && config?.is_intercom_enabled && config.intercom_app_id) {
Intercom({
app_id: config.intercom_app_id || "",
user_id: user.id,
name: `${user.first_name} ${user.last_name}`,
email: user.email,
});
}
}, [user, config]);
return <>{children}</>;
});
export default IntercomProvider;

View file

@ -46,6 +46,11 @@ const nextConfig = {
destination: "/",
permanent: true,
},
{
source: "/signin",
destination: "/",
permanent: true,
},
{
source: "/register",
destination: "/sign-up",

View file

@ -17,6 +17,7 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.3",
"@intercom/messenger-js-sdk": "^0.0.12",
"@nivo/bar": "0.80.0",
"@nivo/calendar": "0.80.0",
"@nivo/core": "0.80.0",

View file

@ -1640,6 +1640,11 @@
resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
"@intercom/messenger-js-sdk@^0.0.12":
version "0.0.12"
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.12.tgz#1b80acf6b2a59ef9ce4010e0920522d579a590fa"
integrity sha512-xoUGlKLD8nIcZaH7AesR/LfwXH4QQUdPZMV4sApK/zvVFBgAY/A9IWp1ey/jUcp+776ejtZeEqreJZxG4LdEuw==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@ -4446,7 +4451,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48":
"@types/react@*", "@types/react@18.2.48", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48":
version "18.2.48"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==