@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
149 lines (134 loc) • 5.53 kB
text/typescript
import { useState, useEffect, useCallback } from "react";
import { Subscription } from "rxjs";
import isEqual from "lodash/isEqual";
import type { Device } from "../../hw/actions/types";
import { DeviceOnboardingStatePollingError } from "@ledgerhq/errors";
import type {
OnboardingStatePollingResult,
GetOnboardingStatePollingArgs,
GetOnboardingStatePollingResult,
} from "../../hw/getOnboardingStatePolling";
import { getOnboardingStatePolling as defaultGetOnboardingStatePolling } from "../../hw/getOnboardingStatePolling";
import { OnboardingState } from "../../hw/extractOnboardingState";
export type UseOnboardingStatePollingResult = OnboardingStatePollingResult & {
fatalError: Error | null;
resetStates: () => void;
};
export type UseOnboardingStatePollingDependencies = {
getOnboardingStatePolling?: (
args: GetOnboardingStatePollingArgs,
) => GetOnboardingStatePollingResult;
};
export type UseOnboardingStatePollingArgs = UseOnboardingStatePollingDependencies & {
device: Device | null;
pollingPeriodMs: number;
stopPolling?: boolean;
allowedErrorChecks?: ((error: unknown) => boolean)[];
};
/**
* Polls the current device onboarding state, and notify the hook consumer of
* any allowed errors and fatal errors
* @param getOnboardingStatePolling A polling function, by default set to live-common/hw/getOnboardingStatePolling.
* This dependency injection is needed for LLD to have the polling working on the internal thread
* @param device A Device object
* @param pollingPeriodMs The period in ms after which the device onboarding state is fetched again
* @param stopPolling Flag to stop or continue the polling
* @returns An object containing:
* - onboardingState: the device state during the onboarding
* - allowedError: any error that is allowed and does not stop the polling
* - fatalError: any error that is fatal and stops the polling
* - lockedDevice: a boolean set to true if the device is currently locked, false otherwise
* - resetStates: a function to reset the values of: onboardingState, allowedError, fatalError and lockedDevice
*/
export const useOnboardingStatePolling = ({
getOnboardingStatePolling = defaultGetOnboardingStatePolling,
device,
pollingPeriodMs,
stopPolling = false,
allowedErrorChecks = [],
}: UseOnboardingStatePollingArgs): UseOnboardingStatePollingResult => {
const [onboardingState, setOnboardingState] = useState<OnboardingState | null>(null);
const [allowedError, setAllowedError] = useState<Error | null>(null);
const [fatalError, setFatalError] = useState<Error | null>(null);
const [lockedDevice, setLockedDevice] = useState<boolean>(false);
useEffect(() => {
let onboardingStatePollingSubscription: Subscription;
// If stopPolling is updated and set to true, the useEffect hook will call its
// cleanup function (return) and the polling won't restart with the below condition
if (device && !stopPolling) {
onboardingStatePollingSubscription = getOnboardingStatePolling({
deviceId: device.deviceId,
pollingPeriodMs,
allowedErrorChecks,
}).subscribe({
next: (onboardingStatePollingResult: OnboardingStatePollingResult) => {
if (onboardingStatePollingResult) {
setFatalError(null);
setLockedDevice(onboardingStatePollingResult.lockedDevice);
// Only updates if the new allowedError is different
setAllowedError(prevAllowedError =>
getNewAllowedErrorIfChanged(
prevAllowedError,
onboardingStatePollingResult.allowedError,
),
);
// Does not update the onboarding state if an allowed error occurred
if (!onboardingStatePollingResult.allowedError) {
// Only updates if the new onboardingState is different
setOnboardingState(prevOnboardingState =>
getNewOnboardingStateIfChanged(
prevOnboardingState,
onboardingStatePollingResult.onboardingState,
),
);
}
}
},
error: error => {
setAllowedError(null);
setLockedDevice(false);
setFatalError(
error instanceof Error
? error
: new DeviceOnboardingStatePollingError(
`Error from: ${error?.name ?? error} ${error?.message}`,
),
);
},
});
}
return () => {
onboardingStatePollingSubscription?.unsubscribe();
};
}, [
device,
pollingPeriodMs,
setOnboardingState,
setAllowedError,
setFatalError,
stopPolling,
getOnboardingStatePolling,
]);
const resetStates = useCallback(() => {
setOnboardingState(null);
setAllowedError(null);
setFatalError(null);
setLockedDevice(false);
}, []);
return { onboardingState, allowedError, fatalError, lockedDevice, resetStates };
};
const getNewOnboardingStateIfChanged = (
prevOnboardingState: OnboardingState | null,
newOnboardingState: OnboardingState | null,
): OnboardingState | null => {
return isEqual(prevOnboardingState, newOnboardingState)
? prevOnboardingState
: newOnboardingState;
};
const getNewAllowedErrorIfChanged = (
prevError: Error | null,
newError: Error | null,
): Error | null => {
// Only interested if the errors are instances of the same Error class
return prevError?.constructor === newError?.constructor ? prevError : newError;
};