@onekeyfe/hd-transport-react-native 1.2.3-alpha.1 → 1.2.3-alpha.10
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/README.md +1 -1
- package/dist/BleManager.d.ts +6 -5
- package/dist/BleManager.d.ts.map +1 -1
- package/dist/bleEncryption.d.ts +17 -0
- package/dist/bleEncryption.d.ts.map +1 -0
- package/dist/bleKeyMissing.d.ts +7 -0
- package/dist/bleKeyMissing.d.ts.map +1 -0
- package/dist/bleNativeDisconnect.d.ts +3 -0
- package/dist/bleNativeDisconnect.d.ts.map +1 -0
- package/dist/index.d.ts +69 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +951 -139
- package/package.json +6 -6
- package/src/BleManager.ts +113 -5
- package/src/__tests__/androidEncryption.test.ts +482 -0
- package/src/__tests__/androidKeyMissing.test.ts +307 -0
- package/src/__tests__/bleNativeDisconnect.test.ts +33 -0
- package/src/__tests__/connectTimeout.test.ts +427 -3
- package/src/__tests__/enumerate.test.ts +15 -0
- package/src/__tests__/protocolV2Link.test.ts +991 -62
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleEncryption.ts +267 -0
- package/src/bleKeyMissing.ts +106 -0
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/index.ts +785 -156
|
@@ -56,6 +56,34 @@ function createHarness() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
describe('Protocol V1 stale call timeout', () => {
|
|
59
|
+
test('settles a cancelled read and waits for native teardown before completing cancel', async () => {
|
|
60
|
+
const { t, disconnectSpy } = createHarness();
|
|
61
|
+
let finishDisconnect!: () => void;
|
|
62
|
+
disconnectSpy.mockImplementationOnce(
|
|
63
|
+
() =>
|
|
64
|
+
new Promise<void>(resolve => {
|
|
65
|
+
finishDisconnect = resolve;
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
69
|
+
const result = call.catch(error => error);
|
|
70
|
+
await flush();
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
const cleanup = t.cancel().then(() => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
});
|
|
75
|
+
await flush();
|
|
76
|
+
expect(await result).toMatchObject({ errorCode: HardwareErrorCode.CallQueueActionCancelled });
|
|
77
|
+
expect(t.runPromise).toBeNull();
|
|
78
|
+
expect(disconnectSpy).toHaveBeenCalledWith(UUID);
|
|
79
|
+
expect(cancelled).toBe(false);
|
|
80
|
+
finishDisconnect();
|
|
81
|
+
await cleanup;
|
|
82
|
+
jest.advanceTimersByTime(25000);
|
|
83
|
+
await flush();
|
|
84
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
59
87
|
beforeAll(() => {
|
|
60
88
|
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
61
89
|
});
|
|
@@ -172,24 +200,25 @@ describe('Protocol V1 stale call timeout', () => {
|
|
|
172
200
|
expect(secondErrors).toHaveLength(1);
|
|
173
201
|
});
|
|
174
202
|
|
|
175
|
-
test('
|
|
203
|
+
test('a cancelled read cannot disconnect a later transport when its old deadline passes', async () => {
|
|
176
204
|
const { t, disconnectSpy } = createHarness();
|
|
177
205
|
|
|
178
|
-
// cancel() nulls the ownership slot without settling the deferred, so the
|
|
179
|
-
// call's response timer stays armed (reachable via DeviceCommands.dispose).
|
|
180
206
|
const errors: Array<{ errorCode?: unknown }> = [];
|
|
181
207
|
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
182
208
|
p.catch(e => errors.push(e));
|
|
183
209
|
await flush();
|
|
184
210
|
|
|
185
|
-
t.cancel();
|
|
211
|
+
await t.cancel();
|
|
212
|
+
await flush();
|
|
213
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
214
|
+
disconnectSpy.mockClear();
|
|
186
215
|
|
|
187
216
|
jest.advanceTimersByTime(5000);
|
|
188
217
|
await flush();
|
|
189
218
|
|
|
190
219
|
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
191
220
|
expect(errors).toHaveLength(1);
|
|
192
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.
|
|
221
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.CallQueueActionCancelled);
|
|
193
222
|
});
|
|
194
223
|
|
|
195
224
|
test('timeout on the active call still disconnects the transport', async () => {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import BleUtils from '@onekeyfe/react-native-ble-utils';
|
|
3
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
hasBleKeyMissingSince,
|
|
7
|
+
onBleKeyMissing,
|
|
8
|
+
startBleKeyMissingTracking,
|
|
9
|
+
} from './bleKeyMissing';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Android 16+ starts encrypting a bonded LE link on its own right after connecting, with the
|
|
13
|
+
* stored key. A GATT request that needs encryption and is sent before that attempt finishes is
|
|
14
|
+
* rejected by the peer, and the framework retries it by encrypting again with the same key. When
|
|
15
|
+
* the peer has lost the bond, that second failure makes firmware that tolerates a single one per
|
|
16
|
+
* link drop the connection, before the system's own re-pairing can finish.
|
|
17
|
+
*
|
|
18
|
+
* Tracking the link's encryption result lets the transport hold GATT until then, so a lost bond
|
|
19
|
+
* is re-paired in place (the system shows a pairing request) instead of failing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** HCI "PIN or Key Missing": the peer no longer has the keys of the stored bond. */
|
|
23
|
+
export const HCI_PIN_OR_KEY_MISSING = 0x06;
|
|
24
|
+
|
|
25
|
+
export type AndroidLinkEncryption =
|
|
26
|
+
/** This link was encrypted with the stored bond. */
|
|
27
|
+
| 'encrypted'
|
|
28
|
+
/** The link was reused and had already been encrypted. */
|
|
29
|
+
| 'already-encrypted'
|
|
30
|
+
/** The stored bond was lost and the system re-paired on this link. */
|
|
31
|
+
| 're-paired'
|
|
32
|
+
/** No result arrived in time; callers proceed as if nothing was known. */
|
|
33
|
+
| 'unresolved';
|
|
34
|
+
|
|
35
|
+
type EncryptionRecord = { at: number; status: number; enabled: boolean };
|
|
36
|
+
|
|
37
|
+
// An app can still resolve an older react-native-ble-utils, and the JS bundle can run on a native
|
|
38
|
+
// build that predates the events, so none of these members is assumed to exist.
|
|
39
|
+
type EncryptionCapableBleUtils = Partial<
|
|
40
|
+
Pick<
|
|
41
|
+
typeof BleUtils,
|
|
42
|
+
'supportsDeviceEncryptionChange' | 'onDeviceEncryptionChange' | 'onDeviceAclDisconnected'
|
|
43
|
+
>
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
type LinkListener = (deviceId: string) => void;
|
|
47
|
+
|
|
48
|
+
const lastEncryption = new Map<string, EncryptionRecord>();
|
|
49
|
+
const lastAclDisconnectAt = new Map<string, number>();
|
|
50
|
+
const listeners = new Set<LinkListener>();
|
|
51
|
+
let unsubscribeNative: Array<() => void> | undefined;
|
|
52
|
+
|
|
53
|
+
const normalizeDeviceId = (deviceId: string) => deviceId.toLowerCase();
|
|
54
|
+
|
|
55
|
+
const isEncrypted = (record: EncryptionRecord) => record.status === 0 && record.enabled;
|
|
56
|
+
|
|
57
|
+
const getCapableBleUtils = (): EncryptionCapableBleUtils | undefined => {
|
|
58
|
+
if (Platform.OS !== 'android') return undefined;
|
|
59
|
+
const bleUtils: EncryptionCapableBleUtils = BleUtils;
|
|
60
|
+
if (
|
|
61
|
+
typeof bleUtils.supportsDeviceEncryptionChange !== 'function' ||
|
|
62
|
+
typeof bleUtils.onDeviceEncryptionChange !== 'function' ||
|
|
63
|
+
typeof bleUtils.onDeviceAclDisconnected !== 'function'
|
|
64
|
+
) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return bleUtils.supportsDeviceEncryptionChange() ? bleUtils : undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const notify = (deviceId: string) => {
|
|
71
|
+
listeners.forEach(listener => listener(deviceId));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** False means the encryption result can never be observed here, so nothing should wait for it. */
|
|
75
|
+
export const isBleEncryptionTrackingSupported = () => getCapableBleUtils() !== undefined;
|
|
76
|
+
|
|
77
|
+
/** Idempotent. Returns whether encryption results are observable on this OS and native build. */
|
|
78
|
+
export const startBleEncryptionTracking = (): boolean => {
|
|
79
|
+
if (unsubscribeNative) return true;
|
|
80
|
+
const bleUtils = getCapableBleUtils();
|
|
81
|
+
if (!bleUtils?.onDeviceEncryptionChange || !bleUtils.onDeviceAclDisconnected) return false;
|
|
82
|
+
|
|
83
|
+
unsubscribeNative = [
|
|
84
|
+
bleUtils.onDeviceEncryptionChange(event => {
|
|
85
|
+
if (typeof event?.id !== 'string') return;
|
|
86
|
+
const deviceId = normalizeDeviceId(event.id);
|
|
87
|
+
lastEncryption.set(deviceId, {
|
|
88
|
+
at: Date.now(),
|
|
89
|
+
status: Number(event.status),
|
|
90
|
+
enabled: event.enabled === true,
|
|
91
|
+
});
|
|
92
|
+
notify(deviceId);
|
|
93
|
+
}),
|
|
94
|
+
bleUtils.onDeviceAclDisconnected(event => {
|
|
95
|
+
if (typeof event?.id !== 'string') return;
|
|
96
|
+
const deviceId = normalizeDeviceId(event.id);
|
|
97
|
+
lastAclDisconnectAt.set(deviceId, Date.now());
|
|
98
|
+
notify(deviceId);
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
return true;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const stopBleEncryptionTracking = () => {
|
|
105
|
+
unsubscribeNative?.forEach(unsubscribe => unsubscribe());
|
|
106
|
+
unsubscribeNative = undefined;
|
|
107
|
+
lastEncryption.clear();
|
|
108
|
+
lastAclDisconnectAt.clear();
|
|
109
|
+
listeners.clear();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Records a link that carried requests needing encryption, so reusing it later does not wait for
|
|
114
|
+
* an encryption result that was reported before tracking started.
|
|
115
|
+
*/
|
|
116
|
+
export const markBleLinkEncrypted = (deviceId: string) => {
|
|
117
|
+
if (!unsubscribeNative) return;
|
|
118
|
+
lastEncryption.set(normalizeDeviceId(deviceId), { at: Date.now(), status: 0, enabled: true });
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type WaitForAndroidLinkEncryptionOptions = {
|
|
122
|
+
deviceId: string;
|
|
123
|
+
/** When the current link attempt began; earlier results describe another link. */
|
|
124
|
+
linkStartedAt: number;
|
|
125
|
+
/** How long to wait for the first encryption result of this link. */
|
|
126
|
+
resultTimeoutMs: number;
|
|
127
|
+
/** How long a system re-pairing may take once the stored bond was reported lost. */
|
|
128
|
+
repairTimeoutMs: number;
|
|
129
|
+
/** After the link drops during re-pairing, how long to wait for the key-missing report. */
|
|
130
|
+
keyMissingGraceMs: number;
|
|
131
|
+
signal?: AbortSignal;
|
|
132
|
+
onRepairStarted?: () => void;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolves once the link can carry requests that need encryption. Rejects with `BleBondInvalid`
|
|
137
|
+
* when the system reports that re-pairing failed, with `BleDeviceDisconnected` when the link drops
|
|
138
|
+
* during re-pairing, and with `BleDeviceNotBonded` when re-pairing does not finish in time.
|
|
139
|
+
*/
|
|
140
|
+
export const waitForAndroidLinkEncryption = ({
|
|
141
|
+
deviceId,
|
|
142
|
+
linkStartedAt,
|
|
143
|
+
resultTimeoutMs,
|
|
144
|
+
repairTimeoutMs,
|
|
145
|
+
keyMissingGraceMs,
|
|
146
|
+
signal,
|
|
147
|
+
onRepairStarted,
|
|
148
|
+
}: WaitForAndroidLinkEncryptionOptions): Promise<AndroidLinkEncryption> => {
|
|
149
|
+
if (!startBleEncryptionTracking()) return Promise.resolve('unresolved');
|
|
150
|
+
startBleKeyMissingTracking();
|
|
151
|
+
const target = normalizeDeviceId(deviceId);
|
|
152
|
+
|
|
153
|
+
const previous = lastEncryption.get(target);
|
|
154
|
+
const previousDropAt = lastAclDisconnectAt.get(target);
|
|
155
|
+
if (
|
|
156
|
+
previous &&
|
|
157
|
+
previous.at < linkStartedAt &&
|
|
158
|
+
isEncrypted(previous) &&
|
|
159
|
+
(previousDropAt === undefined || previousDropAt < previous.at)
|
|
160
|
+
) {
|
|
161
|
+
return Promise.resolve('already-encrypted');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
if (signal?.aborted) {
|
|
166
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Set once this link's first encryption failed because the peer lost the bond. */
|
|
171
|
+
let repairStartedAt: number | undefined;
|
|
172
|
+
let droppedDuringRepair = false;
|
|
173
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
174
|
+
let settled = false;
|
|
175
|
+
|
|
176
|
+
const cleanup = () => {
|
|
177
|
+
settled = true;
|
|
178
|
+
if (timer) clearTimeout(timer);
|
|
179
|
+
listeners.delete(onLinkEvent);
|
|
180
|
+
cleanupKeyMissing();
|
|
181
|
+
signal?.removeEventListener('abort', onAbort);
|
|
182
|
+
};
|
|
183
|
+
const finish = (result: AndroidLinkEncryption) => {
|
|
184
|
+
cleanup();
|
|
185
|
+
resolve(result);
|
|
186
|
+
};
|
|
187
|
+
const fail = (error: Error) => {
|
|
188
|
+
cleanup();
|
|
189
|
+
reject(error);
|
|
190
|
+
};
|
|
191
|
+
const arm = (ms: number, onExpire: () => void) => {
|
|
192
|
+
if (timer) clearTimeout(timer);
|
|
193
|
+
timer = setTimeout(onExpire, ms);
|
|
194
|
+
};
|
|
195
|
+
const bondInvalid = () =>
|
|
196
|
+
ERRORS.TypedError(HardwareErrorCode.BleBondInvalid, undefined, {
|
|
197
|
+
phase: 'connect',
|
|
198
|
+
reason: 'key_missing',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const evaluate = () => {
|
|
202
|
+
if (settled) return;
|
|
203
|
+
const record = lastEncryption.get(target);
|
|
204
|
+
const dropAt = lastAclDisconnectAt.get(target);
|
|
205
|
+
|
|
206
|
+
if (repairStartedAt === undefined) {
|
|
207
|
+
if (hasBleKeyMissingSince(target, linkStartedAt)) {
|
|
208
|
+
fail(bondInvalid());
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (dropAt !== undefined && dropAt >= linkStartedAt) {
|
|
212
|
+
// The link is gone; the caller's next request reports it with its usual error.
|
|
213
|
+
finish('unresolved');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!record || record.at < linkStartedAt) return;
|
|
217
|
+
if (isEncrypted(record)) {
|
|
218
|
+
finish('encrypted');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (record.status !== HCI_PIN_OR_KEY_MISSING) {
|
|
222
|
+
finish('unresolved');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// The system re-pairs on its own after a lost bond. Nothing may touch GATT until then.
|
|
226
|
+
repairStartedAt = record.at;
|
|
227
|
+
onRepairStarted?.();
|
|
228
|
+
arm(repairTimeoutMs, () =>
|
|
229
|
+
fail(
|
|
230
|
+
ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing timed out', {
|
|
231
|
+
phase: 'bond',
|
|
232
|
+
reason: 'timeout',
|
|
233
|
+
})
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (hasBleKeyMissingSince(target, repairStartedAt)) {
|
|
240
|
+
fail(bondInvalid());
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (record && record.at > repairStartedAt && isEncrypted(record)) {
|
|
244
|
+
finish('re-paired');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!droppedDuringRepair && dropAt !== undefined && dropAt >= repairStartedAt) {
|
|
248
|
+
droppedDuringRepair = true;
|
|
249
|
+
// A failed re-pairing drops the link and reports key missing right after.
|
|
250
|
+
arm(keyMissingGraceMs, () =>
|
|
251
|
+
fail(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected))
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const onLinkEvent: LinkListener = id => {
|
|
257
|
+
if (id === target) evaluate();
|
|
258
|
+
};
|
|
259
|
+
const onAbort = () => fail(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
260
|
+
|
|
261
|
+
listeners.add(onLinkEvent);
|
|
262
|
+
const cleanupKeyMissing = onBleKeyMissing(target, evaluate);
|
|
263
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
264
|
+
arm(resultTimeoutMs, () => finish('unresolved'));
|
|
265
|
+
evaluate();
|
|
266
|
+
});
|
|
267
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import BleUtils from '@onekeyfe/react-native-ble-utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Android 16+ keeps its side of a bond the device no longer has keys for, and reports it
|
|
6
|
+
* with ACTION_KEY_MISSING instead of a GATT status. The device then drops the link, so
|
|
7
|
+
* without this signal a wiped device is indistinguishable from any other disconnect.
|
|
8
|
+
*
|
|
9
|
+
* The signal only describes the link that was being encrypted when it fired. Callers must
|
|
10
|
+
* bound it to their own connection attempt with `sinceMs`; reusing an older one would turn
|
|
11
|
+
* an ordinary disconnect (a firmware-update reboot, for example) into a terminal bond error.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// An app can still resolve a react-native-ble-utils older than 0.1.8, and the JS bundle can
|
|
15
|
+
// run on a native build that predates the event, so neither method is assumed to exist.
|
|
16
|
+
type KeyMissingCapableBleUtils = Partial<
|
|
17
|
+
Pick<typeof BleUtils, 'supportsDeviceKeyMissing' | 'onDeviceKeyMissing'>
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
type KeyMissingListener = (deviceId: string) => void;
|
|
21
|
+
|
|
22
|
+
const lastKeyMissingAt = new Map<string, number>();
|
|
23
|
+
const listeners = new Set<KeyMissingListener>();
|
|
24
|
+
let unsubscribeNative: (() => void) | undefined;
|
|
25
|
+
|
|
26
|
+
const normalizeDeviceId = (deviceId: string) => deviceId.toLowerCase();
|
|
27
|
+
|
|
28
|
+
const getCapableBleUtils = (): KeyMissingCapableBleUtils | undefined => {
|
|
29
|
+
if (Platform.OS !== 'android') return undefined;
|
|
30
|
+
const bleUtils: KeyMissingCapableBleUtils = BleUtils;
|
|
31
|
+
if (
|
|
32
|
+
typeof bleUtils.supportsDeviceKeyMissing !== 'function' ||
|
|
33
|
+
typeof bleUtils.onDeviceKeyMissing !== 'function'
|
|
34
|
+
) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return bleUtils.supportsDeviceKeyMissing() ? bleUtils : undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** False means the event can never fire here, so nothing should wait for it. */
|
|
41
|
+
export const isBleKeyMissingSupported = () => getCapableBleUtils() !== undefined;
|
|
42
|
+
|
|
43
|
+
/** Idempotent. Returns whether the signal is available on this OS and native build. */
|
|
44
|
+
export const startBleKeyMissingTracking = (): boolean => {
|
|
45
|
+
if (unsubscribeNative) return true;
|
|
46
|
+
const bleUtils = getCapableBleUtils();
|
|
47
|
+
if (!bleUtils?.onDeviceKeyMissing) return false;
|
|
48
|
+
|
|
49
|
+
unsubscribeNative = bleUtils.onDeviceKeyMissing(event => {
|
|
50
|
+
if (typeof event?.id !== 'string') return;
|
|
51
|
+
const deviceId = normalizeDeviceId(event.id);
|
|
52
|
+
lastKeyMissingAt.set(deviceId, Date.now());
|
|
53
|
+
listeners.forEach(listener => listener(deviceId));
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const stopBleKeyMissingTracking = () => {
|
|
59
|
+
unsubscribeNative?.();
|
|
60
|
+
unsubscribeNative = undefined;
|
|
61
|
+
lastKeyMissingAt.clear();
|
|
62
|
+
listeners.clear();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const hasBleKeyMissingSince = (deviceId: string, sinceMs: number): boolean => {
|
|
66
|
+
const at = lastKeyMissingAt.get(normalizeDeviceId(deviceId));
|
|
67
|
+
return at !== undefined && at >= sinceMs;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Calls back for every later signal on this device until the returned cleanup runs. */
|
|
71
|
+
export const onBleKeyMissing = (deviceId: string, callback: () => void): (() => void) => {
|
|
72
|
+
const target = normalizeDeviceId(deviceId);
|
|
73
|
+
const listener: KeyMissingListener = id => {
|
|
74
|
+
if (id === target) callback();
|
|
75
|
+
};
|
|
76
|
+
listeners.add(listener);
|
|
77
|
+
return () => {
|
|
78
|
+
listeners.delete(listener);
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves true once a signal at or after `sinceMs` exists for the device, false when
|
|
84
|
+
* none arrives within `timeoutMs`. The broadcast and the GATT disconnect it explains are
|
|
85
|
+
* delivered on separate paths, so the disconnect can reach JS first.
|
|
86
|
+
*/
|
|
87
|
+
export const waitForBleKeyMissing = (
|
|
88
|
+
deviceId: string,
|
|
89
|
+
sinceMs: number,
|
|
90
|
+
timeoutMs: number
|
|
91
|
+
): Promise<boolean> => {
|
|
92
|
+
if (!startBleKeyMissingTracking()) return Promise.resolve(false);
|
|
93
|
+
if (hasBleKeyMissingSince(deviceId, sinceMs)) return Promise.resolve(true);
|
|
94
|
+
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
const settle = (found: boolean) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve(found);
|
|
100
|
+
};
|
|
101
|
+
const cleanup = onBleKeyMissing(deviceId, () => {
|
|
102
|
+
if (hasBleKeyMissingSince(deviceId, sinceMs)) settle(true);
|
|
103
|
+
});
|
|
104
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
const BLE_PLX_DEVICE_DISCONNECTED = 201;
|
|
4
|
+
const IOS_PERIPHERAL_DISCONNECTED = 7;
|
|
5
|
+
|
|
6
|
+
type NativeBleErrorFields = {
|
|
7
|
+
errorCode?: unknown;
|
|
8
|
+
iosErrorCode?: unknown;
|
|
9
|
+
reason?: unknown;
|
|
10
|
+
message?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const nativeErrorText = (error: NativeBleErrorFields) =>
|
|
14
|
+
[error.reason, error.message]
|
|
15
|
+
.filter((value): value is string => typeof value === 'string')
|
|
16
|
+
.join(' ');
|
|
17
|
+
|
|
18
|
+
export const isNativeBleDisconnectError = (error: unknown): boolean => {
|
|
19
|
+
if (!error || typeof error !== 'object') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nativeError = error as NativeBleErrorFields;
|
|
24
|
+
return (
|
|
25
|
+
nativeError.errorCode === BLE_PLX_DEVICE_DISCONNECTED ||
|
|
26
|
+
nativeError.iosErrorCode === IOS_PERIPHERAL_DISCONNECTED
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const toBleDisconnectHardwareError = (error: unknown) => {
|
|
31
|
+
if ((error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleDeviceDisconnected) {
|
|
32
|
+
return error as Error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nativeError = (error ?? {}) as NativeBleErrorFields;
|
|
36
|
+
return ERRORS.TypedError(
|
|
37
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
38
|
+
nativeErrorText(nativeError) || undefined
|
|
39
|
+
);
|
|
40
|
+
};
|