@ledgerhq/live-common 24.0.0 → 24.1.0-next.0
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.
- package/lib/__tests__/range.js +8 -0
- package/lib/__tests__/range.js.map +1 -1
- package/lib/families/bitcoin/js-synchronisation.d.ts.map +1 -1
- package/lib/families/bitcoin/js-synchronisation.js +2 -173
- package/lib/families/bitcoin/js-synchronisation.js.map +1 -1
- package/lib/families/bitcoin/logic.d.ts +6 -1
- package/lib/families/bitcoin/logic.d.ts.map +1 -1
- package/lib/families/bitcoin/logic.js +186 -1
- package/lib/families/bitcoin/logic.js.map +1 -1
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts +2 -0
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts.map +1 -0
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js +72 -0
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js.map +1 -0
- package/lib/families/cosmos/api/Cosmos.d.ts.map +1 -1
- package/lib/families/cosmos/api/Cosmos.js +1 -0
- package/lib/families/cosmos/api/Cosmos.js.map +1 -1
- package/lib/families/cosmos/react.test.js +5 -4
- package/lib/families/cosmos/react.test.js.map +1 -1
- package/lib/hw/extractOnboardingState.d.ts +26 -0
- package/lib/hw/extractOnboardingState.d.ts.map +1 -0
- package/lib/hw/extractOnboardingState.js +69 -0
- package/lib/hw/extractOnboardingState.js.map +1 -0
- package/lib/hw/extractOnboardingState.test.d.ts +2 -0
- package/lib/hw/extractOnboardingState.test.d.ts.map +1 -0
- package/lib/hw/extractOnboardingState.test.js +182 -0
- package/lib/hw/extractOnboardingState.test.js.map +1 -0
- package/lib/hw/getOnboardingStatePolling.d.ts +20 -0
- package/lib/hw/getOnboardingStatePolling.d.ts.map +1 -0
- package/lib/hw/getOnboardingStatePolling.js +117 -0
- package/lib/hw/getOnboardingStatePolling.js.map +1 -0
- package/lib/hw/getOnboardingStatePolling.test.d.ts +2 -0
- package/lib/hw/getOnboardingStatePolling.test.d.ts.map +1 -0
- package/lib/hw/getOnboardingStatePolling.test.js +224 -0
- package/lib/hw/getOnboardingStatePolling.test.js.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts +22 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.js +81 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.js.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts +2 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.js +374 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -0
- package/lib/types/manager.d.ts +5 -0
- package/lib/types/manager.d.ts.map +1 -1
- package/lib/types/manager.js +7 -1
- package/lib/types/manager.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/range.ts +13 -0
- package/src/families/bitcoin/js-synchronisation.ts +3 -172
- package/src/families/bitcoin/logic.ts +169 -0
- package/src/families/bitcoin/xpub.txs.txNoOutput.unit.test.ts +37 -0
- package/src/families/cosmos/api/Cosmos.ts +4 -0
- package/src/families/cosmos/react.test.ts +3 -0
- package/src/hw/extractOnboardingState.test.ts +251 -0
- package/src/hw/extractOnboardingState.ts +97 -0
- package/src/hw/getOnboardingStatePolling.test.ts +247 -0
- package/src/hw/getOnboardingStatePolling.ts +153 -0
- package/src/onboarding/hooks/useOnboardingStatePolling.test.ts +304 -0
- package/src/onboarding/hooks/useOnboardingStatePolling.ts +86 -0
- package/src/types/manager.ts +5 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DeviceExtractOnboardingStateError } from "@ledgerhq/errors";
|
|
2
|
+
import { SeedPhraseType } from "../types/manager";
|
|
3
|
+
|
|
4
|
+
const onboardingFlagsBytesLength = 4;
|
|
5
|
+
|
|
6
|
+
const onboardedMask = 0x04;
|
|
7
|
+
const inRecoveryModeMask = 0x01;
|
|
8
|
+
const seedPhraseTypeMask = 0x60;
|
|
9
|
+
const seedPhraseTypeFlagOffset = 5;
|
|
10
|
+
const currentSeedWordIndexMask = 0x1f;
|
|
11
|
+
|
|
12
|
+
const fromBitsToSeedPhraseType = new Map<number, SeedPhraseType>([
|
|
13
|
+
[0, SeedPhraseType.TwentyFour],
|
|
14
|
+
[1, SeedPhraseType.Eighteen],
|
|
15
|
+
[2, SeedPhraseType.Twelve],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export enum OnboardingStep {
|
|
19
|
+
WelcomeScreen = "WELCOME_SCREEN",
|
|
20
|
+
SetupChoice = "SETUP_CHOICE",
|
|
21
|
+
Pin = "PIN",
|
|
22
|
+
NewDevice = "NEW_DEVICE", // path "new device" & currentSeedWordIndex available
|
|
23
|
+
NewDeviceConfirming = "NEW_DEVICE_CONFIRMING", // path "new device" & currentSeedWordIndex available
|
|
24
|
+
RestoreSeed = "RESTORE_SEED", // path "restore seed" & currentSeedWordIndex available
|
|
25
|
+
SafetyWarning = "SAFETY WARNING",
|
|
26
|
+
Ready = "READY",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fromBitsToOnboardingStep = new Map<number, OnboardingStep>([
|
|
30
|
+
[0, OnboardingStep.WelcomeScreen],
|
|
31
|
+
[1, OnboardingStep.SetupChoice],
|
|
32
|
+
[2, OnboardingStep.Pin],
|
|
33
|
+
[3, OnboardingStep.NewDevice],
|
|
34
|
+
[4, OnboardingStep.NewDeviceConfirming],
|
|
35
|
+
[5, OnboardingStep.RestoreSeed],
|
|
36
|
+
[6, OnboardingStep.SafetyWarning],
|
|
37
|
+
[7, OnboardingStep.Ready],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export type OnboardingState = {
|
|
41
|
+
// Device not yet onboarded otherwise
|
|
42
|
+
isOnboarded: boolean;
|
|
43
|
+
// In normal mode otherwise
|
|
44
|
+
isInRecoveryMode: boolean;
|
|
45
|
+
|
|
46
|
+
seedPhraseType: SeedPhraseType;
|
|
47
|
+
|
|
48
|
+
currentOnboardingStep: OnboardingStep;
|
|
49
|
+
currentSeedWordIndex: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extracts the onboarding state of the device
|
|
54
|
+
* @param flagsBytes Buffer of bytes of length onboardingFlagsBytesLength reprensenting the device state flags
|
|
55
|
+
* @returns An OnboardingState
|
|
56
|
+
*/
|
|
57
|
+
export const extractOnboardingState = (flagsBytes: Buffer): OnboardingState => {
|
|
58
|
+
if (!flagsBytes || flagsBytes.length < onboardingFlagsBytesLength) {
|
|
59
|
+
throw new DeviceExtractOnboardingStateError(
|
|
60
|
+
"Incorrect onboarding flags bytes"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isOnboarded = Boolean(flagsBytes[0] & onboardedMask);
|
|
65
|
+
const isInRecoveryMode = Boolean(flagsBytes[0] & inRecoveryModeMask);
|
|
66
|
+
|
|
67
|
+
const seedPhraseTypeBits =
|
|
68
|
+
(flagsBytes[2] & seedPhraseTypeMask) >> seedPhraseTypeFlagOffset;
|
|
69
|
+
const seedPhraseType = fromBitsToSeedPhraseType.get(seedPhraseTypeBits);
|
|
70
|
+
|
|
71
|
+
if (!seedPhraseType) {
|
|
72
|
+
throw new DeviceExtractOnboardingStateError(
|
|
73
|
+
"Incorrect onboarding bits for the seed phrase type"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const currentOnboardingStepBits = flagsBytes[3];
|
|
78
|
+
const currentOnboardingStep = fromBitsToOnboardingStep.get(
|
|
79
|
+
currentOnboardingStepBits
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!currentOnboardingStep) {
|
|
83
|
+
throw new DeviceExtractOnboardingStateError(
|
|
84
|
+
"Incorrect onboarding bits for the current onboarding step"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const currentSeedWordIndex = flagsBytes[2] & currentSeedWordIndexMask;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
isOnboarded,
|
|
92
|
+
isInRecoveryMode,
|
|
93
|
+
seedPhraseType,
|
|
94
|
+
currentOnboardingStep,
|
|
95
|
+
currentSeedWordIndex,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { getOnboardingStatePolling } from "./getOnboardingStatePolling";
|
|
2
|
+
import { from, Subscription, TimeoutError } from "rxjs";
|
|
3
|
+
import * as rxjsOperators from "rxjs/operators";
|
|
4
|
+
import { DeviceModelId } from "@ledgerhq/devices";
|
|
5
|
+
import Transport from "@ledgerhq/hw-transport";
|
|
6
|
+
import {
|
|
7
|
+
DeviceExtractOnboardingStateError,
|
|
8
|
+
DisconnectedDevice,
|
|
9
|
+
} from "@ledgerhq/errors";
|
|
10
|
+
import { withDevice } from "./deviceAccess";
|
|
11
|
+
import getVersion from "./getVersion";
|
|
12
|
+
import {
|
|
13
|
+
extractOnboardingState,
|
|
14
|
+
OnboardingState,
|
|
15
|
+
OnboardingStep,
|
|
16
|
+
} from "./extractOnboardingState";
|
|
17
|
+
import { SeedPhraseType } from "../types/manager";
|
|
18
|
+
|
|
19
|
+
jest.mock("./deviceAccess");
|
|
20
|
+
jest.mock("./getVersion");
|
|
21
|
+
jest.mock("./extractOnboardingState");
|
|
22
|
+
jest.mock("@ledgerhq/hw-transport");
|
|
23
|
+
jest.useFakeTimers();
|
|
24
|
+
|
|
25
|
+
const aDevice = {
|
|
26
|
+
deviceId: "DEVICE_ID_A",
|
|
27
|
+
deviceName: "DEVICE_NAME_A",
|
|
28
|
+
modelId: DeviceModelId.nanoFTS,
|
|
29
|
+
wired: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// As extractOnboardingState is mocked, the firmwareInfo
|
|
33
|
+
// returned by getVersion does not matter
|
|
34
|
+
const aFirmwareInfo = {
|
|
35
|
+
isBootloader: false,
|
|
36
|
+
rawVersion: "",
|
|
37
|
+
targetId: 0,
|
|
38
|
+
mcuVersion: "",
|
|
39
|
+
flags: Buffer.from([]),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const pollingPeriodMs = 1000;
|
|
43
|
+
|
|
44
|
+
const mockedGetVersion = jest.mocked(getVersion);
|
|
45
|
+
|
|
46
|
+
const mockedWithDevice = jest.mocked(withDevice);
|
|
47
|
+
mockedWithDevice.mockReturnValue((job) => from(job(new Transport())));
|
|
48
|
+
|
|
49
|
+
const mockedExtractOnboardingState = jest.mocked(extractOnboardingState);
|
|
50
|
+
|
|
51
|
+
describe("getOnboardingStatePolling", () => {
|
|
52
|
+
let anOnboardingState: OnboardingState;
|
|
53
|
+
let onboardingStatePollingSubscription: Subscription | null;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
anOnboardingState = {
|
|
57
|
+
isOnboarded: false,
|
|
58
|
+
isInRecoveryMode: false,
|
|
59
|
+
seedPhraseType: SeedPhraseType.TwentyFour,
|
|
60
|
+
currentSeedWordIndex: 0,
|
|
61
|
+
currentOnboardingStep: OnboardingStep.NewDevice,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
mockedGetVersion.mockClear();
|
|
67
|
+
mockedExtractOnboardingState.mockClear();
|
|
68
|
+
jest.clearAllTimers();
|
|
69
|
+
onboardingStatePollingSubscription?.unsubscribe();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("When a communication error occurs while fetching the device state", () => {
|
|
73
|
+
describe("and when the error is allowed and thrown before the defined timeout", () => {
|
|
74
|
+
it("should update the onboarding state to null and keep track of the allowed error", (done) => {
|
|
75
|
+
mockedGetVersion.mockRejectedValue(
|
|
76
|
+
new DisconnectedDevice("An allowed error")
|
|
77
|
+
);
|
|
78
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
79
|
+
|
|
80
|
+
const device = aDevice;
|
|
81
|
+
|
|
82
|
+
getOnboardingStatePolling({
|
|
83
|
+
deviceId: device.deviceId,
|
|
84
|
+
pollingPeriodMs,
|
|
85
|
+
}).subscribe({
|
|
86
|
+
next: (value) => {
|
|
87
|
+
expect(value.onboardingState).toBeNull();
|
|
88
|
+
expect(value.allowedError).toBeInstanceOf(DisconnectedDevice);
|
|
89
|
+
done();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The timeout is equal to pollingPeriodMs by default
|
|
94
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("and when a timeout occurred before the error (or the fetch took too long)", () => {
|
|
99
|
+
it("should update the allowed error value to notify the consumer - default value for the timeout", (done) => {
|
|
100
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
101
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
102
|
+
|
|
103
|
+
const device = aDevice;
|
|
104
|
+
|
|
105
|
+
getOnboardingStatePolling({
|
|
106
|
+
deviceId: device.deviceId,
|
|
107
|
+
pollingPeriodMs,
|
|
108
|
+
}).subscribe({
|
|
109
|
+
next: (value) => {
|
|
110
|
+
expect(value.onboardingState).toBeNull();
|
|
111
|
+
expect(value.allowedError).toBeInstanceOf(TimeoutError);
|
|
112
|
+
done();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Waits more than the timeout
|
|
117
|
+
jest.advanceTimersByTime(pollingPeriodMs + 1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should update the allowed error value to notify the consumer - timeout value set by the consumer", (done) => {
|
|
121
|
+
const fetchingTimeoutMs = pollingPeriodMs + 500;
|
|
122
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
123
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
124
|
+
|
|
125
|
+
const device = aDevice;
|
|
126
|
+
|
|
127
|
+
getOnboardingStatePolling({
|
|
128
|
+
deviceId: device.deviceId,
|
|
129
|
+
pollingPeriodMs,
|
|
130
|
+
fetchingTimeoutMs,
|
|
131
|
+
}).subscribe({
|
|
132
|
+
next: (value) => {
|
|
133
|
+
expect(value.onboardingState).toBeNull();
|
|
134
|
+
expect(value.allowedError).toBeInstanceOf(TimeoutError);
|
|
135
|
+
done();
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Waits more than the timeout
|
|
140
|
+
jest.advanceTimersByTime(fetchingTimeoutMs + 1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("and when the error is fatal and thrown before the defined timeout", () => {
|
|
145
|
+
it("should notify the consumer that a unallowed error occurred", (done) => {
|
|
146
|
+
mockedGetVersion.mockRejectedValue(new Error("Unknown error"));
|
|
147
|
+
|
|
148
|
+
const device = aDevice;
|
|
149
|
+
|
|
150
|
+
getOnboardingStatePolling({
|
|
151
|
+
deviceId: device.deviceId,
|
|
152
|
+
pollingPeriodMs,
|
|
153
|
+
}).subscribe({
|
|
154
|
+
error: (error) => {
|
|
155
|
+
expect(error).toBeInstanceOf(Error);
|
|
156
|
+
expect(error?.message).toBe("Unknown error");
|
|
157
|
+
done();
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("When the fetched device state is incorrect", () => {
|
|
167
|
+
it("should return a null onboarding state, and keep track of the extract error", (done) => {
|
|
168
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
169
|
+
mockedExtractOnboardingState.mockImplementation(() => {
|
|
170
|
+
throw new DeviceExtractOnboardingStateError(
|
|
171
|
+
"Some incorrect device info"
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const device = aDevice;
|
|
176
|
+
|
|
177
|
+
onboardingStatePollingSubscription = getOnboardingStatePolling({
|
|
178
|
+
deviceId: device.deviceId,
|
|
179
|
+
pollingPeriodMs,
|
|
180
|
+
}).subscribe({
|
|
181
|
+
next: (value) => {
|
|
182
|
+
expect(value.onboardingState).toBeNull();
|
|
183
|
+
expect(value.allowedError).toBeInstanceOf(
|
|
184
|
+
DeviceExtractOnboardingStateError
|
|
185
|
+
);
|
|
186
|
+
done();
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("When polling returns a correct device state", () => {
|
|
195
|
+
it("should return a correct onboarding state", (done) => {
|
|
196
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
197
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
198
|
+
|
|
199
|
+
const device = aDevice;
|
|
200
|
+
|
|
201
|
+
onboardingStatePollingSubscription = getOnboardingStatePolling({
|
|
202
|
+
deviceId: device.deviceId,
|
|
203
|
+
pollingPeriodMs,
|
|
204
|
+
}).subscribe({
|
|
205
|
+
next: (value) => {
|
|
206
|
+
expect(value.allowedError).toBeNull();
|
|
207
|
+
expect(value.onboardingState).toEqual(anOnboardingState);
|
|
208
|
+
done();
|
|
209
|
+
},
|
|
210
|
+
error: (error) => {
|
|
211
|
+
done(error);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should poll a new onboarding state after the defined period of time", (done) => {
|
|
219
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
220
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
221
|
+
|
|
222
|
+
const device = aDevice;
|
|
223
|
+
|
|
224
|
+
// Did not manage to test that the polling is repeated by using jest's fake timer
|
|
225
|
+
// and advanceTimersByTime method or equivalent.
|
|
226
|
+
// Hacky test: spy on the repeat operator to see if it has been called.
|
|
227
|
+
const spiedRepeat = jest.spyOn(rxjsOperators, "repeat");
|
|
228
|
+
|
|
229
|
+
onboardingStatePollingSubscription = getOnboardingStatePolling({
|
|
230
|
+
deviceId: device.deviceId,
|
|
231
|
+
pollingPeriodMs,
|
|
232
|
+
}).subscribe({
|
|
233
|
+
next: (value) => {
|
|
234
|
+
expect(value.onboardingState).toEqual(anOnboardingState);
|
|
235
|
+
expect(value.allowedError).toBeNull();
|
|
236
|
+
expect(spiedRepeat).toHaveBeenCalledTimes(1);
|
|
237
|
+
done();
|
|
238
|
+
},
|
|
239
|
+
error: (error) => {
|
|
240
|
+
done(error);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
jest.runOnlyPendingTimers();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
from,
|
|
3
|
+
merge,
|
|
4
|
+
partition,
|
|
5
|
+
of,
|
|
6
|
+
throwError,
|
|
7
|
+
Observable,
|
|
8
|
+
TimeoutError,
|
|
9
|
+
} from "rxjs";
|
|
10
|
+
import { map, catchError, repeat, first, timeout } from "rxjs/operators";
|
|
11
|
+
import getVersion from "./getVersion";
|
|
12
|
+
import { withDevice } from "./deviceAccess";
|
|
13
|
+
import {
|
|
14
|
+
TransportStatusError,
|
|
15
|
+
DeviceOnboardingStatePollingError,
|
|
16
|
+
DeviceExtractOnboardingStateError,
|
|
17
|
+
DisconnectedDevice,
|
|
18
|
+
CantOpenDevice,
|
|
19
|
+
} from "@ledgerhq/errors";
|
|
20
|
+
import { FirmwareInfo } from "../types/manager";
|
|
21
|
+
import {
|
|
22
|
+
extractOnboardingState,
|
|
23
|
+
OnboardingState,
|
|
24
|
+
} from "./extractOnboardingState";
|
|
25
|
+
|
|
26
|
+
export type OnboardingStatePollingResult = {
|
|
27
|
+
onboardingState: OnboardingState | null;
|
|
28
|
+
allowedError: Error | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Polls the device onboarding state at a given frequency
|
|
33
|
+
* @param deviceId A device id
|
|
34
|
+
* @param pollingPeriodMs The period in ms after which the device onboarding state is fetched again
|
|
35
|
+
* @param fetchingTimeoutMs The time to wait while fetching for the device onboarding state before throwing an error, in ms
|
|
36
|
+
* @returns An Observable that polls the device onboarding state
|
|
37
|
+
*/
|
|
38
|
+
export const getOnboardingStatePolling = ({
|
|
39
|
+
deviceId,
|
|
40
|
+
pollingPeriodMs,
|
|
41
|
+
fetchingTimeoutMs = pollingPeriodMs,
|
|
42
|
+
}: {
|
|
43
|
+
deviceId: string;
|
|
44
|
+
pollingPeriodMs: number;
|
|
45
|
+
fetchingTimeoutMs?: number;
|
|
46
|
+
}): Observable<OnboardingStatePollingResult> => {
|
|
47
|
+
let firstRun = true;
|
|
48
|
+
|
|
49
|
+
const delayedOnceOnboardingStateObservable: Observable<OnboardingStatePollingResult> =
|
|
50
|
+
new Observable((subscriber) => {
|
|
51
|
+
const delayMs = firstRun ? 0 : pollingPeriodMs;
|
|
52
|
+
firstRun = false;
|
|
53
|
+
|
|
54
|
+
const getOnboardingStateOnce = () => {
|
|
55
|
+
const firmwareInfoOrAllowedErrorObservable = withDevice(deviceId)((t) =>
|
|
56
|
+
from(getVersion(t))
|
|
57
|
+
).pipe(
|
|
58
|
+
timeout(fetchingTimeoutMs), // Throws a TimeoutError
|
|
59
|
+
first(),
|
|
60
|
+
catchError((error: any) => {
|
|
61
|
+
if (isAllowedOnboardingStatePollingError(error)) {
|
|
62
|
+
// Pushes the error to the next step to be processed (no retry from the beginning)
|
|
63
|
+
return of(error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return throwError(error);
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// If an error is catched previously, and this error is "allowed",
|
|
71
|
+
// the value from the observable is not a FirmwareInfo but an Error
|
|
72
|
+
const [firmwareInfoObservable, allowedErrorObservable] = partition(
|
|
73
|
+
firmwareInfoOrAllowedErrorObservable,
|
|
74
|
+
// TS cannot infer correctly the value given to RxJS partition
|
|
75
|
+
(value: any) => Boolean(value?.flags)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const onboardingStateFromFirmwareInfoObservable =
|
|
79
|
+
firmwareInfoObservable.pipe(
|
|
80
|
+
map((firmwareInfo: FirmwareInfo) => {
|
|
81
|
+
let onboardingState: OnboardingState | null = null;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
onboardingState = extractOnboardingState(firmwareInfo.flags);
|
|
85
|
+
} catch (error: any) {
|
|
86
|
+
if (error instanceof DeviceExtractOnboardingStateError) {
|
|
87
|
+
return {
|
|
88
|
+
onboardingState: null,
|
|
89
|
+
allowedError: error,
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
return {
|
|
93
|
+
onboardingState: null,
|
|
94
|
+
allowedError: new DeviceOnboardingStatePollingError(
|
|
95
|
+
`SyncOnboarding: Unknown error while extracting the onboarding state ${
|
|
96
|
+
error?.name ?? error
|
|
97
|
+
} ${error?.message}`
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { onboardingState, allowedError: null };
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Handles the case of an (allowed) Error value
|
|
107
|
+
const onboardingStateFromAllowedErrorObservable =
|
|
108
|
+
allowedErrorObservable.pipe(
|
|
109
|
+
map((allowedError: Error) => {
|
|
110
|
+
return {
|
|
111
|
+
onboardingState: null,
|
|
112
|
+
allowedError: allowedError,
|
|
113
|
+
};
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return merge(
|
|
118
|
+
onboardingStateFromFirmwareInfoObservable,
|
|
119
|
+
onboardingStateFromAllowedErrorObservable
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Delays the fetch of the onboarding state
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
getOnboardingStateOnce().subscribe({
|
|
126
|
+
next: (value: OnboardingStatePollingResult) => {
|
|
127
|
+
subscriber.next(value);
|
|
128
|
+
},
|
|
129
|
+
error: (error: any) => {
|
|
130
|
+
subscriber.error(error);
|
|
131
|
+
},
|
|
132
|
+
complete: () => subscriber.complete(),
|
|
133
|
+
});
|
|
134
|
+
}, delayMs);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return delayedOnceOnboardingStateObservable.pipe(repeat());
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const isAllowedOnboardingStatePollingError = (error: Error): boolean => {
|
|
141
|
+
if (
|
|
142
|
+
error &&
|
|
143
|
+
// Timeout error is thrown by rxjs's timeout
|
|
144
|
+
(error instanceof TimeoutError ||
|
|
145
|
+
error instanceof DisconnectedDevice ||
|
|
146
|
+
error instanceof CantOpenDevice ||
|
|
147
|
+
error instanceof TransportStatusError)
|
|
148
|
+
) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false;
|
|
153
|
+
};
|