From 69378f60d5f725b3ce615a65060176f41d1de3de Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 28 Apr 2026 16:16:23 +0200 Subject: [PATCH 1/5] feat(backups): show dialog to download backup keys --- src/App.tsx | 10 ++ src/app/banners/BannerWrapper.tsx | 6 +- .../ActionDialogManager.context.tsx | 1 + .../hooks/useDownloadBackupKeys.test.ts | 138 ++++++++++++++++++ .../hooks/useDownloadBackupKeys.ts | 52 +++++++ .../DownloadBackupKeysDialog/index.tsx | 63 ++++++++ src/app/i18n/locales/en.json | 13 +- src/services/local-storage.service.test.ts | 64 +++++++- src/services/local-storage.service.ts | 42 +++++- src/services/storage-keys.ts | 4 + src/utils/backupKeyUtils.ts | 9 ++ 11 files changed, 398 insertions(+), 4 deletions(-) create mode 100644 src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.test.ts create mode 100644 src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts create mode 100644 src/app/drive/components/DownloadBackupKeysDialog/index.tsx diff --git a/src/App.tsx b/src/App.tsx index 123b98e90..91641c2f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,8 @@ import useVpnAuth from './hooks/useVpnAuth'; import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?raw'; import { eventHandler } from 'services/sockets/event-handler.service'; import RealtimeService from 'services/sockets/socket.service'; +import { DownloadBackupKeysDialog } from 'app/drive/components/DownloadBackupKeysDialog'; +import { useDownloadBackupKeys } from 'app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys'; const blob = new Blob([workerUrl], { type: 'application/javascript' }); pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(blob); @@ -66,6 +68,7 @@ const App = (props: AppProps): JSX.Element => { const { isDialogOpen } = useActionDialog(); const isOpen = isDialogOpen(ActionDialog.ModifyStorage); + const { openBackupKeysDialog } = useDownloadBackupKeys(t); const token = localStorageService.get(LocalStorageItem.UserToken); const newToken = localStorageService.get(LocalStorageItem.NewToken); const params = new URLSearchParams(window.location.search); @@ -90,6 +93,12 @@ const App = (props: AppProps): JSX.Element => { i18next.changeLanguage(); }, []); + useEffect(() => { + if (isAuthenticated) { + openBackupKeysDialog(); + } + }, [isAuthenticated]); + useEffect(() => { try { const realtimeService = RealtimeService.getInstance(); @@ -229,6 +238,7 @@ const App = (props: AppProps): JSX.Element => { /> {isOpen && } + {isAuthenticated && } {isFileViewerOpen && fileViewerItem && ( diff --git a/src/app/banners/BannerWrapper.tsx b/src/app/banners/BannerWrapper.tsx index 6542d8314..f3d548c06 100644 --- a/src/app/banners/BannerWrapper.tsx +++ b/src/app/banners/BannerWrapper.tsx @@ -7,6 +7,8 @@ import { useEffect, useMemo, useState } from 'react'; import { userSelectors } from 'app/store/slices/user'; import FeaturesBanner from './FeaturesBanner'; import newStorageService from 'app/drive/services/new-storage.service'; +import { ActionDialog } from 'app/contexts/dialog-manager/ActionDialogManager.context'; +import { useActionDialog } from 'app/contexts/dialog-manager/useActionDialog'; const OFFER_END_DAY = new Date('2026-04-26'); const TIMEOUT = 90000; @@ -15,6 +17,8 @@ const BannerWrapper = (): JSX.Element => { const user = useSelector((state: RootState) => state.user.user) as UserSettings; const plan = useSelector((state) => state.plan); const isNewAccount = useSelector((state: RootState) => userSelectors.hasSignedToday(state)); + const { isDialogOpen } = useActionDialog(); + const isBackupKeysDialogOpen = isDialogOpen(ActionDialog.DownloadBackupKey); const bannerManager = useMemo(() => new BannerManager(user, plan, OFFER_END_DAY), [user, plan, isNewAccount]); const [bannersToShow, setBannersToShow] = useState({ showFreeBanner: false, showSubscriptionBanner: false }); const [showDelayedBanner, setShowDelayedBanner] = useState(false); @@ -50,7 +54,7 @@ const BannerWrapper = (): JSX.Element => { return ( <> - {bannersToShow.showFreeBanner && showDelayedBanner && ( + {bannersToShow.showFreeBanner && showDelayedBanner && !isBackupKeysDialogOpen && ( onCloseBanner('showFreeBanner')} showBanner /> )} diff --git a/src/app/contexts/dialog-manager/ActionDialogManager.context.tsx b/src/app/contexts/dialog-manager/ActionDialogManager.context.tsx index 49efb6292..c26675438 100644 --- a/src/app/contexts/dialog-manager/ActionDialogManager.context.tsx +++ b/src/app/contexts/dialog-manager/ActionDialogManager.context.tsx @@ -12,6 +12,7 @@ export enum ActionDialog { NameCollision = 'name-collision', ModifyStorage = 'modify-storage', CryptoPayment = 'crypto-payment', + DownloadBackupKey = 'download-backup-key', } interface ActionDialogState { diff --git a/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.test.ts b/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.test.ts new file mode 100644 index 000000000..fd2c18e15 --- /dev/null +++ b/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.test.ts @@ -0,0 +1,138 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, expect, vi, beforeEach, test } from 'vitest'; +import { FOURTEEN_DAYS, useDownloadBackupKeys } from './useDownloadBackupKeys'; +import { handleExportBackupKey } from 'utils'; +import { localStorageService } from 'services'; + +const mockOpenDialog = vi.fn(); +const mockCloseDialog = vi.fn(); +const mockIsDialogOpen = vi.fn(); + +vi.mock('app/contexts/dialog-manager/useActionDialog', () => ({ + useActionDialog: () => ({ + openDialog: mockOpenDialog, + closeDialog: mockCloseDialog, + isDialogOpen: mockIsDialogOpen, + }), +})); + +vi.mock('utils', () => ({ + handleExportBackupKey: vi.fn(), + generateCaptchaToken: vi.fn().mockResolvedValue('mocked-captcha-token'), +})); + +const translate = vi.fn((key: string) => key); + +describe('Download Backup Keys - Custom hook', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockIsDialogOpen.mockReturnValue(false); + }); + + test('When the user has never seen the dialog, then the dialog opens', () => { + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: null }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.openBackupKeysDialog()); + + expect(mockOpenDialog).toHaveBeenCalledOnce(); + }); + + test('When the user already saved the backup key, then the dialog does not open', () => { + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: true, remindMeLater: null }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.openBackupKeysDialog()); + + expect(mockOpenDialog).not.toHaveBeenCalled(); + }); + + describe('Remind me later', () => { + test('When the user clicked remind me later less than 14 days ago, then the dialog does not open', () => { + const recentDate = new Date(Date.now() - FOURTEEN_DAYS + 1000).toISOString(); + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: recentDate }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.openBackupKeysDialog()); + + expect(mockOpenDialog).not.toHaveBeenCalled(); + }); + + test('When the user clicked remind me later more than 14 days ago, then the dialog opens again', () => { + const expiredDate = new Date(Date.now() - FOURTEEN_DAYS - 1000).toISOString(); + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: expiredDate }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.openBackupKeysDialog()); + + expect(mockOpenDialog).toHaveBeenCalledOnce(); + }); + + test('When the user clicks remind me later, then the current date is saved and the dialog closes', () => { + const backupRemindLaterSpy = vi.spyOn(localStorageService, 'setBackupKeysRemindLater'); + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: null }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.onRemindMeLaterButtonClicked()); + + expect(backupRemindLaterSpy).toHaveBeenCalledOnce(); + expect(mockCloseDialog).toHaveBeenCalledOnce(); + }); + }); + + describe('Download backup key saved', () => { + test('When the user confirms the backup key is saved, then the saved flag is persisted and the dialog closes', () => { + const backupSavedSpy = vi.spyOn(localStorageService, 'setBackupKeysAcknowledged'); + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: null }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.onBackupSavedButtonClicked()); + + expect(backupSavedSpy).toHaveBeenCalledOnce(); + expect(mockCloseDialog).toHaveBeenCalledOnce(); + }); + + test('When the user confirms the backup key is saved and had a remind me later, then the remind me later entry is removed', () => { + const removeItem = vi.spyOn(localStorageService, 'removeItem'); + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ + saved: false, + remindMeLater: new Date().toISOString(), + }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.onBackupSavedButtonClicked()); + + expect(removeItem).toHaveBeenCalledOnce(); + }); + + test('When the user confirms the backup key is saved without a previous remind me later, then the remind me later entry is not removed', () => { + const removeItem = vi.spyOn(localStorageService, 'removeItem'); + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: null }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.onBackupSavedButtonClicked()); + + expect(removeItem).not.toHaveBeenCalled(); + }); + }); + + describe('Downloading keys', () => { + test('When the user clicks the download button, then the key download starts', () => { + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: null }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.onDownloadBackupKeysButtonClicked()); + + expect(handleExportBackupKey).toHaveBeenCalledWith(translate); + }); + + test('When the user clicks the download button, then the downloaded state is marked as true', () => { + vi.spyOn(localStorageService, 'getBackupKeys').mockReturnValue({ saved: false, remindMeLater: null }); + + const { result } = renderHook(() => useDownloadBackupKeys(translate)); + act(() => result.current.onDownloadBackupKeysButtonClicked()); + + expect(result.current.isDownloadedKeys).toBe(true); + }); + }); +}); diff --git a/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts b/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts new file mode 100644 index 000000000..11bee1330 --- /dev/null +++ b/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts @@ -0,0 +1,52 @@ +import { ActionDialog } from 'app/contexts/dialog-manager/ActionDialogManager.context'; +import { useActionDialog } from 'app/contexts/dialog-manager/useActionDialog'; +import { Translate } from 'app/i18n/types'; +import { useState } from 'react'; +import { localStorageService, STORAGE_KEYS } from 'services'; +import { handleExportBackupKey } from 'utils'; + +export const FOURTEEN_DAYS = 14 * 24 * 60 * 60 * 1000; + +export const useDownloadBackupKeys = (translate: Translate) => { + const [isDownloadedKeys, setIsDownloadedKeys] = useState(false); + const { isDialogOpen, openDialog, closeDialog } = useActionDialog(); + const backupKeysLocalStorage = localStorageService.getBackupKeys(); + const isBackupKeysDialogOpen = isDialogOpen(ActionDialog.DownloadBackupKey); + + const openBackupKeysDialog = () => { + const remindMeLater = backupKeysLocalStorage.remindMeLater; + const remindTimestamp = remindMeLater ? new Date(remindMeLater).getTime() : 0; + + const hasExpired = !remindTimestamp || Date.now() - remindTimestamp >= FOURTEEN_DAYS; + + if (backupKeysLocalStorage.saved || !hasExpired) return; + + openDialog(ActionDialog.DownloadBackupKey); + }; + + const onRemindMeLaterButtonClicked = () => { + const now = new Date(); + localStorageService.setBackupKeysRemindLater(now.toISOString()); + closeDialog(ActionDialog.DownloadBackupKey, { closeAllDialogsFirst: true }); + }; + + const onBackupSavedButtonClicked = () => { + localStorageService.setBackupKeysAcknowledged(); + if (backupKeysLocalStorage?.remindMeLater) localStorageService.removeItem(STORAGE_KEYS.BACKUP_KEY.REMIND_LATER_AT); + closeDialog(ActionDialog.DownloadBackupKey, { closeAllDialogsFirst: true }); + }; + + const onDownloadBackupKeysButtonClicked = () => { + handleExportBackupKey(translate); + setIsDownloadedKeys(true); + }; + + return { + isDownloadedKeys, + isBackupKeysDialogOpen, + openBackupKeysDialog, + onRemindMeLaterButtonClicked, + onBackupSavedButtonClicked, + onDownloadBackupKeysButtonClicked, + }; +}; diff --git a/src/app/drive/components/DownloadBackupKeysDialog/index.tsx b/src/app/drive/components/DownloadBackupKeysDialog/index.tsx new file mode 100644 index 000000000..a80fd75bd --- /dev/null +++ b/src/app/drive/components/DownloadBackupKeysDialog/index.tsx @@ -0,0 +1,63 @@ +import { BaseDialog, Button } from '@internxt/ui'; +import { DownloadSimple, Info } from '@phosphor-icons/react'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import { useDownloadBackupKeys } from './hooks/useDownloadBackupKeys'; + +export const DownloadBackupKeysDialog = () => { + const { translate } = useTranslationContext(); + const { + isBackupKeysDialogOpen, + isDownloadedKeys, + onDownloadBackupKeysButtonClicked, + onBackupSavedButtonClicked, + onRemindMeLaterButtonClicked, + } = useDownloadBackupKeys(translate); + + const onCloseButtonClicked = () => { + if (isDownloadedKeys) onBackupSavedButtonClicked(); + else onRemindMeLaterButtonClicked(); + }; + + return ( + +
+

{translate('modals.downloadBackupsKey.description')}

+ + {/* Download backup key */} +
+ +
+ + {/* Info */} +
+ +
+

{translate('modals.downloadBackupsKey.info.text')}

+

{translate('modals.downloadBackupsKey.info.path')}

+
+
+
+ + +
+
+
+ ); +}; diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 0fe2edfda..7409565d5 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -856,6 +856,15 @@ "itemsSelected": "items selected" }, "modals": { + "downloadBackupsKey": { + "title": "Remember to download your backup key", + "description": "Your backup key is needed to recover your encrypted data. Internxt doesn’t store it — only you have it.", + "info": { + "text": "You can find your backup key anytime in ", + "path": "Settings → Account → Security" + }, + "download": "Download backup keys" + }, "renameItemDialog": { "title": "Rename", "label": "Name" @@ -1578,7 +1587,9 @@ "continue": "Continue", "keepCurrent": "Keep current", "logOut": "Log out", - "locked": "Locked" + "locked": "Locked", + "remindMeLater": "Remind me later", + "backupKeySaved": "I've saved my key" }, "toastNotification": { "textCopied": "Text copied to clipboard" diff --git a/src/services/local-storage.service.test.ts b/src/services/local-storage.service.test.ts index 122fab681..9d7bdf829 100644 --- a/src/services/local-storage.service.test.ts +++ b/src/services/local-storage.service.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeEach, describe, expect, it, test, vi } from 'vitest'; import localStorageService from './local-storage.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { LocalStorageItem, Workspace } from 'app/core/types'; @@ -331,6 +331,68 @@ describe('Testing the local storage service', () => { }); }); + describe('Backup key acknowledgment', () => { + const userId = mockUserSettings.uuid; + const remindLaterKey = `backup_key_remind_later_at_${userId}`; + const acknowledgedKey = `backup_key_acknowledged_at_${userId}`; + + describe('Get backup keys', () => { + test('When neither saved nor remind me later are set, then both are returned as empty', () => { + const { saved, remindMeLater } = localStorageService.getBackupKeys(); + + expect(saved).toBe(false); + expect(remindMeLater).toBeNull(); + }); + + test('When the user has acknowledged the backup key, then saved is true', () => { + localStorage.setItem(acknowledgedKey, 'true'); + + const { saved } = localStorageService.getBackupKeys(); + + expect(saved).toBe(true); + }); + + test('When the user set a remind me later date, then the date is returned', () => { + const date = new Date().toISOString(); + localStorage.setItem(remindLaterKey, date); + + const { remindMeLater } = localStorageService.getBackupKeys(); + + expect(remindMeLater).toBe(date); + }); + }); + + describe('Set backup key saved', () => { + test('When the user saves the backup key, then the acknowledged flag is persisted for that user', () => { + localStorageService.setBackupKeysAcknowledged(); + + expect(localStorage.getItem(acknowledgedKey)).toBe('true'); + }); + }); + + describe('setBackupKeysRemindLater', () => { + test('When the user sets remind me later, then the date is persisted for that user', () => { + const date = new Date().toISOString(); + + localStorageService.setBackupKeysRemindLater(date); + + expect(localStorage.getItem(remindLaterKey)).toBe(date); + }); + }); + + describe('clear', () => { + test('When the user logs out, then the remind me later date is removed but the acknowledged flag is kept', () => { + const date = new Date().toISOString(); + localStorage.setItem(remindLaterKey, date); + localStorage.setItem(acknowledgedKey, 'true'); + + localStorageService.clear(); + + expect(localStorage.getItem(remindLaterKey)).toBeNull(); + expect(localStorage.getItem(acknowledgedKey)).toBe('true'); + }); + }); + }); describe('Clearing local storage', () => { it('When clear storage is requested, then removes all keys', () => { diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index 0ad330e53..3b36a9050 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -11,6 +11,37 @@ function set(key: string, value: string): void { return localStorage.setItem(key, value); } +function getBackupKeyStorageKeys() { + const user = getUser(); + const userId = user?.uuid ?? 'anonymous'; + return { + remindLaterAt: `${STORAGE_KEYS.BACKUP_KEY.REMIND_LATER_AT}_${userId}`, + acknowledgedAt: `${STORAGE_KEYS.BACKUP_KEY.ACKNOWLEDGED_AT}_${userId}`, + }; +} + +function setBackupKeysAcknowledged(): void { + const { acknowledgedAt } = getBackupKeyStorageKeys(); + localStorage.setItem(acknowledgedAt, 'true'); +} + +function setBackupKeysRemindLater(date: string): void { + const { remindLaterAt } = getBackupKeyStorageKeys(); + localStorage.setItem(remindLaterAt, date); +} + +function getBackupKeys(): { + remindMeLater: string | null; + saved: boolean; +} { + const { remindLaterAt, acknowledgedAt } = getBackupKeyStorageKeys(); + const isAcknowledged = localStorage.getItem(acknowledgedAt) === 'true'; + return { + remindMeLater: localStorage.getItem(remindLaterAt), + saved: isAcknowledged, + }; +} + function getUser(): UserSettings | null { const stringUser: string | null = localStorage.getItem(LocalStorageItem.User); @@ -51,10 +82,10 @@ function hasCompletedTutorial(id?: string): boolean { return localStorage.getItem(STORAGE_KEYS.TUTORIAL_COMPLETED_ID) === id; } - function clear(): void { localStorage.setItem('theme', 'system'); + localStorage.removeItem(getBackupKeyStorageKeys().remindLaterAt); Object.values(STORAGE_KEYS.THEMES).forEach((key) => localStorage.removeItem(key)); Object.values(LocalStorageItem).forEach((key) => localStorage.removeItem(key)); localStorage.removeItem('theme:isDark'); @@ -66,6 +97,9 @@ function clear(): void { const localStorageService = { set, get, + setBackupKeysAcknowledged, + setBackupKeysRemindLater, + getBackupKeys, getUser, getWorkspace, hasCompletedTutorial, @@ -81,6 +115,12 @@ export default localStorageService; export interface LocalStorageService { set: (key: string, value: string) => void; get: (key: string) => string | null; + setBackupKeysAcknowledged: () => void; + setBackupKeysRemindLater: (date: string) => void; + getBackupKeys: () => { + remindMeLater: string | null; + saved: boolean; + }; getUser: () => UserSettings | null; getWorkspace: () => string; removeItem: (key: string) => void; diff --git a/src/services/storage-keys.ts b/src/services/storage-keys.ts index 2e7a08392..59303deec 100644 --- a/src/services/storage-keys.ts +++ b/src/services/storage-keys.ts @@ -6,6 +6,10 @@ export const STORAGE_KEYS = { FILE_ACCESS_TOKEN: 'fileAccessToken', GCLID: 'gclid', HAS_SEEN_TRASH_DISPOSAL_DIALOG: 'hasSeenTrashDisposalDialog', + BACKUP_KEY: { + REMIND_LATER_AT: 'backup_key_remind_later_at', + ACKNOWLEDGED_AT: 'backup_key_acknowledged_at', + }, THEMES: { MANAGEMENTID_THEME_AVAILABLE_LOCAL_STORAGE_KEY: 'managementid_theme_enabled', ID_MANAGEMENT_THEME_AVAILABLE_LOCAL_STORAGE_KEY: 'id_management_theme_enabled', diff --git a/src/utils/backupKeyUtils.ts b/src/utils/backupKeyUtils.ts index 49a2c6cbf..bef74b992 100644 --- a/src/utils/backupKeyUtils.ts +++ b/src/utils/backupKeyUtils.ts @@ -21,9 +21,12 @@ import { LocalStorageItem } from 'app/core/types'; export interface BackupData { mnemonic: string; privateKey: string; + publicKey?: string; keys: { ecc: string; + eccPublicKey?: string; kyber: string; + kyberPublicKey?: string; }; } @@ -46,9 +49,12 @@ export function handleExportBackupKey(translate) { const backupData: BackupData = { mnemonic, privateKey: user.privateKey, + publicKey: user.publicKey, keys: { ecc: user.keys?.ecc?.privateKey || user.privateKey, + eccPublicKey: user.keys?.ecc?.publicKey || user.publicKey, kyber: user.keys?.kyber?.privateKey || '', + kyberPublicKey: user.keys?.kyber?.publicKey || '', }, }; @@ -81,9 +87,12 @@ export const detectBackupKeyFormat = ( const backupData: BackupData = { mnemonic: parsedData.mnemonic, privateKey: parsedData.privateKey, + publicKey: parsedData.publicKey, keys: { ecc: parsedData.keys.ecc, + eccPublicKey: parsedData.keys.eccPublicKey, kyber: parsedData.keys.kyber, + kyberPublicKey: parsedData.keys.kyberPublicKey, }, }; return { From 17934beb1fcd124ed02ec062b7a3a886fdebec2f Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 28 Apr 2026 16:43:44 +0200 Subject: [PATCH 2/5] feat: add translations --- .../hooks/useDownloadBackupKeys.ts | 2 ++ .../components/DownloadBackupKeysDialog/index.tsx | 10 +++++----- src/app/i18n/locales/de.json | 14 +++++++++++++- src/app/i18n/locales/en.json | 5 +++-- src/app/i18n/locales/es.json | 14 +++++++++++++- src/app/i18n/locales/fr.json | 14 +++++++++++++- src/app/i18n/locales/it.json | 14 +++++++++++++- src/app/i18n/locales/ru.json | 14 +++++++++++++- src/app/i18n/locales/tw.json | 14 +++++++++++++- src/app/i18n/locales/zh.json | 14 +++++++++++++- 10 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts b/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts index 11bee1330..d86be6f10 100644 --- a/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts +++ b/src/app/drive/components/DownloadBackupKeysDialog/hooks/useDownloadBackupKeys.ts @@ -1,6 +1,7 @@ import { ActionDialog } from 'app/contexts/dialog-manager/ActionDialogManager.context'; import { useActionDialog } from 'app/contexts/dialog-manager/useActionDialog'; import { Translate } from 'app/i18n/types'; +import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import { useState } from 'react'; import { localStorageService, STORAGE_KEYS } from 'services'; import { handleExportBackupKey } from 'utils'; @@ -33,6 +34,7 @@ export const useDownloadBackupKeys = (translate: Translate) => { const onBackupSavedButtonClicked = () => { localStorageService.setBackupKeysAcknowledged(); if (backupKeysLocalStorage?.remindMeLater) localStorageService.removeItem(STORAGE_KEYS.BACKUP_KEY.REMIND_LATER_AT); + notificationsService.show({ text: translate('modals.downloadBackupKeys.success'), type: ToastType.Success }); closeDialog(ActionDialog.DownloadBackupKey, { closeAllDialogsFirst: true }); }; diff --git a/src/app/drive/components/DownloadBackupKeysDialog/index.tsx b/src/app/drive/components/DownloadBackupKeysDialog/index.tsx index a80fd75bd..10bb5b635 100644 --- a/src/app/drive/components/DownloadBackupKeysDialog/index.tsx +++ b/src/app/drive/components/DownloadBackupKeysDialog/index.tsx @@ -21,7 +21,7 @@ export const DownloadBackupKeysDialog = () => { return ( { dataTest="backup-keys-dialog" >
-

{translate('modals.downloadBackupsKey.description')}

+

{translate('modals.downloadBackupKeys.description')}

{/* Download backup key */}
@@ -45,8 +45,8 @@ export const DownloadBackupKeysDialog = () => {
-

{translate('modals.downloadBackupsKey.info.text')}

-

{translate('modals.downloadBackupsKey.info.path')}

+

{translate('modals.downloadBackupKeys.info.text')}

+

{translate('modals.downloadBackupKeys.info.path')}

diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 621c5fb84..c8ae632b2 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -759,6 +759,16 @@ "itemsSelected": "Elemente ausgewählt" }, "modals": { + "downloadBackupKeys": { + "title": "Denk daran, deinen Backup-Schlüssel herunterzuladen", + "description": "Dein Backup-Schlüssel wird benötigt, um deine verschlüsselten Daten wiederherzustellen. Internxt speichert ihn nicht — nur du hast ihn.", + "info": { + "text": "Du kannst deinen Backup-Schlüssel jederzeit unter ", + "path": "Einstellungen → Konto → Sicherheit" + }, + "download": "Backup-Schlüssel herunterladen", + "success": "Du hast deinen Backup-Schlüssel. Bewahre ihn an einem sicheren Ort auf." + }, "renameItemDialog": { "title": "Umbenennen", "label": "Name" @@ -1496,7 +1506,9 @@ "continue": "Fortsetzen", "keepCurrent": "Aktuell behalten", "logOut": "Abmelden", - "locked": "Gesperrt" + "locked": "Gesperrt", + "remindMeLater": "Später erinnern", + "backupKeySaved": "Ich habe meinen Schlüssel gespeichert" }, "toastNotification": { "textCopied": "Text in die Zwischenablage kopiert" diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 7409565d5..2b1a9b04d 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -856,14 +856,15 @@ "itemsSelected": "items selected" }, "modals": { - "downloadBackupsKey": { + "downloadBackupKeys": { "title": "Remember to download your backup key", "description": "Your backup key is needed to recover your encrypted data. Internxt doesn’t store it — only you have it.", "info": { "text": "You can find your backup key anytime in ", "path": "Settings → Account → Security" }, - "download": "Download backup keys" + "download": "Download backup keys", + "success": "You have your backup key. Make sure to store it somewhere safe." }, "renameItemDialog": { "title": "Rename", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index f2252d813..89078ac02 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -838,6 +838,16 @@ "itemsSelected": "archivos seleccionados" }, "modals": { + "downloadBackupKeys": { + "title": "Recuerda descargar tu clave de copia de seguridad", + "description": "Tu clave de copia de seguridad es necesaria para recuperar tus datos cifrados. Internxt no la almacena — solo tú la tienes.", + "info": { + "text": "Puedes encontrar tu clave de copia de seguridad en cualquier momento en ", + "path": "Ajustes → Cuenta → Seguridad" + }, + "download": "Descargar claves de copia de seguridad", + "success": "Tienes tu clave de copia de seguridad. Asegúrate de guardarla en un lugar seguro." + }, "renameItemDialog": { "title": "Renombrar", "label": "Nombre" @@ -1556,7 +1566,9 @@ "continue": "Continuar", "keepCurrent": "Mantener actual", "logOut": "Cerrar sesión", - "locked": "Bloqueado" + "locked": "Bloqueado", + "remindMeLater": "Recordármelo más tarde", + "backupKeySaved": "Ya he guardado mi clave" }, "toastNotification": { "textCopied": "Texto copiado al portapapeles" diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index edd4d9bfa..a95a6d186 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -786,6 +786,16 @@ "itemsSelected": "Éléments sélectionnés" }, "modals": { + "downloadBackupKeys": { + "title": "N'oubliez pas de télécharger votre clé de sauvegarde", + "description": "Votre clé de sauvegarde est nécessaire pour récupérer vos données chiffrées. Internxt ne la stocke pas — seul vous la possédez.", + "info": { + "text": "Vous pouvez retrouver votre clé de sauvegarde à tout moment dans ", + "path": "Paramètres → Compte → Sécurité" + }, + "download": "Télécharger les clés de sauvegarde", + "success": "Vous avez votre clé de sauvegarde. Assurez-vous de la conserver dans un endroit sûr." + }, "renameItemDialog": { "title": "Renommer", "label": "Nom" @@ -1502,7 +1512,9 @@ "continue": "Continuer", "keepCurrent": "Garder actuel", "logOut": "Se déconnecter", - "locked": "Verrouillé" + "locked": "Verrouillé", + "remindMeLater": "Me le rappeler plus tard", + "backupKeySaved": "J'ai sauvegardé ma clé" }, "toastNotification": { "textCopied": "Texte copié dans le presse-papiers" diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 4207b9802..290b20148 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -892,6 +892,16 @@ "itemsSelected": "elementi selezionati" }, "modals": { + "downloadBackupKeys": { + "title": "Ricordati di scaricare la tua chiave di backup", + "description": "La tua chiave di backup è necessaria per recuperare i tuoi dati crittografati. Internxt non la conserva — solo tu ce l'hai.", + "info": { + "text": "Puoi trovare la tua chiave di backup in qualsiasi momento in ", + "path": "Impostazioni → Account → Sicurezza" + }, + "download": "Scarica le chiavi di backup", + "success": "Hai la tua chiave di backup. Assicurati di conservarla in un posto sicuro." + }, "renameItemDialog": { "title": "Rinominare", "label": "Nome" @@ -1609,7 +1619,9 @@ "continue": "Continua", "keepCurrent": "Mantieni attuale", "logOut": "Disconnettersi", - "locked": "Bloccato" + "locked": "Bloccato", + "remindMeLater": "Ricordamelo più tardi", + "backupKeySaved": "Ho salvato la mia chiave" }, "toastNotification": { "textCopied": "Testo copiato negli appunti" diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index f5276db56..77febece9 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -799,6 +799,16 @@ "itemsSelected": "выбранные файлы" }, "modals": { + "downloadBackupKeys": { + "title": "Не забудьте скачать ключ резервного копирования", + "description": "Ключ резервного копирования необходим для восстановления зашифрованных данных. Internxt его не хранит — он есть только у вас.", + "info": { + "text": "Вы можете найти ключ резервного копирования в любое время в ", + "path": "Настройки → Аккаунт → Безопасность" + }, + "download": "Скачать ключи резервного копирования", + "success": "У вас есть ключ резервного копирования. Обязательно храните его в надёжном месте." + }, "renameItemDialog": { "title": "Переименовать", "label": "Имя." @@ -1517,7 +1527,9 @@ "continue": "Продолжить", "keepCurrent": "Сохранить текущее", "logOut": "Выйти", - "locked": "Заблокировано" + "locked": "Заблокировано", + "remindMeLater": "Напомнить позже", + "backupKeySaved": "Я сохранил свой ключ" }, "toastNotification": { "textCopied": "Текст скопирован в буфер обмена" diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index 2b076749e..f8fb6c589 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -786,6 +786,16 @@ "itemsSelected": "已選擇項目" }, "modals": { + "downloadBackupKeys": { + "title": "請記得下載您的備份金鑰", + "description": "您的備份金鑰是恢復加密資料的必要憑證。Internxt 不會儲存它——只有您自己擁有它。", + "info": { + "text": "您可以隨時在以下位置找到您的備份金鑰:", + "path": "設定 → 帳戶 → 安全性" + }, + "download": "下載備份金鑰", + "success": "您已擁有備份金鑰。請務必將其儲存在安全的地方。" + }, "renameItemDialog": { "title": "重命名", "label": "名稱" @@ -1508,7 +1518,9 @@ "continue": "繼續", "keepCurrent": "保持目前", "logOut": "登出", - "locked": "已鎖定" + "locked": "已鎖定", + "remindMeLater": "稍後提醒我", + "backupKeySaved": "我已儲存我的金鑰" }, "toastNotification": { "textCopied": "文字已複製到剪貼簿" diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index 242cfdcdb..975f1b211 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -822,6 +822,16 @@ "itemsSelected": "个文件 已选择 " }, "modals": { + "downloadBackupKeys": { + "title": "请记得下载您的备份密钥", + "description": "您的备份密钥是恢复加密数据的必要凭证。Internxt 不会存储它——只有您自己拥有它。", + "info": { + "text": "您可以随时在以下位置找到您的备份密钥:", + "path": "设置 → 账户 → 安全" + }, + "download": "下载备份密钥", + "success": "您已拥有备份密钥。请务必将其存储在安全的地方。" + }, "renameItemDialog": { "title": "重命名", "label": "名称" @@ -1543,7 +1553,9 @@ "continue": "继续", "keepCurrent": "保持当前", "logOut": "登出", - "locked": "已锁定" + "locked": "已锁定", + "remindMeLater": "稍后提醒我", + "backupKeySaved": "我已保存我的密钥" }, "toastNotification": { "textCopied": "文本已复制到剪贴板" From 1a1652ab42efe225c54c997dcebfbcd5177b3c66 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Wed, 29 Apr 2026 09:58:09 +0200 Subject: [PATCH 3/5] fix: dialog styles --- .../DownloadBackupKeysDialog/index.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/app/drive/components/DownloadBackupKeysDialog/index.tsx b/src/app/drive/components/DownloadBackupKeysDialog/index.tsx index 10bb5b635..988eb9831 100644 --- a/src/app/drive/components/DownloadBackupKeysDialog/index.tsx +++ b/src/app/drive/components/DownloadBackupKeysDialog/index.tsx @@ -24,31 +24,32 @@ export const DownloadBackupKeysDialog = () => { title={translate('modals.downloadBackupKeys.title')} dialogRounded={true} panelClasses="w-screen max-w-lg" - titleClasses="font-medium text-left" - closeClass="shrink-0 flex items-center justify-center h-10 w-10 hover:bg-black/2 rounded-md focus:bg-black/5" + titleClasses="font-medium pt-1.5 text-left" + closeClass="self-center shrink-0 flex items-center justify-center h-10 w-10 hover:bg-black/2 rounded-md focus:bg-black/5" onClose={onCloseButtonClicked} weightIcon="light" dataTest="backup-keys-dialog" > -
+

{translate('modals.downloadBackupKeys.description')}

- {/* Download backup key */} + {/* Download backup key + info */}
- -
- - {/* Info */} -
- -
-

{translate('modals.downloadBackupKeys.info.text')}

-

{translate('modals.downloadBackupKeys.info.path')}

+
+ +
+ +
+

{translate('modals.downloadBackupKeys.info.text')}

+

{translate('modals.downloadBackupKeys.info.path')}

+
+
+