@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
342 lines • 15 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectAppEventMapper = void 0;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const device_management_kit_1 = require("@ledgerhq/device-management-kit");
const errors_1 = require("@ledgerhq/errors");
const ids_1 = require("@ledgerhq/client-ids/ids");
const types_1 = require("../apps/types");
const getDeviceInfo_1 = require("../deviceSDK/tasks/getDeviceInfo");
const errors_2 = require("../errors");
class ConnectAppEventMapper {
dmk;
sessionId;
appName;
events;
openAppRequested = false;
permissionRequested = false;
lastSeenDeviceSent = false;
installPlan = null;
deviceId = undefined;
eventSubject = new rxjs_1.Subject();
constructor(dmk, sessionId, appName, events) {
this.dmk = dmk;
this.sessionId = sessionId;
this.appName = appName;
this.events = events;
}
map() {
const cancelAction = this.events.cancel;
const unsubscribe = new rxjs_1.Subject();
// Create a shared observable for device session state
const deviceSessionState = this.dmk
.getDeviceSessionState({ sessionId: this.sessionId })
.pipe((0, rxjs_1.share)());
// Subscribe to device action events
this.events.observable
.pipe((0, operators_1.withLatestFrom)(deviceSessionState), (0, operators_1.tap)(([event, deviceState]) => this.handleEvent(event, deviceState)), (0, rxjs_1.takeUntil)(unsubscribe), (0, operators_1.catchError)(error => this.handleError(error)))
.subscribe();
// Subscribe to device session state events
deviceSessionState
.pipe((0, operators_1.tap)(deviceState => this.handleDeviceState(deviceState)), (0, rxjs_1.takeUntil)(unsubscribe))
.subscribe();
return new rxjs_1.Observable(observer => {
const sub = this.eventSubject.subscribe(observer);
return () => {
sub.unsubscribe();
cancelAction();
unsubscribe.next();
};
});
}
handleDeviceState(deviceState) {
if (deviceState.sessionStateType === device_management_kit_1.DeviceSessionStateType.Connected) {
return;
}
if (deviceState.firmwareVersion?.metadata &&
deviceState.firmwareUpdateContext &&
!this.lastSeenDeviceSent) {
this.lastSeenDeviceSent = true;
this.eventSubject.next({
type: "device-update-last-seen",
deviceInfo: this.mapDeviceInfo(deviceState.firmwareVersion.metadata),
latestFirmware: this.mapLatestFirmware(deviceState.firmwareUpdateContext),
});
}
}
handleEvent(event, deviceState) {
switch (event.status) {
case device_management_kit_1.DeviceActionStatus.Pending:
this.handlePendingEvent(event.intermediateValue);
break;
case device_management_kit_1.DeviceActionStatus.Completed:
this.handleCompletedEvent(event.output, deviceState);
break;
case device_management_kit_1.DeviceActionStatus.Error:
this.handleErrorEvent(event.error, deviceState);
break;
case device_management_kit_1.DeviceActionStatus.NotStarted:
case device_management_kit_1.DeviceActionStatus.Stopped:
this.eventSubject.error(new Error("Unexpected device action status"));
break;
}
}
handlePendingEvent(intermediateValue) {
switch (intermediateValue.requiredUserInteraction) {
case device_management_kit_1.UserInteractionRequired.ConfirmOpenApp:
if (!this.openAppRequested) {
this.openAppRequested = true;
this.eventSubject.next({ type: "ask-open-app", appName: this.appName });
}
break;
case device_management_kit_1.UserInteractionRequired.AllowSecureConnection:
if (!this.permissionRequested) {
this.permissionRequested = true;
this.eventSubject.next({ type: "device-permission-requested" });
}
break;
case device_management_kit_1.UserInteractionRequired.UnlockDevice:
this.eventSubject.next({ type: "lockedDevice" });
break;
case device_management_kit_1.UserInteractionRequired.None:
if (this.openAppRequested) {
this.openAppRequested = false;
this.eventSubject.next({ type: "device-permission-granted" });
}
if (this.permissionRequested) {
this.permissionRequested = false;
this.eventSubject.next({ type: "device-permission-granted" });
// Simulate apps listing (not systematic step in LDMK)
this.eventSubject.next({ type: "listing-apps" });
}
if (intermediateValue.installPlan !== null) {
this.handleInstallPlan(intermediateValue.installPlan);
}
if (intermediateValue.deviceId) {
const deviceIdString = Buffer.from(intermediateValue.deviceId).toString("hex");
if (deviceIdString !== this.deviceId) {
this.deviceId = deviceIdString;
this.eventSubject.next({
type: "device-id",
deviceId: ids_1.DeviceId.fromString(deviceIdString),
});
}
}
break;
case "device-deprecation":
if (intermediateValue.deviceDeprecation) {
this.eventSubject.next({
type: "deprecation",
deprecate: {
...intermediateValue.deviceDeprecation,
},
});
}
}
}
handleInstallPlan(installPlan) {
// Handle install plan resolved events
if (this.installPlan === null) {
// Skipped applications
const alreadyInstalled = this.mapSkippedApps(installPlan.alreadyInstalled, types_1.SkipReason.AppAlreadyInstalled);
const missing = this.mapSkippedApps(installPlan.missingApplications, types_1.SkipReason.NoSuchAppOnProvider);
const skippedAppOps = [...alreadyInstalled, ...missing];
if (skippedAppOps.length > 0) {
this.eventSubject.next({
type: "some-apps-skipped",
skippedAppOps,
});
}
// Install queue content
this.eventSubject.next({
type: "listed-apps",
installQueue: installPlan.installPlan.map(app => app.versionName),
});
}
// Handle ongoing install events
this.eventSubject.next({
type: "inline-install",
progress: installPlan.currentProgress,
itemProgress: installPlan.currentIndex,
currentAppOp: {
type: "install",
name: installPlan.installPlan[installPlan.currentIndex].versionName,
},
installQueue: installPlan.installPlan
.map(app => app.versionName)
.slice(installPlan.currentIndex),
});
this.installPlan = installPlan;
}
handleCompletedEvent(output, deviceState) {
if (deviceState.sessionStateType !== device_management_kit_1.DeviceSessionStateType.Connected) {
// Handle opened app
const currentApp = deviceState.currentApp;
let flags = 0;
if (typeof currentApp.flags === "number") {
flags = currentApp.flags;
}
else if (currentApp.flags !== undefined) {
flags = Buffer.from(currentApp.flags);
}
this.eventSubject.next({
type: "opened",
app: {
name: currentApp.name,
version: currentApp.version,
flags,
},
derivation: output.derivation ? { address: output.derivation } : undefined,
});
}
this.eventSubject.complete();
}
handleErrorEvent(error, deviceState) {
if (error instanceof device_management_kit_1.OutOfMemoryDAError && this.installPlan !== null) {
const appNames = this.installPlan.installPlan.map(app => app.versionName);
this.eventSubject.next({
type: "app-not-installed",
appNames,
appName: appNames[0],
});
this.eventSubject.complete();
}
else if ("_tag" in error &&
error._tag === "UnsupportedFirmwareDAError" &&
deviceState.sessionStateType !== device_management_kit_1.DeviceSessionStateType.Connected) {
this.eventSubject.error(new errors_1.LatestFirmwareVersionRequired("LatestFirmwareVersionRequired", {
current: deviceState.firmwareUpdateContext.currentFirmware.version,
latest: deviceState.firmwareUpdateContext?.availableUpdate?.finalFirmware.version ||
deviceState.firmwareUpdateContext.currentFirmware.version,
}));
}
else if ("_tag" in error &&
error._tag === "UnsupportedApplicationDAError" &&
deviceState.sessionStateType !== device_management_kit_1.DeviceSessionStateType.Connected) {
if (deviceState.deviceModelId === device_management_kit_1.DeviceModelId.NANO_S) {
// This will show an error modal with upsell link
this.eventSubject.error(new errors_2.NoSuchAppOnProvider(`Ledger Nano S does not support this feature`, {
appName: this.appName,
}));
return;
}
// This will show an error modal with contact support link
this.eventSubject.error(new errors_1.UnsupportedFeatureError(`App ${this.appName} not supported on this device`, {
appName: this.appName,
deviceModelId: deviceState.deviceModelId,
deviceVersion: deviceState.firmwareVersion?.os,
}));
}
else if ("_tag" in error && error._tag === "DeviceLockedError") {
this.eventSubject.next({ type: "lockedDevice" });
this.eventSubject.complete();
}
else if ("_tag" in error && error._tag === "RefusedByUserDAError") {
this.eventSubject.error(new errors_1.UserRefusedAllowManager());
}
else if ("_tag" in error && error._tag === "DeviceDisconnectedWhileSendingError") {
this.eventSubject.next({ type: "disconnected", expected: false });
}
else if ("_tag" in error && error._tag === "WebHidSendReportError") {
this.eventSubject.next({ type: "disconnected", expected: false });
this.eventSubject.complete();
}
else if ("errorCode" in error && typeof error.errorCode === "string" && "_tag" in error) {
this.eventSubject.error(this.mapDeviceError(error.errorCode, error._tag));
}
else {
this.eventSubject.error(error);
}
}
handleError(error) {
this.eventSubject.error(error);
return (0, rxjs_1.of)();
}
mapDeviceError(errorCode, defaultMessage) {
switch (errorCode) {
case "5501":
return new errors_1.UserRefusedOnDevice();
default:
return new Error(defaultMessage);
}
}
mapSkippedApps(appNames, reason) {
return appNames.map(name => ({
reason,
appOp: {
type: "install",
name,
},
}));
}
mapDeviceInfo(osVersion) {
return (0, getDeviceInfo_1.parseDeviceInfo)({
isBootloader: osVersion.isBootloader,
rawVersion: osVersion.seVersion, // Always the SE version since at this step we cannot be in bootloader mode
targetId: osVersion.targetId,
seVersion: osVersion.seVersion,
seTargetId: osVersion.seTargetId,
mcuBlVersion: undefined, // We cannot be in bootloader mode at this step
mcuVersion: osVersion.mcuSephVersion,
mcuTargetId: osVersion.mcuTargetId,
flags: Buffer.from(osVersion.seFlags),
bootloaderVersion: osVersion.mcuBootloaderVersion,
hardwareVersion: parseInt(osVersion.hwVersion, 16),
languageId: osVersion.langId,
});
}
mapLatestFirmware(updateContext) {
if (!updateContext.availableUpdate) {
return null;
}
const availableUpdate = updateContext.availableUpdate;
const osu = availableUpdate.osuFirmware;
const final = availableUpdate.finalFirmware;
return {
osu: {
id: osu.id,
perso: osu.perso,
firmware: osu.firmware,
firmware_key: osu.firmwareKey,
hash: osu.hash || "",
next_se_firmware_final_version: osu.nextFinalFirmware,
// Following fields are inherited from dto, but unused
description: undefined,
display_name: undefined,
notes: undefined,
name: "",
date_creation: "",
date_last_modified: "",
device_versions: [],
providers: [],
previous_se_firmware_final_version: [],
},
final: {
id: final.id,
name: final.version,
version: final.version,
perso: final.perso,
firmware: final.firmware || "",
firmware_key: final.firmwareKey || "",
hash: final.hash || "",
bytes: final.bytes || 0,
mcu_versions: final.mcuVersions,
// Following fields are inherited from dto, but unused
description: undefined,
display_name: undefined,
notes: undefined,
se_firmware: 0,
date_creation: "",
date_last_modified: "",
device_versions: [],
application_versions: [],
osu_versions: [],
providers: [],
},
shouldFlashMCU: availableUpdate.mcuUpdateRequired,
};
}
}
exports.ConnectAppEventMapper = ConnectAppEventMapper;
//# sourceMappingURL=connectAppEventMapper.js.map