Add support chat (#1066)
* Add support chat Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Refactor the chat logic Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add HMAC signing Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Update the button styles Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Update the button styles Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Refactor the chat logic Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Fix the chat not loading Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Fix the chat not loading Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> --------- Co-authored-by: v1b3m <vibenjamin6@gmail.com>
This commit is contained in:
@ -548,6 +548,7 @@ export type ClientConfig = {
|
||||
authProviders: AuthProviders;
|
||||
debugMode: Scalars['Boolean'];
|
||||
signInPrefilled: Scalars['Boolean'];
|
||||
supportChat: SupportChat;
|
||||
telemetry: Telemetry;
|
||||
};
|
||||
|
||||
@ -1906,6 +1907,12 @@ export type StringNullableFilter = {
|
||||
startsWith?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SupportChat = {
|
||||
__typename?: 'SupportChat';
|
||||
supportDriver: Scalars['String'];
|
||||
supportFrontendKey?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type Telemetry = {
|
||||
__typename?: 'Telemetry';
|
||||
anonymizationEnabled: Scalars['Boolean'];
|
||||
@ -1935,6 +1942,7 @@ export type User = {
|
||||
phoneNumber?: Maybe<Scalars['String']>;
|
||||
settings: UserSettings;
|
||||
settingsId: Scalars['String'];
|
||||
supportHMACKey?: Maybe<Scalars['String']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
workspaceMember?: Maybe<WorkspaceMember>;
|
||||
};
|
||||
@ -2448,7 +2456,7 @@ export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __ty
|
||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean } } };
|
||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, supportChat: { __typename?: 'SupportChat', supportDriver: string, supportFrontendKey?: string | null } } };
|
||||
|
||||
export type GetCompaniesQueryVariables = Exact<{
|
||||
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
||||
@ -2677,7 +2685,7 @@ export type SearchActivityQuery = { __typename?: 'Query', searchResults: Array<{
|
||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null, canImpersonate: boolean, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, locale: string, colorScheme: ColorScheme } } };
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null, canImpersonate: boolean, supportHMACKey?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, locale: string, colorScheme: ColorScheme } } };
|
||||
|
||||
export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@ -3648,6 +3656,10 @@ export const GetClientConfigDocument = gql`
|
||||
enabled
|
||||
anonymizationEnabled
|
||||
}
|
||||
supportChat {
|
||||
supportDriver
|
||||
supportFrontendKey
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -4895,6 +4907,7 @@ export const GetCurrentUserDocument = gql`
|
||||
locale
|
||||
colorScheme
|
||||
}
|
||||
supportHMACKey
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { telemetryState } from '@/client-config/states/telemetryState';
|
||||
import { useGetClientConfigQuery } from '~/generated/graphql';
|
||||
|
||||
@ -15,6 +16,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const [, setSignInPrefilled] = useRecoilState(isSignInPrefilledState);
|
||||
const [, setTelemetry] = useRecoilState(telemetryState);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const setSupportChat = useSetRecoilState(supportChatState);
|
||||
|
||||
const { data, loading } = useGetClientConfigQuery();
|
||||
|
||||
@ -31,6 +33,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setDebugMode(data?.clientConfig.debugMode);
|
||||
setSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||
setTelemetry(data?.clientConfig.telemetry);
|
||||
setSupportChat(data?.clientConfig.supportChat);
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
@ -40,6 +43,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setTelemetry,
|
||||
setIsLoading,
|
||||
loading,
|
||||
setSupportChat,
|
||||
]);
|
||||
|
||||
return isLoading ? <></> : <>{children}</>;
|
||||
|
||||
@ -13,6 +13,10 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
enabled
|
||||
anonymizationEnabled
|
||||
}
|
||||
supportChat {
|
||||
supportDriver
|
||||
supportFrontendKey
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
11
front/src/modules/client-config/states/supportChatState.ts
Normal file
11
front/src/modules/client-config/states/supportChatState.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { SupportChat } from '~/generated/graphql';
|
||||
|
||||
export const supportChatState = atom<SupportChat>({
|
||||
key: 'supportChatState',
|
||||
default: {
|
||||
supportDriver: 'front',
|
||||
supportFrontendKey: null,
|
||||
},
|
||||
});
|
||||
3
front/src/modules/types/custom.d.ts
vendored
Normal file
3
front/src/modules/types/custom.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare interface Window {
|
||||
FrontChat?: (method: string, ...args: any[]) => void;
|
||||
}
|
||||
@ -2,20 +2,29 @@ import styled from '@emotion/styled';
|
||||
|
||||
import NavItemsContainer from './NavItemsContainer';
|
||||
import NavWorkspaceButton from './NavWorkspaceButton';
|
||||
import SupportChat from './SupportChat';
|
||||
|
||||
type OwnProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2.5)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default function MainNavbar({ children }: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<NavWorkspaceButton />
|
||||
<NavItemsContainer>{children}</NavItemsContainer>
|
||||
<div>
|
||||
<NavWorkspaceButton />
|
||||
<NavItemsContainer>{children}</NavItemsContainer>
|
||||
</div>
|
||||
<SupportChat />
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
115
front/src/modules/ui/navbar/components/SupportChat.tsx
Normal file
115
front/src/modules/ui/navbar/components/SupportChat.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
ButtonVariant,
|
||||
} from '@/ui/button/components/Button';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledQuestionMark = styled.div`
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-width: ${({ theme }) => theme.spacing(0.25)};
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(3.5)};
|
||||
justify-content: center;
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
width: ${({ theme }) => theme.spacing(3.5)};
|
||||
`;
|
||||
|
||||
// insert a script tag into the DOM right before the closing body tag
|
||||
function insertScript({
|
||||
src,
|
||||
innerHTML,
|
||||
onLoad,
|
||||
}: {
|
||||
src?: string;
|
||||
innerHTML?: string;
|
||||
onLoad?: (...args: any[]) => void;
|
||||
}) {
|
||||
const script = document.createElement('script');
|
||||
if (src) script.src = src;
|
||||
if (innerHTML) script.innerHTML = innerHTML;
|
||||
if (onLoad) script.onload = onLoad;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
function configureFront(chatId: string) {
|
||||
const url = 'https://chat-assets.frontapp.com/v1/chat.bundle.js';
|
||||
// check if Front Chat script is already loaded
|
||||
const script = document.querySelector(`script[src="${url}"]`);
|
||||
|
||||
if (!script) {
|
||||
// insert script and initialize Front Chat when it loads
|
||||
insertScript({
|
||||
src: url,
|
||||
onLoad: () => {
|
||||
window.FrontChat?.('init', {
|
||||
chatId,
|
||||
useDefaultLauncher: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function SupportChat() {
|
||||
const user = useRecoilValue(currentUserState);
|
||||
const supportChatConfig = useRecoilValue(supportChatState);
|
||||
const [isFrontChatLoaded, setIsFrontChatLoaded] = useState(false);
|
||||
const [isChatShowing, setIsChatShowing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
supportChatConfig?.supportDriver === 'front' &&
|
||||
supportChatConfig.supportFrontendKey &&
|
||||
!isFrontChatLoaded
|
||||
) {
|
||||
configureFront(supportChatConfig.supportFrontendKey);
|
||||
setIsFrontChatLoaded(true);
|
||||
}
|
||||
if (user?.email && isFrontChatLoaded) {
|
||||
window.FrontChat?.('identity', {
|
||||
email: user.email,
|
||||
name: user.displayName,
|
||||
userHash: user?.supportHMACKey,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isFrontChatLoaded,
|
||||
supportChatConfig?.supportDriver,
|
||||
supportChatConfig.supportFrontendKey,
|
||||
user?.displayName,
|
||||
user?.email,
|
||||
user?.supportHMACKey,
|
||||
]);
|
||||
|
||||
function handleSupportClick() {
|
||||
if (supportChatConfig?.supportDriver === 'front') {
|
||||
const action = isChatShowing ? 'hide' : 'show';
|
||||
setIsChatShowing(!isChatShowing);
|
||||
window.FrontChat?.(action);
|
||||
}
|
||||
}
|
||||
|
||||
return isFrontChatLoaded ? (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
variant={ButtonVariant.Tertiary}
|
||||
size={ButtonSize.Small}
|
||||
title="Support"
|
||||
icon={<StyledQuestionMark>?</StyledQuestionMark>}
|
||||
onClick={handleSupportClick}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
) : null;
|
||||
}
|
||||
@ -27,6 +27,7 @@ export const GET_CURRENT_USER = gql`
|
||||
locale
|
||||
colorScheme
|
||||
}
|
||||
supportHMACKey
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -207,6 +207,10 @@ export const graphqlMocks = [
|
||||
debugMode: false,
|
||||
authProviders: { google: true, password: true, magicLink: false },
|
||||
telemetry: { enabled: false, anonymizationEnabled: true },
|
||||
supportChat: {
|
||||
supportDriver: 'front',
|
||||
supportFrontendKey: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user