Skip to content

Commit

Permalink
feat: subscription page (#30551)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugocostadev authored Oct 31, 2023
1 parent afdcad7 commit a31d533
Show file tree
Hide file tree
Showing 56 changed files with 1,363 additions and 130 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-donuts-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": minor
---

Added a new admin page called `Subscription`, this page is responsible of managing the current workspace subscription and it has a overview of the usage and limits of the plan
50 changes: 50 additions & 0 deletions apps/meteor/app/api/server/v1/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { check } from 'meteor/check';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { hasRoleAsync } from '../../../authorization/server/functions/hasRole';
import { getCheckoutUrl } from '../../../cloud/server/functions/getCheckoutUrl';
import { getConfirmationPoll } from '../../../cloud/server/functions/getConfirmationPoll';
import { registerPreIntentWorkspaceWizard } from '../../../cloud/server/functions/registerPreIntentWorkspaceWizard';
import { retrieveRegistrationStatus } from '../../../cloud/server/functions/retrieveRegistrationStatus';
import { saveRegistrationData } from '../../../cloud/server/functions/saveRegistrationData';
import { startRegisterWorkspaceSetupWizard } from '../../../cloud/server/functions/startRegisterWorkspaceSetupWizard';
import { syncWorkspace } from '../../../cloud/server/functions/syncWorkspace';
import { API } from '../api';

API.v1.addRoute(
Expand Down Expand Up @@ -126,3 +128,51 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'cloud.syncWorkspace',
{
authRequired: true,
permissionsRequired: ['manage-cloud'],
rateLimiterOptions: { numRequestsAllowed: 2, intervalTimeInMS: 60000 },
},
{
async post() {
try {
await syncWorkspace();

return API.v1.success({ success: true });
} catch (error) {
return API.v1.failure('Error during workspace sync');
}
},
},
);

/**
* Declaring endpoint here because we don't want this available to the sdk client
*/
declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Endpoints {
'/v1/cloud.checkoutUrl': {
GET: () => { url: string };
};
}
}

API.v1.addRoute(
'cloud.checkoutUrl',
{ authRequired: true, permissionsRequired: ['manage-cloud'] },
{
async get() {
const checkoutUrl = await getCheckoutUrl();

if (!checkoutUrl.url) {
return API.v1.failure();
}

return API.v1.success({ url: checkoutUrl.url });
},
},
);
45 changes: 45 additions & 0 deletions apps/meteor/app/cloud/server/functions/getCheckoutUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { serverFetch as fetch } from '@rocket.chat/server-fetch';

import { SystemLogger } from '../../../../server/lib/logger/system';
import { settings } from '../../../settings/server';
import { getURL } from '../../../utils/server/getURL';
import { getWorkspaceAccessTokenOrThrow } from './getWorkspaceAccessToken';

export const getCheckoutUrl = async () => {
try {
const token = await getWorkspaceAccessTokenOrThrow(false, 'workspace:billing');

const subscriptionURL = getURL('admin/subscription', {
full: true,
});

const body = {
okCallback: `${subscriptionURL}?subscriptionSuccess=true`,
cancelCallback: subscriptionURL,
};

const billingUrl = settings.get<string>('Cloud_Billing_Url');

const response = await fetch(`${billingUrl}/api/v2/checkout`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body,
});

if (!response.ok) {
throw new Error(await response.json());
}

return response.json();
} catch (err: any) {
SystemLogger.error({
msg: 'Failed to get Checkout URL with Rocket.Chat Billing Service',
url: '/api/v2/checkout',
err,
});

throw err;
}
};
14 changes: 13 additions & 1 deletion apps/meteor/app/utils/lib/getURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,19 @@ export const _getURL = (
export const getURLWithoutSettings = (
path: string,
// eslint-disable-next-line @typescript-eslint/naming-convention
{ cdn = true, full = false, cloud = false, cloud_route = '', cloud_params = {} }: Record<string, any> = {},
{
cdn = true,
full = false,
cloud = false,
cloud_route = '',
cloud_params = {},
}: {
cdn?: boolean;
full?: boolean;
cloud?: boolean;
cloud_route?: string;
cloud_params?: Record<string, string>;
},
cdnPrefix: string,
siteUrl: string,
cloudDeepLinkUrl?: string,
Expand Down
8 changes: 7 additions & 1 deletion apps/meteor/app/utils/server/getURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { getURLWithoutSettings } from '../lib/getURL';

export const getURL = function (
path: string, // eslint-disable-next-line @typescript-eslint/naming-convention
params: Record<string, any> = {},
params: {
cdn?: boolean;
full?: boolean;
cloud?: boolean;
cloud_route?: string;
cloud_params?: Record<string, string>;
} = {},
cloudDeepLinkUrl?: string,
): string {
const cdnPrefix = settings.get<string>('CDN_PREFIX') || '';
Expand Down
16 changes: 3 additions & 13 deletions apps/meteor/client/hooks/useIsEnterprise.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import type { OperationResult } from '@rocket.chat/rest-typings';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

const queryKey = ['licenses', 'isEnterprise'] as const;

export const useIsEnterprise = (
options?: UseQueryOptions<
OperationResult<'GET', '/v1/licenses.isEnterprise'>,
unknown,
OperationResult<'GET', '/v1/licenses.isEnterprise'>,
typeof queryKey
>,
): UseQueryResult<OperationResult<'GET', '/v1/licenses.isEnterprise'>> => {
export const useIsEnterprise = (): UseQueryResult<OperationResult<'GET', '/v1/licenses.isEnterprise'>> => {
const isEnterpriseEdition = useEndpoint('GET', '/v1/licenses.isEnterprise');

return useQuery(queryKey, () => isEnterpriseEdition(), {
return useQuery(['licenses', 'isEnterprise'], () => isEnterpriseEdition(), {
keepPreviousData: true,
staleTime: Infinity,
...options,
});
};
9 changes: 9 additions & 0 deletions apps/meteor/client/hooks/useIsSelfHosted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useStatistics } from '../views/hooks/useStatistics';

export const useIsSelfHosted = (): { isSelfHosted: boolean; isLoading: boolean } => {
const { data, isLoading } = useStatistics();

const isSelfHosted = data?.deploy?.platform !== 'rocket-cloud';

return { isSelfHosted, isLoading };
};
8 changes: 6 additions & 2 deletions apps/meteor/client/hooks/useLicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { useEffect } from 'react';

type LicenseDataType = Awaited<OperationResult<'GET', '/v1/licenses.info'>>['license'];

type LicenseParams = {
loadValues?: boolean;
};

const invalidateQueryClientLicenses = (() => {
let timeout: ReturnType<typeof setTimeout> | undefined;

Expand All @@ -19,7 +23,7 @@ const invalidateQueryClientLicenses = (() => {
};
})();

export const useLicense = (): UseQueryResult<Serialized<LicenseDataType>> => {
export const useLicense = (params?: LicenseParams): UseQueryResult<Serialized<LicenseDataType>> => {
const getLicenses = useEndpoint('GET', '/v1/licenses.info');

const queryClient = useQueryClient();
Expand All @@ -28,7 +32,7 @@ export const useLicense = (): UseQueryResult<Serialized<LicenseDataType>> => {

useEffect(() => notify('license', () => invalidateQueryClientLicenses(queryClient)), [notify, queryClient]);

return useQuery(['licenses', 'getLicenses'], () => getLicenses({}), {
return useQuery(['licenses', 'getLicenses', params?.loadValues], () => getLicenses({ ...params }), {
staleTime: Infinity,
keepPreviousData: true,
select: (data) => data.license,
Expand Down
10 changes: 8 additions & 2 deletions apps/meteor/client/hooks/useRegistrationStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

export const useRegistrationStatus = (): UseQueryResult<OperationResult<'GET', '/v1/cloud.registrationStatus'>> => {
type useRegistrationStatusReturnType = {
isRegistered?: boolean;
} & UseQueryResult<OperationResult<'GET', '/v1/cloud.registrationStatus'>>;

export const useRegistrationStatus = (): useRegistrationStatusReturnType => {
const getRegistrationStatus = useEndpoint('GET', '/v1/cloud.registrationStatus');
const canViewregistrationStatus = usePermission('manage-cloud');

return useQuery(
const queryResult = useQuery(
['getRegistrationStatus'],
() => {
if (!canViewregistrationStatus) {
Expand All @@ -20,4 +24,6 @@ export const useRegistrationStatus = (): UseQueryResult<OperationResult<'GET', '
staleTime: Infinity,
},
);

return { isRegistered: !queryResult.isLoading && queryResult.data?.registrationStatus?.workspaceRegistered, ...queryResult };
};
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ export const useAdministrationItems = (): GenericMenuItemProps[] => {
const isAdmin = useRole('admin');
const setModal = useSetModal();

const { data: registrationStatusData } = useRegistrationStatus();
const workspaceRegistered = registrationStatusData?.registrationStatus?.workspaceRegistered ?? false;
const { isRegistered } = useRegistrationStatus();

const handleRegisterWorkspaceClick = (): void => {
const handleModalClose = (): void => setModal(null);
Expand Down Expand Up @@ -102,10 +101,10 @@ export const useAdministrationItems = (): GenericMenuItemProps[] => {
};
const adminItem: GenericMenuItemProps = {
id: 'registration',
content: workspaceRegistered ? t('Registration') : t('Register'),
content: isRegistered ? t('Registration') : t('Register'),
icon: 'cloud-plus',
onClick: () => {
if (workspaceRegistered) {
if (isRegistered) {
cloudRoute.push({ context: '/' });
return;
}
Expand Down
15 changes: 7 additions & 8 deletions apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ const RegisterWorkspace = () => {
const t = useTranslation();
const setModal = useSetModal();

const { data: registrationStatusData, isLoading, isError, refetch } = useRegistrationStatus();
const isWorkspaceRegistered = registrationStatusData?.registrationStatus?.workspaceRegistered ?? false;
const { isRegistered, isLoading, isError, refetch } = useRegistrationStatus();

if (isLoading || isError) {
return null;
Expand All @@ -26,7 +25,7 @@ const RegisterWorkspace = () => {
setModal(null);
refetch();
};
if (isWorkspaceRegistered) {
if (isRegistered) {
setModal(<ConnectWorkspaceModal onClose={handleModalClose} onStatusChange={refetch} />);
} else setModal(<RegisterWorkspaceModal onClose={handleModalClose} onStatusChange={refetch} />);
};
Expand All @@ -43,7 +42,7 @@ const RegisterWorkspace = () => {
<Page background='tint'>
<Page.Header title={t('Registration')}>
<RegisterWorkspaceMenu
isWorkspaceRegistered={isWorkspaceRegistered}
isWorkspaceRegistered={isRegistered || false}
onClick={handleRegisterWorkspaceClick}
onStatusChange={refetch}
onClickOfflineRegistration={handleManualWorkspaceRegistrationButton}
Expand All @@ -52,14 +51,14 @@ const RegisterWorkspace = () => {

<Page.ScrollableContentWithShadow>
<Box display='flex'>
{!isWorkspaceRegistered && <Tag variant='secondary-danger'>{t('RegisterWorkspace_NotRegistered_Title')}</Tag>}
{isWorkspaceRegistered && <Tag variant='primary'>{t('Workspace_registered')}</Tag>}
{!isRegistered && <Tag variant='secondary-danger'>{t('RegisterWorkspace_NotRegistered_Title')}</Tag>}
{isRegistered && <Tag variant='primary'>{t('Workspace_registered')}</Tag>}
</Box>

<Box pb={8}>
<Box fontScale='h3'>
{!isWorkspaceRegistered && t('RegisterWorkspace_NotRegistered_Subtitle')}
{isWorkspaceRegistered && t('RegisterWorkspace_Registered_Description')}
{!isRegistered && t('RegisterWorkspace_NotRegistered_Subtitle')}
{isRegistered && t('RegisterWorkspace_Registered_Description')}
</Box>
<RegisterWorkspaceCards />
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';

import { GenericResourceUsage, GenericResourceUsageSkeleton } from '../../../components/GenericResourceUsage';
import { useActiveConnections } from './hooks/useActiveConnections';
import { useActiveConnections } from '../../hooks/useActiveConnections';

const CustomUserActiveConnections = () => {
const t = useTranslation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import React from 'react';

import { ContextualbarContent, ContextualbarFooter } from '../../../components/Contextualbar';
import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
import { useActiveConnections } from './hooks/useActiveConnections';
import { useActiveConnections } from '../../hooks/useActiveConnections';

const CustomUserStatusService = () => {
const t = useTranslation();
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/client/views/admin/info/InformationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import type { IInstance } from '@rocket.chat/rest-typings';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { memo } from 'react';

import SeatsCard from '../../../../ee/client/views/admin/info/SeatsCard';
import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap';
import Page from '../../../components/Page';
import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
import SeatsCard from '../subscription/components/cards/SeatsCard';
import DeploymentCard from './DeploymentCard';
import LicenseCard from './LicenseCard';
import UsageCard from './UsageCard';
Expand Down Expand Up @@ -95,9 +95,9 @@ const InformationPage = memo(function InformationPage({
<Grid.Item xl={12} height={!showSeatCap ? '50%' : 'full'}>
<LicenseCard />
</Grid.Item>
{!showSeatCap && (
{seatsCap && seatsCap.maxActiveUsers !== Infinity && (
<Grid.Item xl={12} height='50%'>
<SeatsCard seatsCap={seatsCap} />
<SeatsCard value={seatsCap.activeUsers} max={seatsCap.maxActiveUsers} />
</Grid.Item>
)}
</Grid.Item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ComponentProps } from 'react';
import React from 'react';

import { useAutoSequence } from '../../../stories/hooks/useAutoSequence';
import UsagePieGraph from './UsagePieGraph';
import UsagePieGraph from '../subscription/components/UsagePieGraph';

export default {
title: 'Admin/Info/UsagePieGraph',
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/client/views/admin/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ declare module '@rocket.chat/ui-contexts' {
pathname: `/admin/moderation${`/${string}` | ''}${`/${string}` | ''}`;
pattern: '/admin/moderation/:context?/:id?';
};
'subscription': {
pathname: `/admin/subscription`;
pattern: '/admin/subscription';
};
}
}

Expand Down Expand Up @@ -237,3 +241,8 @@ registerAdminRoute('/device-management/:context?/:id?', {
name: 'device-management',
component: lazy(() => import('../../../ee/client/views/admin/deviceManagement/DeviceManagementAdminRoute')),
});

registerAdminRoute('/subscription', {
name: 'subscription',
component: lazy(() => import('./subscription/SubscriptionRoute')),
});
6 changes: 6 additions & 0 deletions apps/meteor/client/views/admin/sidebarItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const {
icon: 'info-circled',
permissionGranted: (): boolean => hasPermission('view-statistics'),
},
{
href: '/admin/subscription',
i18nLabel: 'Subscription',
icon: 'card',
permissionGranted: (): boolean => hasPermission('manage-cloud'),
},
{
href: '/admin/registration',
i18nLabel: 'Registration',
Expand Down
Loading

0 comments on commit a31d533

Please sign in to comment.