snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
import type { IWorkspaceInfo } from '@rocket.chat/core-typings';
import { Box, Card, CardBody, CardCol, CardControls, CardHeader, CardTitle, Icon } from '@rocket.chat/fuselage';
import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
import { ExternalLink, useLicense, useLicenseName } from '@rocket.chat/ui-client';
import type { LocationPathname } from '@rocket.chat/ui-contexts';
import { useSetModal, useMediaUrl } from '@rocket.chat/ui-contexts';
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import VersionCardActionButton from './components/VersionCardActionButton';
import type { VersionActionItem } from './components/VersionCardActionItem';
import VersionCardActionItem from './components/VersionCardActionItem';
import { VersionCardSkeleton } from './components/VersionCardSkeleton';
import { VersionTag } from './components/VersionTag';
import { getVersionStatus } from './getVersionStatus';
import RegisterWorkspaceModal from './modals/RegisterWorkspaceModal';
import { useFormatDate } from '../../../../hooks/useFormatDate';
import { useRegistrationStatus } from '../../../../hooks/useRegistrationStatus';
import { links } from '../../../../lib/links';
import { isOverLicenseLimits } from '../../../../lib/utils/isOverLicenseLimits';
const SUPPORT_EXTERNAL_LINK = links.go.versionSupport;
const RELEASES_EXTERNAL_LINK = links.go.updateProduct;
export type VersionCardProps = {
serverInfo: IWorkspaceInfo;
};
const VersionCard = ({ serverInfo }: VersionCardProps) => {
const breakpoints = useBreakpoints();
const isExtraLargeOrBigger = breakpoints.includes('xl');
const getUrl = useMediaUrl();
const cardBackground = {
backgroundImage: `url(${getUrl('images/globe.png')})`,
backgroundRepeat: 'no-repeat',
backgroundPosition: isExtraLargeOrBigger ? 'right 20px center' : 'left 450px center',
backgroundSize: 'auto',
};
const setModal = useSetModal();
const { t } = useTranslation();
const formatDate = useFormatDate();
const { data: licenseData, isPending, refetch: refetchLicense } = useLicense({ loadValues: true });
const { isRegistered, canViewRegistrationStatus } = useRegistrationStatus();
const { license, limits } = licenseData || {};
const isAirgapped = license?.information?.offline;
const licenseName = useLicenseName();
const serverVersion = serverInfo.version;
const { versionStatus, versions } = useMemo(() => {
const supportedVersions = serverInfo?.supportedVersions?.signed ? decodeBase64(serverInfo?.supportedVersions?.signed) : undefined;
if (!supportedVersions) {
return {};
}
const versionStatus = getVersionStatus(serverVersion, supportedVersions?.versions);
return {
versionStatus,
versions: supportedVersions?.versions,
};
}, [serverInfo?.supportedVersions?.signed, serverVersion]);
const isOverLimits = limits && isOverLicenseLimits(limits);
const actionButton:
| undefined
| {
path: LocationPathname;
label: ReactNode;
}
| {
action: () => void;
label: ReactNode;
} = useMemo(() => {
if (canViewRegistrationStatus && !isRegistered) {
return {
action: () => {
const handleModalClose = (): void => {
setModal(null);
refetchLicense();
};
setModal(<RegisterWorkspaceModal onClose={handleModalClose} onStatusChange={refetchLicense} />);
},
label: t('RegisterWorkspace_Button'),
};
}
if (versionStatus?.label === 'outdated') {
return {
action: () => {
window.open(RELEASES_EXTERNAL_LINK, '_blank');
},
label: t('Update_version'),
};
}
if (isOverLimits) {
return { path: '/admin/subscription', label: t('Manage_subscription') };
}
}, [canViewRegistrationStatus, isRegistered, versionStatus, isOverLimits, t, setModal, refetchLicense]);
const actionItems = useMemo(() => {
return (
[
isOverLimits
? {
danger: true,
icon: 'warning',
label: t('Plan_limits_reached'),
}
: {
icon: 'check',
label: t('Operating_withing_plan_limits'),
},
(isAirgapped || !versions) && {
icon: 'warning',
label: (
<Trans i18nKey='Check_support_availability'>
Check
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>support</ExternalLink>
availability
</Trans>
),
},
versionStatus?.label !== 'outdated' &&
versionStatus?.expiration && {
icon: 'check',
label: (
<Trans i18nKey='Version_supported_until' values={{ date: formatDate(versionStatus?.expiration) }}>
Version
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>supported</ExternalLink>
until {formatDate(versionStatus?.expiration)}
</Trans>
),
},
versionStatus?.label === 'outdated' && {
danger: true,
icon: 'warning',
label: (
<Trans i18nKey='Version_not_supported'>
Version
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>not supported</ExternalLink>
</Trans>
),
},
canViewRegistrationStatus &&
(isRegistered
? {
icon: 'check',
label: t('Workspace_registered'),
}
: {
danger: true,
icon: 'warning',
label: t('Workspace_not_registered'),
}),
].filter(Boolean) as VersionActionItem[]
).sort((a) => (a.danger ? -1 : 1));
}, [
isOverLimits,
t,
isAirgapped,
versions,
versionStatus?.label,
versionStatus?.expiration,
formatDate,
canViewRegistrationStatus,
isRegistered,
]);
if (isPending && !licenseData) {
return (
<Card style={{ ...cardBackground }}>
<VersionCardSkeleton />
</Card>
);
}
return (
<Card style={{ ...cardBackground }}>
<CardCol>
<CardHeader>
<CardTitle variant='h3'>{t('Version_version', { version: serverVersion })}</CardTitle>
{!isAirgapped && versions && <VersionTag versionStatus={versionStatus?.label} title={versionStatus.version} />}
</CardHeader>
<Box color='secondary-info'>
<Icon name='rocketchat' size={16} /> {licenseName.data}
</Box>
</CardCol>
<CardBody flexDirection='column'>
{actionItems.length > 0 && actionItems.map((item, index) => <VersionCardActionItem key={index} {...item} />)}
</CardBody>
{actionButton && (
<CardControls>
<VersionCardActionButton {...actionButton} />
</CardControls>
)}
</Card>
);
};
export default VersionCard;
const decodeBase64 = (b64: string): SupportedVersions | undefined => {
const [, bodyEncoded] = b64.split('.');
if (!bodyEncoded) {
return;
}
return JSON.parse(atob(bodyEncoded));
};

View File

@@ -0,0 +1,36 @@
import { Button } from '@rocket.chat/fuselage';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import type { LocationPathname } from '@rocket.chat/ui-contexts';
import { useRouter } from '@rocket.chat/ui-contexts';
import type { ReactNode } from 'react';
import { memo } from 'react';
export type VersionCardActionButtonProps =
| {
path: LocationPathname;
label: ReactNode;
}
| {
action: () => void;
label: ReactNode;
};
const VersionCardActionButton = (item: VersionCardActionButtonProps) => {
const router = useRouter();
const handleActionButton = useStableCallback(() => {
if ('action' in item) {
return item.action();
}
router.navigate(item.path);
});
return (
<Button primary onClick={() => handleActionButton()}>
{item.label}
</Button>
);
};
export default memo(VersionCardActionButton);

View File

@@ -0,0 +1,22 @@
import { Box, FramedIcon } from '@rocket.chat/fuselage';
import type { Keys } from '@rocket.chat/icons';
import type { ReactNode } from 'react';
export type VersionActionItem = {
danger?: boolean;
icon: Keys;
label: ReactNode;
};
export type VersionCardActionItemProps = VersionActionItem;
const VersionCardActionItem = ({ icon, label, danger }: VersionCardActionItemProps) => {
return (
<Box display='flex' alignItems='center' color={danger ? 'danger' : 'secondary-info'} fontScale='p2m'>
<FramedIcon danger={danger} icon={icon} />
<Box marginInlineStart={12}>{label}</Box>
</Box>
);
};
export default VersionCardActionItem;

View File

@@ -0,0 +1,23 @@
import { Box, Skeleton } from '@rocket.chat/fuselage';
export const VersionCardSkeleton = () => {
return (
<>
<Skeleton height={28} width={160} />
<Skeleton height={28} width={150} />
<Box display='flex' width={160} height={28} alignItems='center'>
<Skeleton height={38} width={28} marginInlineEnd={12} />
<Skeleton height={20} width={120} />
</Box>
<Box display='flex' width={180} height={28} alignItems='center'>
<Skeleton height={38} width={28} marginInlineEnd={12} />
<Skeleton height={20} width={140} />
</Box>
<Box display='flex' width={160} height={28} alignItems='center'>
<Skeleton height={38} width={28} marginInlineEnd={12} />
<Skeleton height={20} width={120} />
</Box>
<Skeleton height={50} width={160} />
</>
);
};

View File

@@ -0,0 +1,34 @@
import { Tag } from '@rocket.chat/fuselage';
import { useTranslation } from 'react-i18next';
export type VersionStatus = 'outdated' | 'latest' | 'available_version' | undefined;
export type VersionTagProps = {
versionStatus: VersionStatus;
title?: string;
};
export const VersionTag = ({ versionStatus, title }: VersionTagProps) => {
const { t } = useTranslation();
if (versionStatus === 'outdated') {
return (
<Tag title={title} variant='danger'>
{t('Outdated')}
</Tag>
);
}
if (versionStatus === 'latest') {
return (
<Tag title={title} variant='primary'>
{t('Latest')}
</Tag>
);
}
return (
<Tag title={title} variant='secondary'>
{t('New_version_available')}
</Tag>
);
};

View File

@@ -0,0 +1,185 @@
import { getVersionStatus } from './getVersionStatus';
describe('if the server version from server and the highest version from cloud are the same', () => {
describe('the expiration date is in the future', () => {
it('should return as latest version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('latest');
});
});
describe('the expiration date is in the past', () => {
it('should return as outdated version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date('2020-01-01'),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
});
});
});
describe('if the server version is not in the list of supported versions', () => {
it('should return as outdated version', () => {
const status = getVersionStatus('2.0.0', [
{
version: '3.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
});
});
describe('if the server version is in the list of supported versions but is not the highest', () => {
describe('the expiration date is in the future', () => {
it('should return as available version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
{
version: '4.0.0',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('available_version');
});
});
describe('the expiration date is in the past', () => {
it('should return as outdated version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date('2020-01-01'),
security: false,
infoUrl: '',
},
{
version: '4.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
});
});
});
describe('if the server version is not in the list of supported versions but is the highest', () => {
it('should return as latest version', () => {
const status = getVersionStatus('4.0.0', [
{
version: '2.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
{
version: '3.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
expect(status.version).toBe('4.0.0');
});
});
describe('prerelease version', () => {
it('should not suggest available version the highest version is a prerelease from a different tag', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
{
version: '4.0.0-develop',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('latest');
expect(status.version).toBe('3.0.0');
});
it('should suggest available version the highest version is a prerelease from the same tag', () => {
const status = getVersionStatus('6.5.0-rc.14', [
{
version: '6.5.0-rc.14',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
{
version: '6.5.0-rc.15',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
{
version: '4.0.0-develop',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('available_version');
expect(status.version).toBe('6.5.0-rc.15');
});
it('should suggest available version the highest version even if the current one is a prerelease', () => {
const status = getVersionStatus('6.5.0-rc.14', [
{
version: '6.5.0-rc.14',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
{
version: '6.5.0-rc.15',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
{
version: '7.0.0',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('available_version');
expect(status.version).toBe('7.0.0');
});
});

View File

@@ -0,0 +1,37 @@
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
import semver from 'semver';
import type { VersionStatus } from './components/VersionTag';
export const getVersionStatus = (
serverVersion: string,
versions: SupportedVersions['versions'],
): { label: VersionStatus; expiration: Date | undefined; version: string } => {
const highestVersion = versions
.sort((prev, current) => semver.compare(current.version, prev.version))
.filter((v) => {
const [prerelease] = semver.prerelease(v.version) || [];
const [currentPrerelease] = semver.prerelease(serverVersion) || [];
return !prerelease || prerelease === currentPrerelease;
})[0];
const currentVersionData =
versions.find((v) => v.version === serverVersion) ||
versions.find((v) =>
semver.satisfies(String(semver.coerce(v.version)), `=${semver.major(serverVersion)}.${semver.minor(serverVersion)}.x`),
);
const currentVersionIsExpired = currentVersionData?.expiration && new Date(currentVersionData.expiration) < new Date();
const isSupported = !currentVersionIsExpired && !!currentVersionData;
return {
label: 'outdated',
version: serverVersion,
...(isSupported && semver.gte(currentVersionData?.version, highestVersion.version) && { label: 'latest' }),
...(isSupported &&
semver.gt(highestVersion.version, currentVersionData?.version) && { label: 'available_version', version: highestVersion.version }),
expiration: currentVersionData?.expiration,
};
};

View File

@@ -0,0 +1,41 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type featureBulletsType = {
key: number;
title: string;
description: string;
disconnect: string;
};
const useFeatureBullets = () => {
const { t } = useTranslation();
const featureBullets: featureBulletsType[] = useMemo(
() => [
{
key: 1,
title: t('RegisterWorkspace_Features_MobileNotifications_Title'),
description: t('RegisterWorkspace_Features_MobileNotifications_Description'),
disconnect: t('RegisterWorkspace_Features_MobileNotifications_Disconnect'),
},
{
key: 2,
title: t('RegisterWorkspace_Features_Marketplace_Title'),
description: t('RegisterWorkspace_Features_Marketplace_Description'),
disconnect: t('RegisterWorkspace_Features_Marketplace_Disconnect'),
},
{
key: 3,
title: t('RegisterWorkspace_Features_Omnichannel_Title'),
description: t('RegisterWorkspace_Features_Omnichannel_Description'),
disconnect: t('RegisterWorkspace_Features_Omnichannel_Disconnect'),
},
],
[t],
);
return featureBullets;
};
export default useFeatureBullets;

View File

@@ -0,0 +1,85 @@
import {
Box,
Button,
ButtonGroup,
Modal,
ModalClose,
ModalContent,
ModalFooter,
ModalHeader,
ModalHeaderText,
ModalTitle,
} from '@rocket.chat/fuselage';
import { ExternalLink } from '@rocket.chat/ui-client';
import { useSetModal } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
import RegisterWorkspaceSetupModal from './RegisterWorkspaceSetupModal';
import RegisterWorkspaceTokenModal from './RegisterWorkspaceTokenModal';
import { links } from '../../../../../lib/links';
import useFeatureBullets from '../hooks/useFeatureBullets';
export type RegisterWorkspaceModalProps = {
onClose: () => void;
onStatusChange?: () => void;
};
const documentationLink = links.go.registerInfoCollected;
const RegisterWorkspaceModal = ({ onClose, onStatusChange, ...props }: RegisterWorkspaceModalProps) => {
const setModal = useSetModal();
const bulletFeatures = useFeatureBullets();
const { t } = useTranslation();
const handleTokenModal = (): void => {
const handleModalClose = (): void => setModal(null);
setModal(<RegisterWorkspaceTokenModal onClose={handleModalClose} onStatusChange={onStatusChange} />);
};
const handleSetupModal = (): void => {
const handleModalClose = (): void => setModal(null);
setModal(<RegisterWorkspaceSetupModal onClose={handleModalClose} onStatusChange={onStatusChange} />);
};
return (
<Modal {...props}>
<ModalHeader>
<ModalHeaderText>
<ModalTitle>{t('RegisterWorkspace_NotRegistered_Title')}</ModalTitle>
</ModalHeaderText>
<ModalClose onClick={onClose} />
</ModalHeader>
<ModalContent>
<Box withRichContent>
<span>{`${t('RegisterWorkspace_NotRegistered_Subtitle')}:`}</span>
<ul>
{bulletFeatures.map((features) => (
<li key={features.key}>
<strong>{features.title}</strong>
<Box is='p' marginBlockStart={4}>
{features.description}
</Box>
</li>
))}
</ul>
<Box is='p' fontSize='p2'>
{t('RegisterWorkspace_Registered_Benefits')}
</Box>
</Box>
</ModalContent>
<ModalFooter>
<Box is='div' display='flex' justifyContent='space-between' alignItems='center' width='full'>
<ExternalLink to={documentationLink}>{t('Learn_more')}</ExternalLink>
<ButtonGroup align='end'>
<Button onClick={handleTokenModal}>{t('Use_token')}</Button>
<Button primary onClick={handleSetupModal}>
{t('RegisterWorkspace_Button')}
</Button>
</ButtonGroup>
</Box>
</ModalFooter>
</Modal>
);
};
export default RegisterWorkspaceModal;

View File

@@ -0,0 +1,71 @@
import { validateEmail } from '@rocket.chat/tools';
import { useSetModal } from '@rocket.chat/ui-contexts';
import { useEffect, useState } from 'react';
import RegisteredWorkspaceModal from '../RegisteredWorkspaceModal';
import RegisterWorkspaceSetupStepOneModal from './RegisterWorkspaceSetupStepOneModal';
import RegisterWorkspaceSetupStepTwoModal from './RegisterWorkspaceSetupStepTwoModal';
export type RegisterWorkspaceSetupModalProps = {
onClose: () => void;
onStatusChange?: () => void;
};
const RegisterWorkspaceSetupModal = ({ onClose }: RegisterWorkspaceSetupModalProps) => {
const setModal = useSetModal();
const [step, setStep] = useState(1);
const [email, setEmail] = useState('');
const [terms, setTerms] = useState(false);
const [validInfo, setValidInfo] = useState(false);
const [intentData, setIntentData] = useState({
device_code: '',
interval: 0,
user_code: '',
});
// reset validInfo when users go back to step 1
useEffect(() => {
setValidInfo(false);
}, [step]);
useEffect(() => {
if (step === 1) {
setValidInfo(validateEmail(email) && terms);
}
}, [email, terms]);
const onSuccess = () => {
const handleModalClose = (): void => setModal(null);
setModal(<RegisteredWorkspaceModal onClose={handleModalClose} />);
};
return (
<>
{step === 1 ? (
<RegisterWorkspaceSetupStepOneModal
email={email}
setEmail={setEmail}
step={step}
setStep={setStep}
terms={terms}
setTerms={setTerms}
onClose={onClose}
validInfo={validInfo}
setIntentData={setIntentData}
/>
) : (
<RegisterWorkspaceSetupStepTwoModal
email={email}
step={step}
setStep={setStep}
onClose={onClose}
intentData={intentData}
onSuccess={onSuccess}
/>
)}
</>
);
};
export default RegisterWorkspaceSetupModal;

View File

@@ -0,0 +1,135 @@
import {
Modal,
Box,
Field,
FieldLabel,
FieldRow,
TextInput,
CheckBox,
ButtonGroup,
Button,
ModalHeader,
ModalHeaderText,
ModalTagline,
ModalTitle,
ModalClose,
ModalContent,
ModalFooter,
} from '@rocket.chat/fuselage';
import { ExternalLink } from '@rocket.chat/ui-client';
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useId } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { links } from '../../../../../../lib/links';
import WorkspaceRegistrationModal from '../RegisterWorkspaceModal';
type Props = {
email: string;
setEmail: (email: string) => void;
step: number;
setStep: (step: number) => void;
terms: boolean;
setTerms: (terms: boolean) => void;
onClose: () => void;
validInfo: boolean;
setIntentData: (intentData: any) => void;
};
const RegisterWorkspaceSetupStepOneModal = ({
email,
setEmail,
step,
setStep,
terms,
setTerms,
onClose,
validInfo,
setIntentData,
...props
}: Props) => {
const setModal = useSetModal();
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const createRegistrationIntent = useEndpoint('POST', '/v1/cloud.createRegistrationIntent');
const handleBack = (): void => {
const handleModalClose = (): void => setModal(null);
setModal(<WorkspaceRegistrationModal onClose={handleModalClose} />);
};
const handleRegisterWorkspace = async () => {
try {
const { intentData } = await createRegistrationIntent({ resend: false, email });
setIntentData(intentData);
setStep(step + 1);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
};
const emailField = useId();
const termsField = useId();
return (
<Modal {...props}>
<ModalHeader>
<ModalHeaderText>
<ModalTagline>{t('RegisterWorkspace_Setup_Steps', { step, numberOfSteps: 2 })}</ModalTagline>
<ModalTitle>{t('RegisterWorkspace_with_email')}</ModalTitle>
</ModalHeaderText>
<ModalClose onClick={onClose} />
</ModalHeader>
<ModalContent>
<Box>
<Box is='p' fontSize='p2' withRichContent>
{t('RegisterWorkspace_Setup_Subtitle')}
</Box>
<Field paddingBlockStart={10}>
<FieldLabel htmlFor={emailField}>{t('RegisterWorkspace_Setup_Label')}</FieldLabel>
<FieldRow>
<TextInput
id={emailField}
onChange={(e) => {
setEmail((e.target as HTMLInputElement).value);
}}
/>
</FieldRow>
</Field>
<Box marginBlock={16} fontSize='c1'>
<Box is='p'>
<strong>{t('RegisterWorkspace_Setup_Have_Account_Title')}</strong>
</Box>
<Box is='p'>{t('RegisterWorkspace_Setup_Have_Account_Subtitle')}</Box>
<Box is='p' paddingBlockStart={16}>
<strong>{t('RegisterWorkspace_Setup_No_Account_Title')}</strong>
</Box>
<Box is='p'>{t('RegisterWorkspace_Setup_No_Account_Subtitle')}</Box>
</Box>
<Field>
<FieldRow justifyContent='initial'>
<FieldLabel display='block' fontScale='c1' htmlFor={termsField}>
<Trans i18nKey='RegisterWorkspace_Setup_Terms_Privacy'>
I agree with <ExternalLink to={links.terms}>Terms and Conditions</ExternalLink> and{' '}
<ExternalLink to={links.privacy}>Privacy Policy</ExternalLink>
</Trans>
</FieldLabel>
<CheckBox id={termsField} checked={terms} onChange={() => setTerms(!terms)} />
</FieldRow>
</Field>
</Box>
</ModalContent>
<ModalFooter>
<ButtonGroup align='end'>
<Button onClick={handleBack}>{t('Back')}</Button>
<Button primary onClick={handleRegisterWorkspace} disabled={!validInfo}>
{t('Next')}
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
);
};
export default RegisterWorkspaceSetupStepOneModal;

View File

@@ -0,0 +1,123 @@
import {
Modal,
Box,
Field,
FieldLabel,
FieldRow,
TextInput,
ModalHeader,
ModalHeaderText,
ModalTagline,
ModalTitle,
ModalClose,
ModalContent,
ModalFooter,
} from '@rocket.chat/fuselage';
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useCallback, useEffect } from 'react';
import { useTranslation, Trans } from 'react-i18next';
type Props = {
email: string;
step: number;
setStep: (step: number) => void;
onClose: () => void;
intentData: {
device_code: string;
interval: number;
user_code: string;
};
onSuccess: () => void;
};
const setIntervalTime = (interval?: number): number => (interval ? interval * 1000 : 0);
const RegisterWorkspaceSetupStepTwoModal = ({ email, step, setStep, onClose, intentData, onSuccess, ...props }: Props) => {
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const cloudConfirmationPoll = useEndpoint('GET', '/v1/cloud.confirmationPoll');
const createRegistrationIntent = useEndpoint('POST', '/v1/cloud.createRegistrationIntent');
const handleBackFromConfirmation = (): void => setStep(step - 1);
const handleResendRegistrationEmail = async () => {
try {
await createRegistrationIntent({ resend: true, email });
dispatchToastMessage({ type: 'success', message: t('Email_sent') });
} catch (error: unknown) {
dispatchToastMessage({ type: 'error', message: error });
}
};
const getConfirmation = useCallback(async () => {
try {
const { pollData } = await cloudConfirmationPoll({
deviceCode: intentData.device_code,
});
if ('successful' in pollData && pollData.successful) {
dispatchToastMessage({ type: 'success', message: t('Workspace_registered') });
onSuccess();
}
} catch (error: any) {
console.log(error);
}
}, [cloudConfirmationPoll, intentData.device_code, dispatchToastMessage, t, onSuccess]);
useEffect(() => {
const pollInterval = setInterval(() => getConfirmation(), setIntervalTime(intentData.interval));
return (): void => clearInterval(pollInterval);
}, [getConfirmation, intentData]);
return (
<Modal {...props}>
<ModalHeader>
<ModalHeaderText>
<ModalTagline>{t('RegisterWorkspace_Setup_Steps', { step, numberOfSteps: 2 })}</ModalTagline>
<ModalTitle>{t('Awaiting_confirmation')}</ModalTitle>
</ModalHeaderText>
<ModalClose onClick={onClose} />
</ModalHeader>
<ModalContent>
<Box fontSize='p2'>
<Box>
<Trans i18nKey='cloud.RegisterWorkspace_Setup_Email_Confirmation'>
<Box is='p'>
Email sent to{' '}
<Box is='span' fontScale='p2b'>
{email}{' '}
</Box>
with a confirmation link.
</Box>
</Trans>
</Box>
<Box is='p'>{t('RegisterWorkspace_Setup_Email_Verification')}</Box>
<Field paddingBlockStart={10}>
<FieldLabel>{t('Security_code')}</FieldLabel>
<FieldRow>
<TextInput defaultValue={intentData.user_code} disabled />
</FieldRow>
</Field>
</Box>
</ModalContent>
<ModalFooter>
<Box is='div' display='flex' justifyContent='start' fontSize='c1' width='full'>
<Trans i18nKey='RegisterWorkspace_Didnt_Receive_Email'>
Didn't receive email?{' '}
<Box is='a' paddingInline={4} onClick={handleResendRegistrationEmail}>
Resend
</Box>{' '}
or{' '}
<Box is='a' paddingInline={4} onClick={handleBackFromConfirmation}>
change email
</Box>
</Trans>
</Box>
</ModalFooter>
</Modal>
);
};
export default RegisterWorkspaceSetupStepTwoModal;

View File

@@ -0,0 +1 @@
export { default } from './RegisterWorkspaceSetupModal';

View File

@@ -0,0 +1,113 @@
import {
Box,
Button,
ButtonGroup,
Field,
FieldLabel,
FieldRow,
FieldError,
Modal,
TextInput,
ModalHeader,
ModalHeaderText,
ModalTitle,
ModalClose,
ModalContent,
ModalFooter,
} from '@rocket.chat/fuselage';
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import type { ChangeEvent } from 'react';
import { useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import WorkspaceRegistrationModal from './RegisterWorkspaceModal';
export type RegisterWorkspaceTokenModalProps = {
onClose: () => void;
onStatusChange?: () => void;
};
const RegisterWorkspaceTokenModal = ({ onClose, onStatusChange, ...props }: RegisterWorkspaceTokenModalProps) => {
const setModal = useSetModal();
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const connectWorkspace = useEndpoint('POST', '/v1/cloud.connectWorkspace');
const [token, setToken] = useState('');
const [processing, setProcessing] = useState(false);
const [error, setError] = useState(false);
const handleBackAction = (): void => {
const handleModalClose = (): void => setModal(null);
setModal(<WorkspaceRegistrationModal onClose={handleModalClose} />);
};
const handleTokenChange = (event: ChangeEvent<HTMLInputElement>) => {
setToken(event.target.value);
};
const isToken = token.length > 0;
const handleConnectButtonClick = async (): Promise<void> => {
setProcessing(true);
setError(false);
try {
await connectWorkspace({ token });
setModal(null);
dispatchToastMessage({ type: 'success', message: t('Connected') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
setError(true);
} finally {
await onStatusChange?.();
setProcessing(false);
}
};
return (
<Modal {...props}>
<ModalHeader>
<ModalHeaderText>
<ModalTitle>{t('RegisterWorkspace_Token_Title')}</ModalTitle>
</ModalHeaderText>
<ModalClose onClick={onClose} />
</ModalHeader>
<ModalContent>
<Box is='p'>
<Trans i18nKey='RegisterWorkspace_Token_Step_One'>
1. Go to:{' '}
<Box is='span' fontWeight={600}>
cloud.rocket.chat {'>'} Workspaces
</Box>{' '}
and click{' '}
<Box is='span' fontWeight={600}>
"Register self-managed"
</Box>
.
</Trans>
</Box>
<Box is='p' fontSize='p2'>{`2. ${t('RegisterWorkspace_Token_Step_Two')}`}</Box>
<Field paddingBlockStart={10}>
<FieldLabel>{t('Registration_Token')}</FieldLabel>
<FieldRow>
<TextInput onChange={handleTokenChange} value={token} />
</FieldRow>
{error && <FieldError>{t('Token_Not_Recognized')}</FieldError>}
</Field>
</ModalContent>
<ModalFooter>
<ButtonGroup align='end'>
<Button onClick={handleBackAction}>{t('Back')}</Button>
<Button primary disabled={!isToken} loading={processing} onClick={handleConnectButtonClick}>
{t('Next')}
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
);
};
export default RegisterWorkspaceTokenModal;

View File

@@ -0,0 +1,89 @@
import {
Box,
Button,
ButtonGroup,
Modal,
ModalClose,
ModalContent,
ModalFooter,
ModalHeader,
ModalHeaderText,
ModalTitle,
} from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import useFeatureBullets from '../hooks/useFeatureBullets';
export type RegisteredWorkspaceModalProps = {
onClose: () => void;
onStatusChange?: () => void;
};
const RegisteredWorkspaceModal = ({ onClose, onStatusChange, ...props }: RegisteredWorkspaceModalProps) => {
const { t } = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const bulletFeatures = useFeatureBullets();
const [isSyncing, setSyncing] = useSafely(useState(false));
const syncWorkspace = useEndpoint('POST', '/v1/cloud.syncWorkspace');
const handleSyncAction = async () => {
setSyncing(true);
try {
const { success } = await syncWorkspace();
if (!success) {
throw Error(t('RegisterWorkspace_Syncing_Error'));
}
dispatchToastMessage({ type: 'success', message: t('RegisterWorkspace_Syncing_Complete') });
setModal(null);
} catch (error) {
const message = error instanceof Error ? error.message : t('RegisterWorkspace_Syncing_Error');
dispatchToastMessage({ type: 'error', message });
} finally {
onStatusChange?.();
setSyncing(false);
}
};
return (
<Modal {...props}>
<ModalHeader>
<ModalHeaderText>
<ModalTitle>{t('Workspace_registered')}</ModalTitle>
</ModalHeaderText>
<ModalClose onClick={onClose} />
</ModalHeader>
<ModalContent>
<Box withRichContent>
<span>{`${t('RegisterWorkspace_Registered_Subtitle')}: `}</span>
<ul>
{bulletFeatures.map((item, index) => (
<li key={index}>
<strong>{item.title}</strong>
<Box is='p' marginBlockStart={4}>
{item.description}
</Box>
</li>
))}
</ul>
</Box>
</ModalContent>
<ModalFooter>
<ButtonGroup align='end'>
<Button icon='reload' onClick={handleSyncAction} loading={isSyncing}>
{t('Sync')}
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
);
};
export default RegisteredWorkspaceModal;