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;
+}