diff --git a/apps/meteor/client/views/marketplace/AccordionLoading.tsx b/apps/meteor/client/views/marketplace/AccordionLoading.tsx
deleted file mode 100644
index 5591baf268d8..000000000000
--- a/apps/meteor/client/views/marketplace/AccordionLoading.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Box, Skeleton, Margins } from '@rocket.chat/fuselage';
-import type { FC } from 'react';
-import React from 'react';
-
-const AccordionLoading: FC = () => (
-
-
-
-
-
-
-
-);
-
-export default AccordionLoading;
diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx
index 11fada9305e9..a653a22478a8 100644
--- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx
+++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx
@@ -4,7 +4,7 @@ import type { ReactElement } from 'react';
import React from 'react';
import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime';
-import AccordionLoading from '../../../AccordionLoading';
+import AccordionLoading from '../../../components/AccordionLoading';
import { useLogs } from '../../../hooks/useLogs';
import AppLogsItem from './AppLogsItem';
diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleases.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleases.tsx
index 96d04a134893..cf8118f2a766 100644
--- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleases.tsx
+++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleases.tsx
@@ -5,10 +5,9 @@ import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React from 'react';
-import AccordionLoading from '../../../AccordionLoading';
+import AccordionLoading from '../../../components/AccordionLoading';
import AppReleasesItem from './AppReleasesItem';
-// TODO: replace useEndpointData
const AppReleases = ({ id }: { id: App['id'] }): ReactElement => {
const getVersions = useEndpoint('GET', '/apps/:id/versions', { id });
const dispatchToastMessage = useToastMessageDispatch();
diff --git a/apps/meteor/client/views/marketplace/AppInstallPage.js b/apps/meteor/client/views/marketplace/AppInstallPage.js
deleted file mode 100644
index 3355fd69896f..000000000000
--- a/apps/meteor/client/views/marketplace/AppInstallPage.js
+++ /dev/null
@@ -1,250 +0,0 @@
-import { Button, ButtonGroup, Icon, Field, FieldGroup, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage';
-import { useUniqueId } from '@rocket.chat/fuselage-hooks';
-import {
- useSetModal,
- useEndpoint,
- useUpload,
- useTranslation,
- useRouteParameter,
- useRouter,
- useSearchParameter,
-} from '@rocket.chat/ui-contexts';
-import React, { useCallback, useState } from 'react';
-import { useForm, Controller } from 'react-hook-form';
-
-import { AppClientOrchestratorInstance } from '../../../ee/client/apps/orchestrator';
-import { Page, PageHeader, PageScrollableContent } from '../../components/Page';
-import { useAppsReload } from '../../contexts/hooks/useAppsReload';
-import { useExternalLink } from '../../hooks/useExternalLink';
-import { useSingleFileInput } from '../../hooks/useSingleFileInput';
-import { useCheckoutUrl } from '../admin/subscription/hooks/useCheckoutUrl';
-import AppPermissionsReviewModal from './AppPermissionsReviewModal';
-import AppUpdateModal from './AppUpdateModal';
-import AppInstallModal from './components/AppInstallModal/AppInstallModal';
-import { handleAPIError } from './helpers/handleAPIError';
-import { handleInstallError } from './helpers/handleInstallError';
-import { useAppsCountQuery } from './hooks/useAppsCountQuery';
-import { getManifestFromZippedApp } from './lib/getManifestFromZippedApp';
-
-const placeholderUrl = 'https://rocket.chat/apps/package.zip';
-
-function AppInstallPage() {
- const t = useTranslation();
-
- const reload = useAppsReload();
-
- const router = useRouter();
-
- const context = useRouteParameter('context');
-
- const setModal = useSetModal();
-
- const appId = useSearchParameter('id');
- const queryUrl = useSearchParameter('url');
-
- const [installing, setInstalling] = useState(false);
-
- const endpointAddress = appId ? `/apps/${appId}` : '/apps';
- const downloadApp = useEndpoint('POST', endpointAddress);
- const uploadAppEndpoint = useUpload(endpointAddress);
- const uploadUpdateApp = useUpload(`${endpointAddress}/update`);
-
- const appCountQuery = useAppsCountQuery('private');
-
- const openExternalLink = useExternalLink();
- const manageSubscriptionUrl = useCheckoutUrl()({ target: 'marketplace-app-install', action: 'Enable_unlimited_apps' });
-
- const { control, setValue, watch } = useForm({ defaultValues: { url: queryUrl || '' } });
- const { file, url } = watch();
-
- const canSave = !!url || !!file?.name;
-
- const [handleUploadButtonClick] = useSingleFileInput((value) => setValue('file', value), 'app');
-
- const sendFile = async (permissionsGranted, appFile, appId) => {
- let app;
- const fileData = new FormData();
- fileData.append('app', appFile, appFile.name);
- fileData.append('permissions', JSON.stringify(permissionsGranted));
-
- try {
- if (appId) {
- await uploadUpdateApp(fileData);
- } else {
- app = await uploadAppEndpoint(fileData);
- }
-
- router.navigate({
- name: 'marketplace',
- params: {
- context: 'private',
- page: 'info',
- id: appId || app.app.id,
- },
- });
-
- reload();
- } catch (e) {
- handleAPIError(e);
- } finally {
- setInstalling(false);
- setModal(null);
- }
- };
-
- const cancelAction = useCallback(() => {
- setInstalling(false);
- setModal(null);
- }, [setInstalling, setModal]);
-
- const isAppInstalled = async (appId) => {
- try {
- const app = await AppClientOrchestratorInstance.getApp(appId);
- return !!app || false;
- } catch (e) {
- return false;
- }
- };
-
- const handleAppPermissionsReview = async (permissions, appFile, appId) => {
- setModal(
- sendFile(permissionsGranted, appFile, appId)}
- />,
- );
- };
-
- 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;
- if (url) {
- const { buff } = await downloadApp({ url, downloadOnly: true });
- const fileData = Uint8Array.from(buff.data);
- manifest = await getManifestFromZippedApp(fileData);
- appFile = new File([fileData], 'app.zip', { type: 'application/zip' });
- } else {
- appFile = file;
- manifest = await getManifestFromZippedApp(appFile);
- }
-
- return { appFile, manifest };
- } catch (error) {
- handleInstallError(error);
-
- 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={() => {
- openExternalLink(manageSubscriptionUrl);
- setModal(null);
- }}
- />,
- );
- };
-
- const handleCancel = () => {
- router.navigate({
- name: 'marketplace',
- params: {
- context,
- page: 'list',
- },
- });
- };
-
- const urlField = useUniqueId();
- const fileField = useUniqueId();
-
- return (
-
-
-
-
-
- {t('App_Url_to_Install_From')}
-
- (
- } {...field} />
- )}
- />
-
-
-
- {t('App_Url_to_Install_From_File')}
-
- (
-
- {t('Browse_Files')}
-
- }
- />
- )}
- />
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default AppInstallPage;
diff --git a/apps/meteor/client/views/marketplace/AppInstallPage.tsx b/apps/meteor/client/views/marketplace/AppInstallPage.tsx
new file mode 100644
index 000000000000..5168f978b4bc
--- /dev/null
+++ b/apps/meteor/client/views/marketplace/AppInstallPage.tsx
@@ -0,0 +1,94 @@
+import { Button, ButtonGroup, Icon, Field, FieldGroup, FieldLabel, FieldRow, TextInput, Callout } from '@rocket.chat/fuselage';
+import { useUniqueId } from '@rocket.chat/fuselage-hooks';
+import { useTranslation, useRouter, useSearchParameter } from '@rocket.chat/ui-contexts';
+import React, { useCallback } from 'react';
+import { useForm, Controller } from 'react-hook-form';
+
+import { Page, PageHeader, PageScrollableContent } from '../../components/Page';
+import { useSingleFileInput } from '../../hooks/useSingleFileInput';
+import { useInstallApp } from './hooks/useInstallApp';
+
+const PLACEHOLDER_URL = 'https://rocket.chat/apps/package.zip';
+
+const AppInstallPage = () => {
+ const t = useTranslation();
+ const router = useRouter();
+
+ const queryUrl = useSearchParameter('url');
+
+ const { control, setValue, watch } = useForm<{ file: File; url: string }>({ defaultValues: { url: queryUrl || '' } });
+ const { file, url } = watch();
+ const { install, isInstalling } = useInstallApp(file, url);
+
+ const [handleUploadButtonClick] = useSingleFileInput((value) => setValue('file', value), 'app');
+
+ const handleCancel = useCallback(() => {
+ router.navigate({
+ name: 'marketplace',
+ params: {
+ context: 'private',
+ page: 'list',
+ },
+ });
+ }, [router]);
+
+ const urlField = useUniqueId();
+ const fileField = useUniqueId();
+
+ return (
+
+
+
+
+
+ {t('App_Url_to_Install_From')}
+
+ {t('App_Installation_Deprecation')}
+
+
+ (
+ } {...field} />
+ )}
+ />
+
+
+
+ {t('App_Url_to_Install_From_File')}
+
+ (
+
+ {t('Browse_Files')}
+
+ }
+ />
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AppInstallPage;
diff --git a/apps/meteor/client/views/marketplace/components/AccordionLoading.tsx b/apps/meteor/client/views/marketplace/components/AccordionLoading.tsx
new file mode 100644
index 000000000000..a3c561677a32
--- /dev/null
+++ b/apps/meteor/client/views/marketplace/components/AccordionLoading.tsx
@@ -0,0 +1,14 @@
+import { Skeleton } from '@rocket.chat/fuselage';
+import React from 'react';
+
+const SKELETON_ITEMS = 3;
+
+const AccordionLoading = () => (
+ <>
+ {Array.from({ length: SKELETON_ITEMS }, (_v, k) => (
+
+ ))}
+ >
+);
+
+export default AccordionLoading;
diff --git a/apps/meteor/client/views/marketplace/hooks/useInstallApp.tsx b/apps/meteor/client/views/marketplace/hooks/useInstallApp.tsx
new file mode 100644
index 000000000000..78a0283ef8a9
--- /dev/null
+++ b/apps/meteor/client/views/marketplace/hooks/useInstallApp.tsx
@@ -0,0 +1,167 @@
+import type { App, AppPermission } from '@rocket.chat/core-typings';
+import { useRouter, useSetModal, useUpload, useEndpoint } from '@rocket.chat/ui-contexts';
+import { useMutation } from '@tanstack/react-query';
+import React, { useCallback, useState } from 'react';
+
+import { AppClientOrchestratorInstance } from '../../../../ee/client/apps/orchestrator';
+import { useAppsReload } from '../../../contexts/hooks/useAppsReload';
+import { useExternalLink } from '../../../hooks/useExternalLink';
+import { useCheckoutUrl } from '../../admin/subscription/hooks/useCheckoutUrl';
+import AppPermissionsReviewModal from '../AppPermissionsReviewModal';
+import AppUpdateModal from '../AppUpdateModal';
+import AppInstallationModal from '../components/AppInstallModal/AppInstallModal';
+import { handleAPIError } from '../helpers/handleAPIError';
+import { handleInstallError } from '../helpers/handleInstallError';
+import { getManifestFromZippedApp } from '../lib/getManifestFromZippedApp';
+import { useAppsCountQuery } from './useAppsCountQuery';
+
+export const useInstallApp = (file: File, url: string): { install: () => void; isInstalling: boolean } => {
+ const reloadAppsList = useAppsReload();
+ const openExternalLink = useExternalLink();
+ const setModal = useSetModal();
+
+ const router = useRouter();
+
+ const appCountQuery = useAppsCountQuery('private');
+ const manageSubscriptionUrl = useCheckoutUrl()({ target: 'marketplace-app-install', action: 'Enable_unlimited_apps' });
+
+ const uploadAppEndpoint = useUpload('/apps');
+ const uploadUpdateEndpoint = useUpload('/apps/update');
+
+ // TODO: This function should not be called in a next major version, it will be changed by an endpoint deprecation.
+ const downloadPrivateAppFromUrl = useEndpoint('POST', '/apps');
+
+ const [isInstalling, setInstalling] = useState(false);
+
+ const { mutate: sendFile } = useMutation(
+ ['apps/installPrivateApp'],
+ ({ permissionsGranted, appFile, appId }: { permissionsGranted: AppPermission[]; appFile: File; appId?: string }) => {
+ const fileData = new FormData();
+ fileData.append('app', appFile, appFile.name);
+ fileData.append('permissions', JSON.stringify(permissionsGranted));
+
+ if (appId) {
+ return uploadUpdateEndpoint(fileData);
+ }
+
+ return uploadAppEndpoint(fileData) as any;
+ },
+ {
+ onSuccess: (data: { app: App }) => {
+ router.navigate({
+ name: 'marketplace',
+ params: {
+ context: 'private',
+ page: 'info',
+ id: data.app.id,
+ },
+ });
+ },
+ onError: (e) => {
+ handleAPIError(e);
+ },
+ onSettled: () => {
+ setInstalling(false);
+ setModal(null);
+ reloadAppsList();
+ },
+ },
+ );
+
+ const cancelAction = useCallback(() => {
+ setInstalling(false);
+ setModal(null);
+ }, [setInstalling, setModal]);
+
+ const isAppInstalled = async (appId: string) => {
+ try {
+ const app = await AppClientOrchestratorInstance.getApp(appId);
+ return !!app || false;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const handleAppPermissionsReview = async (permissions: AppPermission[], appFile: File, appId?: string) => {
+ setModal(
+ sendFile({ permissionsGranted, appFile, appId })}
+ />,
+ );
+ };
+
+ const uploadFile = async (appFile: File, { id, permissions }: { id: string; permissions: AppPermission[] }) => {
+ const isInstalled = await isAppInstalled(id);
+
+ if (isInstalled) {
+ return setModal( handleAppPermissionsReview(permissions, appFile, id)} />);
+ }
+
+ await handleAppPermissionsReview(permissions, appFile);
+ };
+
+ /** @deprecated */
+ const getAppFile = async (): Promise => {
+ try {
+ const { buff } = (await downloadPrivateAppFromUrl({ url, downloadOnly: true })) as { buff: { data: ArrayLike } };
+
+ return new File([Uint8Array.from(buff.data)], 'app.zip', { type: 'application/zip' });
+ } catch (error) {
+ handleInstallError(error as Error);
+ }
+ };
+
+ const extractManifestFromAppFile = async (appFile: File) => {
+ const manifest = await getManifestFromZippedApp(appFile);
+ return manifest;
+ };
+
+ const install = async () => {
+ let appFile: File | undefined;
+
+ setInstalling(true);
+
+ if (!appCountQuery.data) {
+ return cancelAction();
+ }
+
+ if (!file) {
+ appFile = await getAppFile();
+ } else {
+ appFile = file;
+ }
+
+ if (!appFile) {
+ return cancelAction();
+ }
+
+ const manifest = await extractManifestFromAppFile(appFile);
+
+ if (!manifest) {
+ return cancelAction();
+ }
+
+ if (appCountQuery.data.hasUnlimitedApps) {
+ return uploadFile(appFile, manifest);
+ }
+
+ setModal(
+ uploadFile(appFile as File, manifest)}
+ handleEnableUnlimitedApps={() => {
+ openExternalLink(manageSubscriptionUrl);
+ setModal(null);
+ }}
+ />,
+ );
+ };
+
+ return { install, isInstalling };
+};
diff --git a/apps/meteor/client/views/marketplace/lib/getManifestFromZippedApp.ts b/apps/meteor/client/views/marketplace/lib/getManifestFromZippedApp.ts
index e9a292de6181..70f0b8827aa1 100644
--- a/apps/meteor/client/views/marketplace/lib/getManifestFromZippedApp.ts
+++ b/apps/meteor/client/views/marketplace/lib/getManifestFromZippedApp.ts
@@ -1,7 +1,8 @@
+import type { AppPermission } from '@rocket.chat/core-typings';
import { unzipSync, strFromU8 } from 'fflate';
type Uint8ArrayObject = { [fileName: string]: Uint8Array };
-type AppManifestSchema = { [key: string]: string };
+type AppManifestSchema = { id: string; name: string; permissions: AppPermission[] };
async function fileToUint8Array(file: File): Promise {
return new Promise((resolve, reject) => {
diff --git a/apps/meteor/ee/client/apps/orchestrator.ts b/apps/meteor/ee/client/apps/orchestrator.ts
index eaaf367796ae..a921c8cb61be 100644
--- a/apps/meteor/ee/client/apps/orchestrator.ts
+++ b/apps/meteor/ee/client/apps/orchestrator.ts
@@ -91,12 +91,12 @@ class AppClientOrchestrator {
}
public async installApp(appId: string, version: string, permissionsGranted?: IPermission[]): Promise {
- const { app } = await sdk.rest.post<'/apps/'>('/apps/', {
+ const { app } = (await sdk.rest.post<'/apps'>('/apps', {
appId,
marketplace: true,
version,
permissionsGranted,
- });
+ })) as { app: App };
return app;
}
diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts
index d4c9165a9281..ea259fef5f0c 100644
--- a/apps/meteor/ee/server/apps/communication/rest.ts
+++ b/apps/meteor/ee/server/apps/communication/rest.ts
@@ -321,16 +321,18 @@ export class AppsRestApi {
});
}
- buff = Buffer.from(await response.arrayBuffer());
+ buff = await response.buffer();
} catch (e: any) {
orchestrator.getRocketChatLogger().error('Error getting the app from url:', e.response.data);
return API.v1.internalError();
}
if (this.bodyParams.downloadOnly) {
+ apiDeprecationLogger.parameter(this.request.route, 'downloadOnly', '7.0.0', this.response);
+
return API.v1.success({ buff });
}
- } else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
+ } else if ('appId' in this.bodyParams && this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
const baseUrl = orchestrator.getMarketplaceUrl();
const headers = getDefaultHeaders();
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index c16658c67ec2..b157de26916a 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -506,6 +506,8 @@
"App_Information": "App Information",
"Apps_context_enterprise": "Enterprise",
"App_Installation": "App Installation",
+ "App_Installation_Deprecation_Title": "Deprecation Warning",
+ "App_Installation_Deprecation": "Install apps from URL is deprecated and will be removed in the next major release.",
"App_not_enabled": "App not enabled",
"App_not_found": "App not found",
"App_status_auto_enabled": "Enabled",
diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts
index 5d4dfa8f4588..b4362d87ba41 100644
--- a/packages/rest-typings/src/apps/index.ts
+++ b/packages/rest-typings/src/apps/index.ts
@@ -209,7 +209,7 @@ export type AppsEndpoints = {
};
};
- '/apps/': {
+ '/apps': {
GET:
| ((params: { buildExternalUrl: 'true'; purchaseType?: 'buy' | 'subscription'; appId?: string; details?: 'true' | 'false' }) => {
url: string;
@@ -239,15 +239,27 @@ export type AppsEndpoints = {
}[])
| (() => { apps: App[] });
- POST: (params: {
- appId: string;
- marketplace: boolean;
- version: string;
- permissionsGranted?: IPermission[];
- url?: string;
- downloadOnly?: boolean;
- }) => {
- app: App;
+ POST: {
+ (
+ params:
+ | {
+ appId: string;
+ marketplace: boolean;
+ version: string;
+ permissionsGranted?: IPermission[];
+ url?: string;
+ downloadOnly?: boolean;
+ }
+ | { url: string; downloadOnly?: boolean },
+ ):
+ | {
+ app: App;
+ }
+ | {
+ buff: {
+ data: ArrayLike;
+ };
+ };
};
};
diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts
index 3b8197ce20bf..044282784cdf 100644
--- a/packages/rest-typings/src/index.ts
+++ b/packages/rest-typings/src/index.ts
@@ -186,7 +186,7 @@ export type MatchPathPattern = TPath extends any ? Extract = Extract<
PathPattern,
- `${TBasePath}/${TSubPathPattern}` | TSubPathPattern
+ `${TBasePath}${TSubPathPattern extends '' ? TSubPathPattern : `/${TSubPathPattern}`}` | TSubPathPattern
>;
type GetParams = TOperation extends (...args: any) => any ? Parameters[0] : never;
diff --git a/yarn.lock b/yarn.lock
index 5953649266f6..93ace4f75e01 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9147,18 +9147,6 @@ __metadata:
languageName: node
linkType: hard
-"@rocket.chat/fuselage-hooks@npm:^0.33.0":
- version: 0.33.0
- resolution: "@rocket.chat/fuselage-hooks@npm:0.33.0"
- dependencies:
- use-sync-external-store: ~1.2.0
- peerDependencies:
- "@rocket.chat/fuselage-tokens": "*"
- react: ^17.0.2
- checksum: addf78db893c3185d4d5fc347e1aa81710e0939505a08db77e524740d05555c5e8556d2b6f0e6425270621bdd7230d13278add77ce38197c048af30207cce853
- languageName: node
- linkType: hard
-
"@rocket.chat/fuselage-polyfills@npm:~0.31.25":
version: 0.31.25
resolution: "@rocket.chat/fuselage-polyfills@npm:0.31.25"