Skip to content

Commit

Permalink
✨ (app-data): Adds data backup and restore to install / uninstall app
Browse files Browse the repository at this point in the history
  • Loading branch information
valpinkman committed Aug 22, 2024
1 parent d75616a commit c8c273c
Show file tree
Hide file tree
Showing 41 changed files with 385 additions and 137 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-jeans-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-core": patch
---

Export errors from the lib
5 changes: 5 additions & 0 deletions .changeset/rude-ducks-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": minor
---

Implement app data backup and restore when installing, uninstalling and updating apps on the device
5 changes: 5 additions & 0 deletions .changeset/smart-hotels-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-common": minor
---

Add new installAppWithRestore and uninstallAppWithBackup to handle app data restore and backup
5 changes: 5 additions & 0 deletions .changeset/tough-coats-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

Implement new app data backup and restore when installing, uninstalling or updating app on the device
51 changes: 27 additions & 24 deletions apps/ledger-live-desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -77,30 +78,32 @@ const InnerApp = ({ initialCountervalues }: { initialCountervalues: CounterValue
<FirebaseFeatureFlagsProvider getFeature={getFeature}>
<ConnectEnvsToSentry />
<UpdaterProvider>
<CountervaluesMarketcap>
<CountervaluesProvider initialState={initialCountervalues}>
<ToastProvider>
<AnnouncementProviderWrapper>
<Router>
<PostOnboardingProviderWrapped>
<PlatformAppProviderWrapper>
<DrawerProvider>
<NftMetadataProvider getCurrencyBridge={getCurrencyBridge}>
<StorylyProvider>
<QueryClientProvider client={queryClient}>
<Default />
<ReactQueryDevtoolsProvider />
</QueryClientProvider>
</StorylyProvider>
</NftMetadataProvider>
</DrawerProvider>
</PlatformAppProviderWrapper>
</PostOnboardingProviderWrapped>
</Router>
</AnnouncementProviderWrapper>
</ToastProvider>
</CountervaluesProvider>
</CountervaluesMarketcap>
<AppDataStorageProvider>
<CountervaluesMarketcap>
<CountervaluesProvider initialState={initialCountervalues}>
<ToastProvider>
<AnnouncementProviderWrapper>
<Router>
<PostOnboardingProviderWrapped>
<PlatformAppProviderWrapper>
<DrawerProvider>
<NftMetadataProvider getCurrencyBridge={getCurrencyBridge}>
<StorylyProvider>
<QueryClientProvider client={queryClient}>
<Default />
<ReactQueryDevtoolsProvider />
</QueryClientProvider>
</StorylyProvider>
</NftMetadataProvider>
</DrawerProvider>
</PlatformAppProviderWrapper>
</PostOnboardingProviderWrapped>
</Router>
</AnnouncementProviderWrapper>
</ToastProvider>
</CountervaluesProvider>
</CountervaluesMarketcap>
</AppDataStorageProvider>
</UpdaterProvider>
</FirebaseFeatureFlagsProvider>
</FirebaseRemoteConfigProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
StorageProvider,
StorageProvider as AppDataStorageProvider,
AppStorageType,
AppStorageKey,
isAppStorageType,
Expand All @@ -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<AppStorageType> {
export class DesktopAppDataStorageProvider implements AppDataStorageProvider<AppStorageType> {
/**
* Retrieves the value associated with the specified key from the storage.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<AppDataStorageContext.Provider value={storage}>{children}</AppDataStorageContext.Provider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 15 additions & 12 deletions apps/ledger-live-mobile/src/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,18 +30,20 @@ function AppProviders({ initialCountervalues, children }: AppProvidersProps) {
<CountervaluesMarketcap>
<CounterValuesProvider initialState={initialCountervalues}>
<ButtonUseTouchableContext.Provider value={true}>
<OnboardingContextProvider>
<PostOnboardingProviderWrapped>
<ToastProvider>
<NotificationsProvider>
<SnackbarContainer />
<NftMetadataProvider getCurrencyBridge={getCurrencyBridge}>
{children}
</NftMetadataProvider>
</NotificationsProvider>
</ToastProvider>
</PostOnboardingProviderWrapped>
</OnboardingContextProvider>
<AppDataStorageProvider>
<OnboardingContextProvider>
<PostOnboardingProviderWrapped>
<ToastProvider>
<NotificationsProvider>
<SnackbarContainer />
<NftMetadataProvider getCurrencyBridge={getCurrencyBridge}>
{children}
</NftMetadataProvider>
</NotificationsProvider>
</ToastProvider>
</PostOnboardingProviderWrapped>
</OnboardingContextProvider>
</AppDataStorageProvider>
</ButtonUseTouchableContext.Provider>
</CounterValuesProvider>
</CountervaluesMarketcap>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
StorageProvider,
StorageProvider as AppDataStorageProvider,
AppStorageType,
AppStorageKey,
isAppStorageType,
Expand All @@ -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<AppStorageType> {
export class MobileAppDataStorageProvider implements AppDataStorageProvider<AppStorageType> {
/**
* Retrieves the value associated with the specified key from storage.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <StorageContext.Provider value={storage}>{children}</StorageContext.Provider>;
}
4 changes: 3 additions & 1 deletion apps/ledger-live-mobile/src/screens/MyLedgerDevice/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
AppWithDependents,
AppsInstallUninstallWithDependenciesContextProvider,
} from "./AppsInstallUninstallWithDependenciesContext";
import { useAppDataStorage } from "~/hooks/storageProvider/useAppDataStorage";

type NavigationProps = BaseComposite<
StackNavigatorProps<MyLedgerNavigatorStackParamList, ScreenName.MyLedgerDevice>
Expand All @@ -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);
Expand Down
27 changes: 23 additions & 4 deletions apps/ledger-live-mobile/src/screens/MyLedgerDevice/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppStorageType>,
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function backupAppStorage(transport: Transport): Promise<Buffer> {
});
tracer.trace("Start");

const apdu: Readonly<APDU> = [...BACKUP_APP_STORAGE, Buffer.from([0x00])];
const apdu: Readonly<APDU> = [...BACKUP_APP_STORAGE, Buffer.from([])];

const response = await transport.send(...apdu, RESPONSE_STATUS_SET);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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,
Expand All @@ -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);
});
Expand Down
Loading

0 comments on commit c8c273c

Please sign in to comment.