@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
178 lines (162 loc) • 4.49 kB
text/typescript
import { Observable } from "rxjs";
import { scan, tap } from "rxjs/operators";
import { useCallback, useEffect, useState } from "react";
import { log } from "@ledgerhq/logs";
import type { DeviceInfo } from "@ledgerhq/types-live";
import { useReplaySubject } from "../../observable";
import type {
FetchImageEvent,
FetchImageRequest,
Input as FetchImageInput,
} from "../customLockScreenFetch";
import type { Action, Device } from "./types";
import { currentMode } from "./app";
import { getImplementation } from "./implementations";
type State = {
isLoading: boolean;
requestQuitApp: boolean;
unresponsive: boolean;
fetchingImage?: boolean;
imageFetched?: boolean;
device: Device | null | undefined;
deviceInfo: DeviceInfo | null | undefined;
error: Error | null | undefined;
hexImage?: string | undefined;
imgHash?: string | undefined;
progress?: number;
completed?: boolean;
imageAlreadyBackedUp?: boolean;
};
type ActionState = State & {
onRetry: () => void;
};
type FetchImageAction = Action<FetchImageRequest, ActionState, boolean>;
const mapResult = ({ completed, imageFetched, imageAlreadyBackedUp }: State) =>
completed || imageFetched || imageAlreadyBackedUp || null;
type Event =
| FetchImageEvent
| {
type: "error";
error: Error;
}
| {
type: "deviceChange";
device: Device | null | undefined;
};
const getInitialState = (device?: Device | null | undefined): State => ({
isLoading: !!device,
requestQuitApp: false,
unresponsive: false,
fetchingImage: false,
device,
deviceInfo: null,
error: null,
imageAlreadyBackedUp: false,
});
const reducer = (state: State, e: Event): State => {
switch (e.type) {
case "unresponsiveDevice":
return { ...state, unresponsive: true, isLoading: false };
case "deviceChange":
return getInitialState(e.device);
case "error":
return {
...getInitialState(state.device),
error: e.error,
isLoading: false,
};
case "appDetected":
return {
...state,
error: null,
unresponsive: false,
requestQuitApp: true,
isLoading: false,
};
case "imageFetched":
return {
...state,
error: null,
unresponsive: false,
isLoading: false,
fetchingImage: false,
imageFetched: true,
hexImage: e.hexImage,
};
case "currentImageHash":
return {
...state,
error: null,
unresponsive: false,
isLoading: false,
fetchingImage: true,
imgHash: e.imgHash,
};
case "imageAlreadyBackedUp":
return {
...state,
error: null,
unresponsive: false,
isLoading: false,
fetchingImage: false,
imageFetched: false,
imageAlreadyBackedUp: true,
completed: true,
};
case "progress":
return {
...state,
error: null,
unresponsive: false,
isLoading: false,
fetchingImage: true,
progress: e.progress,
};
}
return state; // Nb Needed until e2e tests are better handled.
};
export const createAction = (
task: (arg0: FetchImageInput) => Observable<FetchImageEvent>,
): FetchImageAction => {
const useHook = (device: Device | null | undefined, request: FetchImageRequest): ActionState => {
const [state, setState] = useState(() => getInitialState(device));
const [resetIndex, setResetIndex] = useState(0);
const deviceSubject = useReplaySubject(device);
useEffect(() => {
if (state.imageFetched || state.imageAlreadyBackedUp || state.completed) return;
const impl = getImplementation(currentMode)<FetchImageEvent, FetchImageRequest>({
deviceSubject,
task,
request,
});
const sub = impl
.pipe(
tap((e: any) => log("actions-fetch-custom-lock-screen-event", e.type, e)),
scan(reducer, getInitialState()),
)
.subscribe(setState);
return () => {
sub.unsubscribe();
};
}, [
deviceSubject,
request,
state.completed,
state.imageAlreadyBackedUp,
state.imageFetched,
resetIndex,
]);
const onRetry = useCallback(() => {
setResetIndex(currIndex => currIndex + 1);
setState(s => getInitialState(s.device));
}, []);
return {
...state,
onRetry,
};
};
return {
useHook,
mapResult,
};
};