@ledgerhq/live-common 26.0.0 → 26.1.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +1 -1
  2. package/lib/__tests__/hw/getDeviceInfo.js +152 -0
  3. package/lib/__tests__/hw/getDeviceInfo.js.map +1 -1
  4. package/lib/api/Manager.d.ts +2 -1
  5. package/lib/api/Manager.d.ts.map +1 -1
  6. package/lib/api/Manager.js +37 -2
  7. package/lib/api/Manager.js.map +1 -1
  8. package/lib/ble/hooks/useBleDevicePairing.d.ts +18 -0
  9. package/lib/ble/hooks/useBleDevicePairing.d.ts.map +1 -0
  10. package/lib/ble/hooks/useBleDevicePairing.js +57 -0
  11. package/lib/ble/hooks/useBleDevicePairing.js.map +1 -0
  12. package/lib/ble/hooks/useBleDevicePairing.test.d.ts +2 -0
  13. package/lib/ble/hooks/useBleDevicePairing.test.d.ts.map +1 -0
  14. package/lib/ble/hooks/useBleDevicePairing.test.js +159 -0
  15. package/lib/ble/hooks/useBleDevicePairing.test.js.map +1 -0
  16. package/lib/ble/hooks/useBleDevicesScanning.d.ts +25 -0
  17. package/lib/ble/hooks/useBleDevicesScanning.d.ts.map +1 -0
  18. package/lib/ble/hooks/useBleDevicesScanning.js +117 -0
  19. package/lib/ble/hooks/useBleDevicesScanning.js.map +1 -0
  20. package/lib/ble/hooks/useBleDevicesScanning.test.d.ts +2 -0
  21. package/lib/ble/hooks/useBleDevicesScanning.test.d.ts.map +1 -0
  22. package/lib/ble/hooks/useBleDevicesScanning.test.js +281 -0
  23. package/lib/ble/hooks/useBleDevicesScanning.test.js.map +1 -0
  24. package/lib/ble/types.d.ts +21 -0
  25. package/lib/ble/types.d.ts.map +1 -0
  26. package/lib/ble/types.js +3 -0
  27. package/lib/ble/types.js.map +1 -0
  28. package/lib/errors.d.ts +2 -0
  29. package/lib/errors.d.ts.map +1 -1
  30. package/lib/errors.js +4 -2
  31. package/lib/errors.js.map +1 -1
  32. package/lib/families/cardano/js-estimateMaxSpendable.d.ts.map +1 -1
  33. package/lib/families/cardano/js-estimateMaxSpendable.js +2 -0
  34. package/lib/families/cardano/js-estimateMaxSpendable.js.map +1 -1
  35. package/lib/families/cosmos/specs.js +2 -2
  36. package/lib/families/cosmos/specs.js.map +1 -1
  37. package/lib/families/elrond/specs.js +1 -1
  38. package/lib/families/elrond/specs.js.map +1 -1
  39. package/lib/families/polkadot/bridge.integration.test.js +3 -3
  40. package/lib/families/polkadot/bridge.integration.test.js.map +1 -1
  41. package/lib/families/polkadot/specs.d.ts.map +1 -1
  42. package/lib/families/polkadot/specs.js +4 -3
  43. package/lib/families/polkadot/specs.js.map +1 -1
  44. package/lib/featureFlags/provider.d.ts +5 -1
  45. package/lib/featureFlags/provider.d.ts.map +1 -1
  46. package/lib/featureFlags/provider.js +6 -4
  47. package/lib/featureFlags/provider.js.map +1 -1
  48. package/lib/featureFlags/useFeature.d.ts.map +1 -1
  49. package/lib/featureFlags/useFeature.js +3 -1
  50. package/lib/featureFlags/useFeature.js.map +1 -1
  51. package/lib/generated/account.d.ts.map +1 -1
  52. package/lib/generated/account.js.map +1 -1
  53. package/lib/generated/bridge/js.d.ts.map +1 -1
  54. package/lib/generated/bridge/js.js.map +1 -1
  55. package/lib/generated/bridge/mock.d.ts.map +1 -1
  56. package/lib/generated/bridge/mock.js.map +1 -1
  57. package/lib/generated/cli-transaction.d.ts.map +1 -1
  58. package/lib/generated/cli-transaction.js.map +1 -1
  59. package/lib/generated/deviceTransactionConfig.d.ts.map +1 -1
  60. package/lib/generated/deviceTransactionConfig.js.map +1 -1
  61. package/lib/generated/exchange.d.ts.map +1 -1
  62. package/lib/generated/exchange.js.map +1 -1
  63. package/lib/generated/hw-getAddress.d.ts.map +1 -1
  64. package/lib/generated/hw-getAddress.js.map +1 -1
  65. package/lib/generated/hw-signMessage.d.ts.map +1 -1
  66. package/lib/generated/hw-signMessage.js.map +1 -1
  67. package/lib/generated/mock.d.ts.map +1 -1
  68. package/lib/generated/mock.js.map +1 -1
  69. package/lib/generated/platformAdapter.d.ts.map +1 -1
  70. package/lib/generated/platformAdapter.js.map +1 -1
  71. package/lib/generated/presync.d.ts.map +1 -1
  72. package/lib/generated/presync.js.map +1 -1
  73. package/lib/generated/specs.d.ts.map +1 -1
  74. package/lib/generated/specs.js.map +1 -1
  75. package/lib/generated/speculos-deviceActions.d.ts.map +1 -1
  76. package/lib/generated/speculos-deviceActions.js.map +1 -1
  77. package/lib/generated/transaction.d.ts.map +1 -1
  78. package/lib/generated/transaction.js.map +1 -1
  79. package/lib/generated/types.d.ts.map +1 -1
  80. package/lib/hw/actions/installLanguage.d.ts +21 -0
  81. package/lib/hw/actions/installLanguage.d.ts.map +1 -0
  82. package/lib/hw/actions/installLanguage.js +260 -0
  83. package/lib/hw/actions/installLanguage.js.map +1 -0
  84. package/lib/hw/attemptToQuitApp.d.ts +10 -0
  85. package/lib/hw/attemptToQuitApp.d.ts.map +1 -0
  86. package/lib/hw/attemptToQuitApp.js +22 -0
  87. package/lib/hw/attemptToQuitApp.js.map +1 -0
  88. package/lib/hw/connectManager.d.ts +2 -5
  89. package/lib/hw/connectManager.d.ts.map +1 -1
  90. package/lib/hw/connectManager.js +2 -14
  91. package/lib/hw/connectManager.js.map +1 -1
  92. package/lib/hw/getDeviceInfo.d.ts.map +1 -1
  93. package/lib/hw/getDeviceInfo.js +6 -3
  94. package/lib/hw/getDeviceInfo.js.map +1 -1
  95. package/lib/hw/getVersion.d.ts +3 -3
  96. package/lib/hw/getVersion.d.ts.map +1 -1
  97. package/lib/hw/getVersion.js +53 -5
  98. package/lib/hw/getVersion.js.map +1 -1
  99. package/lib/hw/installLanguage.d.ts +17 -0
  100. package/lib/hw/installLanguage.d.ts.map +1 -0
  101. package/lib/hw/installLanguage.js +173 -0
  102. package/lib/hw/installLanguage.js.map +1 -0
  103. package/lib/hw/uninstallLanguage.d.ts +15 -0
  104. package/lib/hw/uninstallLanguage.d.ts.map +1 -0
  105. package/lib/hw/uninstallLanguage.js +121 -0
  106. package/lib/hw/uninstallLanguage.js.map +1 -0
  107. package/lib/manager/hooks.d.ts +2 -1
  108. package/lib/manager/hooks.d.ts.map +1 -1
  109. package/lib/manager/hooks.js +13 -1
  110. package/lib/manager/hooks.js.map +1 -1
  111. package/lib/manager/index.d.ts +2 -1
  112. package/lib/manager/index.d.ts.map +1 -1
  113. package/lib/manager/index.js +16 -0
  114. package/lib/manager/index.js.map +1 -1
  115. package/lib/manager/localization.d.ts +3 -0
  116. package/lib/manager/localization.d.ts.map +1 -0
  117. package/lib/manager/localization.js +15 -0
  118. package/lib/manager/localization.js.map +1 -0
  119. package/lib/platform/react.d.ts.map +1 -1
  120. package/lib/platform/react.js +6 -3
  121. package/lib/platform/react.js.map +1 -1
  122. package/package.json +18 -18
  123. package/src/__tests__/hw/getDeviceInfo.ts +137 -0
  124. package/src/api/Manager.ts +50 -2
  125. package/src/ble/hooks/useBleDevicePairing.test.ts +91 -0
  126. package/src/ble/hooks/useBleDevicePairing.ts +51 -0
  127. package/src/ble/hooks/useBleDevicesScanning.test.ts +252 -0
  128. package/src/ble/hooks/useBleDevicesScanning.ts +147 -0
  129. package/src/ble/types.ts +42 -0
  130. package/src/errors.ts +9 -0
  131. package/src/families/cardano/js-estimateMaxSpendable.ts +2 -0
  132. package/src/families/cosmos/specs.ts +2 -2
  133. package/src/families/elrond/specs.ts +1 -1
  134. package/src/families/polkadot/bridge.integration.test.ts +3 -3
  135. package/src/families/polkadot/specs.ts +4 -3
  136. package/src/featureFlags/provider.tsx +11 -1
  137. package/src/featureFlags/useFeature.ts +6 -2
  138. package/src/generated/account.ts +0 -8
  139. package/src/generated/bridge/js.ts +0 -18
  140. package/src/generated/bridge/mock.ts +0 -8
  141. package/src/generated/cli-transaction.ts +0 -17
  142. package/src/generated/deviceTransactionConfig.ts +15 -31
  143. package/src/generated/exchange.ts +0 -5
  144. package/src/generated/hw-getAddress.ts +0 -18
  145. package/src/generated/hw-signMessage.ts +0 -3
  146. package/src/generated/mock.ts +0 -3
  147. package/src/generated/platformAdapter.ts +0 -4
  148. package/src/generated/presync.ts +0 -1
  149. package/src/generated/specs.ts +0 -17
  150. package/src/generated/speculos-deviceActions.ts +0 -17
  151. package/src/generated/transaction.ts +0 -18
  152. package/src/generated/types.ts +7 -4
  153. package/src/hw/actions/installLanguage.ts +343 -0
  154. package/src/hw/attemptToQuitApp.ts +32 -0
  155. package/src/hw/connectManager.ts +5 -26
  156. package/src/hw/getDeviceInfo.ts +6 -0
  157. package/src/hw/getVersion.ts +77 -3
  158. package/src/hw/installLanguage.ts +172 -0
  159. package/src/hw/uninstallLanguage.ts +127 -0
  160. package/src/manager/hooks.ts +21 -1
  161. package/src/manager/index.ts +16 -0
  162. package/src/manager/localization.ts +19 -0
  163. package/src/platform/react.ts +7 -3
@@ -1,9 +1,42 @@
1
+ import { DeviceModelId, identifyTargetId } from "@ledgerhq/devices";
1
2
  import Transport from "@ledgerhq/hw-transport";
2
3
  import type { FirmwareInfo } from "@ledgerhq/types-live";
4
+ import { satisfies as versionSatisfies } from "semver";
5
+ import { isDeviceLocalizationSupported } from "../manager/localization";
3
6
 
4
- /**
5
- * Retrieve targetId and firmware version from device
6
- */
7
+ const deviceVersionRangesForBootloaderVersion: {
8
+ [key in DeviceModelId]?: string;
9
+ } = {
10
+ nanoS: ">=2.0.0",
11
+ nanoX: ">=2.0.0 || =2.1.0-lo2 || =2.1.0-lo4 || =2.1.0-lo5", // TODO: remove pre-release version
12
+ nanoSP: ">=1.0.0",
13
+ };
14
+ export const isBootloaderVersionSupported = (
15
+ seVersion: string,
16
+ modelId?: DeviceModelId
17
+ ) =>
18
+ modelId &&
19
+ deviceVersionRangesForBootloaderVersion[modelId] &&
20
+ versionSatisfies(
21
+ seVersion,
22
+ deviceVersionRangesForBootloaderVersion[modelId] as string
23
+ );
24
+
25
+ const deviceVersionRangesForHardwareVersion: {
26
+ [key in DeviceModelId]?: string;
27
+ } = {
28
+ nanoX: ">=2.0.0 || =2.1.0-lo2 || =2.1.0-lo4 || =2.1.0-lo5", // TODO: remove pre-release version
29
+ };
30
+ export const isHardwareVersionSupported = (
31
+ seVersion: string,
32
+ modelId?: DeviceModelId
33
+ ) =>
34
+ modelId &&
35
+ deviceVersionRangesForHardwareVersion[modelId] &&
36
+ versionSatisfies(
37
+ seVersion,
38
+ deviceVersionRangesForHardwareVersion[modelId] as string
39
+ );
7
40
 
8
41
  export default async function getVersion(
9
42
  transport: Transport
@@ -35,8 +68,11 @@ export default async function getVersion(
35
68
  let mcuVersion = "";
36
69
  let mcuBlVersion: string | undefined;
37
70
  let seVersion: string | undefined;
71
+ let bootloaderVersion: string | undefined;
72
+ let hardwareVersion: number | undefined;
38
73
  let mcuTargetId: number | undefined;
39
74
  let seTargetId: number | undefined;
75
+ let languageId: number | undefined;
40
76
 
41
77
  const isBootloader = (targetId & 0xf0000000) !== 0x30000000;
42
78
 
@@ -77,6 +113,41 @@ export default async function getVersion(
77
113
  mcuVersionBuf = mcuVersionBuf.slice(0, mcuVersionBuf.length - 1);
78
114
  }
79
115
  mcuVersion = mcuVersionBuf.toString();
116
+
117
+ const isOSU = rawVersion.includes("-osu");
118
+
119
+ if (!isOSU) {
120
+ const deviceModel = identifyTargetId(targetId);
121
+
122
+ if (isBootloaderVersionSupported(seVersion, deviceModel?.id)) {
123
+ const bootloaderVersionLength = data[i++];
124
+ let bootloaderVersionBuf: Buffer = Buffer.from(
125
+ data.slice(i, i + bootloaderVersionLength)
126
+ );
127
+ i += bootloaderVersionLength;
128
+
129
+ if (bootloaderVersionBuf[bootloaderVersionBuf.length - 1] === 0) {
130
+ bootloaderVersionBuf = bootloaderVersionBuf.slice(
131
+ 0,
132
+ bootloaderVersionBuf.length - 1
133
+ );
134
+ }
135
+ bootloaderVersion = bootloaderVersionBuf.toString();
136
+ }
137
+
138
+ if (isHardwareVersionSupported(seVersion, deviceModel?.id)) {
139
+ const hardwareVersionLength = data[i++];
140
+ hardwareVersion = data
141
+ .slice(i, i + hardwareVersionLength)
142
+ .readUIntBE(0, 1); // ?? string? number?
143
+ i += hardwareVersionLength;
144
+ }
145
+
146
+ if (isDeviceLocalizationSupported(seVersion, deviceModel?.id)) {
147
+ const languageIdLength = data[i++];
148
+ languageId = data.slice(i, i + languageIdLength).readUIntBE(0, 1);
149
+ }
150
+ }
80
151
  }
81
152
 
82
153
  return {
@@ -89,5 +160,8 @@ export default async function getVersion(
89
160
  mcuTargetId,
90
161
  seTargetId,
91
162
  flags,
163
+ bootloaderVersion,
164
+ hardwareVersion,
165
+ languageId,
92
166
  };
93
167
  }
@@ -0,0 +1,172 @@
1
+ import { Observable, from, of, throwError } from "rxjs";
2
+ import { catchError, concatMap, delay, mergeMap } from "rxjs/operators";
3
+ import {
4
+ DeviceOnDashboardExpected,
5
+ TransportError,
6
+ TransportStatusError,
7
+ LanguageNotFound,
8
+ } from "@ledgerhq/errors";
9
+
10
+ import ManagerAPI from "../api/Manager";
11
+ import { withDevice } from "./deviceAccess";
12
+ import getDeviceInfo from "./getDeviceInfo";
13
+ import { Language, LanguagePackage } from "@ledgerhq/types-live";
14
+ import network from "../network";
15
+ import { LanguageInstallRefusedOnDevice } from "../errors";
16
+ import getAppAndVersion from "./getAppAndVersion";
17
+ import { isDashboardName } from "./isDashboardName";
18
+ import Transport from "@ledgerhq/hw-transport";
19
+ import attemptToQuitApp, { AttemptToQuitAppEvent } from "./attemptToQuitApp";
20
+
21
+ export type InstallLanguageEvent =
22
+ | AttemptToQuitAppEvent
23
+ | {
24
+ type: "progress";
25
+ progress: number;
26
+ }
27
+ | {
28
+ type: "devicePermissionRequested";
29
+ }
30
+ | {
31
+ type: "languageInstalled";
32
+ };
33
+
34
+ export type InstallLanguageRequest = {
35
+ deviceId: string;
36
+ language: Language;
37
+ };
38
+
39
+ export default function installLanguage({
40
+ deviceId,
41
+ language,
42
+ }: InstallLanguageRequest): Observable<InstallLanguageEvent> {
43
+ const sub = withDevice(deviceId)(
44
+ (transport) =>
45
+ new Observable((subscriber) => {
46
+ const timeoutSub = of<InstallLanguageEvent>({
47
+ type: "unresponsiveDevice",
48
+ })
49
+ .pipe(delay(1000))
50
+ .subscribe((e) => subscriber.next(e));
51
+
52
+ const sub = from(getDeviceInfo(transport))
53
+ .pipe(
54
+ mergeMap(async (deviceInfo) => {
55
+ timeoutSub.unsubscribe();
56
+
57
+ if (language === "english") {
58
+ await uninstallAllLanguages(transport);
59
+
60
+ subscriber.next({
61
+ type: "languageInstalled",
62
+ });
63
+ subscriber.complete();
64
+
65
+ return;
66
+ }
67
+
68
+ const languages = await ManagerAPI.getLanguagePackagesForDevice(
69
+ deviceInfo
70
+ );
71
+
72
+ const packs: LanguagePackage[] = languages.filter(
73
+ (l: any) => l.language === language
74
+ );
75
+
76
+ if (!packs.length)
77
+ return subscriber.error(new LanguageNotFound(language));
78
+
79
+ const pack = packs[0];
80
+ const { apdu_install_url } = pack;
81
+ const url = apdu_install_url;
82
+
83
+ const { data: rawApdus } = await network({
84
+ method: "GET",
85
+ url,
86
+ });
87
+
88
+ const apdus = rawApdus.split(/\r?\n/).filter(Boolean);
89
+
90
+ await uninstallAllLanguages(transport);
91
+
92
+ for (let i = 0; i < apdus.length; i++) {
93
+ if (apdus[i].startsWith("e030")) {
94
+ subscriber.next({
95
+ type: "devicePermissionRequested",
96
+ });
97
+ }
98
+
99
+ const response = await transport.exchange(
100
+ Buffer.from(apdus[i], "hex")
101
+ );
102
+ const status = response.readUInt16BE(response.length - 2);
103
+ const statusStr = status.toString(16);
104
+
105
+ // Some error handling
106
+ if (status === 0x5501) {
107
+ return subscriber.error(
108
+ new LanguageInstallRefusedOnDevice(statusStr)
109
+ );
110
+ } else if (status !== 0x9000) {
111
+ return subscriber.error(
112
+ new TransportError("Unexpected device response", statusStr)
113
+ );
114
+ }
115
+
116
+ subscriber.next({
117
+ type: "progress",
118
+ progress: (i + 1) / apdus.length,
119
+ });
120
+ }
121
+
122
+ subscriber.next({
123
+ type: "languageInstalled",
124
+ });
125
+
126
+ subscriber.complete();
127
+ }),
128
+ catchError((e: Error) => {
129
+ if (
130
+ e instanceof DeviceOnDashboardExpected ||
131
+ (e &&
132
+ e instanceof TransportStatusError &&
133
+ [0x6e00, 0x6d00, 0x6e01, 0x6d01, 0x6d02].includes(
134
+ // @ts-expect-error typescript not checking agains the instanceof
135
+ e.statusCode
136
+ ))
137
+ ) {
138
+ return from(getAppAndVersion(transport)).pipe(
139
+ concatMap((appAndVersion) => {
140
+ return !isDashboardName(appAndVersion.name)
141
+ ? attemptToQuitApp(transport, appAndVersion)
142
+ : of<InstallLanguageEvent>({
143
+ type: "appDetected",
144
+ });
145
+ })
146
+ );
147
+ }
148
+ return throwError(e);
149
+ })
150
+ )
151
+ .subscribe(subscriber);
152
+
153
+ return () => {
154
+ timeoutSub.unsubscribe();
155
+ sub.unsubscribe();
156
+ };
157
+ })
158
+ );
159
+
160
+ return sub as Observable<InstallLanguageEvent>;
161
+ }
162
+
163
+ const uninstallAllLanguages = async (transport: Transport) => {
164
+ await transport.send(
165
+ 0xe0,
166
+ 0x33,
167
+ 0xff,
168
+ 0x00,
169
+ undefined,
170
+ [0x9000, 0x5501] // Expected responses when uninstalling.
171
+ );
172
+ };
@@ -0,0 +1,127 @@
1
+ import { Observable, from, of, throwError, EMPTY } from "rxjs";
2
+ import { catchError, concatMap, delay, mergeMap } from "rxjs/operators";
3
+ import {
4
+ DeviceOnDashboardExpected,
5
+ TransportStatusError,
6
+ } from "@ledgerhq/errors";
7
+
8
+ import { withDevice } from "./deviceAccess";
9
+ import getDeviceInfo from "./getDeviceInfo";
10
+ import { Language, languageIds } from "@ledgerhq/types-live";
11
+ import { AppAndVersion } from "./connectApp";
12
+ import appSupportsQuitApp from "../appSupportsQuitApp";
13
+ import quitApp from "./quitApp";
14
+ import getAppAndVersion from "./getAppAndVersion";
15
+ import { isDashboardName } from "./isDashboardName";
16
+
17
+ export type UninstallLanguageEvent =
18
+ | {
19
+ type: "appDetected";
20
+ }
21
+ | {
22
+ type: "unresponsiveDevice";
23
+ }
24
+ | {
25
+ type: "languageUninstalled";
26
+ };
27
+
28
+ const attemptToQuitApp = (
29
+ transport,
30
+ appAndVersion?: AppAndVersion
31
+ ): Observable<UninstallLanguageEvent> =>
32
+ appAndVersion && appSupportsQuitApp(appAndVersion)
33
+ ? from(quitApp(transport)).pipe(
34
+ concatMap(() =>
35
+ of(<UninstallLanguageEvent>{
36
+ type: "unresponsiveDevice",
37
+ })
38
+ ),
39
+ catchError((e) => throwError(e))
40
+ )
41
+ : of({
42
+ type: "appDetected",
43
+ });
44
+
45
+ export type UninstallLanguageRequest = {
46
+ deviceId: string;
47
+ language: Language;
48
+ };
49
+
50
+ export default function unistallLanguage({
51
+ deviceId,
52
+ language,
53
+ }: UninstallLanguageRequest): Observable<UninstallLanguageEvent> {
54
+ const sub = withDevice(deviceId)(
55
+ (transport) =>
56
+ new Observable<UninstallLanguageEvent>((subscriber) => {
57
+ const timeoutSub = of<UninstallLanguageEvent>({
58
+ type: "unresponsiveDevice",
59
+ })
60
+ .pipe(delay(1000))
61
+ .subscribe((e) => subscriber.next(e));
62
+
63
+ const sub = from(getDeviceInfo(transport))
64
+ .pipe(
65
+ mergeMap(async (_) => {
66
+ timeoutSub.unsubscribe();
67
+
68
+ const id = languageIds[language];
69
+
70
+ await transport.send(
71
+ 0xe0,
72
+ 0x33,
73
+ id,
74
+ 0x00,
75
+ undefined,
76
+ [0x9000, 0x5501] // Expected responses when uninstalling.
77
+ );
78
+
79
+ subscriber.next({
80
+ type: "languageUninstalled",
81
+ });
82
+
83
+ subscriber.complete();
84
+ }),
85
+ catchError((e: Error) => {
86
+ if (
87
+ e instanceof DeviceOnDashboardExpected ||
88
+ (e &&
89
+ e instanceof TransportStatusError &&
90
+ [0x6e00, 0x6d00, 0x6e01, 0x6d01, 0x6d02].includes(
91
+ // @ts-expect-error typescript not checking agains the instanceof
92
+ e.statusCode
93
+ ))
94
+ ) {
95
+ const quitAppObservable = from(
96
+ getAppAndVersion(transport)
97
+ ).pipe(
98
+ concatMap((appAndVersion) => {
99
+ return !isDashboardName(appAndVersion.name)
100
+ ? attemptToQuitApp(transport, appAndVersion)
101
+ : of<UninstallLanguageEvent>({
102
+ type: "appDetected",
103
+ });
104
+ })
105
+ );
106
+
107
+ quitAppObservable.subscribe(
108
+ (event) => subscriber.next(event),
109
+ (error) => subscriber.error(error)
110
+ );
111
+ }
112
+
113
+ subscriber.error(e);
114
+ return EMPTY;
115
+ })
116
+ )
117
+ .subscribe();
118
+
119
+ return () => {
120
+ timeoutSub.unsubscribe();
121
+ sub.unsubscribe();
122
+ };
123
+ })
124
+ );
125
+
126
+ return sub;
127
+ }
@@ -4,7 +4,11 @@ import { useEnv } from "../env.react";
4
4
  import manager from ".";
5
5
  import { getProviderId } from "./provider";
6
6
  import ManagerAPI from "../api/Manager";
7
- import type { DeviceModelInfo } from "@ledgerhq/types-live";
7
+ import type {
8
+ DeviceModelInfo,
9
+ DeviceInfo,
10
+ Language,
11
+ } from "@ledgerhq/types-live";
8
12
 
9
13
  async function hasOudatedApps({
10
14
  deviceInfo,
@@ -71,3 +75,19 @@ export function useManagerBlueDot(
71
75
  }, [dmi, forceProvider]);
72
76
  return display;
73
77
  }
78
+
79
+ export const useAvailableLanguagesForDevice = (
80
+ deviceInfo: DeviceInfo
81
+ ): Language[] => {
82
+ const [availableLanguages, setAvailableLanguages] = useState<Language[]>([]);
83
+
84
+ useEffect(() => {
85
+ if (deviceInfo) {
86
+ manager
87
+ .getAvailableLanguagesDevice(deviceInfo)
88
+ .then(setAvailableLanguages);
89
+ }
90
+ }, [deviceInfo, setAvailableLanguages]);
91
+
92
+ return availableLanguages;
93
+ };
@@ -8,6 +8,7 @@ import { listCryptoCurrencies } from "../currencies";
8
8
  import ManagerAPI from "../api/Manager";
9
9
  import { getProviderId } from "./provider";
10
10
  import type {
11
+ Language,
11
12
  ApplicationVersion,
12
13
  DeviceInfo,
13
14
  FirmwareUpdateContext,
@@ -158,6 +159,21 @@ const CacheAPI = {
158
159
  shouldFlashMCU,
159
160
  };
160
161
  },
162
+ // get list of available languages for a given deviceInfo
163
+ getAvailableLanguagesDevice: async (
164
+ deviceInfo: DeviceInfo
165
+ ): Promise<Language[]> => {
166
+ const languagePackages = await ManagerAPI.getLanguagePackagesForDevice(
167
+ deviceInfo
168
+ );
169
+ const languages = languagePackages.map((pack) => pack.language);
170
+
171
+ if (!languages.includes("english")) {
172
+ languages.push("english"); // english is always available
173
+ }
174
+
175
+ return languages;
176
+ },
161
177
  // get list of apps for a given deviceInfo
162
178
  getAppsList: async (
163
179
  deviceInfo: DeviceInfo,
@@ -0,0 +1,19 @@
1
+ import { DeviceModelId } from "@ledgerhq/devices";
2
+ import { satisfies as versionSatisfies } from "semver";
3
+
4
+ const deviceVersionRangesForLocalization: { [key in DeviceModelId]?: string } =
5
+ {
6
+ nanoX: ">=2.1.0 || =2.1.0-lo2 || =2.1.0-lo4 || =2.1.0-lo5",
7
+ nanoSP: ">=1.1.0 || =1.1.0-lo1",
8
+ };
9
+
10
+ export const isDeviceLocalizationSupported = (
11
+ seVersion: string,
12
+ modelId?: DeviceModelId
13
+ ) =>
14
+ modelId &&
15
+ deviceVersionRangesForLocalization[modelId] &&
16
+ versionSatisfies(
17
+ seVersion,
18
+ deviceVersionRangesForLocalization[modelId] as string
19
+ );
@@ -30,14 +30,18 @@ export function usePlatformUrl(
30
30
 
31
31
  if (inputs) {
32
32
  for (const key in inputs) {
33
- if (Object.prototype.hasOwnProperty.call(inputs, key)) {
33
+ if (
34
+ Object.prototype.hasOwnProperty.call(inputs, key) &&
35
+ inputs[key] !== undefined
36
+ ) {
34
37
  url.searchParams.set(key, inputs[key]);
35
38
  }
36
39
  }
37
40
  }
38
41
 
39
- url.searchParams.set("backgroundColor", params.background);
40
- url.searchParams.set("textColor", params.text);
42
+ if (params.background)
43
+ url.searchParams.set("backgroundColor", params.background);
44
+ if (params.text) url.searchParams.set("textColor", params.text);
41
45
  if (params.loadDate) {
42
46
  url.searchParams.set("loadDate", params.loadDate.valueOf().toString());
43
47
  }