diff --git a/frontends/web/src/app.tsx b/frontends/web/src/app.tsx index 283286f890..51bd86849e 100644 --- a/frontends/web/src/app.tsx +++ b/frontends/web/src/app.tsx @@ -84,6 +84,7 @@ export const App = () => { deviceIDs.length === 0 && ( currentURL.startsWith('/settings/device-settings/') + || currentURL.startsWith('/settings/backup-functionality/') || currentURL.startsWith('/manage-backups/') ) ) { diff --git a/frontends/web/src/routes/device/bitbox01/backups.tsx b/frontends/web/src/routes/device/bitbox01/backups.tsx index da9c6e1ea3..b954a3563e 100644 --- a/frontends/web/src/routes/device/bitbox01/backups.tsx +++ b/frontends/web/src/routes/device/bitbox01/backups.tsx @@ -17,6 +17,7 @@ import { Restore } from './restore'; type BackupsProps = { deviceID: string; showCreate?: boolean; + showCheck?: boolean; showRestore?: boolean; requireConfirmation?: boolean; onRestore?: () => void; @@ -89,6 +90,7 @@ class Backups extends Component { t, children, showCreate = false, + showCheck = showCreate, showRestore = true, deviceID, requireConfirmation = true, @@ -146,7 +148,7 @@ class Backups extends Component { ) } { - showCreate && ( + showCheck && ( diff --git a/frontends/web/src/routes/device/bitbox02/backups.tsx b/frontends/web/src/routes/device/bitbox02/backups.tsx index 5c5daf1502..46d1e64529 100644 --- a/frontends/web/src/routes/device/bitbox02/backups.tsx +++ b/frontends/web/src/routes/device/bitbox02/backups.tsx @@ -20,6 +20,10 @@ type TProps = { deviceID: string; showRestore?: boolean; showCreate?: boolean; + showCheck?: boolean; + autoStartCreate?: boolean; + autoStartCheck?: boolean; + autoStartID?: string; showRadio: boolean; onSelectBackup?: (backup: Backup) => void; onRestoreBackup?: (success: boolean) => void; @@ -30,6 +34,10 @@ export const BackupsV2 = ({ deviceID, showRestore, showCreate, + showCheck, + autoStartCreate, + autoStartCheck, + autoStartID, showRadio, onSelectBackup, onRestoreBackup, @@ -144,15 +152,23 @@ export const BackupsV2 = ({ } { showCreate && ( - + ) } { - showCreate && ( + showCheck && ( ) } diff --git a/frontends/web/src/routes/device/bitbox02/bitbox02.tsx b/frontends/web/src/routes/device/bitbox02/bitbox02.tsx index ac1aa3ed09..e07c1b2309 100644 --- a/frontends/web/src/routes/device/bitbox02/bitbox02.tsx +++ b/frontends/web/src/routes/device/bitbox02/bitbox02.tsx @@ -3,7 +3,7 @@ import { getStatus, statusChanged } from '@/api/bitbox02'; import type { TDevices } from '@/api/devices'; import { useSync } from '@/hooks/api'; -import { BB02Settings } from '@/routes/settings/bb02-settings'; +import { BB02BackupSettings, BB02Settings } from '@/routes/settings/bb02-settings'; type TProps = { deviceID: string; @@ -21,3 +21,15 @@ export const BitBox02 = ({ deviceID, devices, hasAccounts }: TProps) => { } return ; }; + +export const BitBox02Backup = ({ deviceID, devices, hasAccounts }: TProps) => { + const isBitBox02 = devices[deviceID] === 'bitbox02'; + const status = useSync( + isBitBox02 ? () => getStatus(deviceID) : async () => 'connected' as const, + isBitBox02 ? cb => statusChanged(deviceID, cb) : () => () => undefined + ); + if (!isBitBox02 || status !== 'initialized') { + return null; + } + return ; +}; diff --git a/frontends/web/src/routes/device/bitbox02/checkbackup.tsx b/frontends/web/src/routes/device/bitbox02/checkbackup.tsx index f121367234..721ad81ade 100644 --- a/frontends/web/src/routes/device/bitbox02/checkbackup.tsx +++ b/frontends/web/src/routes/device/bitbox02/checkbackup.tsx @@ -1,37 +1,53 @@ // SPDX-License-Identifier: Apache-2.0 -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import * as bitbox02API from '@/api/bitbox02'; import { BackupsListItem } from '@/routes/device/components/backup'; -import { Backup } from '@/api/backup'; -import { alertUser } from '@/components/alert/Alert'; +import { Backup, getBackupList } from '@/api/backup'; import { Dialog, DialogButtons } from '@/components/dialog/dialog'; import { Button } from '@/components/forms'; import { useTranslation } from 'react-i18next'; +const startedCheckBackupFlows = new Set(); + type TProps = { deviceID: string; backups: Backup[]; disabled: boolean; + autoStart?: boolean; + autoStartID?: string; + showButton?: boolean; }; -export const Check = ({ deviceID, backups, disabled }: TProps) => { +export const Check = ({ + deviceID, + backups, + disabled, + autoStart = false, + autoStartID, + showButton = true, +}: TProps) => { const [activeDialog, setActiveDialog] = useState(false); const [message, setMessage] = useState(''); const [foundBackup, setFoundBackup] = useState(); const [userVerified, setUserVerified] = useState(false); + const checkBackupRef = useRef<() => Promise>(); const { t } = useTranslation(); const checkBackup = async () => { setMessage(''); + setFoundBackup(undefined); + setUserVerified(false); try { const result = await bitbox02API.checkBackup(deviceID, true); if (result.success) { const { backupID } = result; - const foundBackup = backups.find((backup: Backup) => backup.id === backupID); + let foundBackup = backups.find((backup: Backup) => backup.id === backupID); if (!foundBackup) { - alertUser(t('unknownError', { errorMessage: 'Not found' })); - return; + const backupListResult = await getBackupList(deviceID); + if (backupListResult.success) { + foundBackup = backupListResult.backups.find((backup: Backup) => backup.id === backupID); + } } setActiveDialog(true); setFoundBackup(foundBackup); @@ -54,15 +70,30 @@ export const Check = ({ deviceID, backups, disabled }: TProps) => { console.error(error); } }; + checkBackupRef.current = checkBackup; + + useEffect(() => { + if (!autoStart || disabled) { + return; + } + const id = autoStartID || `check-${deviceID}`; + if (startedCheckBackupFlows.has(id)) { + return; + } + startedCheckBackupFlows.add(id); + void checkBackupRef.current?.(); + }, [autoStart, disabled, autoStartID, deviceID]); return ( <> - + {showButton && ( + + )} diff --git a/frontends/web/src/routes/device/bitbox02/createbackup.tsx b/frontends/web/src/routes/device/bitbox02/createbackup.tsx index 092e2e8a2e..7a90e7a6a5 100644 --- a/frontends/web/src/routes/device/bitbox02/createbackup.tsx +++ b/frontends/web/src/routes/device/bitbox02/createbackup.tsx @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { checkBackup, createBackup as createBackupAPI } from '@/api/bitbox02'; import { alertUser } from '@/components/alert/Alert'; import { confirmation } from '@/components/confirm/Confirm'; @@ -8,13 +8,24 @@ import { Button } from '@/components/forms'; import { WaitDialog } from '@/components/wait-dialog/wait-dialog'; import { useTranslation } from 'react-i18next'; +const startedCreateBackupFlows = new Set(); + type TProps = { deviceID: string; + autoStart?: boolean; + autoStartID?: string; + showButton?: boolean; }; -export const Create = ({ deviceID }: TProps) => { +export const Create = ({ + deviceID, + autoStart = false, + autoStartID, + showButton = true, +}: TProps) => { const [creatingBackup, setCreatingBackup] = useState(false); const [disabled, setDisabled] = useState(false); + const maybeCreateBackupRef = useRef<() => Promise>(); const { t } = useTranslation(); const createBackup = () => { @@ -49,15 +60,30 @@ export const Create = ({ deviceID }: TProps) => { console.error(error); } }; + maybeCreateBackupRef.current = maybeCreateBackup; + + useEffect(() => { + if (!autoStart) { + return; + } + const id = autoStartID || `create-${deviceID}`; + if (startedCreateBackupFlows.has(id)) { + return; + } + startedCreateBackupFlows.add(id); + void maybeCreateBackupRef.current?.(); + }, [autoStart, autoStartID, deviceID]); return ( <> - + {showButton && ( + + )} { creatingBackup && ( {t('bitbox02Interact.followInstructions')} diff --git a/frontends/web/src/routes/device/manage-backups/manage-backups.tsx b/frontends/web/src/routes/device/manage-backups/manage-backups.tsx index d1a2403415..7643772ad7 100644 --- a/frontends/web/src/routes/device/manage-backups/manage-backups.tsx +++ b/frontends/web/src/routes/device/manage-backups/manage-backups.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; import { TDevices } from '@/api/devices'; import { SubTitle } from '@/components/title'; import { BackButton } from '@/components/backbutton/backbutton'; @@ -9,18 +10,48 @@ import { Entry } from '@/components/guide/entry'; import { Header } from '@/components/layout'; import { Backups } from '@/routes/device/bitbox01/backups'; import { BackupsV2 } from '@/routes/device/bitbox02/backups'; +import { Create as CreateBackupV2 } from '@/routes/device/bitbox02/createbackup'; +import { Check as CheckBackupV2 } from '@/routes/device/bitbox02/checkbackup'; import { SDCardCheck } from '@/routes/device/bitbox02/sdcardcheck'; +import { HorizontallyCenteredSpinner } from '@/components/spinner/SpinnerAnimation'; type TProps = { deviceID: string | null; devices: TDevices; }; +type TBackupMode = 'create' | 'check' | 'list'; + +const isBackupMode = (mode: string | null): mode is TBackupMode => { + return mode === 'create' || mode === 'check' || mode === 'list'; +}; + +const getBackupMode = (search: string): TBackupMode | undefined => { + const mode = new URLSearchParams(search).get('mode'); + return isBackupMode(mode) ? mode : undefined; +}; + +const getTitle = (mode: TBackupMode | undefined, t: (input: string) => string) => { + switch (mode) { + case 'create': + return t('backup.create.title'); + case 'check': + return t('backup.check.title'); + case 'list': + return t('backup.list'); + default: + return t('backup.title'); + } +}; + export const ManageBackups = ({ deviceID, devices, }: TProps) => { const { t } = useTranslation(); + const location = useLocation(); + const mode = getBackupMode(location.search); + const autoStartID = mode ? `${location.key}-${mode}` : undefined; if (!deviceID || !devices[deviceID]) { return null; @@ -31,12 +62,14 @@ export const ManageBackups = ({
{t('backup.title')}} + title={

{getTitle(mode, t)}

} />
@@ -52,11 +85,17 @@ export const ManageBackups = ({ const BackupsList = ({ deviceID, devices, -}: TProps) => { + mode, + autoStartID, +}: TProps & { mode?: TBackupMode; autoStartID?: string }) => { const { t } = useTranslation(); if (!deviceID) { return null; } + + const showCreate = mode === undefined || mode === 'create'; + const showCheck = mode === undefined || mode === 'check'; + switch (devices[deviceID]) { case 'bitbox': @@ -65,7 +104,8 @@ const BackupsList = ({ {t('backup.list')} {t('button.back')} @@ -74,11 +114,32 @@ const BackupsList = ({ ); case 'bitbox02': + if (mode === 'create') { + return ( + + + + ); + } + if (mode === 'check') { + return ( + + + + ); + } return ( @@ -92,6 +153,48 @@ const BackupsList = ({ } }; +const AutoStartCreateBackup = ({ deviceID, autoStartID }: { deviceID: string; autoStartID?: string }) => { + const { t } = useTranslation(); + return ( +
+ + +
+ + {t('button.back')} + +
+
+ ); +}; + +const AutoStartCheckBackup = ({ deviceID, autoStartID }: { deviceID: string; autoStartID?: string }) => { + const { t } = useTranslation(); + return ( +
+ + +
+ + {t('button.back')} + +
+
+ ); +}; + const ManageBackupGuide = ({ deviceID, devices, diff --git a/frontends/web/src/routes/router.tsx b/frontends/web/src/routes/router.tsx index 45e71f3b89..c55eed1383 100644 --- a/frontends/web/src/routes/router.tsx +++ b/frontends/web/src/routes/router.tsx @@ -30,6 +30,7 @@ import { General } from './settings/general'; import { MobileSettings } from './settings/mobile-settings'; import { About } from './settings/about'; import { AdvancedSettings } from './settings/advanced-settings'; +import { BitBox02Backup } from './device/bitbox02/bitbox02'; import { Bitsurance } from './bitsurance/bitsurance'; import { BitsuranceAccount } from './bitsurance/account'; import { BitsuranceWidget } from './bitsurance/widget'; @@ -197,6 +198,7 @@ export const AppRouter = ({ devices, devicesKey, accounts, activeAccounts }: TAp const PassphraseEl = ; const RecoveryWordsEl = ; const Bip85El = ; + const BitBox02BackupEl = ; const ManageBackupsEl = ( + diff --git a/frontends/web/src/routes/settings/bb02-settings.tsx b/frontends/web/src/routes/settings/bb02-settings.tsx index 81e1c9d0a5..7f95bd72fd 100644 --- a/frontends/web/src/routes/settings/bb02-settings.tsx +++ b/frontends/web/src/routes/settings/bb02-settings.tsx @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -import { useState, useEffect } from 'react'; +import { useState, useEffect, ReactNode } from 'react'; import { useLoad } from '@/hooks/api'; import { useTranslation } from 'react-i18next'; import { runningInIOS } from '@/utils/env'; @@ -8,7 +8,11 @@ import { GuideWrapper, GuidedContent, Header, Main } from '@/components/layout'; import { ViewContent, View } from '@/components/view/view'; import { WithSettingsTabs } from './components/tabs'; import { TPagePropsWithSettingsTabs } from './types'; -import { ManageBackupSetting } from './components/device-settings/manage-backup-setting'; +import { + CheckBackupSetting, + CreateBackupSetting, + ListBackupsSetting, +} from './components/device-settings/manage-backup-setting'; import { ShowRecoveryWordsSetting } from './components/device-settings/show-recovery-words-setting'; import { GoToStartupSettings } from './components/device-settings/go-to-startup-settings'; import { PassphraseSetting } from './components/device-settings/passphrase-setting'; @@ -30,6 +34,8 @@ import { MobileHeader } from './components/mobile-header'; import { ContentWrapper } from '@/components/contentwrapper/contentwrapper'; import { GlobalBanners } from '@/components/banners'; import { SubTitle } from '@/components/title'; +import { Create as CreateBackupFlow } from '@/routes/device/bitbox02/createbackup'; +import { Check as CheckBackupFlow } from '@/routes/device/bitbox02/checkbackup'; import styles from './bb02-settings.module.css'; type TProps = { @@ -37,6 +43,10 @@ type TProps = { }; type TWrapperProps = TProps & TPagePropsWithSettingsTabs; +type TLayoutProps = TPagePropsWithSettingsTabs & { + children: ReactNode; + mobileHeaderTitle: string; +}; export const StyledSkeleton = () => { return ( @@ -46,7 +56,12 @@ export const StyledSkeleton = () => { ); }; -const BB02Settings = ({ deviceID, devices, hasAccounts }: TWrapperProps) => { +const BB02SettingsLayout = ({ + devices, + hasAccounts, + children, + mobileHeaderTitle, +}: TLayoutProps) => { const { t } = useTranslation(); return (
@@ -60,7 +75,7 @@ const BB02Settings = ({ deviceID, devices, hasAccounts }: TWrapperProps) => { title={ <>

{t('sidebar.settings')}

- + }/> @@ -70,7 +85,7 @@ const BB02Settings = ({ deviceID, devices, hasAccounts }: TWrapperProps) => { hideMobileMenu hasAccounts={hasAccounts} > - + {children} @@ -81,7 +96,58 @@ const BB02Settings = ({ deviceID, devices, hasAccounts }: TWrapperProps) => { ); }; -const Content = ({ deviceID }: TProps) => { +const BackupContent = ({ deviceID }: TProps) => { + const { t } = useTranslation(); + const [activeBackupFlow, setActiveBackupFlow] = useState<'create' | 'check'>(); + const [backupFlowCounter, setBackupFlowCounter] = useState(0); + + const startFlow = (flow: 'create' | 'check') => { + setActiveBackupFlow(flow); + setBackupFlowCounter(current => current + 1); + }; + + const flowID = activeBackupFlow ? `${deviceID}-${activeBackupFlow}-${backupFlowCounter}` : undefined; + + return ( +
+ {t('deviceSettings.backups.title')} + startFlow('create')} + /> + startFlow('check')} + /> + + + { + activeBackupFlow === 'create' ? ( + + ) : null + } + { + activeBackupFlow === 'check' ? ( + + ) : null + } +
+ ); +}; + +const ManageDeviceContent = ({ deviceID }: TProps) => { const { t } = useTranslation(); const [deviceInfo, setDeviceInfo] = useState(); @@ -102,13 +168,6 @@ const Content = ({ deviceID }: TProps) => { return ( <> - {/*"Backups" section*/} -
- {t('deviceSettings.backups.title')} - - -
- {/*"Device settings" section*/}
{t('deviceSettings.deviceSettings.title')} @@ -200,4 +259,32 @@ const Content = ({ deviceID }: TProps) => { ); }; -export { BB02Settings }; +const BB02Settings = ({ deviceID, devices, hasAccounts }: TWrapperProps) => { + const { t } = useTranslation(); + + return ( + + + + ); +}; + +const BB02BackupSettings = ({ deviceID, devices, hasAccounts }: TWrapperProps) => { + const { t } = useTranslation(); + + return ( + + + + ); +}; + +export { BB02Settings, BB02BackupSettings }; diff --git a/frontends/web/src/routes/settings/components/device-settings/manage-backup-setting.tsx b/frontends/web/src/routes/settings/components/device-settings/manage-backup-setting.tsx index ea629c7f47..0464715563 100644 --- a/frontends/web/src/routes/settings/components/device-settings/manage-backup-setting.tsx +++ b/frontends/web/src/routes/settings/components/device-settings/manage-backup-setting.tsx @@ -6,19 +6,78 @@ import { SettingsItem } from '@/routes/settings/components/settingsItem/settings type TProps = { deviceID: string; + onClick?: () => void; }; -const ManageBackupSetting = ({ deviceID }: TProps) => { +type TBackupMode = 'create' | 'check' | 'list'; + +type TBackupSettingsItemProps = TProps & { + mode: TBackupMode; + settingName: string; + secondaryText: string; +}; + +const BackupSettingsItem = ({ + deviceID, + onClick, + mode, + settingName, + secondaryText, +}: TBackupSettingsItemProps) => { const navigate = useNavigate(); - const { t } = useTranslation(); + return ( navigate(`/manage-backups/${deviceID}`)} - settingName={t('backup.title')} - secondaryText={t('deviceSettings.backups.manageBackups.description')} + onClick={onClick || (() => navigate(`/manage-backups/${deviceID}?mode=${mode}`))} + settingName={settingName} + secondaryText={secondaryText} + /> + ); +}; + +const CreateBackupSetting = ({ deviceID, onClick }: TProps) => { + const { t } = useTranslation(); + return ( + ); }; +const CheckBackupSetting = ({ deviceID, onClick }: TProps) => { + const { t } = useTranslation(); + return ( + + ); +}; + +const ListBackupsSetting = ({ deviceID, onClick }: TProps) => { + const { t } = useTranslation(); + return ( + + ); +}; -export { ManageBackupSetting }; \ No newline at end of file +export { CreateBackupSetting, CheckBackupSetting, ListBackupsSetting }; diff --git a/frontends/web/src/routes/settings/components/tabs.tsx b/frontends/web/src/routes/settings/components/tabs.tsx index 0daad0926a..026622fada 100644 --- a/frontends/web/src/routes/settings/components/tabs.tsx +++ b/frontends/web/src/routes/settings/components/tabs.tsx @@ -163,17 +163,32 @@ export const Tabs = ({ devices, hideMobileMenu, hasAccounts }: TTabs) => { url="/settings/no-accounts" /> )} - {deviceIDs.length ? deviceIDs.map(id => ( - : } - key={`device-${id}`} - deviceID={id} - device={devices[id] as TPlatformName} - hideMobileMenu={hideMobileMenu} - name={t('sidebar.device')} - url={`/settings/device-settings/${id}`} - /> - )) : ( + {deviceIDs.length ? deviceIDs.flatMap(id => { + const device = devices[id] as TPlatformName; + const tabs = [ + : } + key={`device-${id}`} + deviceID={id} + device={device} + hideMobileMenu={hideMobileMenu} + name={t('sidebar.device')} + url={`/settings/device-settings/${id}`} + /> + ]; + if (device === 'bitbox02') { + tabs.push( + : } + key={`backup-functionality-${id}`} + hideMobileMenu={hideMobileMenu} + name={t('deviceSettings.backups.title')} + url={`/settings/backup-functionality/${id}`} + /> + ); + } + return tabs; + }) : ( : } key="no-device"