[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:
parent
34820eec7a
commit
0619f1b6d1
18 changed files with 221 additions and 40 deletions
|
|
@ -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">
|
||||
|
|
|
|||
82
admin/app/general/intercom.tsx
Normal file
82
admin/app/general/intercom.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -27,4 +27,9 @@ RESTRICTED_WORKSPACE_SLUGS = [
|
|||
"channels",
|
||||
"upgrade",
|
||||
"billing",
|
||||
"sign-in",
|
||||
"sign-up",
|
||||
"signin",
|
||||
"signup",
|
||||
"config",
|
||||
]
|
||||
|
|
|
|||
10
packages/types/src/instance/base.d.ts
vendored
10
packages/types/src/instance/base.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
2
web/core/hooks/store/pages/index.ts
Normal file
2
web/core/hooks/store/pages/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./use-page";
|
||||
export * from "./use-project-page";
|
||||
33
web/core/lib/intercom-provider.tsx
Normal file
33
web/core/lib/intercom-provider.tsx
Normal 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;
|
||||
|
|
@ -46,6 +46,11 @@ const nextConfig = {
|
|||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/signin",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/register",
|
||||
destination: "/sign-up",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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==
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue