diff --git a/.changeset/onboarded-status-from-os-version.md b/.changeset/onboarded-status-from-os-version.md new file mode 100644 index 0000000000..dad1e79c4f --- /dev/null +++ b/.changeset/onboarded-status-from-os-version.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-management-kit": minor +--- + +Detect onboarded device status from `GetOsVersionCommand` (`secureElementFlags.isOnboarded`) in `GetDeviceStatusDeviceAction`. The action now returns `Left(DeviceNotOnboardedError)` for unseeded devices instead of a stubbed `true`. `OpenAppDeviceAction` no longer runs a separate stubbed onboarding check and inherits the real one from its child machine. On success in a ready state, `firmwareVersion` (with `metadata`) and `isSecureConnectionAllowed` are persisted in the session state so consumers can read onboarding without an extra command. diff --git a/packages/device-management-kit/src/api/command/os/__mocks__/GetOsVersionCommand.ts b/packages/device-management-kit/src/api/command/os/__mocks__/GetOsVersionCommand.ts index bbe2395a5f..54f6cdc585 100644 --- a/packages/device-management-kit/src/api/command/os/__mocks__/GetOsVersionCommand.ts +++ b/packages/device-management-kit/src/api/command/os/__mocks__/GetOsVersionCommand.ts @@ -79,6 +79,7 @@ export const getOsVersionCommandResponseMockBuilder = ( isMcuCodeSigned: true, isInRecoveryMode: false, }, + ...props, }, [DeviceModelId.STAX]: { isBootloader: false, diff --git a/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts index 72a550f5c0..da6ad26833 100644 --- a/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts +++ b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts @@ -1,6 +1,8 @@ import { interval, Observable } from "rxjs"; import { CommandResultFactory } from "@api/command/model/CommandResult"; +import { getOsVersionCommandResponseMockBuilder } from "@api/command/os/__mocks__/GetOsVersionCommand"; +import { type GetOsVersionResponse } from "@api/command/os/GetOsVersionCommand"; import { GLOBAL_ERRORS, GlobalCommandError, @@ -24,20 +26,42 @@ import { getDeviceStatusDAStateStep, } from "./types"; +const osVersionCommandResult = ( + props: Partial = {}, + deviceModelId: DeviceModelId = DeviceModelId.NANO_X, +) => + CommandResultFactory({ + data: getOsVersionCommandResponseMockBuilder(deviceModelId, props), + }); + +const onboardCheckPendingState = (): GetDeviceStatusDAState => ({ + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + step: getDeviceStatusDAStateStep.ONBOARD_CHECK, + }, + status: DeviceActionStatus.Pending, +}); + +const onboardCheckPendingStatesWithOsVersionFetch = + (): Array => [ + onboardCheckPendingState(), + onboardCheckPendingState(), + ]; + describe("GetDeviceStatusDeviceAction", () => { const getAppAndVersionMock = vi.fn(); + const getOsVersionMock = vi.fn(); const getDeviceSessionStateMock = vi.fn(); const waitForDeviceUnlockMock = vi.fn(); const setDeviceSessionState = vi.fn(); - const isDeviceOnboardedMock = vi.fn(); function extractDependenciesMock() { return { getAppAndVersion: getAppAndVersionMock, + getOsVersion: getOsVersionMock, getDeviceSessionState: getDeviceSessionStateMock, waitForDeviceUnlock: waitForDeviceUnlockMock, setDeviceSessionState: setDeviceSessionState, - isDeviceOnboarded: isDeviceOnboardedMock, }; } @@ -48,7 +72,7 @@ describe("GetDeviceStatusDeviceAction", () => { } = makeDeviceActionInternalApiMock(); beforeEach(() => { vi.resetAllMocks(); - isDeviceOnboardedMock.mockReturnValue(true); + getOsVersionMock.mockResolvedValue(osVersionCommandResult()); }); describe("without overriding `extractDependencies`", () => { @@ -64,23 +88,19 @@ describe("GetDeviceStatusDeviceAction", () => { deviceModelId: DeviceModelId.NANO_X, }); - sendCommandMock.mockResolvedValue( - CommandResultFactory({ - data: { - name: "BOLOS", - version: "1.0.0", - }, - }), - ); + sendCommandMock + .mockResolvedValueOnce(osVersionCommandResult()) + .mockResolvedValue( + CommandResultFactory({ + data: { + name: "BOLOS", + version: "1.0.0", + }, + }), + ); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { output: { currentApp: "BOLOS", @@ -117,6 +137,7 @@ describe("GetDeviceStatusDeviceAction", () => { }); sendCommandMock + .mockResolvedValueOnce(osVersionCommandResult()) .mockResolvedValueOnce( CommandResultFactory({ error: new GlobalCommandError({ @@ -135,13 +156,7 @@ describe("GetDeviceStatusDeviceAction", () => { ); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { intermediateValue: { requiredUserInteraction: UserInteractionRequired.UnlockDevice, @@ -192,6 +207,7 @@ describe("GetDeviceStatusDeviceAction", () => { }); sendCommandMock + .mockResolvedValueOnce(osVersionCommandResult()) .mockResolvedValueOnce( CommandResultFactory({ error: new GlobalCommandError({ @@ -210,13 +226,7 @@ describe("GetDeviceStatusDeviceAction", () => { ); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { intermediateValue: { requiredUserInteraction: UserInteractionRequired.UnlockDevice, @@ -253,23 +263,19 @@ describe("GetDeviceStatusDeviceAction", () => { deviceModelId: DeviceModelId.NANO_X, }); - sendCommandMock.mockResolvedValue( - CommandResultFactory({ - error: new GlobalCommandError({ - ...GLOBAL_ERRORS["6e00"], - errorCode: "6e00", + sendCommandMock + .mockResolvedValueOnce(osVersionCommandResult()) + .mockResolvedValue( + CommandResultFactory({ + error: new GlobalCommandError({ + ...GLOBAL_ERRORS["6e00"], + errorCode: "6e00", + }), }), - }), - ); + ); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { output: { currentApp: "BOLOS", @@ -319,13 +325,7 @@ describe("GetDeviceStatusDeviceAction", () => { ).mockReturnValue(extractDependenciesMock()); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { status: DeviceActionStatus.Completed, output: { @@ -363,6 +363,7 @@ describe("GetDeviceStatusDeviceAction", () => { getDeviceSessionStateMock.mockReturnValue({ sessionStateType: DeviceSessionStateType.Connected, deviceStatus: DeviceStatus.CONNECTED, + deviceModelId: DeviceModelId.NANO_X, }); getAppAndVersionMock.mockResolvedValue( @@ -383,14 +384,11 @@ describe("GetDeviceStatusDeviceAction", () => { "extractDependencies", ).mockReturnValue(extractDependenciesMock()); + const osVersionData = getOsVersionCommandResponseMockBuilder( + DeviceModelId.NANO_X, + ); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { status: DeviceActionStatus.Completed, output: { @@ -408,6 +406,7 @@ describe("GetDeviceStatusDeviceAction", () => { onDone: () => { // Session should be set as ready if GetAppAndVersionCommand was successful expect(setDeviceSessionState).toHaveBeenCalledWith({ + deviceModelId: DeviceModelId.NANO_X, sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, @@ -416,7 +415,14 @@ describe("GetDeviceStatusDeviceAction", () => { version: "1.0.0", }, installedApps: [], - isSecureConnectionAllowed: false, + isSecureConnectionAllowed: + osVersionData.secureElementFlags.isSecureConnectionAllowed, + firmwareVersion: { + mcu: osVersionData.mcuSephVersion, + bootloader: osVersionData.mcuBootloaderVersion, + os: osVersionData.seVersion, + metadata: osVersionData, + }, }); resolve(); }, @@ -497,13 +503,7 @@ describe("GetDeviceStatusDeviceAction", () => { ).mockReturnValue(extractDependenciesMock()); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { intermediateValue: { requiredUserInteraction: UserInteractionRequired.UnlockDevice, @@ -546,8 +546,18 @@ describe("GetDeviceStatusDeviceAction", () => { sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.LOCKED, currentApp: { name: "mockedCurrentApp", version: "1.0.0" }, + installedApps: [], + deviceModelId: DeviceModelId.NANO_X, + isSecureConnectionAllowed: false, }); - isDeviceOnboardedMock.mockReturnValue(false); + getOsVersionMock.mockResolvedValue( + osVersionCommandResult({ + secureElementFlags: { + ...getOsVersionCommandResponseMockBuilder().secureElementFlags, + isOnboarded: false, + }, + }), + ); const getDeviceStateDeviceAction = new GetDeviceStatusDeviceAction({ input: { unlockTimeout: 500 }, @@ -559,6 +569,7 @@ describe("GetDeviceStatusDeviceAction", () => { ).mockReturnValue(extractDependenciesMock()); const expectedStates: Array = [ + onboardCheckPendingState(), { error: new DeviceNotOnboardedError(), status: DeviceActionStatus.Error, @@ -576,6 +587,62 @@ describe("GetDeviceStatusDeviceAction", () => { ); })); + it("should end in an error if cached metadata says the device is not onboarded", () => + new Promise((resolve, reject) => { + const osVersionData = getOsVersionCommandResponseMockBuilder( + DeviceModelId.NANO_X, + { + secureElementFlags: { + ...getOsVersionCommandResponseMockBuilder().secureElementFlags, + isOnboarded: false, + }, + }, + ); + getDeviceSessionStateMock.mockReturnValue({ + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, + deviceStatus: DeviceStatus.CONNECTED, + currentApp: { name: "mockedCurrentApp", version: "1.0.0" }, + installedApps: [], + deviceModelId: DeviceModelId.NANO_X, + isSecureConnectionAllowed: false, + firmwareVersion: { + mcu: osVersionData.mcuSephVersion, + bootloader: osVersionData.mcuBootloaderVersion, + os: osVersionData.seVersion, + metadata: osVersionData, + }, + }); + + const getDeviceStateDeviceAction = new GetDeviceStatusDeviceAction({ + input: { unlockTimeout: 500 }, + }); + + vi.spyOn( + getDeviceStateDeviceAction, + "extractDependencies", + ).mockReturnValue(extractDependenciesMock()); + + const expectedStates: Array = [ + { + error: new DeviceNotOnboardedError(), + status: DeviceActionStatus.Error, + }, + ]; + + testDeviceActionStates( + getDeviceStateDeviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + { + onDone: () => { + expect(getOsVersionMock).not.toHaveBeenCalled(); + resolve(); + }, + onError: reject, + }, + ); + })); + it("should end in an error if the device is locked and the user does not unlock", () => new Promise((resolve, reject) => { getDeviceSessionStateMock.mockReturnValue({ @@ -626,13 +693,7 @@ describe("GetDeviceStatusDeviceAction", () => { ).mockReturnValue(extractDependenciesMock()); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { intermediateValue: { requiredUserInteraction: UserInteractionRequired.UnlockDevice, @@ -718,13 +779,7 @@ describe("GetDeviceStatusDeviceAction", () => { ).mockReturnValue(extractDependenciesMock()); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { error, status: DeviceActionStatus.Error, @@ -771,13 +826,7 @@ describe("GetDeviceStatusDeviceAction", () => { ).mockReturnValue(extractDependenciesMock()); const expectedStates: Array = [ - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - status: DeviceActionStatus.Pending, - }, + ...onboardCheckPendingStatesWithOsVersionFetch(), { error: new UnknownDAError("error"), status: DeviceActionStatus.Error, @@ -807,27 +856,23 @@ describe("GetDeviceStatusDeviceAction", () => { isSecureConnectionAllowed: false, }); - sendCommandMock.mockResolvedValue( - CommandResultFactory({ - data: { - name: "BOLOS", - version: "1.0.0", - }, - }), - ); + sendCommandMock + .mockResolvedValueOnce(osVersionCommandResult()) + .mockResolvedValue( + CommandResultFactory({ + data: { + name: "BOLOS", + version: "1.0.0", + }, + }), + ); const getDeviceStateDeviceAction = new GetDeviceStatusDeviceAction({ input: { unlockTimeout: 500 }, }); const expectedStates: Array = [ - { - status: DeviceActionStatus.Pending, // get app and version - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - step: getDeviceStatusDAStateStep.ONBOARD_CHECK, - }, - }, + onboardCheckPendingState(), { status: DeviceActionStatus.Stopped, }, diff --git a/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts index 6657dda17e..a168feb50e 100644 --- a/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts +++ b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts @@ -17,6 +17,11 @@ import { GetAppAndVersionCommand, type GetAppAndVersionCommandResult, } from "@api/command/os/GetAppAndVersionCommand"; +import { + GetOsVersionCommand, + type GetOsVersionCommandResult, + type GetOsVersionResponse, +} from "@api/command/os/GetOsVersionCommand"; import { DeviceStatus } from "@api/device/DeviceStatus"; import { type InternalApi } from "@api/device-action/DeviceAction"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; @@ -33,6 +38,7 @@ import { import { type DeviceSessionState, DeviceSessionStateType, + type FirmwareVersion, } from "@api/device-session/DeviceSessionState"; import { @@ -48,11 +54,13 @@ type GetDeviceStatusMachineInternalState = { readonly locked: boolean; readonly currentApp: string | null; readonly currentAppVersion: string | null; + readonly osVersionMetadata: GetOsVersionResponse | null; readonly error: GetDeviceStatusDAError | null; }; export type MachineDependencies = { readonly getAppAndVersion: () => Promise; + readonly getOsVersion: () => Promise; readonly getDeviceSessionState: () => DeviceSessionState; readonly waitForDeviceUnlock: (args: { input: { unlockTimeout: number }; @@ -60,9 +68,17 @@ export type MachineDependencies = { readonly setDeviceSessionState: ( state: DeviceSessionState, ) => DeviceSessionState; - readonly isDeviceOnboarded: () => boolean; }; +const firmwareVersionFromOsVersion = ( + data: GetOsVersionResponse, +): FirmwareVersion => ({ + mcu: data.mcuSephVersion, + bootloader: data.mcuBootloaderVersion, + os: data.seVersion, + metadata: data, +}); + export type ExtractMachineDependencies = ( internalApi: InternalApi, ) => MachineDependencies; @@ -93,14 +109,35 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< const { getAppAndVersion, + getOsVersion, getDeviceSessionState, setDeviceSessionState, waitForDeviceUnlock, - isDeviceOnboarded, } = this.extractDependencies(internalApi); const unlockTimeout = this.input.unlockTimeout ?? DEFAULT_UNLOCK_TIMEOUT_MS; + const updateSessionFromOsVersion = (data: GetOsVersionResponse) => { + const currentState = getDeviceSessionState(); + if (currentState.sessionStateType === DeviceSessionStateType.Connected) { + return; + } + setDeviceSessionState({ + ...currentState, + firmwareVersion: firmwareVersionFromOsVersion(data), + isSecureConnectionAllowed: + data.secureElementFlags.isSecureConnectionAllowed, + }); + }; + + const getCachedOnboardingMetadata = () => { + const state = getDeviceSessionState(); + if (state.sessionStateType === DeviceSessionStateType.Connected) { + return undefined; + } + return state.firmwareVersion?.metadata; + }; + return setup({ types: { input: { @@ -111,10 +148,24 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< }, actors: { getAppAndVersion: fromPromise(getAppAndVersion), + getOsVersion: fromPromise(getOsVersion), waitForDeviceUnlock: fromObservable(waitForDeviceUnlock), }, guards: { - isDeviceOnboarded: () => isDeviceOnboarded(), // TODO: we don't have this info for now, this can be derived from the "flags" obtained in the getVersion command + isOnboardedFromCachedMetadata: () => { + const metadata = getCachedOnboardingMetadata(); + return metadata?.secureElementFlags.isOnboarded === true; + }, + isNotOnboardedFromCachedMetadata: () => { + const metadata = getCachedOnboardingMetadata(); + return ( + metadata !== undefined && !metadata.secureElementFlags.isOnboarded + ); + }, + needsOsVersionFetch: () => getCachedOnboardingMetadata() === undefined, + isOnboardedFromOsVersion: ({ context }) => + context._internalState.osVersionMetadata?.secureElementFlags + .isOnboarded === true, isDeviceLocked: ({ context }) => context._internalState.locked, hasError: ({ context }) => context._internalState.error !== null, }, @@ -173,7 +224,7 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< step: getDeviceStatusDAStateStep.ONBOARD_CHECK, }, _internalState: { - onboarded: false, // we don't know how to check yet + onboarded: false, locked: false, currentApp: sessionStateType === @@ -181,6 +232,7 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< ? sessionState.currentApp.name : null, currentAppVersion: null, + osVersionMetadata: null, error: null, }, }; @@ -192,12 +244,63 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< }, }, OnboardingCheck: { - // TODO: we don't have this info for now always: [ { - guard: { - type: "isDeviceOnboarded", - }, + guard: "isOnboardedFromCachedMetadata", + target: "AppAndVersionCheck", + actions: assign({ + _internalState: (_) => ({ + ..._.context._internalState, + onboarded: true, + }), + }), + }, + { + guard: "isNotOnboardedFromCachedMetadata", + target: "Error", + actions: "assignErrorDeviceNotOnboarded", + }, + { + guard: "needsOsVersionFetch", + target: "GetOsVersion", + }, + ], + }, + GetOsVersion: { + invoke: { + src: "getOsVersion", + onDone: { + target: "OnboardingResultCheck", + actions: assign({ + _internalState: (_) => { + if (isSuccessCommandResult(_.event.output)) { + updateSessionFromOsVersion(_.event.output.data); + return { + ..._.context._internalState, + osVersionMetadata: _.event.output.data, + }; + } + return { + ..._.context._internalState, + error: _.event.output.error, + }; + }, + }), + }, + onError: { + target: "Error", + actions: "assignErrorFromEvent", + }, + }, + }, + OnboardingResultCheck: { + always: [ + { + guard: "hasError", + target: "Error", + }, + { + guard: "isOnboardedFromOsVersion", target: "AppAndVersionCheck", actions: assign({ _internalState: (_) => ({ @@ -257,6 +360,8 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< currentApp: _.event.output.data, }); } else { + const osVersionMetadata = + _.context._internalState.osVersionMetadata; // The device can be set to Ready if GetAppAndVersionCommand was successful setDeviceSessionState({ deviceModelId: state.deviceModelId, @@ -265,7 +370,15 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< deviceStatus: DeviceStatus.CONNECTED, currentApp: _.event.output.data, installedApps: [], - isSecureConnectionAllowed: false, + isSecureConnectionAllowed: + osVersionMetadata?.secureElementFlags + .isSecureConnectionAllowed ?? false, + ...(osVersionMetadata !== null + ? { + firmwareVersion: + firmwareVersionFromOsVersion(osVersionMetadata), + } + : {}), }); } return { @@ -378,11 +491,11 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction< return { getAppAndVersion, + getOsVersion: () => internalApi.sendCommand(new GetOsVersionCommand()), waitForDeviceUnlock, getDeviceSessionState: () => internalApi.getDeviceSessionState(), setDeviceSessionState: (state: DeviceSessionState) => internalApi.setDeviceSessionState(state), - isDeviceOnboarded: () => true, // TODO: we don't have this info for now }; } } diff --git a/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts index 428ebac095..45cad3f809 100644 --- a/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts +++ b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts @@ -33,16 +33,13 @@ describe("OpenAppDeviceAction", () => { const closeAppMock = vi.fn(); const getDeviceSessionStateMock = vi.fn(); const setDeviceSessionStateMock = vi.fn(); - const isDeviceOnboardedMock = vi.fn(); function extractDependenciesMock() { return { getDeviceSessionState: getDeviceSessionStateMock, setDeviceSessionState: setDeviceSessionStateMock, - getAppAndVersion: getAppAndVersionMock, openApp: openAppMock, closeApp: closeAppMock, - isDeviceOnboarded: isDeviceOnboardedMock, }; } @@ -51,7 +48,6 @@ describe("OpenAppDeviceAction", () => { beforeEach(() => { vi.resetAllMocks(); - isDeviceOnboardedMock.mockReturnValue(true); }); describe("without overriding `extractDependencies`", () => { @@ -423,22 +419,35 @@ describe("OpenAppDeviceAction", () => { describe("errors cases", () => { it("should end in an error if the device is not onboarded", () => new Promise((resolve, reject) => { - getDeviceSessionStateMock.mockReturnValue({ + apiGetDeviceSessionStateMock.mockReturnValue({ sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, deviceStatus: DeviceStatus.CONNECTED, currentApp: { name: "mockedCurrentApp", version: "1.0.0" }, + installedApps: [], + deviceModelId: DeviceModelId.NANO_X, + isSecureConnectionAllowed: false, }); - isDeviceOnboardedMock.mockReturnValue(false); + setupGetDeviceStatusMock([new DeviceNotOnboardedError()]); const openAppDeviceAction = new OpenAppDeviceAction({ input: { appName: "Bitcoin" }, }); - vi.spyOn(openAppDeviceAction, "extractDependencies").mockReturnValue( - extractDependenciesMock(), - ); - const expectedStates: Array = [ + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + step: openAppDAStateStep.GET_DEVICE_STATUS, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + step: getDeviceStatusDAStateStep.ONBOARD_CHECK, + }, + }, { error: new DeviceNotOnboardedError(), status: DeviceActionStatus.Error, diff --git a/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts index 4ff094b49b..9fa147232f 100644 --- a/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts +++ b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts @@ -13,7 +13,6 @@ import { import { type InternalApi } from "@api/device-action/DeviceAction"; import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired"; import { DEFAULT_UNLOCK_TIMEOUT_MS } from "@api/device-action/os/Const"; -import { DeviceNotOnboardedError } from "@api/device-action/os/Errors"; import { GetDeviceStatusDeviceAction } from "@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction"; import { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes"; import { @@ -47,7 +46,6 @@ export type MachineDependencies = { }) => Promise; readonly getDeviceSessionState: () => DeviceSessionState; readonly setDeviceSessionState: (state: DeviceSessionState) => void; - readonly isDeviceOnboarded: () => boolean; }; export type ExtractMachineDependencies = ( @@ -96,13 +94,8 @@ export class OpenAppDeviceAction extends XStateDeviceAction< OpenAppStateMachineInternalState >; - const { - closeApp, - openApp, - getDeviceSessionState, - isDeviceOnboarded, - setDeviceSessionState, - } = this.extractDependencies(internalApi); + const { closeApp, openApp, getDeviceSessionState, setDeviceSessionState } = + this.extractDependencies(internalApi); const unlockTimeout = this.input.unlockTimeout ?? DEFAULT_UNLOCK_TIMEOUT_MS; @@ -124,7 +117,6 @@ export class OpenAppDeviceAction extends XStateDeviceAction< getDeviceStatus: getDeviceStatusMachine, }, guards: { - isDeviceOnboarded: () => isDeviceOnboarded(), // TODO: we don't have this info for now, this can be derived from the "flags" obtained in the getVersion command isRequestedAppOpen: ({ context }: { context: types["context"] }) => { if (context._internalState.currentlyRunningApp === null) return false; return ( @@ -143,12 +135,6 @@ export class OpenAppDeviceAction extends XStateDeviceAction< hasError: ({ context }) => context._internalState.error !== null, }, actions: { - assignErrorDeviceNotOnboarded: assign({ - _internalState: (_) => ({ - ..._.context._internalState, - error: new DeviceNotOnboardedError(), - }), - }), assignUserActionNeededOpenApp: assign({ intermediateValue: (_) => ({ @@ -201,28 +187,11 @@ export class OpenAppDeviceAction extends XStateDeviceAction< }, states: { DeviceReady: { - // check device capabilities & status known always: { - target: "OnboardingCheck", + target: "GetDeviceStatus", }, }, - OnboardingCheck: { - // check onboarding status provided by device session - always: [ - { - target: "GetDeviceStatus", - guard: { - type: "isDeviceOnboarded", - }, - }, - { - target: "Error", - actions: "assignErrorDeviceNotOnboarded", - }, - ], - }, - GetDeviceStatus: { // We run the GetDeviceStatus flow to get information about the device state invoke: { @@ -442,7 +411,6 @@ export class OpenAppDeviceAction extends XStateDeviceAction< getDeviceSessionState: () => internalApi.getDeviceSessionState(), setDeviceSessionState: (state: DeviceSessionState) => internalApi.setDeviceSessionState(state), - isDeviceOnboarded: () => true, // TODO: we don't have this info for now, this can be derived from the "flags" obtained in the getVersion command }; } }