Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(common/listApps): custom lock screen size for all compatible models #6856

Merged
merged 3 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-houses-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-common": patch
---

Fix device available storage computation for all devices supporting custom lock screens.
3 changes: 2 additions & 1 deletion libs/ledger-live-common/src/apps/listApps/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { calculateDependencies, polyfillApp, polyfillApplication } from "../poly
import { getDeviceName } from "../../device/use-cases/getDeviceNameUseCase";
import { getLatestFirmwareForDeviceUseCase } from "../../device/use-cases/getLatestFirmwareForDeviceUseCase";
import { ManagerApiRepository } from "../../device/factories/HttpManagerApiRepositoryFactory";
import { isCustomLockScreenSupported } from "../../device/use-cases/isCustomLockScreenSupported";

const appsThatKeepChangingHashes = ["Fido U2F", "Security Key"];

Expand Down Expand Up @@ -258,7 +259,7 @@ export const listApps = (
.filter(Boolean);

let customImageBlocks = 0;
if (deviceModelId === DeviceModelId.stax && !deviceInfo.isRecoveryMode) {
if (isCustomLockScreenSupported(deviceModelId) && !deviceInfo.isRecoveryMode) {
const customImageSize = await customLockScreenFetchSize(transport);
if (customImageSize) {
customImageBlocks = Math.ceil(customImageSize / bytesPerBlock);
Expand Down
97 changes: 76 additions & 21 deletions libs/ledger-live-common/src/apps/listApps/v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,26 @@ import {
ManagerApiRepository,
StubManagerApiRepository,
} from "../../device/factories/HttpManagerApiRepositoryFactory";
import { supportedDeviceModelIds as clsSupportedDeviceModelIds } from "../../device/use-cases/isCustomLockScreenSupported";
import { DeviceModel } from "@ledgerhq/devices";
import customLockScreenFetchSize from "../../hw/customLockScreenFetchSize";
import { getDeviceName } from "../../device/use-cases/getDeviceNameUseCase";
import { currenciesByMarketcap, listCryptoCurrencies } from "../../currencies";

jest.useFakeTimers();
jest.mock("../../hw/customLockScreenFetchSize");
jest.mock("../../device/use-cases/getDeviceNameUseCase");
jest.mock("../../currencies");

const mockedCustomLockScreenFetchSize = jest.mocked(customLockScreenFetchSize);
const mockedGetDeviceName = jest.mocked(getDeviceName);
const mockedListCryptoCurrencies = jest.mocked(listCryptoCurrencies);
const mockedCurrenciesByMarketCap = jest.mocked(currenciesByMarketcap);

describe("listApps v2", () => {
let mockedManagerApiRepository: ManagerApiRepository;
let listAppsCommandSpy: jest.SpyInstance;
let listInstalledAppsSpy: jest.SpyInstance;

beforeEach(() => {
jest
Expand All @@ -21,13 +36,21 @@ describe("listApps v2", () => {
"getLatestFirmwareForDeviceUseCase",
)
.mockReturnValue(Promise.resolve(null));

mockedManagerApiRepository = new StubManagerApiRepository();
mockedGetDeviceName.mockReturnValue(Promise.resolve("Mocked device name"));
mockedCurrenciesByMarketCap.mockReturnValue(Promise.resolve([]));
mockedListCryptoCurrencies.mockReturnValue([]);

listAppsCommandSpy = jest
.spyOn(jest.requireActual("../../hw/listApps"), "default")
.mockReturnValue(Promise.resolve([]));

listInstalledAppsSpy = jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
});

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

it("should return an observable that errors if deviceInfo.isOSU is true", done => {
Expand All @@ -45,7 +68,7 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

Expand All @@ -67,7 +90,7 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

Expand All @@ -89,19 +112,14 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

it("should call hwListApps() if deviceInfo.managerAllowed is true", done => {
const listAppsCommandSpy = jest
.spyOn(jest.requireActual("../../hw/listApps"), "default")
.mockReturnValue(Promise.resolve([]));
const listInstalledAppsSpy = jest.spyOn(ManagerAPI, "listInstalledApps");

const transport = aTransportBuilder();
const deviceInfo = aDeviceInfoBuilder({
isOSU: false,
Expand Down Expand Up @@ -131,11 +149,6 @@ describe("listApps v2", () => {
});

it("should call ManagerAPI.listInstalledApps() if deviceInfo.managerAllowed is false", () => {
const listAppsCommandSpy = jest.spyOn(jest.requireActual("../../hw/listApps"), "default");
const listInstalledAppsSpy = jest
.spyOn(ManagerAPI, "listInstalledApps")
.mockReturnValue(from([]));

const transport = aTransportBuilder();
const deviceInfo = aDeviceInfoBuilder({
isOSU: false,
Expand All @@ -157,7 +170,6 @@ describe("listApps v2", () => {
});

it("should return an observable that errors if getDeviceVersion() throws", done => {
jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
jest.spyOn(mockedManagerApiRepository, "getDeviceVersion").mockImplementation(() => {
throw new Error("getDeviceVersion failed");
});
Expand All @@ -181,15 +193,14 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

it("should return an observable that errors if catalogForDevice() throws", done => {
jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
jest.spyOn(mockedManagerApiRepository, "catalogForDevice").mockImplementation(() => {
throw new Error("catalogForDevice failed");
});
Expand All @@ -213,15 +224,14 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

it("should return an observable that errors if getLanguagePackagesForDevice() throws", done => {
jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
jest
.spyOn(mockedManagerApiRepository, "getLanguagePackagesForDevice")
.mockImplementation(() => {
Expand All @@ -247,10 +257,55 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

clsSupportedDeviceModelIds.forEach(deviceModelId => {
it(`should return customImageBlocks different than 0 for a ${deviceModelId} device with a custom lock screen`, done => {
const transport = aTransportBuilder({ deviceModel: { id: deviceModelId } as DeviceModel });
const deviceInfo = aDeviceInfoBuilder({
isOSU: false,
isBootloader: false,
managerAllowed: true,
targetId: 0x33200000,
});

mockedCustomLockScreenFetchSize.mockReturnValue(Promise.resolve(10));

let gotResult = false;

listApps({
transport,
deviceInfo,
managerApiRepository: mockedManagerApiRepository,
forceProvider: 1,
}).subscribe({
next: listAppsEvent => {
if (listAppsEvent.type === "result") {
gotResult = true;
const {
result: { customImageBlocks },
} = listAppsEvent;
try {
expect(customImageBlocks).not.toBe(0);
} catch (e) {
done(e);
}
}
},
complete: () => {
gotResult ? done() : done("this observable should not complete without a result");
},
error: error => {
done(error);
},
});

jest.advanceTimersByTime(1);
});
});
});
7 changes: 4 additions & 3 deletions libs/ledger-live-common/src/apps/listApps/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getLatestFirmwareForDeviceUseCase } from "../../device/use-cases/getLat
import { getProviderIdUseCase } from "../../device/use-cases/getProviderIdUseCase";
import { mapApplicationV2ToApp } from "../polyfill";
import { ManagerApiRepository } from "../../device/factories/HttpManagerApiRepositoryFactory";
import { isCustomLockScreenSupported } from "../../device/use-cases/isCustomLockScreenSupported";

// Hash discrepancies for these apps do NOT indicate a potential update,
// these apps have a mechanism that makes their hash change every time.
Expand Down Expand Up @@ -271,14 +272,14 @@ export const listApps = ({

/**
* Obtain remaining metadata:
* - Ledger Stax custom picture: number of blocks taken in storage
* - Ledger Stax/Europa custom picture: number of blocks taken in storage
* - Installed language pack
* - Device name
* */

// Stax specific, account for the size of the CLS for the storage bar.
// Stax/Europa specific, account for the size of the CLS for the storage bar.
let customImageBlocks = 0;
if (deviceModelId === DeviceModelId.stax && !deviceInfo.isRecoveryMode) {
if (isCustomLockScreenSupported(deviceModelId) && !deviceInfo.isRecoveryMode) {
const customImageSize = await customLockScreenFetchSize(transport);
if (customImageSize) {
customImageBlocks = Math.ceil(customImageSize / bytesPerBlock);
Expand Down
Loading