@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
221 lines (199 loc) • 5.83 kB
text/typescript
import { useState, useReducer, useEffect, useMemo } from "react";
import type { Exec, State, Action, ListAppsResult } from "./types";
import type { AppType, FilterOptions, SortOptions } from "./filtering";
import { useSortedFilteredApps } from "./filtering";
import {
reducer,
initState,
getNextAppOp,
isOutOfMemoryState,
predictOptimisticState,
} from "./logic";
import { runAppOp } from "./runner";
import { App } from "@ledgerhq/types-live";
import { useFeatureFlags } from "../featureFlags";
type UseAppsRunnerResult = [State, (arg0: Action) => void];
// use for React apps. support dynamic change of the state.
export const useAppsRunner = (
listResult: ListAppsResult,
exec: Exec,
appsToRestore?: string[],
): UseAppsRunnerResult => {
// $FlowFixMe for ledger-live-mobile older react/flow version
const [state, dispatch] = useReducer(reducer, null, () => initState(listResult, appsToRestore));
const nextAppOp = useMemo(() => getNextAppOp(state), [state]);
const appOp = state.currentAppOp || nextAppOp;
useEffect(() => {
dispatch({
type: "reset",
initialState: initState(listResult, appsToRestore),
});
}, [listResult, appsToRestore]);
useEffect(() => {
if (appOp) {
const sub = runAppOp({ state, appOp, exec }).subscribe(event => {
dispatch({
type: "onRunnerEvent",
event,
});
});
return () => {
sub.unsubscribe();
};
} // we only want to redo the effect on appOp changes here
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listResult, appOp, exec]);
return [state, dispatch];
};
export function useNotEnoughMemoryToInstall(state: State, name: string): boolean {
return useMemo(
() =>
isOutOfMemoryState(
predictOptimisticState(
reducer(state, {
type: "install",
name,
}),
),
),
[name, state],
);
}
type AppsSections = {
catalog: App[];
device: App[];
update: App[];
};
type AppsSectionsOpts = {
query: string;
appFilter: AppType;
sort: SortOptions;
};
export function useAppsSections(state: State, opts: AppsSectionsOpts): AppsSections {
const { updateAllQueue, appByName, installed, installQueue, apps } = state;
const appsUpdating = useMemo(
() => updateAllQueue.map(name => appByName[name]).filter(Boolean),
[appByName, updateAllQueue],
);
const updatableAppList = useMemo(
() => apps.filter(({ name }) => installed.some(i => i.name === name && !i.updated)),
[apps, installed],
);
const { getFeature, isFeature } = useFeatureFlags();
const update = appsUpdating.length > 0 ? appsUpdating : updatableAppList;
const filterParam: FilterOptions = useMemo(
() => ({
query: opts.query,
installedApps: installed,
type: [opts.appFilter],
getFeature,
isFeature,
}),
[getFeature, installed, isFeature, opts.appFilter, opts.query],
);
const catalog = useSortedFilteredApps(apps, filterParam, opts.sort);
const installedAppList = useSortedFilteredApps(
apps,
{
query: opts.query,
installedApps: installed,
installQueue,
type: ["installed"],
getFeature,
isFeature,
},
{
type: "default",
order: "asc",
},
);
const device = installedAppList.sort(({ name: _name }, { name }) => {
// place install queue on top of list
// with the app being installed at the top
let pos1 = installQueue.indexOf(_name);
let pos2 = installQueue.indexOf(name);
pos1 = pos1 < 0 ? Number.MAX_VALUE : pos1;
pos2 = pos2 < 0 ? Number.MAX_VALUE : pos2;
return pos1 - pos2;
});
return {
update,
catalog,
device,
};
}
export function useAppInstallProgress(state: State, name: string): number {
const { currentProgressSubject, currentAppOp } = state;
const [progress, setProgress] = useState(0);
useEffect(() => {
if (!currentAppOp || !currentProgressSubject || currentAppOp.name !== name) {
setProgress(0);
return;
}
const sub = currentProgressSubject.subscribe(setProgress);
return () => sub.unsubscribe();
}, [currentProgressSubject, currentAppOp, name]);
if (currentProgressSubject && currentAppOp && currentAppOp.name === name) {
return progress;
}
return 0;
}
// if the app needs deps to be installed, we want to display a modal
// this should returns all params the modal also need (so we don't do things twice)
export function useAppInstallNeedsDeps(
state: State,
app: App,
):
| {
app: App;
dependencies: App[];
}
| null
| undefined {
const { appByName, installed: installedList, installQueue } = state;
const res = useMemo(() => {
const dependencies = (app.dependencies || [])
.map(name => appByName[name])
.filter(
dep =>
dep &&
!installedList.some(app => app.name === dep.name) &&
!installQueue.includes(dep.name),
);
if (dependencies.length) {
return {
app,
dependencies,
};
}
return null;
}, [appByName, app, installQueue, installedList]);
return res;
}
// if the app needs deps to be installed, we want to display a modal
// this should returns all params the modal also need (so we don't do things twice)
export function useAppUninstallNeedsDeps(
state: State,
app: App,
):
| {
dependents: App[];
app: App;
}
| null
| undefined {
const { apps, installed } = state;
const res = useMemo(() => {
const dependents = apps.filter(
a => installed.some(i => i.name === a.name) && a.dependencies.includes(app.name),
);
if (dependents.length) {
return {
dependents,
app,
};
}
return null;
}, [app, apps, installed]);
return res;
}