@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
165 lines (151 loc) • 5.72 kB
text/typescript
import { useCallback, useEffect, useState, useRef } from "react";
import { UnresponsiveDeviceError, UserRefusedAllowManager } from "@ledgerhq/errors";
import type { DeviceId } from "@ledgerhq/types-live";
import { getGenuineCheckFromDeviceId as defaultGetGenuineCheckFromDeviceId } from "../getGenuineCheckFromDeviceId";
import type {
GetGenuineCheckFromDeviceIdArgs,
GetGenuineCheckFromDeviceIdResult,
GetGenuineCheckFromDeviceIdOutput,
} from "../getGenuineCheckFromDeviceId";
import { isDmkError } from "../../deviceSDK/tasks/core";
import { DmkError } from "@ledgerhq/device-management-kit";
export type GenuineState = "unchecked" | "genuine" | "non-genuine";
export type DevicePermissionState =
| "unrequested"
| "unlock-needed"
| "unlocked"
| "requested"
| "granted"
| "refused";
export type UseGenuineCheckArgs = {
isHookEnabled?: boolean;
deviceId: DeviceId;
deviceName: string | null;
lockedDeviceTimeoutMs?: number;
permissionTimeoutMs?: number;
};
export type UseGenuineCheckDependencies = {
getGenuineCheckFromDeviceId?: (
args: GetGenuineCheckFromDeviceIdArgs,
) => GetGenuineCheckFromDeviceIdOutput;
};
export type UseGenuineCheckResult = {
genuineState: GenuineState;
devicePermissionState: DevicePermissionState;
error: Error | DmkError | null;
resetGenuineCheckState: () => void;
};
const SOCKET_EVENT_PAYLOAD_GENUINE = "0000";
const clearTimeoutRef = (timeoutRef: { current: NodeJS.Timeout | null }) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
const isDmkDeviceDisconnectedError = (e: unknown): e is DmkError =>
isDmkError(e) && e._tag === "DeviceDisconnectedWhileSendingError";
/**
* Hook to check that a device is genuine
* It replaces a DeviceAction if we're only interested in getting the genuine check
* @param getGenuineCheckFromDeviceId An optional function to get a genuine check for a given device id,
* by default set to live-common/hw/getGenuineCheckFromDeviceId.
* This dependency injection is needed for LLD to have the hook working on the internal thread
* @param isHookEnabled A boolean to enable (true, default value) or disable (false) the hook
* @param deviceId A device id, or an empty string if device is usb plugged
* @param lockedDeviceTimeoutMs Time of no response from device after which the device is considered locked, in ms. Default 1000ms.
* @param permissionTimeoutMs Time to wait for a device response to a permission request, in ms. Default 60s.
* @returns An object containing:
* - genuineState: the current GenuineState
* - devicePermissionState: the current DevicePermissionState
* - error: any error that occurred during the genuine check, or null
*/
export const useGenuineCheck = ({
getGenuineCheckFromDeviceId = defaultGetGenuineCheckFromDeviceId,
isHookEnabled = true,
deviceId,
deviceName,
lockedDeviceTimeoutMs = 1000,
permissionTimeoutMs = 60 * 1000,
}: UseGenuineCheckArgs & UseGenuineCheckDependencies): UseGenuineCheckResult => {
const [genuineState, setGenuineState] = useState<GenuineState>("unchecked");
const [devicePermissionState, setDevicePermissionState] =
useState<DevicePermissionState>("unrequested");
const [error, setError] = useState<Error | DmkError | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const resetGenuineCheckState = useCallback(() => {
setDevicePermissionState("unrequested");
setGenuineState("unchecked");
setError(null);
}, []);
useEffect(() => {
if (!isHookEnabled) {
return;
}
const sub = getGenuineCheckFromDeviceId({
deviceId,
deviceName,
lockedDeviceTimeoutMs,
}).subscribe({
next: ({ socketEvent, lockedDevice }: GetGenuineCheckFromDeviceIdResult) => {
if (socketEvent) {
switch (socketEvent.type) {
case "device-permission-requested":
setDevicePermissionState("requested");
timeoutRef.current = setTimeout(() => {
setError(new UnresponsiveDeviceError());
clearTimeoutRef(timeoutRef);
}, permissionTimeoutMs);
break;
case "device-permission-granted":
clearTimeoutRef(timeoutRef);
setDevicePermissionState("granted");
break;
case "result":
clearTimeoutRef(timeoutRef);
if (socketEvent.payload === SOCKET_EVENT_PAYLOAD_GENUINE) {
setGenuineState("genuine");
} else {
setGenuineState("non-genuine");
}
break;
}
} else {
clearTimeoutRef(timeoutRef);
// If no socketEvent, the device is locked or has been unlocked
if (lockedDevice) {
setDevicePermissionState("unlock-needed");
} else {
setDevicePermissionState("unlocked");
}
}
},
error: (e: any) => {
clearTimeoutRef(timeoutRef);
if (e instanceof UserRefusedAllowManager) {
setDevicePermissionState("refused");
} else if (e instanceof Error || isDmkDeviceDisconnectedError(e)) {
// Probably an error of type DisconnectedDeviceDuringOperation or something else
setError(e);
} else {
setError(new Error(`Unknown error: ${e}`));
}
},
});
return () => {
sub.unsubscribe();
};
}, [
isHookEnabled,
deviceId,
deviceName,
lockedDeviceTimeoutMs,
getGenuineCheckFromDeviceId,
permissionTimeoutMs,
]);
return {
genuineState,
devicePermissionState,
error,
resetGenuineCheckState,
};
};