Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Omnichannel contact verification status #33550

Merged
merged 6 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useRole, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';

import { getURL } from '../../../../app/utils/client/getURL';
import GenericUpsellModal from '../../../components/GenericUpsellModal';
import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks';
import { useExternalLink } from '../../../hooks/useExternalLink';
import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule';

type AdvancedContactModalProps = {
onCancel: () => void;
};

const AdvancedContactModal = ({ onCancel }: AdvancedContactModalProps) => {
const t = useTranslation();
const isAdmin = useRole('admin');
const hasLicense = useHasLicenseModule('contact-id-verification') as boolean;
const { shouldShowUpsell, handleManageSubscription } = useUpsellActions(hasLicense);
const openExternalLink = useExternalLink();

return (
<GenericUpsellModal
title={t('Advanced_contact_profile')}
description={t('Advanced_contact_profile_description')}
img={getURL('images/single-contact-id-upsell.png')}
onClose={onCancel}
onCancel={shouldShowUpsell ? onCancel : () => openExternalLink('https://go.rocket.chat/i/omnichannel-docs')}
cancelText={!shouldShowUpsell ? t('Learn_more') : undefined}
onConfirm={shouldShowUpsell ? handleManageSubscription : undefined}
annotation={!shouldShowUpsell && !isAdmin ? t('Ask_enable_advanced_contact_profile') : undefined}
/>
);
};

export default AdvancedContactModal;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SidebarToggler from '../../../../components/SidebarToggler';
import { useOmnichannelRoom } from '../../contexts/RoomContext';
import RoomHeader from '../RoomHeader';
import { BackButton } from './BackButton';
import OmnichannelRoomHeaderTag from './OmnichannelRoomHeaderTag';
import QuickActions from './QuickActions';

type OmnichannelRoomHeaderProps = {
Expand Down Expand Up @@ -44,9 +45,10 @@ const OmnichannelRoomHeader = ({ slots: parentSlot }: OmnichannelRoomHeaderProps
<BackButton routeName={currentRouteName} />
</HeaderToolbar>
),
insideContent: <OmnichannelRoomHeaderTag room={room} />,
posContent: <QuickActions />,
}),
[isMobile, currentRouteName, parentSlot],
[isMobile, currentRouteName, parentSlot, room],
);

return <RoomHeader slots={slots} room={room} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { Box, Icon, Tag } from '@rocket.chat/fuselage';
import { useEndpoint, useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React from 'react';

import AdvancedContactModal from '../../../omnichannel/contactInfo/AdvancedContactModal';

type OmnichannelRoomHeaderTagProps = {
room: IOmnichannelRoom;
};

const OmnichannelRoomHeaderTag = ({ room }: OmnichannelRoomHeaderTagProps) => {
const t = useTranslation();
const setModal = useSetModal();
const {
v: { _id, contactId },
} = room;

const getContactById = useEndpoint('GET', '/v1/omnichannel/contacts.get');
const { data } = useQuery(['getContactById', contactId], () => getContactById({ contactId: contactId || _id }));
const isVerifiedContact = data?.contact?.channels?.some((channel) => channel.verified);

if (isVerifiedContact) {
return <Icon mis={4} size='x16' name='success-circle' color='stroke-highlight' />;
}

return (
<Box mis={4} withTruncatedText>
<Tag
onClick={() => setModal(<AdvancedContactModal onCancel={() => setModal(null)} />)}
icon={<Icon size='x12' mie={4} name='question-mark' />}
>
{t('Unverified')}
</Tag>
</Box>
);
};

export default OmnichannelRoomHeaderTag;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SidebarToggler from '../../../../components/SidebarToggler';
import { useOmnichannelRoom } from '../../contexts/RoomContext';
import RoomHeader from '../RoomHeader';
import BackButton from './BackButton';
import OmnichannelRoomHeaderTag from './OmnichannelRoomHeaderTag';
import QuickActions from './QuickActions';

type OmnichannelRoomHeaderProps = {
Expand Down Expand Up @@ -44,9 +45,10 @@ const OmnichannelRoomHeader = ({ slots: parentSlot }: OmnichannelRoomHeaderProps
<BackButton routeName={currentRouteName} />
</HeaderToolbar>
),
insideContent: <OmnichannelRoomHeaderTag room={room} />,
posContent: <QuickActions />,
}),
[isMobile, currentRouteName, parentSlot],
[isMobile, currentRouteName, parentSlot, room],
);

return <RoomHeader slots={slots} room={room} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { Box, Icon, Tag } from '@rocket.chat/fuselage';
import { useEndpoint, useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React from 'react';

import AdvancedContactModal from '../../../omnichannel/contactInfo/AdvancedContactModal';

type OmnichannelRoomHeaderTagProps = {
room: IOmnichannelRoom;
};

const OmnichannelRoomHeaderTag = ({ room }: OmnichannelRoomHeaderTagProps) => {
const t = useTranslation();
const setModal = useSetModal();
const {
v: { _id, contactId },
} = room;

const getContactById = useEndpoint('GET', '/v1/omnichannel/contacts.get');
const { data } = useQuery(['getContactById', contactId], () => getContactById({ contactId: contactId || _id }));
const isVerifiedContact = data?.contact?.channels?.some((channel) => channel.verified);

if (isVerifiedContact) {
return <Icon mis={4} size='x16' name='success-circle' color='stroke-highlight' />;
}

return (
<Box mis={4} withTruncatedText>
<Tag
onClick={() => setModal(<AdvancedContactModal onCancel={() => setModal(null)} />)}
icon={<Icon size='x12' mie={4} name='question-mark' />}
>
{t('Unverified')}
</Tag>
</Box>
);
};

export default OmnichannelRoomHeaderTag;
Original file line number Diff line number Diff line change
@@ -1,52 +1,79 @@
import { MessageFooterCallout } from '@rocket.chat/ui-composer';
import { useTranslation, useUserId } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';

import { useIsRoomOverMacLimit } from '../../../../hooks/omnichannel/useIsRoomOverMacLimit';
import { useOmnichannelRoom, useUserIsSubscribed } from '../../contexts/RoomContext';
import type { ComposerMessageProps } from '../ComposerMessage';
import ComposerMessage from '../ComposerMessage';
import ComposerOmnichannelCallout from './ComposerOmnichannelCallout';
import { ComposerOmnichannelInquiry } from './ComposerOmnichannelInquiry';
import { ComposerOmnichannelJoin } from './ComposerOmnichannelJoin';
import { ComposerOmnichannelOnHold } from './ComposerOmnichannelOnHold';

const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement => {
const ComposerOmnichannel = (props: ComposerMessageProps) => {
const t = useTranslation();
const userId = useUserId();
const room = useOmnichannelRoom();

const { servedBy, queuedAt, open, onHold } = room;
const userId = useUserId();

const isSubscribed = useUserIsSubscribed();

const t = useTranslation();

const isInquired = !servedBy && queuedAt;

const isSameAgent = servedBy?._id === userId;

const isRoomOverMacLimit = useIsRoomOverMacLimit(room);

if (!open) {
return <MessageFooterCallout color='default'>{t('This_conversation_is_already_closed')}</MessageFooterCallout>;
return (
<>
<ComposerOmnichannelCallout />
<MessageFooterCallout color='default'>{t('This_conversation_is_already_closed')}</MessageFooterCallout>
</>
);
}

if (isRoomOverMacLimit) {
return <MessageFooterCallout color='default'>{t('Workspace_exceeded_MAC_limit_disclaimer')}</MessageFooterCallout>;
return (
<>
<ComposerOmnichannelCallout />
<MessageFooterCallout color='default'>{t('Workspace_exceeded_MAC_limit_disclaimer')}</MessageFooterCallout>
</>
);
}

if (onHold) {
return <ComposerOmnichannelOnHold />;
return (
<>
<ComposerOmnichannelCallout />
<ComposerOmnichannelOnHold />
</>
);
}

if (isInquired) {
return <ComposerOmnichannelInquiry />;
return (
<>
<ComposerOmnichannelCallout />
<ComposerOmnichannelInquiry />
</>
);
}

if (!isSubscribed && !isSameAgent) {
return <ComposerOmnichannelJoin />;
return (
<>
<ComposerOmnichannelCallout />
<ComposerOmnichannelJoin />
</>
);
}

return <ComposerMessage {...props} />;
return (
<>
<ComposerOmnichannelCallout />
<ComposerMessage {...props} />
</>
);
};

export default ComposerOmnichannel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Box, Button, ButtonGroup, Callout } from '@rocket.chat/fuselage';
import { useAtLeastOnePermission, useEndpoint, useRouter } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';

import { useOmnichannelRoom } from '../../contexts/RoomContext';

const ComposerOmnichannelCallout = () => {
const { t } = useTranslation();
const room = useOmnichannelRoom();
const { navigate, buildRoutePath } = useRouter();
const securityPrivacyRoute = buildRoutePath('/omnichannel/security-privacy');
const canViewSecurityPrivacy = useAtLeastOnePermission([
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
]);

const {
_id,
v: { contactId },
} = room;

if (!contactId) {
throw Error('No contactId provided');
}

const getContactById = useEndpoint('GET', '/v1/omnichannel/contacts.get');
const { data } = useQuery(['getContactById', contactId], () => getContactById({ contactId }));

if (!data?.contact?.unknown) {
return null;
}

return (
<Callout
mbe={16}
title='Contact unverified and unknown'
actions={
<ButtonGroup>
<Button onClick={() => navigate(`/live/${_id}/contact-profile/edit`)} small>
{t('Add_contact')}
</Button>
</ButtonGroup>
}
>
<Trans i18nKey='Add_to_contact_or_enable_verification_description'>
Add to contact list manually or
<Box is={canViewSecurityPrivacy ? 'a' : 'span'} href={securityPrivacyRoute}>
enable verification
</Box>
using multi-factor authentication.
</Trans>
</Callout>
);
};

export default ComposerOmnichannelCallout;
4 changes: 2 additions & 2 deletions apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,15 @@
"@rocket.chat/forked-matrix-appservice-bridge": "^4.0.2",
"@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.3",
"@rocket.chat/freeswitch": "workspace:^",
"@rocket.chat/fuselage": "^0.59.1",
"@rocket.chat/fuselage": "0.59.2",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/fuselage-polyfills": "~0.31.25",
"@rocket.chat/fuselage-toastbar": "^0.33.0",
"@rocket.chat/fuselage-tokens": "^0.33.1",
"@rocket.chat/fuselage-ui-kit": "workspace:^",
"@rocket.chat/gazzodown": "workspace:^",
"@rocket.chat/i18n": "workspace:^",
"@rocket.chat/icons": "~0.38.0",
"@rocket.chat/icons": "^0.39.0",
"@rocket.chat/instance-status": "workspace:^",
"@rocket.chat/jwt": "workspace:^",
"@rocket.chat/layout": "~0.31.27",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1667,6 +1667,7 @@
"Desktop_Notifications_Not_Enabled": "Desktop Notifications are Not Enabled",
"Unselected_by_default": "Unselected by default",
"Unseen_features": "Unseen features",
"Unverified": "Unverified",
"Details": "Details",
"Device_Changes_Not_Available": "Device changes not available in this browser. For guaranteed availability, please use Rocket.Chat's official desktop app.",
"Device_Changes_Not_Available_Insecure_Context": "Device changes are only available on secure contexts (e.g. https://)",
Expand Down Expand Up @@ -6647,5 +6648,10 @@
"Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.",
"Show_channels_description": "Show team channels in second sidebar",
"Show_discussions_description": "Show team discussions in second sidebar",
"Advanced_contact_profile": "Advanced contact profile",
"Advanced_contact_profile_description": "Manage multiple emails and phone numbers for a single contact, enabling a comprehensive multi-channel history that keeps you well-informed and improves communication efficiency.",
"Add_contact": "Add contact",
"Add_to_contact_or_enable_verification_description": "Add to contact list manually or <1>enable verification</1> using multi-factor authentication.",
"Ask_enable_advanced_contact_profile": "Ask your workspace admin to enable advanced contact profile",
"close-blocked-room-comment": "This channel has been blocked"
}
Loading
Loading