diff --git a/package.json b/package.json index 4f0be9d3b6..f3935a5c21 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@iconscout/react-unicons": "^1.1.6", "@internxt/css-config": "1.1.0", "@internxt/lib": "1.4.1", - "@internxt/sdk": "=1.12.6", + "@internxt/sdk": "=1.13.2", "@internxt/ui": "=0.1.4", "@phosphor-icons/react": "^2.1.7", "@popperjs/core": "^2.11.6", diff --git a/src/app/core/types.ts b/src/app/core/types.ts index 816c75f5cf..4dee68d507 100644 --- a/src/app/core/types.ts +++ b/src/app/core/types.ts @@ -67,16 +67,6 @@ export interface AppViewConfig { hideSearch?: boolean; } -export default class AppError extends Error { - readonly status?: number; - - constructor(message: string, status?: number) { - super(message); - - this.status = status; - } -} - export enum Workspace { Individuals = 'personal', Business = 'business', diff --git a/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.ts b/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.ts index 7589fd36e3..107e862c06 100644 --- a/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.ts +++ b/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.ts @@ -52,9 +52,11 @@ export const useShareItemUserRoles = ({ isRestrictedSharingAvailable, itemToShar actionDispatch(setAccessMode(mode)); } catch (error) { errorService.reportError(error); + const castedError = errorService.castError(error); notificationsService.show({ text: translate('modals.shareModal.errors.update-sharing-access'), type: ToastType.Error, + requestId: castedError.requestId, }); } actionDispatch(setIsLoading(false)); @@ -83,7 +85,12 @@ export const useShareItemUserRoles = ({ isRestrictedSharingAvailable, itemToShar } } catch (error) { errorService.reportError(error); - notificationsService.show({ text: translate('modals.shareModal.errors.updatingRole'), type: ToastType.Error }); + const castedError = errorService.castError(error); + notificationsService.show({ + text: translate('modals.shareModal.errors.updatingRole'), + type: ToastType.Error, + requestId: castedError.requestId, + }); } }; diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index f3b5b40168..72896f8a79 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -1086,7 +1086,8 @@ "maxSizeUploadLimitError": "Datei zu groß (größer als 40 GB)", "braveNotSupportMultiplePhotosDowload": "Brave unterstützt nicht das Herunterladen mehrerer Fotos.", "updateAvatarError": "Fehler beim Aktualisieren des Avatars", - "featuresUnavailable": "Einige Funktionen sind möglicherweise nicht verfügbar" + "featuresUnavailable": "Einige Funktionen sind möglicherweise nicht verfügbar", + "errorSettingWorkspace": "Fehler beim Konfigurieren des Arbeitsbereichs" }, "backups": { "backups-from": "Backups von {{deviceName}}", @@ -1425,6 +1426,9 @@ "logOut": "Abmelden", "locked": "Gesperrt" }, + "toastNotification": { + "textCopied": "Text in die Zwischenablage kopiert" + }, "drive": { "usage": "Verwendung", "currentPlan": "Derzeitiger Plan", diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index c06c3d068d..bb5e6badef 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -1161,7 +1161,8 @@ "connectionLostError": "Internet connection lost", "errorLoadingTrashItems": "Error loading trash items", "updateAvatarError": "Error updating avatar", - "featuresUnavailable": "Some features may be unavailable" + "featuresUnavailable": "Some features may be unavailable", + "errorSettingWorkspace": "Error setting up workspace" }, "backups": { "backups-from": "Backups from {{deviceName}}", @@ -1507,6 +1508,9 @@ "logOut": "Log out", "locked": "Locked" }, + "toastNotification": { + "textCopied": "Text copied to clipboard" + }, "drive": { "usage": "Usage", "currentPlan": "Current plan", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index 19bc8565a6..f375a39657 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -1138,7 +1138,8 @@ "connectionLostError": "Conexión a internet perdida", "errorLoadingTrashItems": "Error al cargar items de la papelera", "updateAvatarError": "Error al actualizar el avatar", - "featuresUnavailable": "Algunas funciones pueden no estar disponibles" + "featuresUnavailable": "Algunas funciones pueden no estar disponibles", + "errorSettingWorkspace": "Error al configurar el espacio de trabajo" }, "backups": { "backups-from": "Copias de seguridad de {{deviceName}}", @@ -1485,6 +1486,9 @@ "logOut": "Cerrar sesión", "locked": "Bloqueado" }, + "toastNotification": { + "textCopied": "Texto copiado al portapapeles" + }, "drive": { "usage": "Uso", "currentPlan": "Actual plan", diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 8f3d74539f..6099dcaa02 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -1089,7 +1089,8 @@ "connectionLostError": "Lost Internet connection", "errorLoadingTrashItems": "Erreur de chargement des éléments de la corbeille", "updateAvatarError": "Erreur lors de la mise à jour de l'avatar", - "featuresUnavailable": "Certaines fonctionnalités peuvent être indisponibles" + "featuresUnavailable": "Certaines fonctionnalités peuvent être indisponibles", + "errorSettingWorkspace": "Erreur lors de la configuration de l'espace de travail" }, "backups": { "backups-from": "Sauvagardes de {{deviceName}}", @@ -1431,6 +1432,9 @@ "logOut": "Se déconnecter", "locked": "Verrouillé" }, + "toastNotification": { + "textCopied": "Texte copié dans le presse-papiers" + }, "drive": { "usage": "Usage", "currentPlan": "Plan actuel", diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 3106ba4b8e..f3f5cb434a 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -1196,7 +1196,8 @@ "connectionLostError": "Connessione Internet persa", "errorLoadingTrashItems": "Errore nel caricamento degli elementi del cestino", "updateAvatarError": "Errore nell'aggiornamento dell'avatar", - "featuresUnavailable": "Alcune funzionalità potrebbero non essere disponibili" + "featuresUnavailable": "Alcune funzionalità potrebbero non essere disponibili", + "errorSettingWorkspace": "Errore nella configurazione dello spazio di lavoro" }, "backups": { "backups-from": "Backup da {{deviceName}}", @@ -1538,6 +1539,9 @@ "logOut": "Disconnettersi", "locked": "Bloccato" }, + "toastNotification": { + "textCopied": "Testo copiato negli appunti" + }, "drive": { "usage": "Utilizzo", "currentPlan": "Piano attuale", diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index a80bcca5b3..13d2af2fd8 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -1102,7 +1102,8 @@ "connectionLostError": "Потеряно подключение к Интернету", "errorLoadingTrashItems": "Ошибка при загрузке элементов корзины", "updateAvatarError": "Ошибка при обновлении аватара", - "featuresUnavailable": "Некоторые функции могут быть недоступны" + "featuresUnavailable": "Некоторые функции могут быть недоступны", + "errorSettingWorkspace": "Ошибка при настройке рабочего стола" }, "backups": { "backups-from": "Резервные копии с {{deviceName}}", @@ -1444,6 +1445,9 @@ "logOut": "Выйти", "locked": "Заблокировано" }, + "toastNotification": { + "textCopied": "Текст скопирован в буфер обмена" + }, "drive": { "usage": "Использование", "currentPlan": "Текущий план", diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index 25c4645256..41414bf26c 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -1091,7 +1091,8 @@ "connectionLostError": "網絡連接已丟失", "errorLoadingTrashItems": "加載垃圾桶項目時出錯", "updateAvatarError": "更新頭像時發生錯誤", - "featuresUnavailable": "某些功能可能無法使用" + "featuresUnavailable": "某些功能可能無法使用", + "errorSettingWorkspace": "設定工作區時出錯" }, "backups": { "backups-from": "來自{{deviceName}}的備份", @@ -1437,6 +1438,9 @@ "logOut": "登出", "locked": "已鎖定" }, + "toastNotification": { + "textCopied": "文字已複製到剪貼簿" + }, "drive": { "usage": "使用量", "currentPlan": "當前計劃", diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index 688c33df0e..d05b2b54d9 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -1126,7 +1126,8 @@ "connectionLostError": "互联网连接丢失", "errorLoadingTrashItems": "加载垃圾箱内的项目时出错", "updateAvatarError": "更新头像时出错", - "featuresUnavailable": "某些功能可能无法使用" + "featuresUnavailable": "某些功能可能无法使用", + "errorSettingWorkspace": "设置工作区时出错" }, "backups": { "backups-from": "来自 {{deviceName}} 的备份", @@ -1472,6 +1473,9 @@ "logOut": "登出", "locked": "已锁定" }, + "toastNotification": { + "textCopied": "文本已复制到剪贴板" + }, "drive": { "usage": "使用情况", "currentPlan": "目前的计划", diff --git a/src/app/network/DownloadManager.ts b/src/app/network/DownloadManager.ts index 0a4d91904e..60d585b1fa 100644 --- a/src/app/network/DownloadManager.ts +++ b/src/app/network/DownloadManager.ts @@ -276,6 +276,7 @@ export class DownloadManager { notificationsService.show({ text: t(errorText, { message: castedError.message || '' }), type: ToastType.Error, + requestId: castedError.requestId, }); }; diff --git a/src/app/network/UploadFolderManager.test.ts b/src/app/network/UploadFolderManager.test.ts index 8c928bacb2..ee89a22418 100644 --- a/src/app/network/UploadFolderManager.test.ts +++ b/src/app/network/UploadFolderManager.test.ts @@ -1,5 +1,5 @@ import errorService from 'services/error.service'; -import AppError from 'app/core/types'; +import { AppError } from '@internxt/sdk'; import { DriveFolderData } from 'app/drive/types'; import { createFolder } from 'app/store/slices/storage/folderUtils/createFolder'; import { checkFolderDuplicated } from 'app/store/slices/storage/folderUtils/checkFolderDuplicated'; diff --git a/src/app/network/UploadManager.test.ts b/src/app/network/UploadManager.test.ts index 718b1fac48..711e981618 100644 --- a/src/app/network/UploadManager.test.ts +++ b/src/app/network/UploadManager.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { uploadFileWithManager } from './UploadManager'; import tasksService from 'app/tasks/services/tasks.service'; import errorService from 'services/error.service'; -import AppError from 'app/core/types'; +import { AppError } from '@internxt/sdk'; import uploadFile from 'app/drive/services/file.service/uploadFile'; import DatabaseUploadRepository from 'app/repositories/DatabaseUploadRepository'; import { DriveFileData } from 'app/drive/types'; diff --git a/src/app/notifications/components/NotificationToast/LongNotificationToast.tsx b/src/app/notifications/components/NotificationToast/LongNotificationToast.tsx index ef624a1870..d470d42093 100644 --- a/src/app/notifications/components/NotificationToast/LongNotificationToast.tsx +++ b/src/app/notifications/components/NotificationToast/LongNotificationToast.tsx @@ -2,7 +2,9 @@ import { Transition } from '@headlessui/react'; import { Loader } from '@internxt/ui'; import { CheckCircle, Info, Warning, WarningOctagon, X } from '@phosphor-icons/react'; import { NavLink } from 'react-router-dom'; -import { ToastShowProps, ToastType } from '../../services/notifications.service'; +import notificationsService, { ToastShowProps, ToastType } from '../../services/notifications.service'; +import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; const LongNotificationToast = ({ text, @@ -10,10 +12,20 @@ const LongNotificationToast = ({ action, visible, closable, + requestId, onClose, }: Omit & { visible: boolean; onClose: () => void }): JSX.Element => { let Icon: typeof CheckCircle | undefined; let IconColor: string | undefined; + const { translate } = useTranslationContext(); + + const handleCopyRequestId = () => { + if (requestId) { + copyTextToClipboard(requestId); + notificationsService.show({ text: translate('toastNotification.textCopied'), type: ToastType.Success }); + } + }; + switch (type) { case ToastType.Success: Icon = CheckCircle; @@ -55,7 +67,18 @@ const LongNotificationToast = ({ {Icon && }
-

{text}

+
+

{text}

+ {requestId && type === ToastType.Error && ( + + )} +
{action && (action.to ? ( & { visible: boolean; onClose: () => void }): JSX.Element => { + const { translate } = useTranslationContext(); + const handleCopyRequestId = () => { + if (requestId) { + navigator.clipboard.writeText(requestId); + notificationsService.show({ text: translate('toastNotification.textCopied'), type: ToastType.Success }); + } + }; let Icon: typeof CheckCircle | undefined; let IconColor: string | undefined; @@ -55,7 +64,18 @@ const NotificationToast = ({ {type === ToastType.Loading && } {Icon && } -

{text}

+
+

{text}

+ {requestId && type === ToastType.Error && ( + + )} +
{action && (action.to ? ( { + describe('Long notification is created', () => { + test('When the request id is present, then it should be created with it', async () => { + vi.spyOn(toast, 'custom').mockReturnValue('toast-id-123'); + + longNotificationsService.show({ + text: 'test', + type: ToastType.Error, + duration: 5000, + closable: true, + requestId: 'test-request-id', + }); + + expect(toast.custom).toHaveBeenCalledWith(expect.any(Function), { duration: 5000 }); + + // Invoke the render function and check the resulting React element has requestId in props + const renderFn = (toast.custom as ReturnType).mock.calls[0][0]; + const element = renderFn({ visible: true }); + + expect(element.props).toMatchObject({ requestId: 'test-request-id' }); + }); + }); +}); diff --git a/src/app/notifications/services/longNotification.service.ts b/src/app/notifications/services/longNotification.service.ts index a742e40cc0..d60456ded2 100644 --- a/src/app/notifications/services/longNotification.service.ts +++ b/src/app/notifications/services/longNotification.service.ts @@ -16,10 +16,11 @@ export type ToastShowProps = { action?: { text: string; to?: string; onClick: () => void }; duration?: number; closable?: boolean; + requestId?: string; }; const longNotificationsService = { - show: ({ text, type, action, duration = 5000, closable = true }: ToastShowProps): string => { + show: ({ text, type, action, duration = 5000, closable = true, requestId }: ToastShowProps): string => { const id = toast.custom( (t) => createElement(LongNotificationToast, { @@ -28,6 +29,7 @@ const longNotificationsService = { visible: t.visible, action, closable, + requestId, onClose() { toast.dismiss(id); }, diff --git a/src/app/notifications/services/notifications.service.ts b/src/app/notifications/services/notifications.service.ts index 525464b089..20832e133b 100644 --- a/src/app/notifications/services/notifications.service.ts +++ b/src/app/notifications/services/notifications.service.ts @@ -16,10 +16,11 @@ export type ToastShowProps = { action?: { text: string; to?: string; onClick: () => void }; duration?: number; closable?: boolean; + requestId?: string; }; const notificationsService = { - show: ({ text, type, action, duration = 5000, closable = true }: ToastShowProps): string => { + show: ({ text, type, action, duration = 5000, closable = true, requestId }: ToastShowProps): string => { const id = toast.custom( (t) => createElement(NotificationToast, { @@ -28,6 +29,7 @@ const notificationsService = { visible: t.visible, action, closable, + requestId, onClose() { toast.dismiss(id); }, diff --git a/src/app/share/services/share.service.test.ts b/src/app/share/services/share.service.test.ts index 629d832497..8a8d10d95d 100644 --- a/src/app/share/services/share.service.test.ts +++ b/src/app/share/services/share.service.test.ts @@ -11,8 +11,9 @@ import { } from '../../crypto/services/pgp.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; -import { decryptMnemonic } from './share.service'; +import shareService, { decryptMnemonic, getPublicShareLink } from './share.service'; import { stringUtils } from '@internxt/lib'; +import notificationsService from 'app/notifications/services/notifications.service'; vi.mock('utils', () => ({ generateCaptchaToken: vi.fn(() => 'mocked-captcha-token'), @@ -37,7 +38,9 @@ describe('Encryption and Decryption', () => { })); vi.mock('services/error.service', () => ({ default: { - castError: vi.fn().mockImplementation((e) => ({ message: e.message || 'Default error message' })), + castError: vi + .fn() + .mockImplementation((e) => ({ message: e.message || 'Default error message', requestId: 'test-request-id' })), reportError: vi.fn(), }, })); @@ -289,9 +292,20 @@ describe('Inviting user to Shared Folder', () => { createShareClient: mockCreateShareClientFn, } as any); - await expect(inviteUserToSharedFolder(mockProps)).rejects.toEqual({ - message: 'API Error', - }); + await expect(inviteUserToSharedFolder(mockProps)).rejects.toEqual( + expect.objectContaining({ message: originalError.message }), + ); expect(errorService.castError).toHaveBeenCalledWith(originalError); }); }); + +describe('Get shared link', () => { + test('When an error occurs fetching a shared link, then a notification is shown', async () => { + vi.spyOn(shareService, 'createPublicSharingItem').mockRejectedValue(new Error('Unexpected error')); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + + await getPublicShareLink('uuid', 'file'); + + expect(showNotificationSpy).toHaveBeenCalledWith(expect.objectContaining({ requestId: 'test-request-id' })); + }); +}); diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index 6b76c6fb7a..1509939248 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -346,9 +346,11 @@ export const getPublicShareLink = async ( notificationsService.show({ text: t('shared-links.toast.copy-to-clipboard'), type: ToastType.Success }); return publicSharingItemData; } catch (error) { + const castedError = errorService.castError(error); notificationsService.show({ text: t('modals.shareModal.errors.copy-to-clipboard'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); } diff --git a/src/app/store/slices/sharedLinks/index.test.ts b/src/app/store/slices/sharedLinks/index.test.ts index 8d2bf0fa2d..7997f63855 100644 --- a/src/app/store/slices/sharedLinks/index.test.ts +++ b/src/app/store/slices/sharedLinks/index.test.ts @@ -5,7 +5,7 @@ import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import navigationService from 'services/navigation.service'; import shareService from 'app/share/services/share.service'; import { Buffer } from 'buffer'; -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest'; import { RootState } from '../..'; import userService from 'services/user.service'; import { @@ -13,7 +13,15 @@ import { generateNewKeys, hybridDecryptMessageWithPrivateKey, } from '../../../crypto/services/pgp.service'; -import { HYBRID_ALGORITHM, sharedThunks, ShareFileWithUserPayload, STANDARD_ALGORITHM } from './index'; +import { + HYBRID_ALGORITHM, + removeUserFromSharedFolder, + sharedThunks, + ShareFileWithUserPayload, + STANDARD_ALGORITHM, + stopSharingItem, +} from './index'; +import notificationsService from 'app/notifications/services/notifications.service'; const { shareItemWithUser } = sharedThunks; describe('Encryption and Decryption', () => { @@ -26,6 +34,8 @@ describe('Encryption and Decryption', () => { inviteUserToSharedFolder: vi.fn(), getSharedFolderInvitationsAsInvitedUser: vi.fn(), getSharingRoles: vi.fn(), + stopSharingItem: vi.fn(), + removeUserRole: vi.fn(), }, })); vi.mock('services/user.service', () => ({ @@ -39,7 +49,9 @@ describe('Encryption and Decryption', () => { vi.mock('services/error.service', () => ({ default: { - castError: vi.fn().mockImplementation((e) => ({ message: e.message || 'Default error message' })), + castError: vi + .fn() + .mockImplementation((e) => ({ message: e.message || 'Default error message', requestId: 'test-request-id' })), reportError: vi.fn(), }, })); @@ -480,4 +492,78 @@ describe('Encryption and Decryption', () => { }), ); }); + + test('When an error occurs, then a notification is shown', async () => { + const mockPayload: ShareFileWithUserPayload = { + itemId: 'mock-itemId', + itemType: 'file', + notifyUser: false, + notificationMessage: 'mock-notificationMessage', + sharedWith: 'mock-sharedWith', + encryptionAlgorithm: 'mock-ecc', + roleId: 'mock-roleId', + }; + + const mockUser: Partial = { + mnemonic: + 'truck arch rather sell tilt return warm nurse rack vacuum rubber tribe unfold scissors copper sock panel ozone harsh ahead danger soda legal state', + }; + + const mockRootState: Partial = { + user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, + }; + + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + + vi.spyOn(shareService, 'inviteUserToSharedFolder').mockRejectedValue(new Error('mock-error')); + + const getStateMock = vi.fn(() => mockRootState as RootState); + const dispatchMock = vi.fn(); + + const thunk = shareItemWithUser(mockPayload); + await thunk(dispatchMock, getStateMock, undefined); + + expect(showNotificationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + requestId: 'test-request-id', + }), + ); + }); + + describe('Stop sharing item', () => { + test('When an error occurs when stopping sharing, then a notification is shown', async () => { + vi.spyOn(shareService, 'stopSharingItem').mockRejectedValue(new Error('Unexpected error')); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + + const thunk = stopSharingItem({ itemId: 'mock-itemId', itemType: 'file', itemName: 'mock-itemName' }); + await thunk(vi.fn(), vi.fn(), undefined); + + expect(showNotificationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + requestId: 'test-request-id', + }), + ); + }); + }); + + describe('Removing user from folder', () => { + test('When an error occurs when removing user from folder, then a notification is shown', async () => { + vi.spyOn(shareService, 'removeUserRole').mockRejectedValue(new Error('Unexpected error')); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + + const thunk = removeUserFromSharedFolder({ + itemId: 'mock-itemId', + itemType: 'file', + userEmail: 'mock-userEmail', + userId: 'mock-userId', + }); + await thunk(vi.fn(), vi.fn(), undefined); + + expect(showNotificationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + requestId: 'test-request-id', + }), + ); + }); + }); }); diff --git a/src/app/store/slices/sharedLinks/index.ts b/src/app/store/slices/sharedLinks/index.ts index 7c4a2f44bf..7035995525 100644 --- a/src/app/store/slices/sharedLinks/index.ts +++ b/src/app/store/slices/sharedLinks/index.ts @@ -95,6 +95,7 @@ const shareItemWithUser = createAsyncThunk ({ default: { diff --git a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts index a9a7e01dc0..9d67ba8adf 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts @@ -1,5 +1,10 @@ -import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; -import { uploadItemsParallelThunk, uploadItemsThunk, uploadItemsThunkExtraReducers } from './uploadItemsThunk'; +import { describe, it, expect, vi, beforeEach, Mock, test } from 'vitest'; +import { + uploadItemsParallelThunk, + uploadItemsThunk, + uploadItemsThunkExtraReducers, + uploadSharedItemsThunk, +} from './uploadItemsThunk'; import { RootState } from '../../..'; import { prepareFilesToUpload } from '../fileUtils/prepareFilesToUpload'; import { uploadFileWithManager } from '../../../../network/UploadManager'; @@ -7,6 +12,10 @@ import notificationsService, { ToastType } from 'app/notifications/services/noti import RetryManager from 'app/network/RetryManager'; import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { StorageState } from '../storage.model'; +import errorService from 'services/error.service'; +import { AppError } from '@internxt/sdk'; +import shareService from '../../../../share/services/share.service'; +import workspacesSelectors from '../../workspaces/workspaces.selectors'; vi.mock('../../../../share/services/share.service', () => ({ default: { @@ -124,6 +133,105 @@ describe('uploadItemsThunk', () => { expect(RetryChangeStatusSpy).toHaveBeenCalledWith('task1', 'failed'); }); + + test('when there is an error while uploading items, then a notification is shown', async () => { + const randomError = new AppError('Test Error', undefined, undefined, { + 'x-request-id': 'test-request-id', + }); + const castErrorSpy = vi.spyOn(errorService, 'castError'); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + (prepareFilesToUpload as Mock).mockResolvedValue({ + filesToUpload: [mockFile], + }); + (uploadFileWithManager as Mock).mockRejectedValue(randomError); + + await uploadItemsThunk({ + files: [mockFile], + parentFolderId: 'parent1', + })(dispatch, getState as () => RootState, {}); + + expect(castErrorSpy).toHaveBeenCalledWith(randomError); + expect(showNotificationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + requestId: 'test-request-id', + }), + ); + }); +}); + +describe('upload shared items thunk', () => { + const dispatch = vi.fn(); + const getState = () => { + return { user: { user: { email: 'test@test.com' } } }; + }; + const mockFile = new File(['content'], 'file.txt', { type: 'text/plain' }); + + beforeEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + }); + + test('when there is an error while uploading shared items, then a notification is shown', async () => { + const randomError = new AppError('Test Error', undefined, undefined, { + 'x-request-id': 'test-request-id', + }); + const castErrorSpy = vi.spyOn(errorService, 'castError'); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + (workspacesSelectors.getSelectedWorkspace as Mock).mockReturnValue(null); + (shareService.getSharedFolderContent as Mock).mockResolvedValue({ items: [] }); + (uploadFileWithManager as Mock).mockRejectedValue(randomError); + + await uploadSharedItemsThunk({ + files: [mockFile], + parentFolderId: 'parent1', + currentFolderId: 'folder1', + isDeepFolder: false, + })(dispatch, getState as () => RootState, {}); + + expect(castErrorSpy).toHaveBeenCalledWith(randomError); + expect(showNotificationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + requestId: 'test-request-id', + }), + ); + }); +}); + +describe('Upload items in parallel thunk', () => { + const dispatch = vi.fn(); + const getState = () => { + return { user: { user: { email: 'test@test.com' } } }; + }; + const mockFile = new File(['content'], 'file.txt', { type: 'text/plain' }); + + beforeEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + }); + + test('when there is an error while uploading items in parallel, then a notification is shown', async () => { + const randomError = new AppError('Test Error', undefined, undefined, { + 'x-request-id': 'test-request-id', + }); + const castErrorSpy = vi.spyOn(errorService, 'castError'); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + (prepareFilesToUpload as Mock).mockResolvedValue({ + filesToUpload: [mockFile], + }); + (uploadFileWithManager as Mock).mockRejectedValue(randomError); + + await uploadItemsParallelThunk({ + files: [mockFile], + parentFolderId: 'parent1', + })(dispatch, getState as () => RootState, {}); + + expect(castErrorSpy).toHaveBeenCalledWith(randomError); + expect(showNotificationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + requestId: 'test-request-id', + }), + ); + }); }); describe('uploadItemsThunkExtraReducers', () => { diff --git a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts index e2c97db3ee..91e58869ec 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts @@ -26,6 +26,7 @@ import RetryManager from 'app/network/RetryManager'; import { FileToUpload } from 'app/drive/services/file.service/types'; import { prepareFilesToUpload } from '../fileUtils/prepareFilesToUpload'; import { StorageState } from '../storage.model'; +import { AppError } from '@internxt/sdk'; interface UploadItemsThunkOptions { relatedTaskId: string; @@ -106,7 +107,7 @@ export const uploadItemsThunk = createAsyncThunk { const user = getState().user.user as UserSettings; - const errors: Error[] = []; + const errors: AppError[] = []; const options = { ...DEFAULT_OPTIONS, ...payloadOptions }; const state = getState(); @@ -183,7 +184,7 @@ export const uploadItemsThunk = createAsyncThunk 0) { for (const error of errors) { - if (error.message) notificationsService.show({ text: error.message, type: ToastType.Error }); + if (error.message) + notificationsService.show({ text: error.message, type: ToastType.Error, requestId: error.requestId }); } } }, @@ -394,7 +396,7 @@ export const uploadItemsParallelThunk = createAsyncThunk 0) { for (const error of errors) { - if (error.message) notificationsService.show({ text: error.message, type: ToastType.Error }); + if (error.message) + notificationsService.show({ text: error.message, type: ToastType.Error, requestId: error.requestId }); } throw new Error(t('error.uploadingItems') as string); diff --git a/src/app/store/slices/workspaces/workspacesStore.test.ts b/src/app/store/slices/workspaces/workspacesStore.test.ts index 8d0c209166..5f94b86640 100644 --- a/src/app/store/slices/workspaces/workspacesStore.test.ts +++ b/src/app/store/slices/workspaces/workspacesStore.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { workspaceThunks } from './workspacesStore'; -const { setupWorkspace } = workspaceThunks; import { PendingWorkspace } from '@internxt/sdk/dist/workspaces'; import { generateNewKeys, hybridDecryptMessageWithPrivateKey } from '../../../crypto/services/pgp.service'; import localStorageService from 'services/local-storage.service'; @@ -10,6 +9,7 @@ import { RootState } from '../..'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { Buffer } from 'buffer'; import notificationsService from 'app/notifications/services/notifications.service'; +const { setupWorkspace } = workspaceThunks; vi.mock('i18next', () => ({ t: vi.fn((key, params) => `${key} ${params?.reason ?? ''}`), @@ -65,6 +65,12 @@ vi.mock('services/local-storage.service', () => ({ getB2BWorkspace: vi.fn(), }, })); +vi.mock('services', () => ({ + errorService: { + reportError: vi.fn(), + castError: vi.fn((err) => ({ message: err?.message || 'Unknown error', requestId: 'test-request-id' })), + }, +})); describe('Encryption and Decryption', () => { beforeEach(() => { @@ -167,8 +173,9 @@ describe('Encryption and Decryption', () => { expect(mockWorkspaceService.setupWorkspace).not.toHaveBeenCalled(); expect(showSpy).toHaveBeenCalledWith({ - text: 'Error setting up workspace', + text: expect.any(String), type: 'error', + requestId: 'test-request-id', }); }); }); diff --git a/src/app/store/slices/workspaces/workspacesStore.ts b/src/app/store/slices/workspaces/workspacesStore.ts index 568301a678..025c10767b 100644 --- a/src/app/store/slices/workspaces/workspacesStore.ts +++ b/src/app/store/slices/workspaces/workspacesStore.ts @@ -17,6 +17,7 @@ import { decryptMnemonic } from '../../../share/services/share.service'; import { planThunks } from '../plan'; import sessionThunks from '../session/session.thunks'; import workspacesSelectors from './workspaces.selectors'; +import { errorService } from 'services'; export interface WorkspacesState { workspaces: WorkspaceData[]; @@ -182,7 +183,12 @@ const setupWorkspace = createAsyncThunk { vi.restoreAllMocks(); }); - const createAxiosError = (data: unknown, status?: number): AxiosError => { + const createAxiosError = (data: unknown, status?: number, headers?: Record): AxiosError => { const error = new AxiosError('Request failed'); if (status !== undefined) { - error.response = { data, status, statusText: '', headers: {}, config: {} as never } as AxiosResponse; + error.response = { data, status, statusText: '', headers: headers ?? {}, config: {} as never } as AxiosResponse; } return error; }; @@ -42,7 +42,6 @@ describe('Error Service', () => { describe('AxiosError handling', () => { it('uses the error field from API responses when both error and message are present', () => { const result1 = errorService.castError(createAxiosError({ error: 'Custom error message' }, 400)); - expect(result1).toBeInstanceOf(AppError); expect(result1.message).toBe('Custom error message'); expect(result1.status).toBe(400); @@ -60,19 +59,26 @@ describe('Error Service', () => { it('shows a generic message when API responses contain no error details', () => { const result1 = errorService.castError(createAxiosError({}, 503)); - expect(result1.message).toBe('Unknown error'); + expect(result1.message).toBe('Request failed'); expect(result1.status).toBe(503); + }); - const result2 = errorService.castError(new AxiosError('Network error')); - expect(result2.message).toBe('Unknown error'); - expect(result2.status).toBeUndefined(); + it('falls back to the axios error message when there is no response', () => { + const result = errorService.castError(new AxiosError('Network error')); + expect(result.message).toBe('Network error'); + expect(result.status).toBeUndefined(); + }); + + it('extracts requestId from response headers', () => { + const result = errorService.castError(createAxiosError({ error: 'Fail' }, 500, { 'x-request-id': 'req-123' })); + expect(result.requestId).toBe('req-123'); + expect(result.status).toBe(500); }); }); describe('String and Error instance handling', () => { it('converts simple text error messages into standardized error objects', () => { const result = errorService.castError('Simple error message'); - expect(result).toBeInstanceOf(AppError); expect(result.message).toBe('Simple error message'); expect(result.status).toBeUndefined(); }); @@ -92,12 +98,29 @@ describe('Error Service', () => { const result = errorService.castError(new Error('')); expect(result.message).toBe('Unknown error'); }); + + it('extracts requestId from headers when Error has them', () => { + const errorWithHeaders = Object.assign(new Error('Error with headers'), { + status: 500, + headers: { 'x-request-id': 'req-456' }, + }); + const result = errorService.castError(errorWithHeaders); + expect(result.requestId).toBe('req-456'); + expect(result.status).toBe(500); + }); + }); + + describe('AppError instance', () => { + it('returns the same error when the instance is already an AppError', () => { + const original = new AppError('Original error', 418, 'ERR_CODE'); + const result = errorService.castError(original); + expect(result).toStrictEqual(original); + }); }); describe('Object and edge cases', () => { it('extracts error information from objects containing message and status', () => { const result = errorService.castError({ message: 'Object error', status: 422 }); - expect(result).toBeInstanceOf(AppError); expect(result.message).toBe('Object error'); expect(result.status).toBe(422); }); @@ -107,7 +130,6 @@ describe('Error Service', () => { for (const error of testCases) { const result = errorService.castError(error); - expect(result).toBeInstanceOf(AppError); expect(result.message).toBe('Unknown error'); } }); diff --git a/src/services/error.service.ts b/src/services/error.service.ts index 7bc0dd5f92..2d55027d1b 100644 --- a/src/services/error.service.ts +++ b/src/services/error.service.ts @@ -1,5 +1,5 @@ import { AxiosError } from 'axios'; -import AppError from 'app/core/types'; +import { AppError } from '@internxt/sdk'; import envService from './env.service'; interface AxiosErrorResponse { @@ -9,6 +9,7 @@ interface AxiosErrorResponse { interface ErrorWithStatus extends Error { status?: number; + headers?: Record; } const errorService = { @@ -27,18 +28,22 @@ const errorService = { castError(err: unknown): AppError { let castedError: AppError = new AppError('Unknown error'); + if (err instanceof AppError) { + return err; + } + if (err instanceof AxiosError) { const axiosError = err as AxiosError; const responseData = axiosError.response?.data; + const headers = axiosError.response?.headers as Record; + const message = responseData?.error || responseData?.message || axiosError.message || 'Unknown error'; - castedError = new AppError( - responseData?.error || responseData?.message || 'Unknown error', - axiosError.response?.status, - ); + castedError = new AppError(message, axiosError.response?.status, undefined, headers); } else if (typeof err === 'string') { castedError = new AppError(err); } else if (err instanceof Error) { - castedError = new AppError(err.message || 'Unknown error', (err as ErrorWithStatus).status); + const headers = (err as ErrorWithStatus).headers; + castedError = new AppError(err.message || 'Unknown error', (err as ErrorWithStatus).status, undefined, headers); } else { const map = err as Record; castedError = map.message ? new AppError(map.message as string, map.status as number) : castedError; diff --git a/src/views/Backups/BackupsView.tsx b/src/views/Backups/BackupsView.tsx index 4908048155..4677856c9d 100644 --- a/src/views/Backups/BackupsView.tsx +++ b/src/views/Backups/BackupsView.tsx @@ -124,9 +124,11 @@ export default function BackupsView(): JSX.Element { return true; } catch (error) { errorService.reportError(error); + const castedError = errorService.castError(error); notificationsService.show({ text: translate('notificationMessages.errorDeletingItems'), type: ToastType.Error, + requestId: castedError.requestId, }); return false; } finally { diff --git a/src/views/Backups/hooks/useBackupsPagination.test.ts b/src/views/Backups/hooks/useBackupsPagination.test.ts index 2de5101318..9b2c8bd1ae 100644 --- a/src/views/Backups/hooks/useBackupsPagination.test.ts +++ b/src/views/Backups/hooks/useBackupsPagination.test.ts @@ -22,9 +22,14 @@ vi.mock('app/drive/services/new-storage.service', () => ({ vi.mock('services/error.service', () => ({ default: { reportError: vi.fn(), + castError: vi.fn((err) => ({ message: err?.message || 'Unknown error', requestId: 'test-request-id' })), }, })); +vi.mock('i18next', () => ({ + t: vi.fn((key: string) => key), +})); + describe('useBackupsPagination', () => { const clearSelectedItems = vi.fn(); @@ -173,6 +178,8 @@ describe('useBackupsPagination', () => { await waitFor(() => { expect(notificationsServiceSpy).toHaveBeenCalledWith({ type: ToastType.Error, + text: 'notificationMessages.errorWhileFetchingMoreItems', + requestId: 'test-request-id', }); }); }); diff --git a/src/views/Backups/hooks/useBackupsPagination.ts b/src/views/Backups/hooks/useBackupsPagination.ts index a9d3d13bab..6619d0ffde 100644 --- a/src/views/Backups/hooks/useBackupsPagination.ts +++ b/src/views/Backups/hooks/useBackupsPagination.ts @@ -5,6 +5,7 @@ import newStorageService from 'app/drive/services/new-storage.service'; import { DriveItemData } from 'app/drive/types'; import errorService from 'services/error.service'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; +import { AppError } from '@internxt/sdk'; const DEFAULT_LIMIT = 50; @@ -23,11 +24,12 @@ export const useBackupsPagination = (folderUuid: string | undefined, clearSelect } }, [folderUuid]); - const handleError = useCallback((error: Error) => { + const handleError = useCallback((error: AppError) => { errorService.reportError(error.message); notificationsService.show({ type: ToastType.Error, text: t('notificationMessages.errorWhileFetchingMoreItems'), + requestId: error.requestId, }); setHasMoreItems(false); }, []); @@ -65,7 +67,8 @@ export const useBackupsPagination = (folderUuid: string | undefined, clearSelect setHasMoreItems(false); } } catch (err) { - handleError(err as Error); + const castedError = errorService.castError(err); + handleError(castedError); } finally { setAreFetchingItems(false); } diff --git a/src/views/Checkout/components/CryptoPaymentDialog.tsx b/src/views/Checkout/components/CryptoPaymentDialog.tsx index 5fec282e4d..f7912ccb58 100644 --- a/src/views/Checkout/components/CryptoPaymentDialog.tsx +++ b/src/views/Checkout/components/CryptoPaymentDialog.tsx @@ -10,6 +10,7 @@ import notificationsService, { ToastType } from 'app/notifications/services/noti import checkoutService from '../services/checkout.service'; import { Currency } from '../types'; import { useEffect, useState } from 'react'; +import { errorService } from 'services'; export const CRYPTO_PAYMENT_DIALOG_KEY = ActionDialog.CryptoPayment; @@ -74,9 +75,11 @@ export const CryptoPaymentDialog = () => { } } catch (error) { console.error('Error while verifying crypto payment', error); + const castedError = errorService.castError(error); notificationsService.show({ text: translate('checkout.confirmCryptoPayment.notifications.unexpectedError'), type: ToastType.Error, + requestId: castedError.requestId, }); } }; diff --git a/src/views/Checkout/hooks/useInitializeCheckout.ts b/src/views/Checkout/hooks/useInitializeCheckout.ts index f0d7eb24d2..ba99c625e0 100644 --- a/src/views/Checkout/hooks/useInitializeCheckout.ts +++ b/src/views/Checkout/hooks/useInitializeCheckout.ts @@ -66,9 +66,11 @@ export const useInitializeCheckout = ({ user, price, checkoutTheme, translate }: setAvailableCryptoCurrencies(availableCryptoCurrencies); } catch (error) { console.error('Error fetching available crypto currencies', error); + const castedError = errorService.castError(error); notificationsService.show({ text: translate('checkout.error.fetchingCryptoCurrencies'), type: ToastType.Error, + requestId: castedError.requestId, }); } } diff --git a/src/views/Checkout/hooks/usePromotionalCode.ts b/src/views/Checkout/hooks/usePromotionalCode.ts index 188f3e407f..aa9ebed385 100644 --- a/src/views/Checkout/hooks/usePromotionalCode.ts +++ b/src/views/Checkout/hooks/usePromotionalCode.ts @@ -4,6 +4,7 @@ import notificationsService, { ToastType } from 'app/notifications/services/noti import { checkoutService } from '../services'; import { CouponCodeData } from '../types'; import { STATUS_CODE_ERROR } from '../constants'; +import { errorService } from 'services'; interface UsePromotionalCodeProps { priceId: string | null; @@ -46,6 +47,7 @@ export const usePromotionalCode = ({ priceId, promoCodeName }: UsePromotionalCod const onPromoCodeError = (err: unknown, showNotification?: boolean) => { const error = err as Error; + const castedError = errorService.castError(err); const statusCode = (err as any).status; let errorMessage = translate('notificationMessages.errorApplyingCoupon'); if (statusCode) { @@ -63,6 +65,7 @@ export const usePromotionalCode = ({ priceId, promoCodeName }: UsePromotionalCod notificationsService.show({ text: errorMessage, type: ToastType.Error, + requestId: castedError.requestId, }); } }; diff --git a/src/views/Checkout/views/CheckoutViewWrapper.tsx b/src/views/Checkout/views/CheckoutViewWrapper.tsx index 0d8d25c199..c3e37d85cb 100644 --- a/src/views/Checkout/views/CheckoutViewWrapper.tsx +++ b/src/views/Checkout/views/CheckoutViewWrapper.tsx @@ -11,7 +11,8 @@ import errorService from 'services/error.service'; import localStorageService from 'services/local-storage.service'; import navigationService from 'services/navigation.service'; import { STORAGE_KEYS } from 'services/storage-keys'; -import AppError, { AppView, IFormValues } from 'app/core/types'; +import { AppError } from '@internxt/sdk'; +import { AppView, IFormValues } from 'app/core/types'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import ChangePlanDialog from 'views/NewSettings/components/Sections/Account/Plans/components/ChangePlanDialog'; import longNotificationsService from 'app/notifications/services/longNotification.service'; @@ -221,11 +222,13 @@ const CheckoutViewWrapper = () => { notificationsService.show({ text: defaultErrorMessage, type: ToastType.Error, + requestId: error?.requestId, }); } else { longNotificationsService.show({ type: ToastType.Error, text: error?.message, + requestId: error?.requestId, }); } }; diff --git a/src/views/Home/components/PendingInvitationsDialog.tsx b/src/views/Home/components/PendingInvitationsDialog.tsx index 4cc980d627..3a6520e95f 100644 --- a/src/views/Home/components/PendingInvitationsDialog.tsx +++ b/src/views/Home/components/PendingInvitationsDialog.tsx @@ -8,7 +8,6 @@ import { Button, Modal } from '@internxt/ui'; import { useAppDispatch } from 'app/store/hooks'; import { workspaceThunks } from 'app/store/slices/workspaces/workspacesStore'; import dayjs from 'dayjs'; -import AppError from 'app/core/types'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import { wait } from 'utils/timeUtils'; @@ -49,18 +48,19 @@ const PendingInvitationsDialog = ({ await wait(3000); dispatch(workspaceThunks.fetchWorkspaces()); } catch (err) { - const appError = err as AppError; - if (appError.status === WORKSPACE_INVITATION_BAD_REQUEST) { + const error = errorService.castError(err); + if (error.status === WORKSPACE_INVITATION_BAD_REQUEST) { notificationsService.show({ text: translate('notificationMessages.invalidWorkspaceInvitationError'), type: ToastType.Error, + requestId: error.requestId, }); } else { - const error = errorService.castError(err); errorService.reportError(error); notificationsService.show({ text: translate('notificationMessages.errorAcceptingWorkspaceInvitation'), type: ToastType.Error, + requestId: error.requestId, }); } } finally { diff --git a/src/views/Login/BlockedAccountView.tsx b/src/views/Login/BlockedAccountView.tsx index bb4f6e60a1..96451368c9 100644 --- a/src/views/Login/BlockedAccountView.tsx +++ b/src/views/Login/BlockedAccountView.tsx @@ -70,10 +70,12 @@ export default function BlockedAccountView(): JSX.Element { await authService.requestUnblockAccount(userEmail); setEnableResendButton(false); } catch (error) { + const castedError = errorService.castError(error); errorService.reportError(error); notificationsService.show({ text: translate('error.serverError'), type: ToastType.Error, + requestId: castedError.requestId, }); } finally { setSendingEmail(false); diff --git a/src/views/Login/components/ChangePassword.tsx b/src/views/Login/components/ChangePassword.tsx index 777e40ccfa..905f3ca9e6 100644 --- a/src/views/Login/components/ChangePassword.tsx +++ b/src/views/Login/components/ChangePassword.tsx @@ -30,22 +30,25 @@ export default function ChangePassword(props: Readonly): JS try { const backupData = JSON.parse(uploadedBackupKeyContent); - if (backupData.mnemonic && validateMnemonic(backupData.mnemonic)) { - setBackupKeyContent(uploadedBackupKeyContent); - return; + if (!backupData.mnemonic || !validateMnemonic(backupData.mnemonic)) { + throw new Error('Invalid mnemonic in backup key'); } + + setBackupKeyContent(uploadedBackupKeyContent); } catch (err) { errorService.reportError(err); + const castedError = errorService.castError(err); if (validateMnemonic(uploadedBackupKeyContent)) { setBackupKeyContent(uploadedBackupKeyContent); return; } - } - notificationsService.show({ - text: translate('auth.recoverAccount.changePassword.backupKeyError'), - type: ToastType.Error, - }); + notificationsService.show({ + text: translate('auth.recoverAccount.changePassword.backupKeyError'), + type: ToastType.Error, + requestId: castedError.requestId, + }); + } }; const onSendNewPassword = async (password: string) => { @@ -68,9 +71,11 @@ export default function ChangePassword(props: Readonly): JS localStorageService.clear(); setIsEmailSent(true); } catch (error) { + const castedError = errorService.castError(error); notificationsService.show({ text: translate('auth.recoverAccount.changePassword.serverError'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); } diff --git a/src/views/Login/components/LogIn.tsx b/src/views/Login/components/LogIn.tsx index 3b949aba56..94a65d4eb5 100644 --- a/src/views/Login/components/LogIn.tsx +++ b/src/views/Login/components/LogIn.tsx @@ -16,7 +16,8 @@ import { twoFactorRegexPattern } from 'services/validation.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { Button } from '@internxt/ui'; import { WarningCircle } from '@phosphor-icons/react'; -import AppError, { AppView, IFormValues } from 'app/core/types'; +import { AppError } from '@internxt/sdk'; +import { AppView, IFormValues } from 'app/core/types'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import shareService from 'app/share/services/share.service'; diff --git a/src/views/Login/components/RestartAccount.tsx b/src/views/Login/components/RestartAccount.tsx index 5f0380e554..0b5f9e3cf1 100644 --- a/src/views/Login/components/RestartAccount.tsx +++ b/src/views/Login/components/RestartAccount.tsx @@ -33,9 +33,11 @@ export default function RestartAccount(props: Readonly): JSX.Ele await authService.resetAccountWithToken(token, password); setIsEmailSent(true); } catch (error) { + const castedError = errorService.castError(error); notificationsService.show({ text: translate('auth.restartAccount.error'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); } diff --git a/src/views/NewSettings/components/Sections/Account/Account/AccountSection.tsx b/src/views/NewSettings/components/Sections/Account/Account/AccountSection.tsx index b497e6dea3..8063977bed 100644 --- a/src/views/NewSettings/components/Sections/Account/Account/AccountSection.tsx +++ b/src/views/NewSettings/components/Sections/Account/Account/AccountSection.tsx @@ -59,9 +59,11 @@ const AccountSection = ({ changeSection, onClosePreferences }: AccountSectionPro }; const onChangeEmailError = (error: unknown) => { + const castedError = errorService.castError(error); notificationsService.show({ text: translate('views.account.tabs.account.accountDetails.changeEmail.errorSendingVerification'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); }; @@ -104,12 +106,14 @@ const AccountSection = ({ changeSection, onClosePreferences }: AccountSectionPro lastname={user.lastname} email={user.email} onUpdateUserProfileData={updateUserProfile} - onErrorUpdatingUserProfileData={() => + onErrorUpdatingUserProfileData={(err) => { + const castedError = errorService.castError(err); notificationsService.show({ text: translate('views.account.tabs.account.accountDetails.editProfile.errorUpdatingProfile'), type: ToastType.Error, - }) - } + requestId: castedError.requestId, + }); + }} /> Promise; - onErrorUpdatingUserProfileData: () => void; + onErrorUpdatingUserProfileData: (err) => void; }) => { const { translate } = useTranslationContext(); @@ -62,9 +62,9 @@ const AccountDetailsModal = ({ setStatus({ tag: 'loading' }); await onUpdateUserProfileData({ name: nameValue, lastname: lastnameValue }); onClose(); - } catch { + } catch (err) { setStatus({ tag: 'error', type: 'UNKNOWN' }); - onErrorUpdatingUserProfileData(); + onErrorUpdatingUserProfileData(err); } }; diff --git a/src/views/NewSettings/components/Sections/Account/Account/containers/DeleteAccountContainer.tsx b/src/views/NewSettings/components/Sections/Account/Account/containers/DeleteAccountContainer.tsx index 9af58eec65..eeef6aabdb 100644 --- a/src/views/NewSettings/components/Sections/Account/Account/containers/DeleteAccountContainer.tsx +++ b/src/views/NewSettings/components/Sections/Account/Account/containers/DeleteAccountContainer.tsx @@ -40,7 +40,7 @@ const DeleteAccountContainer = ({ onClosePreferences }: { onClosePreferences: () onClose(); } catch (err: unknown) { const castedError = errorService.castError(err); - notificationsService.show({ text: castedError.message, type: ToastType.Error }); + notificationsService.show({ text: castedError.message, type: ToastType.Error, requestId: castedError.requestId }); } finally { setIsLoading(false); } diff --git a/src/views/NewSettings/components/Sections/Account/Account/containers/UserHeaderContainer.tsx b/src/views/NewSettings/components/Sections/Account/Account/containers/UserHeaderContainer.tsx index 8c339a79cd..1598018bd4 100644 --- a/src/views/NewSettings/components/Sections/Account/Account/containers/UserHeaderContainer.tsx +++ b/src/views/NewSettings/components/Sections/Account/Account/containers/UserHeaderContainer.tsx @@ -32,8 +32,13 @@ const UserHeaderContainer = () => { await dispatch(updateUserAvatarThunk({ avatar })).unwrap(); notificationsService.show({ type: ToastType.Success, text: translate('views.account.avatar.success') }); } catch (err) { + const castedError = errorService.castError(err); errorService.reportError(err); - notificationsService.show({ type: ToastType.Error, text: translate('views.account.avatar.error') }); + notificationsService.show({ + type: ToastType.Error, + text: translate('views.account.avatar.error'), + requestId: castedError.requestId, + }); } }; diff --git a/src/views/NewSettings/components/Sections/Account/Billing/BillingAccountSection.tsx b/src/views/NewSettings/components/Sections/Account/Billing/BillingAccountSection.tsx index b9a2074be6..0583634b6e 100644 --- a/src/views/NewSettings/components/Sections/Account/Billing/BillingAccountSection.tsx +++ b/src/views/NewSettings/components/Sections/Account/Billing/BillingAccountSection.tsx @@ -13,6 +13,7 @@ import CancelSubscription from './components/CancelSubscription'; import BillingAccountOverview from './containers/BillingAccountOverview'; import { UserType } from '@internxt/sdk/dist/drive/payments/types/types'; import { getCurrentUsage, getPlanInfo, getPlanName } from '../../../../utils/planUtils'; +import { errorService } from 'services'; interface BillingAccountSectionProps { changeSection: ({ section, subsection }) => void; @@ -44,10 +45,11 @@ const BillingAccountSection = ({ changeSection, onClosePreferences }: BillingAcc notificationsService.show({ text: t('notificationMessages.successCancelSubscription') }); setIsCancelSubscriptionModalOpen(false); } catch (err) { - console.error(err); + const castedError = errorService.castError(err); notificationsService.show({ text: t('notificationMessages.errorCancelSubscription'), type: ToastType.Error, + requestId: castedError.requestId, }); } finally { setCancellingSubscription(false); diff --git a/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx b/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx index d3f2aa7726..b1edc82388 100644 --- a/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx +++ b/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx @@ -1,5 +1,6 @@ import { DisplayPrice, UserType } from '@internxt/sdk/dist/drive/payments/types/types'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { AppError } from '@internxt/sdk'; import { AppView } from 'app/core/types'; import Section from '../../../Section'; import { useCallback, useEffect, useState } from 'react'; @@ -169,10 +170,11 @@ const PlansSection = ({ changeSection, onClosePreferences }: PlansSectionProps) }, []); const showCancelSubscriptionErrorNotification = useCallback( - (error?: Error) => + (error?: AppError) => notificationsService.show({ text: error?.message ?? translate('notificationMessages.errorCancelSubscription'), type: ToastType.Error, + requestId: error?.requestId, }), [translate], ); @@ -232,10 +234,12 @@ const PlansSection = ({ changeSection, onClosePreferences }: PlansSectionProps) notificationsService.show({ text: translate('notificationMessages.successCancelSubscription') }); setIsCancelSubscriptionModalOpen(false); } catch (error) { + const castedError = errorService.castError(error); errorService.reportError(error); notificationsService.show({ text: translate('notificationMessages.errorCancelSubscription'), type: ToastType.Error, + requestId: castedError.requestId, }); } finally { setCancellingSubscription(false); diff --git a/src/views/NewSettings/components/Sections/Account/Security/components/TwoFactorAuthenticationDisableModal.tsx b/src/views/NewSettings/components/Sections/Account/Security/components/TwoFactorAuthenticationDisableModal.tsx index cd194eb922..31e68e817e 100644 --- a/src/views/NewSettings/components/Sections/Account/Security/components/TwoFactorAuthenticationDisableModal.tsx +++ b/src/views/NewSettings/components/Sections/Account/Security/components/TwoFactorAuthenticationDisableModal.tsx @@ -41,7 +41,11 @@ const TwoFactorAuthenticationDisableModal = ({ } catch (err) { setStatus('error'); const castedError = errorService.castError(err); - notificationsService.show({ text: castedError.message || translate('error.serverError'), type: ToastType.Error }); + notificationsService.show({ + text: castedError.message || translate('error.serverError'), + type: ToastType.Error, + requestId: castedError.requestId, + }); } } diff --git a/src/views/NewSettings/components/Sections/Workspace/Billing/BillingWorkspaceSection.tsx b/src/views/NewSettings/components/Sections/Workspace/Billing/BillingWorkspaceSection.tsx index 7f9e7bf558..250a32a267 100644 --- a/src/views/NewSettings/components/Sections/Workspace/Billing/BillingWorkspaceSection.tsx +++ b/src/views/NewSettings/components/Sections/Workspace/Billing/BillingWorkspaceSection.tsx @@ -23,7 +23,6 @@ import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { UpdateMembersCard } from './UpdateMembersCard'; import { SelectNewMembersModal } from './components/UpdateMembers/SelectNewMembersModal'; import { ConfirmUpdatedMembersModal } from './components/UpdateMembers/ConfirmUpdatedMembersModal'; -import AppError from 'app/core/types'; import errorService from 'services/error.service'; import workspacesService from 'services/workspace.service'; import { WorkspaceUser } from '@internxt/sdk/dist/workspaces'; @@ -79,10 +78,12 @@ const BillingWorkspaceSection = ({ onClosePreferences }: BillingWorkspaceSection const members = await workspacesService.getWorkspacesMembers(workspaceId); setCurrentWorkspaceMembers([...members.activatedUsers, ...members.disabledUsers]); } catch (error) { + const castedError = errorService.castError(error); errorService.reportError(error); notificationsService.show({ text: translate('notificationMessages.errorWhileFetchingCurrentWorkspaceMembers'), type: ToastType.Error, + requestId: castedError.requestId, }); } finally { setAreFetchingCurrentMembers(false); @@ -102,10 +103,11 @@ const BillingWorkspaceSection = ({ onClosePreferences }: BillingWorkspaceSection dispatch(planThunks.initializeThunk()); }, 3000); } catch (err) { - console.error(err); + const castedError = errorService.castError(err); notificationsService.show({ text: translate('notificationMessages.errorCancelSubscription'), type: ToastType.Error, + requestId: castedError.requestId, }); } }; @@ -179,20 +181,22 @@ const BillingWorkspaceSection = ({ onClosePreferences }: BillingWorkspaceSection setIsConfirmingMembersWorkspace(false); setIsConfirmingMembersWorkspaceModalOpen(false); } catch (err) { - const error = err as AppError; - if (error.status === UPDATE_MEMBERS_BAD_RESPONSE) { + const castedError = errorService.castError(err); + if (castedError.status === UPDATE_MEMBERS_BAD_RESPONSE) { notificationsService.show({ - text: error.message, + text: castedError.message, type: ToastType.Error, + requestId: castedError.requestId, }); } else { notificationsService.show({ text: translate('notificationMessages.errorWhileUpdatingWorkspaceMembers'), type: ToastType.Error, + requestId: castedError.requestId, }); } - errorService.reportError(error); + errorService.reportError(castedError); } }; diff --git a/src/views/NewSettings/components/Sections/Workspace/Billing/CancelSubscriptionModal.tsx b/src/views/NewSettings/components/Sections/Workspace/Billing/CancelSubscriptionModal.tsx index 6fc8bb37f5..57bc563020 100644 --- a/src/views/NewSettings/components/Sections/Workspace/Billing/CancelSubscriptionModal.tsx +++ b/src/views/NewSettings/components/Sections/Workspace/Billing/CancelSubscriptionModal.tsx @@ -9,6 +9,7 @@ import { paymentService } from 'views/Checkout/services'; import { Button, Modal } from '@internxt/ui'; import { useAppDispatch } from 'app/store/hooks'; import { planThunks } from 'app/store/slices/plan'; +import { errorService } from 'services'; interface CancelSubscriptionModalProps { isOpen: boolean; @@ -45,10 +46,11 @@ const CancelSubscriptionModal = ({ setCouponAvailable(response.elegible); }) .catch((error) => { - console.error(error); + const castedError = errorService.castError(error); notificationsService.show({ text: translate('notificationMessages.errorApplyCoupon'), type: ToastType.Error, + requestId: castedError.requestId, }); }); }, [isOpen]); @@ -67,16 +69,19 @@ const CancelSubscriptionModal = ({ dispatch(planThunks.initializeThunk()).unwrap(); }, 1000); } catch (error: any) { + const castedError = errorService.castError(error); const errorMessage = JSON.parse(error.message); if (errorMessage.message === 'User already applied coupon') { notificationsService.show({ text: translate('notificationMessages.alreadyAppliedCoupon'), type: ToastType.Error, + requestId: castedError.requestId, }); } else { notificationsService.show({ text: translate('notificationMessages.errorApplyCoupon'), type: ToastType.Error, + requestId: castedError.requestId, }); } } finally { diff --git a/src/views/NewSettings/components/Sections/Workspace/Members/containers/InviteDialogContainer.tsx b/src/views/NewSettings/components/Sections/Workspace/Members/containers/InviteDialogContainer.tsx index 0ec2fc8c0f..e913779e79 100644 --- a/src/views/NewSettings/components/Sections/Workspace/Members/containers/InviteDialogContainer.tsx +++ b/src/views/NewSettings/components/Sections/Workspace/Members/containers/InviteDialogContainer.tsx @@ -82,6 +82,7 @@ const processInvitation = async ( notificationsService.show({ text: castedError.message, type: ToastType.Error, + requestId: castedError.requestId, }); } }; diff --git a/src/views/NewSettings/components/Sections/Workspace/Members/containers/MemberDetailsContainer.tsx b/src/views/NewSettings/components/Sections/Workspace/Members/containers/MemberDetailsContainer.tsx index 37be9903ea..754ce631a7 100644 --- a/src/views/NewSettings/components/Sections/Workspace/Members/containers/MemberDetailsContainer.tsx +++ b/src/views/NewSettings/components/Sections/Workspace/Members/containers/MemberDetailsContainer.tsx @@ -23,7 +23,6 @@ import { RootState } from 'app/store'; import { ActionDialog } from 'app/contexts/dialog-manager/ActionDialogManager.context'; import { useActionDialog } from 'app/contexts/dialog-manager/useActionDialog'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; -import AppError from 'app/core/types'; interface MemberDetailsContainer { member: WorkspaceUser; @@ -90,16 +89,18 @@ const MemberDetailsContainer = ({ type: ToastType.Success, }); } catch (error) { - const appError = error as AppError; - if (appError.status === UPDATED_SPACE_IS_NOT_VALID_FOR_MEMBER) { + const castedError = errorService.castError(error); + if (castedError.status === UPDATED_SPACE_IS_NOT_VALID_FOR_MEMBER) { notificationsService.show({ text: translate('notificationMessages.errorModifyingStorage'), type: ToastType.Error, + requestId: castedError.requestId, }); } else { notificationsService.show({ text: translate('notificationMessages.generalErrorWhileModifyingStorage'), type: ToastType.Error, + requestId: castedError.requestId, }); } errorService.reportError(error); diff --git a/src/views/NewSettings/components/Sections/Workspace/Overview/OverviewSection.tsx b/src/views/NewSettings/components/Sections/Workspace/Overview/OverviewSection.tsx index 810e4d8e1f..6737f0efbd 100644 --- a/src/views/NewSettings/components/Sections/Workspace/Overview/OverviewSection.tsx +++ b/src/views/NewSettings/components/Sections/Workspace/Overview/OverviewSection.tsx @@ -132,7 +132,7 @@ const OverviewSection = ({ onClosePreferences, changeSection }: OverviewSectionP } catch (error) { errorService.reportError(error); const castedError = errorService.castError(error); - notificationsService.show({ type: ToastType.Error, text: castedError.message }); + notificationsService.show({ type: ToastType.Error, text: castedError.message, requestId: castedError.requestId }); } finally { setIsEditingDetails(false); setIsSavingProfileDetails(false); @@ -150,7 +150,12 @@ const OverviewSection = ({ onClosePreferences, changeSection }: OverviewSectionP notificationsService.show({ type: ToastType.Success, text: t('views.account.avatar.success') }); } catch (err) { errorService.reportError(err); - notificationsService.show({ type: ToastType.Error, text: t('views.account.avatar.error') }); + const castedError = errorService.castError(err); + notificationsService.show({ + type: ToastType.Error, + text: t('views.account.avatar.error'), + requestId: castedError.requestId, + }); } }; diff --git a/src/views/PublicShared/ShareFileView.tsx b/src/views/PublicShared/ShareFileView.tsx index 05a8f1c0eb..b93344992b 100644 --- a/src/views/PublicShared/ShareFileView.tsx +++ b/src/views/PublicShared/ShareFileView.tsx @@ -24,7 +24,7 @@ import errorService from 'services/error.service'; import { binaryStreamToBlob } from 'services/stream.service'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import { HTTP_CODES } from 'app/core/constants'; -import AppError from 'app/core/types'; +import { AppError } from '@internxt/sdk'; import { Button, Loader } from '@internxt/ui'; import { stringUtils } from '@internxt/lib'; import { SendBanner, ShareItemPwdView } from './components'; diff --git a/src/views/PublicShared/ShareFolderView.tsx b/src/views/PublicShared/ShareFolderView.tsx index 0813c4d422..c12b897632 100644 --- a/src/views/PublicShared/ShareFolderView.tsx +++ b/src/views/PublicShared/ShareFolderView.tsx @@ -14,7 +14,7 @@ import { useEffect, useState } from 'react'; import { match } from 'react-router'; import { Link } from 'react-router-dom'; import { HTTP_CODES } from 'app/core/constants'; -import AppError from 'app/core/types'; +import { AppError } from '@internxt/sdk'; import { useAppSelector } from 'app/store/hooks'; import { SendBanner, ShareItemPwdView } from './components'; import './components/ShareView.scss'; diff --git a/src/views/PublicShared/components/ShareItemPwdView.tsx b/src/views/PublicShared/components/ShareItemPwdView.tsx index 61db682ada..aff5453d09 100644 --- a/src/views/PublicShared/components/ShareItemPwdView.tsx +++ b/src/views/PublicShared/components/ShareItemPwdView.tsx @@ -1,7 +1,7 @@ import { WarningCircle } from '@phosphor-icons/react'; import errorService from 'services/error.service'; import validationService from 'services/validation.service'; -import AppError from 'app/core/types'; +import { AppError } from '@internxt/sdk'; import iconService from 'app/drive/services/icon.service'; import transformItemService from 'app/drive/services/item-transform.service'; import sizeService from 'app/drive/services/size.service'; diff --git a/src/views/Signup/components/SignupBlog/services/blogSignup.service.test.ts b/src/views/Signup/components/SignupBlog/services/blogSignup.service.test.ts index 3333fdb608..2f8cf5332d 100644 --- a/src/views/Signup/components/SignupBlog/services/blogSignup.service.test.ts +++ b/src/views/Signup/components/SignupBlog/services/blogSignup.service.test.ts @@ -4,7 +4,7 @@ import { authenticateUser } from 'services/auth.service'; import envService from 'services/env.service'; import errorService from 'services/error.service'; import { AppDispatch } from 'app/store'; -import AppError from 'app/core/types'; +import { AppError } from '@internxt/sdk'; vi.mock('services/auth.service', () => ({ authenticateUser: vi.fn(), diff --git a/src/views/Trash/services/clearTrash.service.test.ts b/src/views/Trash/services/clearTrash.service.test.ts index bc680f98a1..b934aaed52 100644 --- a/src/views/Trash/services/clearTrash.service.test.ts +++ b/src/views/Trash/services/clearTrash.service.test.ts @@ -16,6 +16,7 @@ vi.mock('app/core/factory/sdk', () => ({ vi.mock('services/error.service', () => ({ default: { reportError: vi.fn(), + castError: vi.fn((err) => ({ message: err?.message || 'Unknown error', requestId: 'test-request-id' })), }, })); @@ -116,6 +117,7 @@ describe('clearTrash', () => { expect(notificationsService.show).toHaveBeenCalledWith({ text: 'error.errorDeletingFromTrash', type: 'error', + requestId: 'test-request-id', }); expect(errorService.reportError).toHaveBeenCalledWith(mockError); }); diff --git a/src/views/Trash/services/clearTrash.service.ts b/src/views/Trash/services/clearTrash.service.ts index 6d12c6a274..bd3b8b64ed 100644 --- a/src/views/Trash/services/clearTrash.service.ts +++ b/src/views/Trash/services/clearTrash.service.ts @@ -33,9 +33,11 @@ const clearTrash = async (workspaceId?: string): Promise => { }); } catch (error) { notificationsService.dismiss(deletingItemsToastId); + const castedError = errorService.castError(error); notificationsService.show({ text: t('error.errorDeletingFromTrash'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); diff --git a/src/views/Trash/services/deleteItems.service.test.ts b/src/views/Trash/services/deleteItems.service.test.ts index 694551ac97..7dbb5ae211 100644 --- a/src/views/Trash/services/deleteItems.service.test.ts +++ b/src/views/Trash/services/deleteItems.service.test.ts @@ -18,6 +18,7 @@ vi.mock('app/core/factory/sdk', () => ({ vi.mock('services/error.service', () => ({ default: { reportError: vi.fn(), + castError: vi.fn((err) => ({ message: err?.message || 'Unknown error', requestId: 'test-request-id' })), }, })); @@ -157,6 +158,7 @@ describe('DeleteItems', () => { expect(notificationsService.show).toHaveBeenCalledWith({ text: 'error.errorDeletingFromTrash', type: 'error', + requestId: 'test-request-id', }); expect(errorService.reportError).toHaveBeenCalledWith(mockError); }); diff --git a/src/views/Trash/services/deleteItems.service.ts b/src/views/Trash/services/deleteItems.service.ts index ad9d2fb2d3..47ada07cf3 100644 --- a/src/views/Trash/services/deleteItems.service.ts +++ b/src/views/Trash/services/deleteItems.service.ts @@ -61,9 +61,11 @@ const deleteItems = async (itemsToDelete: DriveItemData[]): Promise => { }), }); } catch (error) { + const castedError = errorService.castError(error); notificationsService.show({ text: t('error.errorDeletingFromTrash'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); } diff --git a/src/views/Trash/services/getTrash.service.test.ts b/src/views/Trash/services/getTrash.service.test.ts index f17e9ce499..65d1b91801 100644 --- a/src/views/Trash/services/getTrash.service.test.ts +++ b/src/views/Trash/services/getTrash.service.test.ts @@ -16,6 +16,7 @@ vi.mock('app/core/factory/sdk', () => ({ vi.mock('services/error.service', () => ({ default: { reportError: vi.fn(), + castError: vi.fn((err) => ({ message: err?.message || 'Unknown error', requestId: 'test-request-id' })), }, })); diff --git a/src/views/Trash/services/getTrash.service.ts b/src/views/Trash/services/getTrash.service.ts index 43f793867d..1eed51be29 100644 --- a/src/views/Trash/services/getTrash.service.ts +++ b/src/views/Trash/services/getTrash.service.ts @@ -46,9 +46,11 @@ function processTrashItems( } function handleTrashError(error: unknown): { finished: boolean; itemsRetrieved: number } { + const castedError = errorService.castError(error); notificationsService.show({ text: t('error.errorLoadingTrashItems'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); diff --git a/src/views/Trash/services/moveItemsToTrash.service.test.ts b/src/views/Trash/services/moveItemsToTrash.service.test.ts index 9c276b7030..925f5851f2 100644 --- a/src/views/Trash/services/moveItemsToTrash.service.test.ts +++ b/src/views/Trash/services/moveItemsToTrash.service.test.ts @@ -19,6 +19,7 @@ vi.mock('app/core/factory/sdk', () => ({ vi.mock('services/error.service', () => ({ default: { reportError: vi.fn(), + castError: vi.fn((err) => ({ message: err?.message || 'Unknown error', requestId: 'test-request-id' })), }, })); @@ -183,6 +184,7 @@ describe('moveItemsToTrash', () => { expect(notificationsService.show).toHaveBeenCalledWith({ text: 'error.errorMovingToTrash', type: 'error', + requestId: 'test-request-id', }); expect(errorService.reportError).toHaveBeenCalledWith(mockError); }); diff --git a/src/views/Trash/services/moveItemsToTrash.service.ts b/src/views/Trash/services/moveItemsToTrash.service.ts index 883ac0bdc4..b62a4f2704 100644 --- a/src/views/Trash/services/moveItemsToTrash.service.ts +++ b/src/views/Trash/services/moveItemsToTrash.service.ts @@ -94,10 +94,12 @@ const moveItemsToTrash = async (itemsToTrash: DriveItemData[], onSuccess?: () => }, }); } catch (error) { + const castedError = errorService.castError(error); notificationsService.dismiss(movingItemsToastId); notificationsService.show({ text: t('error.errorMovingToTrash'), type: ToastType.Error, + requestId: castedError.requestId, }); errorService.reportError(error); diff --git a/yarn.lock b/yarn.lock index 125278b9de..e1d8e98d20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1906,13 +1906,13 @@ version "1.0.2" resolved "https://codeload.github.com/internxt/prettier-config/tar.gz/9fa74e9a2805e1538b50c3809324f1c9d0f3e4f9" -"@internxt/sdk@=1.12.6": - version "1.12.6" - resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.12.6.tgz#6e7e08cfb78aac1a9cde45f0669b5e094f716909" - integrity sha512-V5XfRRb6HaiFLtLq9nTUQScd9c41MOyvr3GT5lvpqj7pd/gMnBV9HBpMqdiiRzPWXgf1WDddcjo2QBNChEzCxQ== +"@internxt/sdk@=1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.13.2.tgz#e789b9280a2c9f5451935d7358bdf1e259e896f4" + integrity sha512-KHC0QlXZ0HMtVOq8P56OkHhEhy3x8OyoUAwvRib+2CIzHWk78xApO2t4R4HScWRFH6xmHIoCgP1fTC6ctCmPqA== dependencies: - axios "1.13.2" - uuid "11.1.0" + axios "1.13.5" + uuid "13.0.0" "@internxt/ui@=0.1.4": version "0.1.4" @@ -3961,16 +3961,7 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@1.13.2: - version "1.13.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687" - integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.4" - proxy-from-env "^1.1.0" - -axios@^1.13.5: +axios@1.13.5, axios@^1.13.5: version "1.13.5" resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43" integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q== @@ -5749,7 +5740,7 @@ flatted@^3.3.1: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== -follow-redirects@^1.15.11, follow-redirects@^1.15.6: +follow-redirects@^1.15.11: version "1.15.11" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== @@ -5776,7 +5767,7 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -form-data@^4.0.4, form-data@^4.0.5: +form-data@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== @@ -9483,7 +9474,12 @@ util@^0.12.0, util@^0.12.4, util@^0.12.5: is-typed-array "^1.1.3" which-typed-array "^1.1.2" -uuid@11.1.0, uuid@^11.1.0: +uuid@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8" + integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w== + +uuid@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==