diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx index a5ace5244d17..89149c2242a3 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx @@ -2,31 +2,18 @@ import type { App } from '@rocket.chat/core-typings'; import { Box, Button, Icon, Throbber, Tag, Margins } from '@rocket.chat/fuselage'; import { useSafely } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { - useRouteParameter, - usePermission, - useSetModal, - useTranslation, - useToastMessageDispatch, - useEndpoint, -} from '@rocket.chat/ui-contexts'; +import { useRouteParameter, usePermission, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useState, memo } from 'react'; import semver from 'semver'; -import { Apps } from '../../../../../../ee/client/apps/orchestrator'; -import AppPermissionsReviewModal from '../../../AppPermissionsReviewModal'; -import IframeModal from '../../../IframeModal'; import type { appStatusSpanResponseProps } from '../../../helpers'; -import { appButtonProps, appMultiStatusProps, handleAPIError, handleInstallError } from '../../../helpers'; +import { appButtonProps, appMultiStatusProps } from '../../../helpers'; import { marketplaceActions } from '../../../helpers/marketplaceActions'; +import type { AppInstallationHandlerParams } from '../../../hooks/useAppInstallationHandler'; +import { useAppInstallationHandler } from '../../../hooks/useAppInstallationHandler'; import AppStatusPriceDisplay from './AppStatusPriceDisplay'; -type AppRequestPostMessage = { - message: string; - status: string; -}; - type AppStatusProps = { app: App; showStatus?: boolean; @@ -38,7 +25,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro const t = useTranslation(); const [endUserRequested, setEndUserRequested] = useState(false); const [loading, setLoading] = useSafely(useState(false)); - const [isAppPurchased, setPurchased] = useSafely(useState(app?.isPurchased)); + const [isAppPurchased, setPurchased] = useSafely(useState(!!app?.isPurchased)); const setModal = useSetModal(); const isAdminUser = usePermission('manage-apps'); const context = useRouteParameter('context'); @@ -54,114 +41,42 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro const totalSeenRequests = app?.appRequestStats?.totalSeen; const totalUnseenRequests = app?.appRequestStats?.totalUnseen; - const dispatchToastMessage = useToastMessageDispatch(); - - const notifyAdmins = useEndpoint('POST', '/apps/notify-admins'); - const requestConfirmAction = (postMessage: AppRequestPostMessage) => { - setModal(null); - setLoading(false); - dispatchToastMessage({ type: 'success', message: 'App request submitted' }); - - setEndUserRequested(true); - - notifyAdmins({ - appId: app.id, - appName: app.name, - appVersion: app.marketplaceVersion, - message: postMessage.message, - }); - }; - - if (button?.action === undefined && button?.action) { - throw new Error('action must not be null'); - } const action = button?.action; - const confirmAction = useCallback( - (permissionsGranted) => { - setModal(null); - if (action === undefined) { - setLoading(false); - return; + const confirmAction = useCallback( + async (action, permissionsGranted) => { + if (action !== 'request' && !!permissionsGranted) { + setPurchased(true); + await marketplaceActions[action]({ ...app, permissionsGranted }); + } else { + setEndUserRequested(true); } - if (action !== 'request') { - marketplaceActions[action]({ ...app, permissionsGranted }).then(() => { - setLoading(false); - }); - } + setLoading(false); }, - [setModal, action, app, setLoading], + [app, setLoading, setPurchased], ); + const cancelAction = useCallback(() => { setLoading(false); setModal(null); }, [setLoading, setModal]); - const showAppPermissionsReviewModal = (): void => { - if (!isAppPurchased) { - setPurchased(true); - } - - if (!app.permissions || app.permissions.length === 0) { - return confirmAction(app.permissions); - } - - if (!Array.isArray(app.permissions)) { - handleInstallError(new Error('The "permissions" property from the app manifest is invalid')); - } - - return setModal(); - }; - - const openIncompatibleModal = async (app: App, action: string, cancel: () => void): Promise => { - try { - const incompatibleData = await Apps.buildIncompatibleExternalUrl(app.id, app.marketplaceVersion, action); - setModal(); - } catch (e: any) { - handleAPIError(e); - } - }; - - const openPurchaseModal = async (app: App): Promise => { - try { - const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); - setModal(); - } catch (error) { - handleAPIError(error); - } - }; - - const handleClick = async (e: React.MouseEvent): Promise => { - e.preventDefault(); - e.stopPropagation(); + const appInstallationHandler = useAppInstallationHandler({ + app, + action: action || 'purchase', + isAppPurchased, + onDismiss: cancelAction, + onSuccess: confirmAction, + }); + const handleAcquireApp = useCallback(() => { setLoading(true); + appInstallationHandler(); + }, [appInstallationHandler, setLoading]); - if (action === 'request') { - try { - const data = await Apps.buildExternalAppRequest(app.id); - setModal(); - } catch (error) { - handleAPIError(error); - } - return; - } - - if (app.versionIncompatible && action !== undefined) { - openIncompatibleModal(app, action, cancelAction); - return; - } - - if (action !== undefined && action === 'purchase' && !isAppPurchased) { - openPurchaseModal(app); - return; - } - - showAppPermissionsReviewModal(); - }; - + // @TODO we should refactor this to not use the label to determine the variant const getStatusVariant = (status: appStatusSpanResponseProps) => { if (isAppRequestsPage && totalUnseenRequests && (status.label === 'request' || status.label === 'requests')) { return 'primary'; @@ -171,7 +86,8 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro return undefined; } - if (status.label === 'Disabled') { + // includes() here because the label can be 'Disabled' or 'Disabled*' + if (status.label.includes('Disabled')) { return 'secondary-danger'; } @@ -211,7 +127,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro primary small disabled={loading || (action === 'request' && (app?.requestedEndUser || endUserRequested))} - onClick={handleClick} + onClick={handleAcquireApp} mie='x8' > {loading ? ( diff --git a/apps/meteor/client/views/marketplace/AppInstallPage.js b/apps/meteor/client/views/marketplace/AppInstallPage.js index 3669f02dfa15..619ed46ae3c4 100644 --- a/apps/meteor/client/views/marketplace/AppInstallPage.js +++ b/apps/meteor/client/views/marketplace/AppInstallPage.js @@ -18,7 +18,9 @@ import { useForm } from '../../hooks/useForm'; import AppPermissionsReviewModal from './AppPermissionsReviewModal'; import AppUpdateModal from './AppUpdateModal'; import { useAppsReload } from './AppsContext'; +import AppInstallModal from './components/AppInstallModal/AppInstallModal'; import { handleAPIError, handleInstallError } from './helpers'; +import { useAppsCountQuery } from './hooks/useAppsCountQuery'; import { getManifestFromZippedApp } from './lib/getManifestFromZippedApp'; const placeholderUrl = 'https://rocket.chat/apps/package.zip'; @@ -32,7 +34,9 @@ function AppInstallPage() { if (!currentRouteName) { throw new Error('No current route name'); } + const router = useRoute(currentRouteName); + const upgradeRoute = useRoute('upgradeRoute'); const context = useRouteParameter('context'); @@ -45,9 +49,11 @@ function AppInstallPage() { const endpointAddress = appId ? `/apps/${appId}` : '/apps'; const downloadApp = useEndpoint('POST', endpointAddress); - const uploadApp = useUpload(endpointAddress); + const uploadAppEndpoint = useUpload(endpointAddress); const uploadUpdateApp = useUpload(`${endpointAddress}/update`); + const appCountQuery = useAppsCountQuery('private'); + const { values, handlers } = useForm({ file: {}, url: queryUrl, @@ -75,7 +81,7 @@ function AppInstallPage() { if (appId) { await uploadUpdateApp(fileData); } else { - app = await uploadApp(fileData); + app = await uploadAppEndpoint(fileData); } } catch (e) { handleAPIError(e); @@ -85,6 +91,7 @@ function AppInstallPage() { reload(); + setInstalling(false); setModal(null); }; @@ -112,9 +119,17 @@ function AppInstallPage() { ); }; - const install = async () => { - setInstalling(true); + const uploadFile = async (appFile, { id, permissions }) => { + const isInstalled = await isAppInstalled(id); + + if (isInstalled) { + return setModal( handleAppPermissionsReview(permissions, appFile, id)} />); + } + await handleAppPermissionsReview(permissions, appFile); + }; + + const getAppFileAndManifest = async () => { try { let manifest; let appFile; @@ -128,22 +143,46 @@ function AppInstallPage() { manifest = await getManifestFromZippedApp(appFile); } - const { permissions, id } = manifest; - - const isInstalled = await isAppInstalled(id); - - if (isInstalled) { - setModal( handleAppPermissionsReview(permissions, appFile, id)} />); - } else { - await handleAppPermissionsReview(permissions, appFile); - } + return { appFile, manifest }; } catch (error) { handleInstallError(error); - } finally { - setInstalling(false); + + return { appFile: null, manifest: null }; } }; + const install = async () => { + setInstalling(true); + + if (!appCountQuery.data) { + return cancelAction(); + } + + const { appFile, manifest } = await getAppFileAndManifest(); + + if (!appFile || !manifest) { + return cancelAction(); + } + + if (appCountQuery.data.hasUnlimitedApps) { + return uploadFile(appFile, manifest); + } + + setModal( + uploadFile(appFile, manifest)} + handleEnableUnlimitedApps={() => { + upgradeRoute.push(); + }} + />, + ); + }; + const handleCancel = () => { router.push({ context, page: 'list' }); }; diff --git a/apps/meteor/client/views/marketplace/AppMenu.js b/apps/meteor/client/views/marketplace/AppMenu.js index c752fc201914..0bfe0948e5a1 100644 --- a/apps/meteor/client/views/marketplace/AppMenu.js +++ b/apps/meteor/client/views/marketplace/AppMenu.js @@ -12,24 +12,15 @@ import { import React, { useMemo, useCallback, useState } from 'react'; import semver from 'semver'; -import { Apps } from '../../../ee/client/apps/orchestrator'; import WarningModal from '../../components/WarningModal'; -import AppPermissionsReviewModal from './AppPermissionsReviewModal'; import IframeModal from './IframeModal'; -import AppInstallModal from './components/AppInstallModal/AppInstallModal'; import UninstallGrandfatheredAppModal from './components/UninstallGrandfatheredAppModal/UninstallGrandfatheredAppModal'; import { appEnabledStatuses, handleAPIError, appButtonProps, warnEnableDisableApp } from './helpers'; import { marketplaceActions } from './helpers/marketplaceActions'; +import { useAppInstallationHandler } from './hooks/useAppInstallationHandler'; import { useAppsCountQuery } from './hooks/useAppsCountQuery'; - -const openIncompatibleModal = async (app, action, cancel, setModal) => { - try { - const incompatibleData = await Apps.buildIncompatibleExternalUrl(app.id, app.marketplaceVersion, action); - setModal(); - } catch (e) { - handleAPIError(e); - } -}; +import { useOpenAppPermissionsReviewModal } from './hooks/useOpenAppPermissionsReviewModal'; +import { useOpenIncompatibleModal } from './hooks/useOpenIncompatibleModal'; function AppMenu({ app, isAppDetailsPage, ...props }) { const t = useTranslation(); @@ -41,7 +32,6 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { throw new Error('No current route name'); } const router = useRoute(currentRouteName); - const upgradeRoute = useRoute('upgradeRoute'); const context = useRouteParameter('context'); const currentTab = useRouteParameter('tab'); @@ -50,7 +40,6 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { const buildExternalUrl = useEndpoint('GET', '/apps'); const syncApp = useEndpoint('POST', `/apps/${app.id}/sync`); const uninstallApp = useEndpoint('DELETE', `/apps/${app.id}`); - const notifyAdmins = useEndpoint('POST', `/apps/notify-admins`); const [loading, setLoading] = useState(false); const [requestedEndUser, setRequestedEndUser] = useState(app.requestedEndUser); @@ -61,43 +50,56 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { const [isAppPurchased, setPurchased] = useState(app?.isPurchased); const isAdminUser = usePermission('manage-apps'); - const button = appButtonProps({ ...app, isAdminUser }); + const appCountQuery = useAppsCountQuery(context); + const openIncompatibleModal = useOpenIncompatibleModal(); - const result = useAppsCountQuery(context); + const button = appButtonProps({ ...app, isAdminUser }); + const action = button?.action || ''; - const cancelAction = useCallback(() => { + const closeModal = useCallback(() => { setModal(null); setLoading(false); - }, [setModal]); + }, [setModal, setLoading]); - const action = button?.action || ''; - const confirmAction = useCallback( - async (permissionsGranted) => { - setModal(null); + const installationSuccess = useCallback( + async (action, permissionsGranted) => { + if (action === 'purchase') { + setPurchased(true); + } - await marketplaceActions[action]({ ...app, permissionsGranted }); + if (action === 'request') { + setRequestedEndUser(true); + } else { + await marketplaceActions[action]({ ...app, permissionsGranted }); + } setLoading(false); }, - [setModal, action, app, setLoading], + [app, setLoading], ); - const showAppPermissionsReviewModal = useCallback(() => { - if (!isAppPurchased) { - setPurchased(true); - } + const openPermissionModal = useOpenAppPermissionsReviewModal({ + app, + onCancel: closeModal, + onConfirm: (permissionsGranted) => installationSuccess(action, permissionsGranted), + }); - return setModal(); - }, [app.permissions, cancelAction, confirmAction, isAppPurchased, setModal, setPurchased]); + const appInstallationHandler = useAppInstallationHandler({ + app, + isAppPurchased, + action, + onDismiss: closeModal, + onSuccess: installationSuccess, + }); - const closeModal = useCallback(() => { - setModal(null); - setLoading(false); - }, [setModal]); + const handleAcquireApp = useCallback(() => { + setLoading(true); + appInstallationHandler(); + }, [appInstallationHandler, setLoading]); const handleSubscription = useCallback(async () => { if (app?.versionIncompatible && !isSubscribed) { - openIncompatibleModal(app, 'subscribe', closeModal, setModal); + openIncompatibleModal(app, 'subscribe', closeModal); return; } @@ -123,76 +125,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { }; setModal(); - }, [app, setModal, closeModal, isSubscribed, buildExternalUrl, syncApp]); - - const acquireApp = useCallback(async () => { - if (action === 'purchase' && !isAppPurchased) { - try { - const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); - setModal(); - } catch (error) { - handleAPIError(error); - } - return; - } - - showAppPermissionsReviewModal(); - }, [action, app, isAppPurchased, showAppPermissionsReviewModal, setModal, cancelAction]); - - const handleAcquireApp = useCallback(async () => { - setLoading(true); - - const requestConfirmAction = (postMessage) => { - setModal(null); - setLoading(false); - setRequestedEndUser(true); - dispatchToastMessage({ type: 'success', message: 'App request submitted' }); - - notifyAdmins({ - appId: app.id, - appName: app.name, - appVersion: app.marketplaceVersion, - message: postMessage.message, - }); - }; - - if (app?.versionIncompatible) { - openIncompatibleModal(app, 'subscribe', closeModal, setModal); - return; - } - - if (action === 'request') { - try { - const data = await Apps.buildExternalAppRequest(app.id); - setModal(); - } catch (error) { - handleAPIError(error); - } - return; - } - - if (!result.data) { - return; - } - - if (result.data.hasUnlimitedApps) { - acquireApp(); - } - - setModal( - setModal(null)} - handleConfirm={acquireApp} - handleEnableUnlimitedApps={() => { - upgradeRoute.push(); - }} - />, - ); - }, [app, action, result.data, setModal, context, acquireApp, dispatchToastMessage, notifyAdmins, closeModal, cancelAction, upgradeRoute]); + }, [app, isSubscribed, setModal, closeModal, openIncompatibleModal, buildExternalUrl, syncApp]); const handleViewLogs = useCallback(() => { router.push({ context, page: 'info', id: app.id, version: app.version, tab: 'logs' }); @@ -255,7 +188,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { ); } - if (!result.data) { + if (!appCountQuery.data) { return; } @@ -264,7 +197,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { , @@ -276,7 +209,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { , ); }, [ - result.data, + appCountQuery.data, app.migrated, app.name, isSubscribed, @@ -320,12 +253,12 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { setLoading(true); if (app?.versionIncompatible) { - openIncompatibleModal(app, 'update', closeModal, setModal); + openIncompatibleModal(app, 'update', closeModal); return; } - showAppPermissionsReviewModal(); - }, [app, closeModal, setModal, showAppPermissionsReviewModal]); + openPermissionModal(); + }, [app, openPermissionModal, openIncompatibleModal, closeModal]); const canUpdate = app.installed && app.version && app.marketplaceVersion && semver.lt(app.version, app.marketplaceVersion); @@ -411,7 +344,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { {t('Enable')} ), - disabled: !result?.data?.hasUnlimitedApps && result?.data?.enabled >= result?.data?.limit, + disabled: !appCountQuery?.data?.hasUnlimitedApps && appCountQuery?.data?.enabled >= appCountQuery?.data?.limit, action: handleEnable, }, }), @@ -458,9 +391,9 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { handleUpdate, isAppEnabled, handleDisable, - result?.data?.hasUnlimitedApps, - result?.data?.enabled, - result?.data?.limit, + appCountQuery?.data?.hasUnlimitedApps, + appCountQuery?.data?.enabled, + appCountQuery?.data?.limit, handleEnable, handleUninstall, ]); diff --git a/apps/meteor/client/views/marketplace/AppPermissionsReviewModal.tsx b/apps/meteor/client/views/marketplace/AppPermissionsReviewModal.tsx index 8f444a709196..743f1006d3f7 100644 --- a/apps/meteor/client/views/marketplace/AppPermissionsReviewModal.tsx +++ b/apps/meteor/client/views/marketplace/AppPermissionsReviewModal.tsx @@ -1,18 +1,19 @@ +import type { App } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; +import type { FC } from 'react'; import React from 'react'; import GenericModal from '../../components/GenericModal'; import AppPermissionsList from './components/AppPermissionsList'; -type AppPermissionsReviewModalProps = { - appPermissions: Array<{ name: string; required?: boolean }>; +export type AppPermissionsReviewModalProps = { + appPermissions: App['permissions']; onCancel: () => void; - onConfirm: (permissionsGranted: any) => void; + onConfirm: (permissionsGranted: AppPermissionsReviewModalProps['appPermissions']) => void; }; -const AppPermissionsReviewModal = ({ appPermissions, onCancel, onConfirm }: AppPermissionsReviewModalProps): ReactElement => { +const AppPermissionsReviewModal: FC = ({ appPermissions, onCancel, onConfirm }) => { const t = useTranslation(); return ( diff --git a/apps/meteor/client/views/marketplace/components/AppInstallModal/AppInstallModal.tsx b/apps/meteor/client/views/marketplace/components/AppInstallModal/AppInstallModal.tsx index 8f2e94abd91b..a592616326e3 100644 --- a/apps/meteor/client/views/marketplace/components/AppInstallModal/AppInstallModal.tsx +++ b/apps/meteor/client/views/marketplace/components/AppInstallModal/AppInstallModal.tsx @@ -3,9 +3,10 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import MarkdownText from '../../../../components/MarkdownText'; +import type { MarketplaceRouteContext } from '../../hooks/useAppsCountQuery'; type AppsInstallationModalProps = { - context: 'private' | 'explore' | 'installed' | 'enterprise' | 'requested'; + context: MarketplaceRouteContext; enabled: number; limit: number; appName: string; diff --git a/apps/meteor/client/views/marketplace/hooks/useAppInstallationHandler.tsx b/apps/meteor/client/views/marketplace/hooks/useAppInstallationHandler.tsx new file mode 100644 index 000000000000..6b0f6fb325b9 --- /dev/null +++ b/apps/meteor/client/views/marketplace/hooks/useAppInstallationHandler.tsx @@ -0,0 +1,131 @@ +import type { App } from '@rocket.chat/core-typings'; +import { useEndpoint, useRoute, useRouteParameter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import { Apps } from '../../../../ee/client/apps/orchestrator'; +import IframeModal from '../IframeModal'; +import AppInstallModal from '../components/AppInstallModal/AppInstallModal'; +import type { Actions } from '../helpers'; +import { handleAPIError } from '../helpers'; +import { isMarketplaceRouteContext, useAppsCountQuery } from './useAppsCountQuery'; +import { useOpenAppPermissionsReviewModal } from './useOpenAppPermissionsReviewModal'; +import { useOpenIncompatibleModal } from './useOpenIncompatibleModal'; + +export type AppInstallationHandlerParams = { + app: App; + action: Actions; + isAppPurchased: boolean; + onDismiss: () => void; + onSuccess: (action: Actions, appPermissions?: App['permissions']) => void; +}; + +export function useAppInstallationHandler({ app, action, isAppPurchased, onDismiss, onSuccess }: AppInstallationHandlerParams) { + const dispatchToastMessage = useToastMessageDispatch(); + const setModal = useSetModal(); + + const upgradeRoute = useRoute('upgradeRoute'); + const routeContext = String(useRouteParameter('context')); + const context = isMarketplaceRouteContext(routeContext) ? routeContext : 'explore'; + + const appCountQuery = useAppsCountQuery(context); + + const notifyAdmins = useEndpoint('POST', `/apps/notify-admins`); + + const openIncompatibleModal = useOpenIncompatibleModal(); + + const closeModal = useCallback(() => { + setModal(null); + onDismiss(); + }, [onDismiss, setModal]); + + const success = useCallback( + (appPermissions?: App['permissions']) => { + setModal(null); + onSuccess(action, appPermissions); + }, + [action, onSuccess, setModal], + ); + + const openPermissionModal = useOpenAppPermissionsReviewModal({ app, onCancel: closeModal, onConfirm: success }); + + const acquireApp = useCallback(async () => { + if (action === 'purchase' && !isAppPurchased) { + try { + const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); + setModal(); + } catch (error) { + handleAPIError(error); + } + return; + } + + openPermissionModal(); + }, [action, isAppPurchased, openPermissionModal, app.id, app.purchaseType, setModal, onDismiss]); + + return useCallback(async () => { + if (app?.versionIncompatible) { + openIncompatibleModal(app, action, closeModal); + return; + } + + if (action === 'request') { + const requestConfirmAction = (postMessage: Record) => { + setModal(null); + dispatchToastMessage({ type: 'success', message: 'App request submitted' }); + + notifyAdmins({ + appId: app.id, + appName: app.name, + appVersion: app.marketplaceVersion, + message: typeof postMessage.message === 'string' ? postMessage.message : '', + }); + + success(); + }; + + try { + const data = await Apps.buildExternalAppRequest(app.id); + setModal(); + } catch (error) { + handleAPIError(error); + } + return; + } + + if (!appCountQuery.data) { + return; + } + + if (appCountQuery.data.hasUnlimitedApps) { + return acquireApp(); + } + + setModal( + { + upgradeRoute.push(); + }} + />, + ); + }, [ + app, + action, + appCountQuery.data, + setModal, + context, + acquireApp, + openIncompatibleModal, + closeModal, + dispatchToastMessage, + notifyAdmins, + success, + onDismiss, + upgradeRoute, + ]); +} diff --git a/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts b/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts index 4c8f2876eaf3..d9ffd6a24051 100644 --- a/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts +++ b/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts @@ -11,7 +11,13 @@ const getProgressBarValues = (numberOfEnabledApps: number, enabledAppsLimit: num percentage: Math.round((numberOfEnabledApps / enabledAppsLimit) * 100), }); -export const useAppsCountQuery = (context: 'private' | 'explore' | 'installed' | 'enterprise' | 'requested') => { +export type MarketplaceRouteContext = 'private' | 'explore' | 'installed' | 'enterprise' | 'requested'; + +export function isMarketplaceRouteContext(context: string): context is MarketplaceRouteContext { + return ['private', 'explore', 'installed', 'enterprise', 'requested'].includes(context); +} + +export const useAppsCountQuery = (context: MarketplaceRouteContext) => { const getAppsCount = useEndpoint('GET', '/apps/count'); return useQuery( diff --git a/apps/meteor/client/views/marketplace/hooks/useOpenAppPermissionsReviewModal.tsx b/apps/meteor/client/views/marketplace/hooks/useOpenAppPermissionsReviewModal.tsx new file mode 100644 index 000000000000..c5d4c3c031ce --- /dev/null +++ b/apps/meteor/client/views/marketplace/hooks/useOpenAppPermissionsReviewModal.tsx @@ -0,0 +1,28 @@ +import type { App } from '@rocket.chat/core-typings'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import type { AppPermissionsReviewModalProps } from '../AppPermissionsReviewModal'; +import AppPermissionsReviewModal from '../AppPermissionsReviewModal'; + +export const useOpenAppPermissionsReviewModal: (params: { + app: App; + onCancel: AppPermissionsReviewModalProps['onCancel']; + onConfirm: AppPermissionsReviewModalProps['onConfirm']; +}) => () => void = ({ app, onCancel, onConfirm }) => { + const setModal = useSetModal(); + + return useCallback(() => { + const handleCancel = () => { + setModal(null); + onCancel(); + }; + + const handleConfirm: typeof onConfirm = (appPermissions) => { + setModal(null); + onConfirm(appPermissions); + }; + + return setModal(); + }, [app.permissions, onCancel, onConfirm, setModal]); +}; diff --git a/apps/meteor/client/views/marketplace/hooks/useOpenIncompatibleModal.tsx b/apps/meteor/client/views/marketplace/hooks/useOpenIncompatibleModal.tsx new file mode 100644 index 000000000000..72b7cef1c19e --- /dev/null +++ b/apps/meteor/client/views/marketplace/hooks/useOpenIncompatibleModal.tsx @@ -0,0 +1,32 @@ +import { useSetModal } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import { Apps } from '../../../../ee/client/apps/orchestrator'; +import IframeModal from '../IframeModal'; +import { handleAPIError } from '../helpers'; + +export const useOpenIncompatibleModal = () => { + const setModal = useSetModal(); + + return useCallback( + async (app, actionName, cancelAction) => { + const handleCancel = () => { + setModal(null); + cancelAction(); + }; + + const handleConfirm = () => { + setModal(null); + cancelAction(); + }; + + try { + const incompatibleData = await Apps.buildIncompatibleExternalUrl(app.id, app.marketplaceVersion, actionName); + setModal(); + } catch (e) { + handleAPIError(e); + } + }, + [setModal], + ); +};