Wrap up Front chat (#1085)

* Wrap up Front chat

* Wrap up Front chat
This commit is contained in:
Charles Bochet 2023-08-04 19:22:54 -07:00 committed by GitHub
parent 57c465176a
commit 6008789a17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 91 additions and 75 deletions

View File

@ -548,7 +548,7 @@ export type ClientConfig = {
authProviders: AuthProviders;
debugMode: Scalars['Boolean'];
signInPrefilled: Scalars['Boolean'];
supportChat: SupportChat;
support: Support;
telemetry: Telemetry;
};
@ -1907,10 +1907,10 @@ export type StringNullableFilter = {
startsWith?: InputMaybe<Scalars['String']>;
};
export type SupportChat = {
__typename?: 'SupportChat';
export type Support = {
__typename?: 'Support';
supportDriver: Scalars['String'];
supportFrontendKey?: Maybe<Scalars['String']>;
supportFrontChatId?: Maybe<Scalars['String']>;
};
export type Telemetry = {
@ -1942,7 +1942,7 @@ export type User = {
phoneNumber?: Maybe<Scalars['String']>;
settings: UserSettings;
settingsId: Scalars['String'];
supportHMACKey?: Maybe<Scalars['String']>;
supportUserHash: Scalars['String'];
updatedAt: Scalars['DateTime'];
workspaceMember?: Maybe<WorkspaceMember>;
};
@ -2437,7 +2437,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: 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, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash: string, 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, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
refreshToken: Scalars['String'];
@ -2451,12 +2451,12 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: 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, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash: string, 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, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
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 }, supportChat: { __typename?: 'SupportChat', supportDriver: string, supportFrontendKey?: string | null } } };
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 }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null } } };
export type GetCompaniesQueryVariables = Exact<{
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
@ -2685,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, 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 GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null, canImpersonate: boolean, supportUserHash: string, 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; }>;
@ -3479,6 +3479,7 @@ export const VerifyDocument = gql`
firstName
lastName
canImpersonate
supportUserHash
workspaceMember {
id
allowImpersonation
@ -3587,6 +3588,7 @@ export const ImpersonateDocument = gql`
firstName
lastName
canImpersonate
supportUserHash
workspaceMember {
id
allowImpersonation
@ -3656,9 +3658,9 @@ export const GetClientConfigDocument = gql`
enabled
anonymizationEnabled
}
supportChat {
support {
supportDriver
supportFrontendKey
supportFrontChatId
}
}
}
@ -4907,7 +4909,7 @@ export const GetCurrentUserDocument = gql`
locale
colorScheme
}
supportHMACKey
supportUserHash
}
}
`;

View File

@ -40,6 +40,7 @@ export const VERIFY = gql`
firstName
lastName
canImpersonate
supportUserHash
workspaceMember {
id
allowImpersonation
@ -99,6 +100,7 @@ export const IMPERSONATE = gql`
firstName
lastName
canImpersonate
supportUserHash
workspaceMember {
id
allowImpersonation

View File

@ -33,7 +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);
setSupportChat(data?.clientConfig.support);
}
}, [
data,

View File

@ -13,9 +13,9 @@ export const GET_CLIENT_CONFIG = gql`
enabled
anonymizationEnabled
}
supportChat {
support {
supportDriver
supportFrontendKey
supportFrontChatId
}
}
}

View File

@ -1,11 +1,11 @@
import { atom } from 'recoil';
import { SupportChat } from '~/generated/graphql';
import { Support } from '~/generated/graphql';
export const supportChatState = atom<SupportChat>({
export const supportChatState = atom<Support>({
key: 'supportChatState',
default: {
supportDriver: 'front',
supportFrontendKey: null,
supportDriver: 'none',
supportFrontChatId: null,
},
});

View File

@ -129,7 +129,7 @@ const StyledButton = styled.button<
return theme.font.weight.medium;
}
}};
gap: ${({ theme }) => theme.spacing(2)};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: flex-start;
padding: ${({ theme, title }) => {

View File

@ -52,3 +52,4 @@ export { IconCalendar } from '@tabler/icons-react';
export { IconPencil } from '@tabler/icons-react';
export { IconCircleDot } from '@tabler/icons-react';
export { IconTag } from '@tabler/icons-react';
export { IconHelpCircle } from '@tabler/icons-react';

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
@ -9,24 +10,12 @@ import {
ButtonSize,
ButtonVariant,
} from '@/ui/button/components/Button';
import { IconHelpCircle } from '@/ui/icon';
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,
@ -63,41 +52,39 @@ function configureFront(chatId: string) {
}
export default function SupportChat() {
const theme = useTheme();
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 &&
supportChatConfig.supportFrontChatId &&
!isFrontChatLoaded
) {
configureFront(supportChatConfig.supportFrontendKey);
configureFront(supportChatConfig.supportFrontChatId);
setIsFrontChatLoaded(true);
}
if (user?.email && isFrontChatLoaded) {
window.FrontChat?.('identity', {
email: user.email,
name: user.displayName,
userHash: user?.supportHMACKey,
userHash: user?.supportUserHash,
});
}
}, [
isFrontChatLoaded,
supportChatConfig?.supportDriver,
supportChatConfig.supportFrontendKey,
supportChatConfig.supportFrontChatId,
user?.displayName,
user?.email,
user?.supportHMACKey,
user?.supportUserHash,
]);
function handleSupportClick() {
if (supportChatConfig?.supportDriver === 'front') {
const action = isChatShowing ? 'hide' : 'show';
setIsChatShowing(!isChatShowing);
window.FrontChat?.(action);
window.FrontChat?.('show');
}
}
@ -107,7 +94,7 @@ export default function SupportChat() {
variant={ButtonVariant.Tertiary}
size={ButtonSize.Small}
title="Support"
icon={<StyledQuestionMark>?</StyledQuestionMark>}
icon={<IconHelpCircle size={theme.icon.size.md} />}
onClick={handleSupportClick}
/>
</StyledButtonContainer>

View File

@ -27,7 +27,7 @@ export const GET_CURRENT_USER = gql`
locale
colorScheme
}
supportHMACKey
supportUserHash
}
}
`;

View File

@ -207,9 +207,9 @@ export const graphqlMocks = [
debugMode: false,
authProviders: { google: true, password: true, magicLink: false },
telemetry: { enabled: false, anonymizationEnabled: true },
supportChat: {
support: {
supportDriver: 'front',
supportFrontendKey: null,
supportFrontChatId: null,
},
},
}),

View File

@ -17,6 +17,7 @@ export const mockedUsersData: Array<MockedUser> = [
lastName: 'Test',
avatarUrl: null,
canImpersonate: false,
supportUserHash: '',
workspaceMember: {
__typename: 'WorkspaceMember',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
@ -45,6 +46,7 @@ export const mockedUsersData: Array<MockedUser> = [
firstName: 'Felix',
lastName: 'Test',
canImpersonate: false,
supportUserHash: '',
workspaceMember: {
__typename: 'WorkspaceMember',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
@ -77,6 +79,7 @@ export const mockedOnboardingUsersData: Array<MockedUser> = [
lastName: '',
avatarUrl: null,
canImpersonate: false,
supportUserHash: '',
workspaceMember: {
__typename: 'WorkspaceMember',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
@ -106,6 +109,7 @@ export const mockedOnboardingUsersData: Array<MockedUser> = [
lastName: '',
avatarUrl: null,
canImpersonate: false,
supportUserHash: '',
workspaceMember: {
__typename: 'WorkspaceMember',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',

View File

@ -19,5 +19,5 @@ SIGN_IN_PREFILLED=true
# STORAGE_TYPE=local
# STORAGE_LOCAL_PATH=.local-storage
# SUPPORT_DRIVER=front
# SUPPORT_HMAC_KEY=replace_me_with_a_random_string
# SUPPORT_FRONTEND_KEY=replace_me_with_a_random_string
# SUPPORT_FRONT_HMAC_KEY=replace_me_with_front_chat_verification_secret
# SUPPORT_FRONT_CHAT_ID=replace_me_with_front_chat_id

View File

@ -22,12 +22,12 @@ class Telemetry {
}
@ObjectType()
class SupportChat {
class Support {
@Field(() => String)
supportDriver: string;
@Field(() => String, { nullable: true })
supportFrontendKey: string | null;
supportFrontChatId: string | undefined;
}
@ObjectType()
@ -44,6 +44,6 @@ export class ClientConfig {
@Field(() => Boolean)
debugMode: boolean;
@Field(() => SupportChat)
supportChat: SupportChat;
@Field(() => Support)
support: Support;
}

View File

@ -23,9 +23,9 @@ export class ClientConfigResolver {
},
signInPrefilled: this.environmentService.isSignInPrefilled(),
debugMode: this.environmentService.isDebugMode(),
supportChat: {
support: {
supportDriver: this.environmentService.getSupportDriver(),
supportFrontendKey: this.environmentService.getSupportFrontendKey(),
supportFrontChatId: this.environmentService.getSupportFrontChatId(),
},
};

View File

@ -1,9 +0,0 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { User } from 'src/core/@generated/user/user.model';
@ObjectType()
export class UserWithHMACKey extends User {
@Field(() => String, { nullable: true })
supportHMACKey: string | null;
}

View File

@ -43,7 +43,7 @@ import { EnvironmentService } from 'src/integrations/environment/environment.ser
import { UserService } from './user.service';
import { UserWithHMACKey } from './dto/user-with-HMAC';
import { SupportDriver } from 'src/integrations/environment/interfaces/support.interface';
function getHMACKey(email?: string, key?: string | null) {
if (!email || !key) return null;
@ -61,26 +61,25 @@ export class UserResolver {
private environmentService: EnvironmentService,
) {}
@Query(() => UserWithHMACKey)
@Query(() => User)
async currentUser(
@AuthUser() { id, email }: User,
@AuthUser() { id }: User,
@PrismaSelector({ modelName: 'User' })
prismaSelect: PrismaSelect<'User'>,
) {
const key = this.environmentService.getSupportHMACKey();
const select = prismaSelect.value;
delete select['supportHMACKey'];
const user = await this.userService.findUnique({
where: {
id,
},
select,
select
});
assert(user, 'User not found');
return { ...user, supportHMACKey: getHMACKey(email, key) };
return user;
}
@UseFilters(ExceptionFilter)
@ -141,6 +140,17 @@ export class UserResolver {
return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`;
}
@ResolveField(() => String, {
nullable: false,
})
supportUserHash(@Parent() parent: User): string | null {
if (this.environmentService.getSupportDriver() !== SupportDriver.Front) {
return null;
}
const key = this.environmentService.getSupportFrontHMACKey();
return getHMACKey(parent.email, key);
}
@Mutation(() => String)
async uploadProfilePicture(
@AuthUser() { id }: User,

View File

@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config';
import { AwsRegion } from './interfaces/aws-region.interface';
import { StorageType } from './interfaces/storage.interface';
import { SupportDriver } from './interfaces/support.interface';
@Injectable()
export class EnvironmentService {
@ -103,14 +104,14 @@ export class EnvironmentService {
}
getSupportDriver(): string {
return this.configService.get<string>('SUPPORT_DRIVER') ?? 'front';
return this.configService.get<string>('SUPPORT_DRIVER') ?? SupportDriver.None;
}
getSupportFrontendKey(): string | null {
return this.configService.get<string>('SUPPORT_FRONTEND_KEY') ?? null;
getSupportFrontChatId(): string | undefined {
return this.configService.get<string>('SUPPORT_FRONT_CHAT_ID');
}
getSupportHMACKey(): string | null {
return this.configService.get<string>('SUPPORT_HMAC_KEY') ?? null;
getSupportFrontHMACKey(): string | undefined {
return this.configService.get<string>('SUPPORT_FRONT_HMAC_KEY');
}
}

View File

@ -16,6 +16,7 @@ import { StorageType } from './interfaces/storage.interface';
import { AwsRegion } from './interfaces/aws-region.interface';
import { IsAWSRegion } from './decorators/is-aws-region.decorator';
import { CastToBoolean } from './decorators/cast-to-boolean.decorator';
import { SupportDriver } from './interfaces/support.interface';
export class EnvironmentVariables {
// Misc
@ -104,6 +105,19 @@ export class EnvironmentVariables {
@IsString()
@ValidateIf((env) => env.STORAGE_TYPE === StorageType.Local)
STORAGE_LOCAL_PATH?: string;
// Support
@IsEnum(SupportDriver)
@IsOptional()
SUPPORT_DRIVER?: SupportDriver;
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
@IsString()
SUPPORT_FRONT_CHAT_ID?: AwsRegion;
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
@IsString()
SUPPORT_FRONT_HMAC_KEY?: string;
}
export function validate(config: Record<string, unknown>) {

View File

@ -0,0 +1,4 @@
export enum SupportDriver {
None = 'none',
Front = 'front',
}