diff --git a/.changeset/eight-jeans-marry.md b/.changeset/eight-jeans-marry.md new file mode 100644 index 000000000000..836cfd081873 --- /dev/null +++ b/.changeset/eight-jeans-marry.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-core": patch +--- + +Export errors from the lib diff --git a/.changeset/rude-ducks-watch.md b/.changeset/rude-ducks-watch.md new file mode 100644 index 000000000000..036861b3dca1 --- /dev/null +++ b/.changeset/rude-ducks-watch.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +Implement app data backup and restore when installing, uninstalling and updating apps on the device diff --git a/.changeset/smart-hotels-knock.md b/.changeset/smart-hotels-knock.md new file mode 100644 index 000000000000..4f690c608a42 --- /dev/null +++ b/.changeset/smart-hotels-knock.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/live-common": minor +--- + +Add new installAppWithRestore and uninstallAppWithBackup to handle app data restore and backup diff --git a/.changeset/tough-coats-buy.md b/.changeset/tough-coats-buy.md new file mode 100644 index 000000000000..a4f4b5604fce --- /dev/null +++ b/.changeset/tough-coats-buy.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": minor +--- + +Implement new app data backup and restore when installing, uninstalling or updating app on the device diff --git a/apps/ledger-live-desktop/src/renderer/App.tsx b/apps/ledger-live-desktop/src/renderer/App.tsx index 7268993fbac7..6f261193bc3b 100644 --- a/apps/ledger-live-desktop/src/renderer/App.tsx +++ b/apps/ledger-live-desktop/src/renderer/App.tsx @@ -32,6 +32,7 @@ import { StorylyProvider } from "~/storyly/StorylyProvider"; import { CounterValuesStateRaw } from "@ledgerhq/live-countervalues/types"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { AppDataStorageProvider } from "~/renderer/hooks/storage-provider/useAppDataStorage"; import { allowDebugReactQuerySelector } from "./reducers/settings"; const reloadApp = (event: KeyboardEvent) => { @@ -77,30 +78,32 @@ const InnerApp = ({ initialCountervalues }: { initialCountervalues: CounterValue - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ledger-live-desktop/src/renderer/DesktopStorageProvider.ts b/apps/ledger-live-desktop/src/renderer/hooks/storage-provider/DesktopAppDataStorageProvider.ts similarity index 94% rename from apps/ledger-live-desktop/src/renderer/DesktopStorageProvider.ts rename to apps/ledger-live-desktop/src/renderer/hooks/storage-provider/DesktopAppDataStorageProvider.ts index 2393ede2515e..042c16d79c5d 100644 --- a/apps/ledger-live-desktop/src/renderer/DesktopStorageProvider.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/storage-provider/DesktopAppDataStorageProvider.ts @@ -1,5 +1,5 @@ import { - StorageProvider, + StorageProvider as AppDataStorageProvider, AppStorageType, AppStorageKey, isAppStorageType, @@ -10,7 +10,7 @@ import { * The storage provider for LLD that implements the StorageProvider interface. * This a temporary placement for the DesktopStorageProvider, it could be moved to the appropriate location if needed. */ -export class DesktopStorageProvider implements StorageProvider { +export class DesktopAppDataStorageProvider implements AppDataStorageProvider { /** * Retrieves the value associated with the specified key from the storage. * diff --git a/apps/ledger-live-desktop/src/renderer/hooks/storage-provider/useAppDataStorage.tsx b/apps/ledger-live-desktop/src/renderer/hooks/storage-provider/useAppDataStorage.tsx new file mode 100644 index 000000000000..e1a70a6df3d4 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/storage-provider/useAppDataStorage.tsx @@ -0,0 +1,19 @@ +import React, { createContext, useContext } from "react"; +import { DesktopAppDataStorageProvider } from "./DesktopAppDataStorageProvider"; + +const AppDataStorageContext = createContext(new DesktopAppDataStorageProvider()); + +export const useAppDataStorageProvider = () => { + return useContext(AppDataStorageContext); +}; + +type Props = { + children: React.ReactNode; +}; + +export function AppDataStorageProvider({ children }: Props) { + const storage = useAppDataStorageProvider(); + return ( + {children} + ); +} diff --git a/apps/ledger-live-desktop/src/renderer/screens/manager/Dashboard.tsx b/apps/ledger-live-desktop/src/renderer/screens/manager/Dashboard.tsx index 8a065f5f709b..27889dd0eac7 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/manager/Dashboard.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/manager/Dashboard.tsx @@ -2,8 +2,8 @@ import React, { useMemo, useState, useEffect, useRef, useContext } from "react"; import { useSelector } from "react-redux"; import { withDevice } from "@ledgerhq/live-common/hw/deviceAccess"; import { execWithTransport } from "@ledgerhq/live-common/device/use-cases/listAppsUseCase"; -import { App, DeviceInfo, FirmwareUpdateContext } from "@ledgerhq/types-live"; -import { AppOp, ListAppsResult } from "@ledgerhq/live-common/apps/types"; +import { DeviceInfo, FirmwareUpdateContext } from "@ledgerhq/types-live"; +import { ExecArgs, ListAppsResult } from "@ledgerhq/live-common/apps/types"; import { distribute, initState } from "@ledgerhq/live-common/apps/logic"; import { mockExecWithInstalledContext } from "@ledgerhq/live-common/apps/mock"; import { getLatestFirmwareForDeviceUseCase } from "@ledgerhq/live-common/device/use-cases/getLatestFirmwareForDeviceUseCase"; @@ -16,6 +16,8 @@ import { getCurrentDevice } from "~/renderer/reducers/devices"; import { getEnv } from "@ledgerhq/live-env"; import { useLocation } from "react-router"; import { context as drawerContext } from "~/renderer/drawers/Provider"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { useAppDataStorageProvider } from "~/renderer/hooks/storage-provider/useAppDataStorage"; type Props = { device: Device; @@ -35,6 +37,8 @@ const Dashboard = ({ onRefreshDeviceInfo, }: Props) => { const { search } = useLocation(); + const appsBackupEnabled = useFeature("enableAppsBackup"); + const storage = useAppDataStorageProvider(); const { state: drawerState } = useContext(drawerContext); const currentDevice = useSelector(getCurrentDevice); @@ -70,16 +74,27 @@ const Dashboard = ({ onReset(appsToRestore); } }, [appsToRestore, onReset, preventResetOnDeviceChange, currentDevice, drawerState.open]); + const exec = useMemo( () => getEnv("MOCK") ? mockExecWithInstalledContext(result?.installed || []) - : (appOp: AppOp, targetId: string | number, app: App) => + : ({ app, appOp, targetId }: ExecArgs) => withDevice(device.deviceId)(transport => - execWithTransport(transport)(appOp, targetId, app), + execWithTransport( + transport, + appsBackupEnabled?.enabled, + )({ + appOp, + targetId, + app, + modelId: device.modelId, + storage, + }), ), - [device, result], + [device, result, appsBackupEnabled, storage], ); + const appsStoragePercentage = useMemo(() => { if (!result) return 0; const d = distribute(initState(result)); diff --git a/apps/ledger-live-desktop/src/renderer/screens/manager/DeviceDashboard/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/manager/DeviceDashboard/index.tsx index 3606f3914856..35b5e8eb8353 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/manager/DeviceDashboard/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/manager/DeviceDashboard/index.tsx @@ -30,6 +30,7 @@ import { hasInstalledAppsSelector, lastSeenCustomImageSelector, } from "~/renderer/reducers/settings"; +import { useAppDataStorageProvider } from "~/renderer/hooks/storage-provider/useAppDataStorage"; const Container = styled.div` display: flex; @@ -85,7 +86,8 @@ const DeviceDashboard = ({ }: Props) => { const { t } = useTranslation(); const { deviceName } = result; - const [state, dispatch] = useAppsRunner(result, exec, appsToRestore); + const storage = useAppDataStorageProvider(); + const [state, dispatch] = useAppsRunner(result, exec, storage, appsToRestore); const optimisticState = useMemo(() => predictOptimisticState(state), [state]); const [appInstallDep, setAppInstallDep] = useState<{ app: App; dependencies: App[] } | undefined>( undefined, diff --git a/apps/ledger-live-mobile/src/AppProviders.tsx b/apps/ledger-live-mobile/src/AppProviders.tsx index 467110480897..c0304ba4c966 100644 --- a/apps/ledger-live-mobile/src/AppProviders.tsx +++ b/apps/ledger-live-mobile/src/AppProviders.tsx @@ -13,6 +13,7 @@ import PostOnboardingProviderWrapped from "~/logic/postOnboarding/PostOnboarding import { CounterValuesStateRaw } from "@ledgerhq/live-countervalues/types"; import { CountervaluesMarketcap } from "@ledgerhq/live-countervalues-react/index"; import { WalletSyncProvider } from "LLM/features/WalletSync/components/WalletSyncContext"; +import { AppDataStorageProvider } from "~/hooks/storageProvider/useAppDataStorage"; type AppProvidersProps = { initialCountervalues?: CounterValuesStateRaw; @@ -29,18 +30,20 @@ function AppProviders({ initialCountervalues, children }: AppProvidersProps) { - - - - - - - {children} - - - - - + + + + + + + + {children} + + + + + + diff --git a/apps/ledger-live-mobile/src/MobileStorageProvider.ts b/apps/ledger-live-mobile/src/hooks/storageProvider/MobileAppDataStorageProvider.ts similarity index 94% rename from apps/ledger-live-mobile/src/MobileStorageProvider.ts rename to apps/ledger-live-mobile/src/hooks/storageProvider/MobileAppDataStorageProvider.ts index 9920413f5d14..7e51091c8bac 100644 --- a/apps/ledger-live-mobile/src/MobileStorageProvider.ts +++ b/apps/ledger-live-mobile/src/hooks/storageProvider/MobileAppDataStorageProvider.ts @@ -1,5 +1,5 @@ import { - StorageProvider, + StorageProvider as AppDataStorageProvider, AppStorageType, AppStorageKey, isAppStorageType, @@ -11,7 +11,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; * The storage provider for LLM that implements the StorageProvider interface. * This a temporary placement, it could be moved to the appropriate location if needed. */ -export class MobileStorageProvider implements StorageProvider { +export class MobileAppDataStorageProvider implements AppDataStorageProvider { /** * Retrieves the value associated with the specified key from storage. * diff --git a/apps/ledger-live-mobile/src/hooks/storageProvider/useAppDataStorage.tsx b/apps/ledger-live-mobile/src/hooks/storageProvider/useAppDataStorage.tsx new file mode 100644 index 000000000000..113fee7f62f5 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/storageProvider/useAppDataStorage.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { MobileAppDataStorageProvider } from "./MobileAppDataStorageProvider"; + +const StorageContext = React.createContext(new MobileAppDataStorageProvider()); + +export const useAppDataStorage = () => { + return React.useContext(StorageContext); +}; + +type Props = { + children: React.ReactNode; +}; + +export function AppDataStorageProvider({ children }: Props) { + const storage = useAppDataStorage(); + return {children}; +} diff --git a/apps/ledger-live-mobile/src/screens/MyLedgerDevice/index.tsx b/apps/ledger-live-mobile/src/screens/MyLedgerDevice/index.tsx index bbfc8abe2b85..8c5ae250816d 100644 --- a/apps/ledger-live-mobile/src/screens/MyLedgerDevice/index.tsx +++ b/apps/ledger-live-mobile/src/screens/MyLedgerDevice/index.tsx @@ -29,6 +29,7 @@ import { AppWithDependents, AppsInstallUninstallWithDependenciesContextProvider, } from "./AppsInstallUninstallWithDependenciesContext"; +import { useAppDataStorage } from "~/hooks/storageProvider/useAppDataStorage"; type NavigationProps = BaseComposite< StackNavigatorProps @@ -48,7 +49,8 @@ const Manager = ({ navigation, route }: NavigationProps) => { const { deviceId, modelId } = device; const { deviceName } = result; - const [state, dispatch] = useApps(result, deviceId, appsToRestore); + const storage = useAppDataStorage(); + const [state, dispatch] = useApps(result, device, storage, appsToRestore); const reduxDispatch = useDispatch(); const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); diff --git a/apps/ledger-live-mobile/src/screens/MyLedgerDevice/shared.ts b/apps/ledger-live-mobile/src/screens/MyLedgerDevice/shared.ts index 277ca4a1b503..31bb221f724b 100644 --- a/apps/ledger-live-mobile/src/screens/MyLedgerDevice/shared.ts +++ b/apps/ledger-live-mobile/src/screens/MyLedgerDevice/shared.ts @@ -3,12 +3,31 @@ import type { Exec, ListAppsResult } from "@ledgerhq/live-common/apps/index"; import { useAppsRunner } from "@ledgerhq/live-common/apps/react"; import { withDevice } from "@ledgerhq/live-common/hw/deviceAccess"; import { execWithTransport } from "@ledgerhq/live-common/device/use-cases/listAppsUseCase"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { + AppStorageType, + StorageProvider, +} from "@ledgerhq/live-common/device/use-cases/appDataBackup/types"; +import { Device } from "@ledgerhq/live-common/hw/actions/types"; + +export function useApps( + listAppsRes: ListAppsResult, + device: Device, + storage: StorageProvider, + appsToRestore?: string[], +) { + const enableAppsBackup = useFeature("enableAppsBackup"); -export function useApps(listAppsRes: ListAppsResult, deviceId: string, appsToRestore?: string[]) { const exec: Exec = useCallback( - (...args) => withDevice(deviceId)(transport => execWithTransport(transport)(...args)), - [deviceId], + args => + withDevice(device.deviceId)(transport => + execWithTransport( + transport, + enableAppsBackup?.enabled, + )({ ...args, storage, modelId: device.modelId }), + ), + [device, enableAppsBackup, storage], ); - return useAppsRunner(listAppsRes, exec, appsToRestore); + return useAppsRunner(listAppsRes, exec, storage, appsToRestore); } diff --git a/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.test.ts b/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.test.ts index 647bd551f91c..11f2d164be52 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.test.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.test.ts @@ -23,7 +23,7 @@ describe("backupAppStorage", () => { it("should call the send function with correct parameters", async () => { await backupAppStorage(transport); - expect(transport.send).toHaveBeenCalledWith(0xe0, 0x6b, 0x00, 0x00, Buffer.from([0x00]), [ + expect(transport.send).toHaveBeenCalledWith(0xe0, 0x6b, 0x00, 0x00, Buffer.from([]), [ StatusCodes.OK, StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT, StatusCodes.GEN_AES_KEY_FAILED, diff --git a/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.ts b/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.ts index e1ef41ac24b8..cf43d6271197 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/backupAppStorage.ts @@ -54,7 +54,7 @@ export async function backupAppStorage(transport: Transport): Promise { }); tracer.trace("Start"); - const apdu: Readonly = [...BACKUP_APP_STORAGE, Buffer.from([0x00])]; + const apdu: Readonly = [...BACKUP_APP_STORAGE, Buffer.from([])]; const response = await transport.send(...apdu, RESPONSE_STATUS_SET); diff --git a/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.test.ts b/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.test.ts index f513eda67270..c180238edb1e 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.test.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.test.ts @@ -8,9 +8,17 @@ jest.mock("@ledgerhq/hw-transport"); describe("getAppStorageInfo", () => { let transport: Transport; const response = Buffer.from([ - 0x00, 0x00, 0x04, 0xd2, 0x31, 0x2e, 0x30, 0x31, 0x00, 0x03, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, - 0x73, 0x68, 0x31, 0x32, 0x33, 0x34, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, - 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x90, 0x00, + // Status code 1234 + 0x00, 0x00, 0x04, 0xd2, + // Data version 1.01 + 0x31, 0x2e, 0x30, 0x31, + // Has settings and data + 0x00, 0x03, + // Hash hashhash1234hashhashhashhashhash + 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x31, 0x32, 0x33, 0x34, 0x68, 0x61, 0x73, 0x68, + 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, + // Status word + 0x90, 0x00, ]); beforeEach(() => { @@ -32,7 +40,7 @@ describe("getAppStorageInfo", () => { 0x6a, 0x00, 0x00, - Buffer.from([0x05, 0x4d, 0x79, 0x41, 0x70, 0x70]), + Buffer.from(appName, "ascii"), [ StatusCodes.OK, StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT, @@ -46,10 +54,10 @@ describe("getAppStorageInfo", () => { it("should parse the response data correctly", () => { const expected: AppStorageInfo = { size: 1234, - dataVersion: "1.01", + dataVersion: Buffer.from("1.01").toString("hex"), hasSettings: true, hasData: true, - hash: "hashhash1234hashhashhashhashhash", + hash: Buffer.from("hashhash1234hashhashhashhashhash").toString("hex"), }; expect(parseResponse(response)).toStrictEqual(expected); }); diff --git a/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.ts b/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.ts index 44dc2db73885..9ff2b4e27716 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/getAppStorageInfo.ts @@ -45,10 +45,8 @@ export async function getAppStorageInfo( }); tracer.trace("Start"); - const params: Buffer = Buffer.concat([ - Buffer.from([appName.length]), - Buffer.from(appName, "ascii"), - ]); + const params: Buffer = Buffer.from(appName, "ascii"); + const apdu: Readonly = [...GET_APP_STORAGE_INFO, params]; const response = await transport.send(...apdu, RESPONSE_STATUS_SET); @@ -80,7 +78,12 @@ export function parseResponse(data: Buffer): AppStorageInfo { let offset = 0; const size = data.readUInt32BE(offset); // Len = 4 offset += 4; - const dataVersion = data.subarray(offset, offset + 4).toString(); // Len = 4 + + if (size === 0) { + return { size, dataVersion: "", hasSettings: false, hasData: false, hash: "" }; + } + + const dataVersion = data.subarray(offset, offset + 4).toString("hex"); // Len = 4 offset += 4; const properties = data.readUInt16BE(offset); offset += 2; @@ -91,7 +94,7 @@ export function parseResponse(data: Buffer): AppStorageInfo { */ const hasSettings = (properties & 1) === 1; const hasData = (properties & 2) === 2; - const hash = data.subarray(offset, offset + 32).toString(); // Len = 32 + const hash = data.subarray(offset, offset + 32).toString("hex"); // Len = 32 return { size, dataVersion, hasSettings, hasData, hash }; } diff --git a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.test.ts b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.test.ts index 319a897ed0b5..d23d77fb2fac 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.test.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.test.ts @@ -21,15 +21,12 @@ describe("restoreAppStorage", () => { it("should call the send function with correct parameters", async () => { const chunk = Buffer.from("106RueduTemple"); - await restoreAppStorage(transport, chunk); - expect(transport.send).toHaveBeenCalledWith( + const args = [ 0xe0, 0x6d, 0x00, 0x00, - Buffer.from([ - 0x0e, 0x31, 0x30, 0x36, 0x52, 0x75, 0x65, 0x64, 0x75, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x65, - ]), + chunk, [ StatusCodes.OK, StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT, @@ -40,7 +37,10 @@ describe("restoreAppStorage", () => { StatusCodes.INVALID_CHUNK_LENGTH, StatusCodes.INVALID_BACKUP_HEADER, ], - ); + ]; + + await restoreAppStorage(transport, chunk); + expect(transport.send).toHaveBeenCalledWith(...args); }); describe("parseResponse", () => { diff --git a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.ts b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.ts index 8b02697b4e29..cc9a8b05ffa3 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorage.ts @@ -55,8 +55,7 @@ export async function restoreAppStorage(transport: Transport, chunk: Buffer): Pr }); tracer.trace("Start"); - const params = Buffer.concat([Buffer.from([chunk.length]), chunk]); - const apdu: Readonly = [...RESTORE_APP_STORAGE, params]; + const apdu: Readonly = [...RESTORE_APP_STORAGE, chunk]; const response = await transport.send(...apdu, RESPONSE_STATUS_SET); diff --git a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.test.ts b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.test.ts index 613540768549..77c9d2b1e9bc 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.test.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.test.ts @@ -21,7 +21,7 @@ describe("restoreAppStorageCommit", () => { it("should call the send function with correct parameters", async () => { await restoreAppStorageCommit(transport); - expect(transport.send).toHaveBeenCalledWith(0xe0, 0x6e, 0x00, 0x00, Buffer.from([0x00]), [ + expect(transport.send).toHaveBeenCalledWith(0xe0, 0x6e, 0x00, 0x00, Buffer.from([]), [ StatusCodes.OK, StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT, StatusCodes.GEN_AES_KEY_FAILED, diff --git a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.ts b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.ts index ad4ea098d7d4..05baf7dbebfd 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageCommit.ts @@ -49,7 +49,7 @@ export async function restoreAppStorageCommit(transport: Transport): Promise = [...RESTORE_APP_STORAGE_COMMIT, Buffer.from([0x00])]; + const apdu: Readonly = [...RESTORE_APP_STORAGE_COMMIT, Buffer.from([])]; const response = await transport.send(...apdu, RESPONSE_STATUS_SET); diff --git a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.test.ts b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.test.ts index 72aa7fb1341d..d56b392be1e2 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.test.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.test.ts @@ -23,12 +23,18 @@ describe("restoreAppStorageInit", () => { const appName = "MyApp"; const backupSize = 1234; await restoreAppStorageInit(transport, appName, backupSize); - expect(transport.send).toHaveBeenCalledWith( + + const data = Buffer.concat([ + Buffer.from(backupSize.toString(16).padStart(8, "0"), "hex"), // BACKUP_LEN + Buffer.from(appName, "ascii"), // APP_NAME + ]); + + const args = [ 0xe0, 0x6c, 0x00, 0x00, - Buffer.from([0x09, 0x00, 0x00, 0x04, 0xd2, 0x4d, 0x79, 0x41, 0x70, 0x70]), + data, [ StatusCodes.OK, StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT, @@ -38,7 +44,9 @@ describe("restoreAppStorageInit", () => { StatusCodes.INVALID_APP_NAME_LENGTH, StatusCodes.INVALID_BACKUP_LENGTH, ], - ); + ]; + + expect(transport.send).toHaveBeenCalledWith(...args); }); describe("parseResponse", () => { diff --git a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.ts b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.ts index cddb0295a8e3..69c54c7f3796 100644 --- a/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.ts +++ b/libs/device-core/src/commands/use-cases/app-backup/restoreAppStorageInit.ts @@ -58,7 +58,6 @@ export async function restoreAppStorageInit( tracer.trace("Start"); const params: Buffer = Buffer.concat([ - Buffer.from([appName.length + 4]), // LC Buffer.from(backupSize.toString(16).padStart(8, "0"), "hex"), // BACKUP_LEN Buffer.from(appName, "ascii"), // APP_NAME ]); diff --git a/libs/device-core/src/index.ts b/libs/device-core/src/index.ts index 2c8c93ac71a0..71938e3267ba 100644 --- a/libs/device-core/src/index.ts +++ b/libs/device-core/src/index.ts @@ -40,3 +40,5 @@ export { supportedDeviceModelIds } from "./capabilities/isCustomLockScreenSuppor export * from "./customLockScreen/screenSpecs"; // src/firmwareUpdate/ export { shouldForceFirmwareUpdate } from "./firmwareUpdate/shouldForceFirmwareUpdate"; +// errors +export * from "./errors"; diff --git a/libs/ledger-live-common/.unimportedrc.json b/libs/ledger-live-common/.unimportedrc.json index 4b8610d426e9..455320359d9c 100644 --- a/libs/ledger-live-common/.unimportedrc.json +++ b/libs/ledger-live-common/.unimportedrc.json @@ -299,14 +299,6 @@ "src/consoleWarnExpectToEqual.ts", "src/device", "src/device/hooks/useLatestFirmware.ts", - "src/device/use-cases/appDataBackup/DesktopStorageProvider.ts", - "src/device/use-cases/appDataBackup/backupAppData.ts", - "src/device/use-cases/appDataBackup/backupAppDataUseCase.ts", - "src/device/use-cases/appDataBackup/backupAppDataUseCaseDI.ts", - "src/device/use-cases/appDataBackup/restoreAppData.ts", - "src/device/use-cases/appDataBackup/restoreAppDataUseCase.ts", - "src/device/use-cases/appDataBackup/restoreAppDataUseCaseDI.ts", - "src/device/use-cases/appDataBackup/types.ts", "src/device/use-cases/isDeviceLocalizationSupported.ts", "src/device/use-cases/isEditDeviceNameSupported.ts", "src/device/use-cases/isSyncOnboardingSupported.ts", diff --git a/libs/ledger-live-common/src/apps/inlineAppInstall.ts b/libs/ledger-live-common/src/apps/inlineAppInstall.ts index d3ac3798747c..7363d9d6621a 100644 --- a/libs/ledger-live-common/src/apps/inlineAppInstall.ts +++ b/libs/ledger-live-common/src/apps/inlineAppInstall.ts @@ -8,6 +8,7 @@ import { runAllWithProgress } from "./runner"; import { InlineAppInstallEvent } from "./types"; import { mergeMap, map, throttleTime } from "rxjs/operators"; import { LocalTracer } from "@ledgerhq/logs"; +import { AppStorageType, StorageProvider } from "../device/use-cases/appDataBackup/types"; /** * Tries to install a list of apps @@ -24,11 +25,13 @@ const inlineAppInstall = ({ appNames, onSuccessObs, allowPartialDependencies = false, + storage, }: { transport: Transport; appNames: string[]; onSuccessObs?: () => Observable; allowPartialDependencies?: boolean; + storage?: StorageProvider; }): Observable => { const tracer = new LocalTracer("hw", { ...transport.getTraceContext(), @@ -101,7 +104,7 @@ const inlineAppInstall = ({ installQueue: state.installQueue, }), maybeSkippedEvent, - runAllWithProgress(state, exec).pipe( + runAllWithProgress(state, exec, storage).pipe( throttleTime(100), map(({ globalProgress, itemProgress, installQueue, currentAppOp }) => ({ type: "inline-install", diff --git a/libs/ledger-live-common/src/apps/logic.test.ts b/libs/ledger-live-common/src/apps/logic.test.ts index 5fb8657b20d1..05a484a5733a 100644 --- a/libs/ledger-live-common/src/apps/logic.test.ts +++ b/libs/ledger-live-common/src/apps/logic.test.ts @@ -439,7 +439,7 @@ test("global progress", async () => { while ((next = getNextAppOp(state))) { state = await firstValueFrom( - runOneAppOp(state, next, mockExecWithInstalledContext(state.installed)), + runOneAppOp({ state, appOp: next, exec: mockExecWithInstalledContext(state.installed) }), ); expect(updateAllProgress(state)).toBe(++i / total); } diff --git a/libs/ledger-live-common/src/apps/mock.ts b/libs/ledger-live-common/src/apps/mock.ts index 349f0817ff12..2266defc1042 100644 --- a/libs/ledger-live-common/src/apps/mock.ts +++ b/libs/ledger-live-common/src/apps/mock.ts @@ -193,7 +193,7 @@ export function mockListAppsResult( export const mockExecWithInstalledContext = (installedInitial: InstalledItem[]): Exec => { let installed = installedInitial.slice(0); - return (appOp: AppOp, targetId: string | number, app: App) => { + return ({ appOp, app }: { appOp: AppOp; targetId: string | number; app: App }) => { if (appOp.name !== app.name) { throw new Error("appOp.name must match app.name"); } diff --git a/libs/ledger-live-common/src/apps/react.ts b/libs/ledger-live-common/src/apps/react.ts index d65e02f5e110..ce8dc35f0431 100644 --- a/libs/ledger-live-common/src/apps/react.ts +++ b/libs/ledger-live-common/src/apps/react.ts @@ -12,12 +12,14 @@ import { import { runAppOp } from "./runner"; import { App } from "@ledgerhq/types-live"; import { useFeatureFlags } from "../featureFlags"; +import { AppStorageType, StorageProvider } from "../device/use-cases/appDataBackup/types"; type UseAppsRunnerResult = [State, (arg0: Action) => void]; // use for React apps. support dynamic change of the state. export const useAppsRunner = ( listResult: ListAppsResult, exec: Exec, + storage: StorageProvider, appsToRestore?: string[], ): UseAppsRunnerResult => { // $FlowFixMe for ledger-live-mobile older react/flow version @@ -34,7 +36,7 @@ export const useAppsRunner = ( useEffect(() => { if (appOp) { - const sub = runAppOp(state, appOp, exec).subscribe(event => { + const sub = runAppOp({ state, appOp, exec, storage }).subscribe(event => { dispatch({ type: "onRunnerEvent", event, diff --git a/libs/ledger-live-common/src/apps/runner.ts b/libs/ledger-live-common/src/apps/runner.ts index 0a79aaa3bf27..7856cdbaffdd 100644 --- a/libs/ledger-live-common/src/apps/runner.ts +++ b/libs/ledger-live-common/src/apps/runner.ts @@ -13,12 +13,20 @@ import type { Exec, State, AppOp, RunnerEvent, Action } from "./types"; import { reducer, getActionPlan, getNextAppOp } from "./logic"; import { delay } from "../promise"; import { getEnv } from "@ledgerhq/live-env"; +import { AppStorageType, StorageProvider } from "../device/use-cases/appDataBackup/types"; -export const runAppOp = ( - { appByName, deviceInfo }: State, - appOp: AppOp, - exec: Exec, -): Observable => { +export const runAppOp = ({ + state, + appOp, + exec, + storage, +}: { + state: State; + appOp: AppOp; + exec: Exec; + storage?: StorageProvider; +}): Observable => { + const { appByName, deviceInfo, deviceModel } = state; const app = appByName[appOp.name]; if (!app) { @@ -42,7 +50,15 @@ export const runAppOp = ( appOp, }), // we need to allow a 1s delay for the action to be achieved without glitch (bug in old firmware when you do things too closely) defer(() => delay(getEnv("MANAGER_INSTALL_DELAY"))).pipe(ignoreElements()), - defer(() => exec(appOp, deviceInfo.targetId, app)).pipe( + defer(() => + exec({ + appOp, + targetId: deviceInfo.targetId, + app, + modelId: deviceModel.id, + ...(storage ? { storage } : {}), + }), + ).pipe( throttleTime(100), materialize(), map(n => { @@ -82,6 +98,7 @@ type InlineInstallProgress = { export const runAllWithProgress = ( state: State, exec: Exec, + storage?: StorageProvider, precision = 100, ): Observable => { const total = state.uninstallQueue.length + state.installQueue.length; @@ -92,7 +109,9 @@ export const runAllWithProgress = ( return p; } - return concat(...getActionPlan(state).map(appOp => runAppOp(state, appOp, exec))).pipe( + return concat( + ...getActionPlan(state).map(appOp => runAppOp({ state, appOp, exec, storage })), + ).pipe( map(event => { if (event.type === "runError") { throw event.error; @@ -130,7 +149,7 @@ export const runAllWithProgress = ( }; // use for CLI, no change of the state over time export const runAll = (state: State, exec: Exec): Observable => - concat(...getActionPlan(state).map(appOp => runAppOp(state, appOp, exec))).pipe( + concat(...getActionPlan(state).map(appOp => runAppOp({ state, appOp, exec }))).pipe( map( event => { @@ -140,8 +159,16 @@ export const runAll = (state: State, exec: Exec): Observable => ), reduce(reducer, state), ); -export const runOneAppOp = (state: State, appOp: AppOp, exec: Exec): Observable => - runAppOp(state, appOp, exec).pipe( +export const runOneAppOp = ({ + state, + appOp, + exec, +}: { + state: State; + appOp: AppOp; + exec: Exec; +}): Observable => + runAppOp({ state, appOp, exec }).pipe( map( event => { @@ -151,8 +178,8 @@ export const runOneAppOp = (state: State, appOp: AppOp, exec: Exec): Observable< ), reduce(reducer, state), ); -export const runOne = (state: State, exec: Exec): Observable => { +export const runOne = ({ state, exec }: { state: State; exec: Exec }): Observable => { const next = getNextAppOp(state); if (!next) return of(state); - return runOneAppOp(state, next, exec); + return runOneAppOp({ state, appOp: next, exec }); }; diff --git a/libs/ledger-live-common/src/apps/types.ts b/libs/ledger-live-common/src/apps/types.ts index 7e980d5d5d32..0fbfabf32b34 100644 --- a/libs/ledger-live-common/src/apps/types.ts +++ b/libs/ledger-live-common/src/apps/types.ts @@ -2,13 +2,20 @@ import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; import type { DeviceModel, DeviceModelId } from "@ledgerhq/devices"; import { App, DeviceInfo, FinalFirmware, LanguagePackage } from "@ledgerhq/types-live"; import type { Observable, Subject } from "rxjs"; -export type Exec = ( - appOp: AppOp, - targetId: string | number, - app: App, -) => Observable<{ +import { AppStorageType, StorageProvider } from "../device/use-cases/appDataBackup/types"; + +export type ExecArgs = { + appOp: AppOp; + targetId: string | number; + app: App; + modelId?: DeviceModelId; + storage?: StorageProvider; +}; + +export type Exec = (args: ExecArgs) => Observable<{ progress: number; }>; + export type InstalledItem = { name: string; updated: boolean; diff --git a/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppData.ts b/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppData.ts index 00aa278677dc..7695a70fead7 100644 --- a/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppData.ts +++ b/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppData.ts @@ -1,6 +1,11 @@ -import { AppStorageInfo, backupAppStorage, getAppStorageInfo } from "@ledgerhq/device-core"; +import { + AppNotFound, + AppStorageInfo, + backupAppStorage, + getAppStorageInfo, +} from "@ledgerhq/device-core"; import Transport from "@ledgerhq/hw-transport"; -import { Observable, from, switchMap } from "rxjs"; +import { Observable, catchError, from, of, switchMap } from "rxjs"; import { AppName, BackupAppDataError, BackupAppDataEvent, BackupAppDataEventType } from "./types"; /** @@ -58,6 +63,15 @@ export function backupAppData( }); subscriber.complete(); }), + catchError(e => { + if (e instanceof AppNotFound) { + subscriber.next({ type: BackupAppDataEventType.NoAppDataToBackup }); + subscriber.complete(); + return of(null); + } + + throw e; + }), ) .subscribe(); diff --git a/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppDataUseCase.ts b/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppDataUseCase.ts index 9e8db01a62e8..e32863996b55 100644 --- a/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppDataUseCase.ts +++ b/libs/ledger-live-common/src/device/use-cases/appDataBackup/backupAppDataUseCase.ts @@ -45,10 +45,12 @@ export function backupAppDataUseCase( } case BackupAppDataEventType.AppDataBackedUp: // Store the app data - await storageProvider.setItem(`${deviceModelId}-${appName}`, { - appDataInfo, - appData: event.data, - }); + if (appDataInfo) { + await storageProvider.setItem(`${deviceModelId}-${appName}`, { + appDataInfo, + appData: event.data, + }); + } // Erase the app data, then return the event return { type: BackupAppDataEventType.AppDataBackedUp, data: "" }; case BackupAppDataEventType.Progress: diff --git a/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppData.ts b/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppData.ts index 5f2c209025fb..29398b4365fd 100644 --- a/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppData.ts +++ b/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppData.ts @@ -1,10 +1,11 @@ import { + AppNotFound, restoreAppStorage, restoreAppStorageCommit, restoreAppStorageInit, } from "@ledgerhq/device-core"; -import Transport from "@ledgerhq/hw-transport"; -import { Observable, from, switchMap } from "rxjs"; +import Transport, { TransportStatusError } from "@ledgerhq/hw-transport"; +import { Observable, catchError, from, of, switchMap } from "rxjs"; import { AppName, RestoreAppDataEvent, RestoreAppDataEventType } from "./types"; /** @@ -54,6 +55,27 @@ export function restoreAppData( }); subscriber.complete(); }), + catchError(e => { + if (e instanceof AppNotFound) { + subscriber.next({ + type: RestoreAppDataEventType.NoAppDataToRestore, + }); + subscriber.complete(); + return of(null); + } + + // User refused on device + if (e instanceof TransportStatusError && e.statusCode === 0x5501) { + subscriber.next({ + type: RestoreAppDataEventType.UserRefused, + }); + subscriber.complete(); + return of(null); + } + + subscriber.complete(); + throw e; + }), ) .subscribe(); diff --git a/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppDataUseCase.ts b/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppDataUseCase.ts index a81281dce8f2..efcf5c28862d 100644 --- a/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppDataUseCase.ts +++ b/libs/ledger-live-common/src/device/use-cases/appDataBackup/restoreAppDataUseCase.ts @@ -1,9 +1,9 @@ -import { from, Observable, switchMap } from "rxjs"; +import { from, Observable, of, switchMap } from "rxjs"; import { AppName, AppStorageType, - RestoreAppDataError, RestoreAppDataEvent, + RestoreAppDataEventType, StorageProvider, } from "./types"; import { DeviceModelId } from "@ledgerhq/devices"; @@ -29,7 +29,7 @@ export function restoreAppDataUseCase( ).pipe( switchMap((appStorage: AppStorageType | null) => { if (!appStorage) { - throw new RestoreAppDataError("No backed up data found"); + return of({ type: RestoreAppDataEventType.NoAppDataToRestore }); } return restoreAppDataFn(appStorage.appData); }), diff --git a/libs/ledger-live-common/src/device/use-cases/appDataBackup/types.ts b/libs/ledger-live-common/src/device/use-cases/appDataBackup/types.ts index 511a3bd01d42..6df0268901e2 100644 --- a/libs/ledger-live-common/src/device/use-cases/appDataBackup/types.ts +++ b/libs/ledger-live-common/src/device/use-cases/appDataBackup/types.ts @@ -105,6 +105,16 @@ export enum RestoreAppDataEventType { * The application data has been restored. */ AppDataRestored = "appDataRestored", + + /** + * There is no application data to restore. + */ + NoAppDataToRestore = "noAppDataToRestore", + + /** + * The user refused to restore the application data. + */ + UserRefused = "userRefused", } export type RestoreAppDataEvent = @@ -120,6 +130,12 @@ export type RestoreAppDataEvent = } | { type: RestoreAppDataEventType.AppDataRestored; + } + | { + type: RestoreAppDataEventType.NoAppDataToRestore; + } + | { + type: RestoreAppDataEventType.UserRefused; }; /** diff --git a/libs/ledger-live-common/src/device/use-cases/listAppsUseCase.ts b/libs/ledger-live-common/src/device/use-cases/listAppsUseCase.ts index 88fb884b6b93..763002f5f5d2 100644 --- a/libs/ledger-live-common/src/device/use-cases/listAppsUseCase.ts +++ b/libs/ledger-live-common/src/device/use-cases/listAppsUseCase.ts @@ -2,33 +2,32 @@ import { Observable } from "rxjs"; import Transport from "@ledgerhq/hw-transport"; import { DeviceInfo } from "@ledgerhq/types-live"; import { listApps } from "../../apps/listApps"; -import { AppOp, Exec, ListAppsEvent } from "../../apps"; +import { Exec, ListAppsEvent } from "../../apps"; import { getEnv } from "@ledgerhq/live-env"; import { DeviceModelId } from "@ledgerhq/devices"; -import { App } from "@ledgerhq/types-live"; import installApp from "../../hw/installApp"; +import installAppWithRestore from "../../hw/installAppWithRestore"; import uninstallApp from "../../hw/uninstallApp"; +import uninstallAppWithBackup from "../../hw/uninstallAppWithBackup"; import { HttpManagerApiRepositoryFactory } from "../factories/HttpManagerApiRepositoryFactory"; import { ManagerApiRepository } from "@ledgerhq/device-core"; export const execWithTransport = - (transport: Transport): Exec => - (appOp: AppOp, targetId: string | number, app: App) => { + (transport: Transport, appsBackupEnabled: boolean = false): Exec => + args => { + const { appOp, targetId, app, modelId, storage } = args; + + // if appsBackupEnabled is true, we will use the backup/restore flow + // modelId & storage are required for the new flow, but can still be + // undefined for the old flow, so we need to check if they are defined + if (appsBackupEnabled && modelId && storage) { + const fn = appOp.type === "install" ? installAppWithRestore : uninstallAppWithBackup; + return fn(transport, targetId, app, modelId, storage); + } + const fn = appOp.type === "install" ? installApp : uninstallApp; return fn(transport, targetId, app); }; - -/** - * The moment we deem the v2 as stable enough and we remove this fork in our - * logic there will be some cleanup to do too. - * Refer to https://ledgerhq.atlassian.net/browse/LIVE-7945 - * - We no longer need the polyfill dependency resolution that is based on the - * currency and parent application. And therefor we no longer need the version - * check that broke that dependency after a certain version for ETH and BTC. - * - Remove all the legacy v1 code, and tests. - * - Cleanup the feature flag that governs this. - */ - export function listAppsUseCase( transport: Transport, deviceInfo: DeviceInfo, diff --git a/libs/ledger-live-common/src/featureFlags/firebaseFeatureFlags.ts b/libs/ledger-live-common/src/featureFlags/firebaseFeatureFlags.ts index 8881c74ab756..50a04578ab4b 100644 --- a/libs/ledger-live-common/src/featureFlags/firebaseFeatureFlags.ts +++ b/libs/ledger-live-common/src/featureFlags/firebaseFeatureFlags.ts @@ -108,8 +108,8 @@ export const getFeature = (args: { } return checkFeatureFlagVersion(feature); - } catch (error) { - console.error(`Failed to retrieve feature "${key}"`); + } catch (_error: unknown) { + // console.error(`Failed to retrieve feature "${key}"`); return null; } }; diff --git a/libs/ledger-live-common/src/hw/installAppWithRestore.ts b/libs/ledger-live-common/src/hw/installAppWithRestore.ts new file mode 100644 index 000000000000..fb2a500995d2 --- /dev/null +++ b/libs/ledger-live-common/src/hw/installAppWithRestore.ts @@ -0,0 +1,25 @@ +import installApp from "./installApp"; +import type { + AppStorageType, + RestoreAppDataEvent, + StorageProvider, +} from "../device/use-cases/appDataBackup/types"; +import { concat, Observable } from "rxjs"; +import Transport from "@ledgerhq/hw-transport"; +import type { App, ApplicationVersion } from "@ledgerhq/types-live"; +import { restoreAppDataUseCaseDI } from "../device/use-cases/appDataBackup/restoreAppDataUseCaseDI"; +import { DeviceModelId } from "@ledgerhq/devices"; + +export default function installAppWithRestore( + transport: Transport, + targetId: string | number, + app: ApplicationVersion | App, + deviceId: DeviceModelId, + storage: StorageProvider, + shouldRestore: boolean = true, +): Observable<{ progress: number } | RestoreAppDataEvent> { + const install = installApp(transport, targetId, app); + const restore = restoreAppDataUseCaseDI(transport, app.name, deviceId, storage); + + return shouldRestore ? concat(install, restore) : install; +} diff --git a/libs/ledger-live-common/src/hw/uninstallAppWithBackup.ts b/libs/ledger-live-common/src/hw/uninstallAppWithBackup.ts new file mode 100644 index 000000000000..7e2654efd9c8 --- /dev/null +++ b/libs/ledger-live-common/src/hw/uninstallAppWithBackup.ts @@ -0,0 +1,20 @@ +import uninstallApp from "./uninstallApp"; +import type { AppStorageType, StorageProvider } from "../device/use-cases/appDataBackup/types"; +import { concat, Observable } from "rxjs"; +import Transport from "@ledgerhq/hw-transport"; +import type { App, ApplicationVersion } from "@ledgerhq/types-live"; +import { backupAppDataUseCaseDI } from "../device/use-cases/appDataBackup/backupAppDataUseCaseDI"; +import { DeviceModelId } from "@ledgerhq/devices"; + +export default function uninstallAppWithBackup( + transport: Transport, + targetId: string | number, + app: ApplicationVersion | App, + deviceId: DeviceModelId, + storage: StorageProvider, + shouldBackup: boolean = true, +): Observable { + const backup = backupAppDataUseCaseDI(transport, app.name, deviceId, storage); + const uninstall = uninstallApp(transport, targetId, app); + return shouldBackup ? concat(backup, uninstall) : uninstall; +}