Skip to content

Commit

Permalink
✨ (llm): delete local app data on uninstall and uninstallAll
Browse files Browse the repository at this point in the history
  • Loading branch information
valpinkman committed Sep 9, 2024
1 parent fcd8d52 commit 7e81505
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 178 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-years-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": patch
---

Delete local app data when uninstalling apps
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const Dashboard = ({
() =>
getEnv("MOCK")
? mockExecWithInstalledContext(result?.installed || [])
: ({ app, appOp, targetId, deleteAppDataBackup }: ExecArgs) =>
: ({ app, appOp, targetId, skipAppDataBackup }: ExecArgs) =>
withDevice(device.deviceId)(transport =>
execWithTransport(
transport,
Expand All @@ -90,7 +90,7 @@ const Dashboard = ({
app,
modelId: device.modelId,
storage,
deleteAppDataBackup,
skipAppDataBackup,
}),
),
[device, result, appsBackupEnabled, storage],
Expand Down
4 changes: 2 additions & 2 deletions apps/ledger-live-mobile/src/screens/MyLedgerDevice/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export function useApps(
const enableAppsBackup = useFeature("enableAppsBackup");

const exec: Exec = useCallback(
args =>
({ skipAppDataBackup, ...args }) =>
withDevice(device.deviceId)(transport =>
execWithTransport(
transport,
enableAppsBackup?.enabled,
)({ ...args, storage, modelId: device.modelId }),
)({ ...args, storage, modelId: device.modelId, skipAppDataBackup }),
),
[device, enableAppsBackup, storage],
);
Expand Down
8 changes: 0 additions & 8 deletions ledger-live.code-workspace

This file was deleted.

8 changes: 4 additions & 4 deletions libs/ledger-live-common/src/apps/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const initState = (
currentError: null,
currentAppOp: null,
skippedAppOps: [],
deleteAppDataBackup: false,
skipAppDataBackup: false,
};

if (appsToRestore) {
Expand Down Expand Up @@ -198,7 +198,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
installed: [],
deleteAppDataBackup: false,
skipAppDataBackup: false,
};
case "wipe":
return {
Expand All @@ -209,7 +209,7 @@ export const reducer = (state: State, action: Action): State => {
state.appByName,
state.installed.map(({ name }) => name),
),
deleteAppDataBackup: true,
skipAppDataBackup: true,
};

case "updateAll": {
Expand Down Expand Up @@ -379,7 +379,7 @@ export const reducer = (state: State, action: Action): State => {
currentError: null,
installQueue,
uninstallQueue,
deleteAppDataBackup: true,
skipAppDataBackup: true,
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion libs/ledger-live-common/src/apps/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const runAppOp = ({
app,
modelId: deviceModel.id,
...(storage ? { storage } : {}),
...(state.deleteAppDataBackup ? { deleteAppDataBackup: true } : {}),
...(state.skipAppDataBackup ? { skipAppDataBackup: true } : {}),
}),
).pipe(
throttleTime(100),
Expand Down
18 changes: 16 additions & 2 deletions libs/ledger-live-common/src/apps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@ import { App, DeviceInfo, FinalFirmware, LanguagePackage } from "@ledgerhq/types
import type { Observable, Subject } from "rxjs";
import { AppStorageType, StorageProvider } from "../device/use-cases/appDataBackup/types";

/**
* ExecArgs
* @param appOp - The app operation to execute (either install or uninstall).
* @param targetId - The target ID (device)
* @param app - The app to install or uninstall
* @param modelId - The device model ID
* @param storage - The storage provider for local backup
* @param skipAppDataBackup - Whether to skip app data backup
*/
export type ExecArgs = {
appOp: AppOp;
targetId: string | number;
app: App;
modelId?: DeviceModelId;
storage?: StorageProvider<AppStorageType>;
deleteAppDataBackup?: boolean;
skipAppDataBackup?: boolean;
};

/**
* Exec
* @param args - The arguments for the execution
* @returns An observable that emits the progress of the execution
*/
export type Exec = (args: ExecArgs) => Observable<{
progress: number;
}>;
Expand Down Expand Up @@ -103,7 +117,7 @@ export type State = {
recentlyInstalledApps: string[];
installQueue: string[];
uninstallQueue: string[];
deleteAppDataBackup: boolean;
skipAppDataBackup: boolean;
skippedAppOps: SkippedAppOp[]; // Nb If an AppOp couldn't be completed, track why.
updateAllQueue: string[]; // queue saved at the time of a "updateAll" action
currentAppOp: AppOp | null | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import Transport from "@ledgerhq/hw-transport";
import { Observable } from "rxjs";
import {
AppStorageType,
DeleteAppDataEvent,
DeleteAppDataEventType,
RestoreAppDataEvent,
RestoreAppDataEventType,
StorageProvider,
} from "./types";
import { RestoreAppDataEvent, RestoreAppDataEventType } from "./types";
import { restoreAppData } from "./restoreAppData";
import { DeviceModelId } from "@ledgerhq/devices";

jest.mock("@ledgerhq/hw-transport");
jest.mock("@ledgerhq/device-core", () => ({
Expand All @@ -25,82 +17,47 @@ describe("restoreAppData", () => {
let transport: Transport;
let appName: string;
let appData: string;
let storageProvider: StorageProvider<AppStorageType>;
let deviceModelId: DeviceModelId;
let deleteAppData: jest.Mock;

beforeEach(() => {
// Initialize the transport, app name and app data before each test
transport = {} as unknown as Transport;
appName = "MyApp";
appData = Buffer.from(DECODED_STORED_DATA).toString("base64");
deviceModelId = DeviceModelId.stax;
storageProvider = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
};
deleteAppData = jest.fn().mockImplementation(() => {
const obs = new Observable<DeleteAppDataEvent>(subscriber => {
subscriber.next({
type: DeleteAppDataEventType.AppDataDeleteStarted,
});
subscriber.next({
type: DeleteAppDataEventType.AppDataDeleted,
});
subscriber.complete();
});
return obs;
});
});

afterEach(() => {
jest.clearAllMocks();
});

it("should restore the app data by emitting relative events sequentially when data size > 255", done => {
const restoreObservable: Observable<RestoreAppDataEvent | DeleteAppDataEvent> = restoreAppData(
const restoreObservable: Observable<RestoreAppDataEvent> = restoreAppData(
transport,
appName,
deviceModelId,
storageProvider,
appData,
deleteAppData,
);
const events: (RestoreAppDataEvent | DeleteAppDataEvent)[] = [];
const events: RestoreAppDataEvent[] = [];

const expectedEvents = [
{ type: RestoreAppDataEventType.AppDataInitialized },
{ type: RestoreAppDataEventType.Progress, data: expect.any(Number) },
{ type: RestoreAppDataEventType.Progress, data: expect.any(Number) },
{ type: RestoreAppDataEventType.Progress, data: expect.any(Number) },
{ type: RestoreAppDataEventType.AppDataRestored },
];

// Subscribe to the observable to receive the restore and delete events
restoreObservable.subscribe({
next: (event: RestoreAppDataEvent | DeleteAppDataEvent) => {
next: (event: RestoreAppDataEvent) => {
events.push(event);
},
complete: () => {
expect(events).toHaveLength(7);
expect(events[0]).toEqual({
type: RestoreAppDataEventType.AppDataInitialized,
});
expect(events[1]).toEqual({
type: RestoreAppDataEventType.Progress,
data: expect.any(Number),
});
expect(events[2]).toEqual({
type: RestoreAppDataEventType.Progress,
data: expect.any(Number),
});
expect(events[3]).toEqual({
type: RestoreAppDataEventType.Progress,
data: expect.any(Number),
});
expect(events[4]).toEqual({
type: RestoreAppDataEventType.AppDataRestored,
});
expect(events[5]).toEqual({
type: DeleteAppDataEventType.AppDataDeleteStarted,
});
expect(events[6]).toEqual({
type: DeleteAppDataEventType.AppDataDeleted,
});
done();
try {
expect(events).toHaveLength(5);
expect(events).toEqual(expectedEvents);
done();
} catch (e) {
done(e);
}
},
error: (e: Error) => {
done(e);
Expand All @@ -109,48 +66,34 @@ describe("restoreAppData", () => {
});

it("should restore the app data by emitting relative events sequentially when data size < 255", done => {
const restoreObservable: Observable<RestoreAppDataEvent | DeleteAppDataEvent> = restoreAppData(
const restoreObservable: Observable<RestoreAppDataEvent> = restoreAppData(
transport,
appName,
deviceModelId,
storageProvider,
appData,
deleteAppData,
);
const events: (RestoreAppDataEvent | DeleteAppDataEvent)[] = [];
const events: RestoreAppDataEvent[] = [];

const expectedEvents = [
{ type: RestoreAppDataEventType.AppDataInitialized },
{ type: RestoreAppDataEventType.Progress, data: expect.any(Number) },
{ type: RestoreAppDataEventType.Progress, data: expect.any(Number) },
{ type: RestoreAppDataEventType.Progress, data: expect.any(Number) },
{ type: RestoreAppDataEventType.AppDataRestored },
];

// Subscribe to the observable to receive the restore events
restoreObservable.subscribe({
next: (event: RestoreAppDataEvent | DeleteAppDataEvent) => {
next: (event: RestoreAppDataEvent) => {
events.push(event);
},
complete: () => {
expect(events).toHaveLength(7);
expect(events[0]).toEqual({
type: RestoreAppDataEventType.AppDataInitialized,
});
expect(events[1]).toEqual({
type: RestoreAppDataEventType.Progress,
data: expect.any(Number),
});
expect(events[2]).toEqual({
type: RestoreAppDataEventType.Progress,
data: expect.any(Number),
});
expect(events[3]).toEqual({
type: RestoreAppDataEventType.Progress,
data: expect.any(Number),
});
expect(events[4]).toEqual({
type: RestoreAppDataEventType.AppDataRestored,
});
expect(events[5]).toEqual({
type: DeleteAppDataEventType.AppDataDeleteStarted,
});
expect(events[6]).toEqual({
type: DeleteAppDataEventType.AppDataDeleted,
});
done();
try {
expect(events).toHaveLength(5);
expect(events).toEqual(expectedEvents);
done();
} catch (e) {
done(e);
}
},
error: (e: Error) => {
done(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,8 @@ import {
restoreAppStorageInit,
} from "@ledgerhq/device-core";
import Transport from "@ledgerhq/hw-transport";
import { Observable, catchError, from, map, of, switchMap } from "rxjs";
import {
AppName,
AppStorageType,
DeleteAppDataEvent,
DeleteAppDataEventType,
RestoreAppDataEvent,
RestoreAppDataEventType,
StorageProvider,
} from "./types";
import { deleteAppDataUseCaseDI } from "./deleteAppDataUseCaseDI";
import { DeviceModelId } from "@ledgerhq/devices";
import { Observable, catchError, from, of, switchMap } from "rxjs";
import { AppName, RestoreAppDataEvent, RestoreAppDataEventType } from "./types";

/**
* Restores the application data for a specific app on a Ledger device.
Expand All @@ -30,12 +20,9 @@ import { DeviceModelId } from "@ledgerhq/devices";
export function restoreAppData(
transport: Transport,
appName: AppName,
deviceModelId: DeviceModelId,
storageProvider: StorageProvider<AppStorageType>,
appData: string,
deleteAppData: typeof deleteAppDataUseCaseDI,
): Observable<RestoreAppDataEvent | DeleteAppDataEvent> {
const obs = new Observable<RestoreAppDataEvent | DeleteAppDataEvent>(subscriber => {
): Observable<RestoreAppDataEvent> {
const obs = new Observable<RestoreAppDataEvent>(subscriber => {
const chunkData = Buffer.from(appData, "base64");
const backupSize = chunkData.length;
const sub = from(restoreAppStorageInit(transport, appName, backupSize))
Expand Down Expand Up @@ -67,18 +54,7 @@ export function restoreAppData(
subscriber.next({
type: RestoreAppDataEventType.AppDataRestored,
});
}),
// Delete the app data from the storage
switchMap(() => deleteAppData(appName, deviceModelId, storageProvider)),
map(event => {
subscriber.next(event);
if (
event.type === DeleteAppDataEventType.AppDataDeleted ||
event.type === DeleteAppDataEventType.NoAppDataToDelete
) {
subscriber.complete();
}
return event;
subscriber.complete();
}),
catchError(e => {
// No app data found on the app or the app does not support it
Expand Down
Loading

0 comments on commit 7e81505

Please sign in to comment.