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 5, 2024
1 parent de1b67f commit c9b5761
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 118 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
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 =>
({ deleteAppDataBackup, ...args }) =>
withDevice(device.deviceId)(transport =>
execWithTransport(
transport,
enableAppsBackup?.enabled,
)({ ...args, storage, modelId: device.modelId }),
)({ ...args, storage, modelId: device.modelId, deleteAppDataBackup }),
),
[device, enableAppsBackup, storage],
);
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,57 +17,33 @@ 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[] = [];

// 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).toHaveLength(5);
expect(events[0]).toEqual({
type: RestoreAppDataEventType.AppDataInitialized,
});
Expand All @@ -94,12 +62,6 @@ describe("restoreAppData", () => {
expect(events[4]).toEqual({
type: RestoreAppDataEventType.AppDataRestored,
});
expect(events[5]).toEqual({
type: DeleteAppDataEventType.AppDataDeleteStarted,
});
expect(events[6]).toEqual({
type: DeleteAppDataEventType.AppDataDeleted,
});
done();
},
error: (e: Error) => {
Expand All @@ -109,23 +71,20 @@ 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[] = [];

// 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).toHaveLength(5);
expect(events[0]).toEqual({
type: RestoreAppDataEventType.AppDataInitialized,
});
Expand All @@ -144,12 +103,6 @@ describe("restoreAppData", () => {
expect(events[4]).toEqual({
type: RestoreAppDataEventType.AppDataRestored,
});
expect(events[5]).toEqual({
type: DeleteAppDataEventType.AppDataDeleteStarted,
});
expect(events[6]).toEqual({
type: DeleteAppDataEventType.AppDataDeleted,
});
done();
},
error: (e: Error) => {
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
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { Observable, firstValueFrom, of } from "rxjs";
import {
DeleteAppDataEvent,
RestoreAppDataError,
RestoreAppDataEvent,
RestoreAppDataEventType,
} from "./types";
import { RestoreAppDataError, RestoreAppDataEvent, RestoreAppDataEventType } from "./types";
import { DeviceModelId } from "@ledgerhq/devices";
import { restoreAppDataUseCase } from "./restoreAppDataUseCase";

Expand All @@ -16,8 +11,25 @@ describe("restoreAppDataUseCase", () => {
};
const appName = "MyApp";
const deviceModelId = DeviceModelId.stax;
let restoreAppDataFnMock: jest.Mock;

it("should transfer the events when backup data found", done => {
restoreAppDataFnMock = jest.fn().mockImplementation(() => {
return new Observable(subscriber => {
subscriber.next({
type: RestoreAppDataEventType.AppDataInitialized,
});
subscriber.next({
type: RestoreAppDataEventType.Progress,
data: 0.25,
});
subscriber.next({
type: RestoreAppDataEventType.AppDataRestored,
});
subscriber.complete();
});
});

it("should transfer the events when backup data found", async () => {
const expectedEvents: RestoreAppDataEvent[] = [
{
type: RestoreAppDataEventType.AppDataInitialized,
Expand All @@ -30,21 +42,39 @@ describe("restoreAppDataUseCase", () => {
type: RestoreAppDataEventType.AppDataRestored,
},
];
for (const event of expectedEvents) {
const restoreAppDataFnMock = jest.fn(_ => of(event));
const restoreAppDataUseCaseObservable: Observable<RestoreAppDataEvent | DeleteAppDataEvent> =
restoreAppDataUseCase(appName, deviceModelId, storageProviderMock, restoreAppDataFnMock);
const firstValue = await firstValueFrom(restoreAppDataUseCaseObservable);
expect(firstValue).toEqual(event);
}

const events: RestoreAppDataEvent[] = [];

restoreAppDataUseCase(
appName,
deviceModelId,
storageProviderMock,
restoreAppDataFnMock,
).subscribe({
next: event => {
events.push(event);
},
complete: () => {
expect(events).toHaveLength(expectedEvents.length);
expect(events).toEqual(expectedEvents);
done();
},
error: (e: Error) => {
done(e);
},
});
});

it("should throw an error when backup data not found", async () => {
const restoreAppDataFnMock = jest.fn(() => of({} as RestoreAppDataEvent));
jest.spyOn(storageProviderMock, "getItem").mockResolvedValue(null);

const restoreAppDataUseCaseObservable: Observable<RestoreAppDataEvent | DeleteAppDataEvent> =
restoreAppDataUseCase(appName, deviceModelId, storageProviderMock, restoreAppDataFnMock);
const restoreAppDataUseCaseObservable: Observable<RestoreAppDataEvent> = restoreAppDataUseCase(
appName,
deviceModelId,
storageProviderMock,
restoreAppDataFnMock,
);

await firstValueFrom(restoreAppDataUseCaseObservable).catch(e => {
expect(e).toBeInstanceOf(RestoreAppDataError);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { from, Observable, of, switchMap } from "rxjs";
import {
AppName,
AppStorageType,
DeleteAppDataEvent,
RestoreAppDataEvent,
RestoreAppDataEventType,
StorageProvider,
Expand All @@ -23,14 +22,14 @@ export function restoreAppDataUseCase(
appName: AppName,
deviceModelId: DeviceModelId,
storageProvider: StorageProvider<AppStorageType>,
restoreAppDataFn: (data: string) => Observable<RestoreAppDataEvent | DeleteAppDataEvent>,
): Observable<RestoreAppDataEvent | DeleteAppDataEvent> {
const obs: Observable<RestoreAppDataEvent | DeleteAppDataEvent> = from(
restoreAppDataFn: (data: string) => Observable<RestoreAppDataEvent>,
): Observable<RestoreAppDataEvent> {
const obs: Observable<RestoreAppDataEvent> = from(
storageProvider.getItem(`${deviceModelId}-${appName}`),
).pipe(
switchMap((appStorage: AppStorageType | null) => {
if (!appStorage) {
return of<RestoreAppDataEvent | DeleteAppDataEvent>({
return of<RestoreAppDataEvent>({
type: RestoreAppDataEventType.NoAppDataToRestore,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
RestoreAppDataEvent,
StorageProvider,
} from "./types";
import { Observable } from "rxjs";
import { Observable, switchMap } from "rxjs";
import { DeviceModelId } from "@ledgerhq/devices";
import { restoreAppDataUseCase } from "./restoreAppDataUseCase";
import { restoreAppData } from "./restoreAppData";
Expand All @@ -28,13 +28,6 @@ export function restoreAppDataUseCaseDI(
storageProvider: StorageProvider<AppStorageType>,
): Observable<RestoreAppDataEvent | DeleteAppDataEvent> {
return restoreAppDataUseCase(appName, deviceModelId, storageProvider, data =>
restoreAppData(
transport,
appName,
deviceModelId,
storageProvider,
data,
deleteAppDataUseCaseDI,
),
);
restoreAppData(transport, appName, data),
).pipe(switchMap(() => deleteAppDataUseCaseDI(appName, deviceModelId, storageProvider)));
}

0 comments on commit c9b5761

Please sign in to comment.