@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
395 lines • 15.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAction = exports.currentMode = void 0;
exports.setDeviceMode = setDeviceMode;
exports.dependenciesToAppRequests = dependenciesToAppRequests;
const invariant_1 = __importDefault(require("invariant"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const react_1 = require("react");
const logs_1 = require("@ledgerhq/logs");
const derivation_1 = require("@ledgerhq/ledger-wallet-framework/derivation");
const observable_1 = require("../../observable");
const apps_1 = require("../../apps");
const account_1 = __importDefault(require("../../generated/account"));
const implementations_1 = require("./implementations");
const accountName_1 = require("@ledgerhq/live-wallet/accountName");
const mapResult = ({ opened, device, appAndVersion, displayUpgradeWarning, }) => opened && device && !displayUpgradeWarning
? {
device,
appAndVersion,
}
: null;
const getInitialState = (device, request) => ({
isLoading: !!device,
requestQuitApp: false,
requestOpenApp: null,
unresponsive: false,
isLocked: false,
requiresAppInstallation: null,
allowOpeningRequestedWording: null,
allowOpeningGranted: false,
allowManagerRequested: false,
allowManagerGranted: false,
device: null,
deviceInfo: null,
deviceId: null,
latestFirmware: null,
opened: false,
appAndVersion: null,
error: null,
derivation: null,
displayUpgradeWarning: false,
installingApp: false,
listingApps: false,
request,
currentAppOp: undefined,
installQueue: [],
listedApps: false, // Nb maybe expose the result
skippedAppOps: [],
itemProgress: 0,
progress: undefined,
});
const reducer = (state, e) => {
switch (e.type) {
case "deprecation":
return { ...state, deviceDeprecationRules: e.deprecate };
case "unresponsiveDevice":
return { ...state, unresponsive: true };
case "lockedDevice":
return { ...state, isLocked: true };
// This event does not set isLocked and unresponsive properties, as
// by itself it does not request anything from the device
case "device-update-last-seen":
return {
...state,
deviceInfo: e.deviceInfo,
latestFirmware: e.latestFirmware,
};
case "disconnected":
// disconnected event can happen for example:
// - when the wired device is unplugged
// - before a ask-open-app event when an other app is already open
return {
...getInitialState(null, state.request),
isLoading: !!e.expected,
};
case "deviceChange":
return { ...getInitialState(e.device, state.request), device: e.device };
case "some-apps-skipped":
return {
...state,
skippedAppOps: e.skippedAppOps,
installQueue: state.installQueue,
};
case "inline-install":
return {
...getInitialState(state.device, state.request),
isLoading: false,
allowOpeningGranted: true,
allowManagerRequested: false,
allowManagerGranted: true,
device: state.device,
installingApp: true,
progress: e.progress || 0,
deviceInfo: undefined,
latestFirmware: undefined,
request: state.request,
skippedAppOps: state.skippedAppOps,
currentAppOp: e.currentAppOp,
listedApps: state.listedApps,
itemProgress: e.itemProgress || 0,
installQueue: e.installQueue || [],
};
case "listing-apps":
return {
...state,
listedApps: false,
listingApps: true,
unresponsive: false,
isLocked: false,
};
case "error":
return {
...getInitialState(state.device, state.request),
device: state.device || null,
error: e.error,
isLoading: false,
listingApps: false,
request: state.request,
skippedAppOps: state.skippedAppOps,
};
case "ask-open-app":
return {
...getInitialState(state.device, state.request),
isLoading: false,
device: state.device,
requestOpenApp: e.appName,
deviceInfo: undefined,
latestFirmware: undefined,
installingApp: undefined,
listingApps: undefined,
installQueue: undefined,
listedApps: undefined,
itemProgress: undefined,
skippedAppOps: state.skippedAppOps,
};
case "ask-quit-app":
return {
...getInitialState(state.device, state.request),
isLoading: false,
device: state.device,
requestQuitApp: true,
installingApp: undefined,
listingApps: undefined,
installQueue: undefined,
listedApps: undefined,
itemProgress: undefined,
skippedAppOps: state.skippedAppOps,
};
case "device-permission-requested":
return {
...getInitialState(state.device, state.request),
isLoading: false,
device: state.device,
allowManagerRequested: true,
deviceInfo: undefined,
latestFirmware: undefined,
installingApp: undefined,
listingApps: undefined,
listedApps: undefined,
itemProgress: undefined,
skippedAppOps: state.skippedAppOps,
installQueue: state.installQueue,
};
case "device-permission-granted":
return {
...getInitialState(state.device, state.request),
isLoading: false,
device: state.device,
allowOpeningGranted: true,
allowManagerGranted: true,
deviceInfo: undefined,
latestFirmware: undefined,
installingApp: undefined,
listingApps: undefined,
itemProgress: undefined,
skippedAppOps: state.skippedAppOps,
installQueue: state.installQueue,
listedApps: state.listedApps,
};
case "device-id":
return {
...state,
deviceId: e.deviceId,
};
case "app-not-installed":
return {
...getInitialState(state.device, state.request),
isLoading: false,
device: state.device,
requiresAppInstallation: {
appNames: e.appNames,
appName: e.appName,
},
deviceInfo: undefined,
latestFirmware: undefined,
installingApp: undefined,
listingApps: undefined,
installQueue: undefined,
listedApps: undefined,
itemProgress: undefined,
skippedAppOps: state.skippedAppOps,
};
case "listed-apps":
return {
...state,
listedApps: true,
installQueue: e.installQueue,
};
case "opened":
return {
...getInitialState(state.device, state.request),
isLoading: false,
device: state.device,
opened: true,
appAndVersion: e.app,
derivation: e.derivation,
deviceInfo: undefined,
latestFirmware: undefined,
installingApp: undefined,
listingApps: undefined,
installQueue: undefined,
itemProgress: undefined,
request: state.request,
skippedAppOps: state.skippedAppOps,
listedApps: state.listedApps,
displayUpgradeWarning: state.device && e.app ? (0, apps_1.shouldUpgrade)(e.app.name, e.app.version) : false,
};
}
return state;
};
/**
* Map between an AppRequest and a ConnectAppRequest, allowing us to
* specify an account or a currency without resolving manually the actual
* applications we depend on in order to access the flow.
*/
function inferCommandParams(appRequest) {
let derivationMode;
let derivationPath;
const { account, requireLatestFirmware, allowPartialDependencies = false, dependencies: appDependencies, } = appRequest;
let { appName, currency } = appRequest;
if (!currency && account) {
currency = account.currency;
}
if (!appName && currency) {
appName = currency.managerAppName;
}
(0, invariant_1.default)(appName, "appName or currency or account is missing");
let dependencies = undefined;
if (appDependencies) {
dependencies = appDependencies.map(d => inferCommandParams(d).appName);
}
if (!currency) {
return {
appName,
dependencies,
requireLatestFirmware,
allowPartialDependencies,
};
}
let extra;
if (account) {
derivationMode = account.derivationMode;
derivationPath = account.freshAddressPath;
const m = account_1.default[account.currency.family];
if (m && m.injectGetAddressParams) {
extra = m.injectGetAddressParams(account);
}
}
else {
const modes = (0, derivation_1.getDerivationModesForCurrency)(currency);
derivationMode = modes[modes.length - 1];
derivationPath = (0, derivation_1.runDerivationScheme)((0, derivation_1.getDerivationScheme)({
currency,
derivationMode,
}), currency);
}
return {
appName,
dependencies,
requireLatestFirmware,
requiresDerivation: {
derivationMode,
path: derivationPath,
currencyId: currency.id,
...extra,
},
allowPartialDependencies,
};
}
exports.currentMode = "event";
function setDeviceMode(mode) {
exports.currentMode = mode;
}
const createAction = (connectAppExec) => {
const useHook = (device, appRequest) => {
const dependenciesResolvedRef = (0, react_1.useRef)(false);
const firmwareResolvedRef = (0, react_1.useRef)(false);
const outdatedAppRef = (0, react_1.useRef)(undefined);
const request = (0, react_1.useMemo)(() => inferCommandParams(appRequest), // for now i don't have better
// eslint-disable-next-line react-hooks/exhaustive-deps
[
appRequest.appName,
appRequest.account?.id,
appRequest.currency?.id,
appRequest.dependencies,
]);
const task = (0, react_1.useCallback)(({ deviceId, deviceName, request }) => {
//To avoid redundant checks, we remove passed checks from the request.
const { dependencies, requireLatestFirmware } = request;
return connectAppExec({
deviceId,
deviceName,
request: {
...request,
dependencies: dependenciesResolvedRef.current ? undefined : dependencies,
requireLatestFirmware: firmwareResolvedRef.current ? undefined : requireLatestFirmware,
outdatedApp: outdatedAppRef.current,
},
}).pipe((0, operators_1.tap)(e => {
// These events signal the resolution of pending checks.
if (e.type === "dependencies-resolved") {
dependenciesResolvedRef.current = true;
}
else if (e.type === "latest-firmware-resolved") {
firmwareResolvedRef.current = true;
}
else if (e.type === "has-outdated-app") {
outdatedAppRef.current = e.outdatedApp;
}
}));
}, []);
// repair modal will interrupt everything and be rendered instead of the background content
const [state, setState] = (0, react_1.useState)(() => getInitialState(device));
const [resetIndex, setResetIndex] = (0, react_1.useState)(0);
const deviceSubject = (0, observable_1.useReplaySubject)(device);
(0, react_1.useEffect)(() => {
if (state.opened)
return;
const impl = (0, implementations_1.getImplementation)(exports.currentMode)({
deviceSubject,
task,
request,
});
const sub = impl
.pipe((0, operators_1.tap)((e) => (0, logs_1.log)("actions-app-event", e.type, e)), (0, operators_1.debounce)((e) => ("replaceable" in e && e.replaceable ? (0, rxjs_1.interval)(100) : (0, rxjs_1.of)(null))), (0, operators_1.scan)(reducer, getInitialState()), (0, operators_1.takeWhile)((s) => !s.requiresAppInstallation && !s.error, true))
.subscribe(setState);
return () => {
sub.unsubscribe();
};
}, [deviceSubject, state.opened, resetIndex, task, request]);
const onRetry = (0, react_1.useCallback)(() => {
// After an error we can't guarantee resolutions.
dependenciesResolvedRef.current = false;
firmwareResolvedRef.current = false;
// The nonce change triggers a refresh.
setResetIndex(i => i + 1);
setState(getInitialState(device));
}, [device]);
const passWarning = (0, react_1.useCallback)(() => {
setState(currState => ({
...currState,
displayUpgradeWarning: false,
}));
}, []);
return {
...state,
inWrongDeviceForAccount: state.derivation && appRequest.account
? state.derivation.address !== appRequest.account.freshAddress &&
state.derivation.address !== appRequest.account.seedIdentifier // Use-case added for Hedera
? {
accountName: (0, accountName_1.getDefaultAccountName)(appRequest.account),
}
: null
: null,
onRetry,
passWarning,
};
};
return {
useHook,
mapResult,
};
};
exports.createAction = createAction;
function dependenciesToAppRequests(dependencies) {
if (!dependencies) {
return [];
}
return dependencies.map(appName => ({ appName }));
}
//# sourceMappingURL=app.js.map