@onekeyfe/hd-transport-react-native 1.2.3 → 1.2.4-alpha.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.
@@ -0,0 +1,16 @@
1
+ export declare const IOS_PEER_TERMINATION_LINK_WINDOW_MS = 10000;
2
+ export declare const IOS_PEER_TERMINATION_REPEAT_WINDOW_MS = 60000;
3
+ export declare const isIosPeerTerminationError: (error: unknown) => boolean;
4
+ export declare class IosPeerTerminationTracker {
5
+ private linkStartedAt;
6
+ private terminatedAt;
7
+ private evidenceAt;
8
+ attemptStarted(deviceId: string): void;
9
+ linkStarted(deviceId: string): void;
10
+ note(deviceId: string, error: unknown): void;
11
+ linkProven(deviceId: string): void;
12
+ forget(deviceId: string): void;
13
+ attemptFailed(deviceId: string): boolean;
14
+ reset(): void;
15
+ }
16
+ //# sourceMappingURL=bleIosStaleBond.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bleIosStaleBond.d.ts","sourceRoot":"","sources":["../src/bleIosStaleBond.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,mCAAmC,QAAS,CAAC;AAE1D,eAAO,MAAM,qCAAqC,QAAS,CAAC;AAE5D,eAAO,MAAM,yBAAyB,UAAW,OAAO,KAAG,OAE9B,CAAC;AAE9B,qBAAa,yBAAyB;IAEpC,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,YAAY,CAA6B;IAGjD,OAAO,CAAC,UAAU,CAA6B;IAE/C,cAAc,CAAC,QAAQ,EAAE,MAAM;IAK/B,WAAW,CAAC,QAAQ,EAAE,MAAM;IAM5B,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAMrC,UAAU,CAAC,QAAQ,EAAE,MAAM;IAI3B,MAAM,CAAC,QAAQ,EAAE,MAAM;IAOvB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAuBxC,KAAK;CAKN"}
package/dist/index.d.ts CHANGED
@@ -175,6 +175,8 @@ declare class ReactNativeBleTransport {
175
175
  private staleBondErrors;
176
176
  /** When the current Android link attempt began; bounds which key-missing signals apply. */
177
177
  private androidLinkStartedAt;
178
+ /** iOS acquire attempts whose new link the peer ended; two in a row mean a stale bond. */
179
+ private iosPeerTermination;
178
180
  /** Strict or previously confirmed V2 target while acquire installs notifications. */
179
181
  private acquiringProtocolV2;
180
182
  private protocolV2Assemblers;
@@ -226,6 +228,11 @@ declare class ReactNativeBleTransport {
226
228
  * connect is re-read as an invalid bond when that signal belongs to the same attempt.
227
229
  */
228
230
  private resolveAndroidBondInvalid;
231
+ /**
232
+ * iPhones that do not report a lost bond only show the device ending each new link. One such
233
+ * attempt keeps its error so Core retries; the second in a row is the stale bond.
234
+ */
235
+ private resolveIosBondInvalid;
229
236
  /**
230
237
  * Android 16+ encrypts a bonded link on its own right after connecting. Subscribing to
231
238
  * notifications before that finishes gets the CCCD write rejected, and the framework's retry
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -494,6 +494,63 @@ const waitForAndroidLinkEncryption = ({ deviceId, linkStartedAt, resultTimeoutMs
494
494
  });
495
495
  };
496
496
 
497
+ const IOS_PERIPHERAL_DISCONNECTED$1 = 7;
498
+ const IOS_PEER_TERMINATION_LINK_WINDOW_MS = 10000;
499
+ const IOS_PEER_TERMINATION_REPEAT_WINDOW_MS = 60000;
500
+ const isIosPeerTerminationError = (error) => (error === null || error === void 0 ? void 0 : error.iosErrorCode) ===
501
+ IOS_PERIPHERAL_DISCONNECTED$1;
502
+ class IosPeerTerminationTracker {
503
+ constructor() {
504
+ this.linkStartedAt = new Map();
505
+ this.terminatedAt = new Map();
506
+ this.evidenceAt = new Map();
507
+ }
508
+ attemptStarted(deviceId) {
509
+ this.linkStartedAt.delete(deviceId);
510
+ this.terminatedAt.delete(deviceId);
511
+ }
512
+ linkStarted(deviceId) {
513
+ this.linkStartedAt.set(deviceId, Date.now());
514
+ this.terminatedAt.delete(deviceId);
515
+ }
516
+ note(deviceId, error) {
517
+ if (!isIosPeerTerminationError(error))
518
+ return;
519
+ if (!this.terminatedAt.has(deviceId))
520
+ this.terminatedAt.set(deviceId, Date.now());
521
+ }
522
+ linkProven(deviceId) {
523
+ this.forget(deviceId);
524
+ }
525
+ forget(deviceId) {
526
+ this.linkStartedAt.delete(deviceId);
527
+ this.terminatedAt.delete(deviceId);
528
+ this.evidenceAt.delete(deviceId);
529
+ }
530
+ attemptFailed(deviceId) {
531
+ const startedAt = this.linkStartedAt.get(deviceId);
532
+ const terminatedAt = this.terminatedAt.get(deviceId);
533
+ const previous = this.evidenceAt.get(deviceId);
534
+ this.forget(deviceId);
535
+ if (startedAt === undefined ||
536
+ terminatedAt === undefined ||
537
+ terminatedAt - startedAt > IOS_PEER_TERMINATION_LINK_WINDOW_MS) {
538
+ return false;
539
+ }
540
+ if (previous !== undefined &&
541
+ terminatedAt - previous <= IOS_PEER_TERMINATION_REPEAT_WINDOW_MS) {
542
+ return true;
543
+ }
544
+ this.evidenceAt.set(deviceId, terminatedAt);
545
+ return false;
546
+ }
547
+ reset() {
548
+ this.linkStartedAt.clear();
549
+ this.terminatedAt.clear();
550
+ this.evidenceAt.clear();
551
+ }
552
+ }
553
+
497
554
  const BLE_PLX_DEVICE_DISCONNECTED = 201;
498
555
  const IOS_PERIPHERAL_DISCONNECTED = 7;
499
556
  const nativeErrorText$1 = (error) => [error.reason, error.message]
@@ -859,6 +916,7 @@ class ReactNativeBleTransport {
859
916
  this.androidGattCacheRefreshes = new Set();
860
917
  this.staleBondErrors = new Map();
861
918
  this.androidLinkStartedAt = new Map();
919
+ this.iosPeerTermination = new IosPeerTerminationTracker();
862
920
  this.acquiringProtocolV2 = new Set();
863
921
  this.protocolV2Assemblers = new Map();
864
922
  this.protocolV2FrameQueues = new Map();
@@ -1301,7 +1359,7 @@ class ReactNativeBleTransport {
1301
1359
  return yield this.acquireUnlocked(input);
1302
1360
  }
1303
1361
  catch (error) {
1304
- throw yield this.resolveAndroidBondInvalid(uuid, error);
1362
+ throw this.resolveIosBondInvalid(input, yield this.resolveAndroidBondInvalid(uuid, error));
1305
1363
  }
1306
1364
  }));
1307
1365
  });
@@ -1312,6 +1370,7 @@ class ReactNativeBleTransport {
1312
1370
  if (this.stopped)
1313
1371
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1314
1372
  const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
1373
+ this.iosPeerTermination.attemptStarted(uuid);
1315
1374
  const shouldMapProtocolV2StaleBond = expectedProtocol
1316
1375
  ? expectedProtocol === 'V2'
1317
1376
  : this.confirmedProtocolV2.has(uuid);
@@ -1605,6 +1664,7 @@ class ReactNativeBleTransport {
1605
1664
  }));
1606
1665
  if (this.stopped)
1607
1666
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1667
+ this.iosPeerTermination.linkProven(uuid);
1608
1668
  const currentTransport = transportCache[uuid];
1609
1669
  if (!currentTransport) {
1610
1670
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotFound);
@@ -1631,6 +1691,8 @@ class ReactNativeBleTransport {
1631
1691
  var _a, _b, _c, _d, _e, _f;
1632
1692
  const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
1633
1693
  if (error) {
1694
+ if (isCurrentMonitor)
1695
+ this.iosPeerTermination.note(uuid, error);
1634
1696
  Log === null || Log === void 0 ? void 0 : Log.debug(`error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${error}`);
1635
1697
  if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
1636
1698
  Log === null || Log === void 0 ? void 0 : Log.debug('notify error ignored during FirmwareUpload write recovery: ', uuid);
@@ -1849,6 +1911,24 @@ class ReactNativeBleTransport {
1849
1911
  });
1850
1912
  });
1851
1913
  }
1914
+ resolveIosBondInvalid(input, error) {
1915
+ if (reactNative.Platform.OS !== 'ios')
1916
+ return error;
1917
+ const { uuid, skipProtocolProbe } = input;
1918
+ if (skipProtocolProbe || hdShared.isBleStaleBondHardwareError(error)) {
1919
+ this.iosPeerTermination.forget(uuid);
1920
+ return error;
1921
+ }
1922
+ if (!this.iosPeerTermination.attemptFailed(uuid))
1923
+ return error;
1924
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] iOS peer ended two new links in a row, bond is invalid:', {
1925
+ connectIdSuffix: uuid.slice(-8),
1926
+ });
1927
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, undefined, {
1928
+ phase: 'connect',
1929
+ reason: 'peer_disconnected',
1930
+ });
1931
+ }
1852
1932
  waitForAndroidLinkSecurity(uuid) {
1853
1933
  return __awaiter(this, void 0, void 0, function* () {
1854
1934
  const linkStartedAt = this.androidLinkStartedAt.get(uuid);
@@ -2088,6 +2168,7 @@ class ReactNativeBleTransport {
2088
2168
  this.androidPriorityResetTimers.clear();
2089
2169
  this.androidHighPriorityDevices.clear();
2090
2170
  this.androidLinkStartedAt.clear();
2171
+ this.iosPeerTermination.reset();
2091
2172
  stopBleKeyMissingTracking();
2092
2173
  stopBleEncryptionTracking();
2093
2174
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
@@ -2343,6 +2424,7 @@ class ReactNativeBleTransport {
2343
2424
  }),
2344
2425
  ]);
2345
2426
  succeeded = true;
2427
+ this.iosPeerTermination.linkStarted(uuid);
2346
2428
  return result;
2347
2429
  }
2348
2430
  catch (error) {
@@ -2389,6 +2471,10 @@ class ReactNativeBleTransport {
2389
2471
  return result;
2390
2472
  }
2391
2473
  catch (error) {
2474
+ this.iosPeerTermination.note(uuid, error);
2475
+ if (reactNative.Platform.OS === 'ios' && isNativeBleStaleBondError(error)) {
2476
+ throw toBleStaleBondHardwareError(error);
2477
+ }
2392
2478
  if (timedOut || isNativeOperationTimeoutError(error)) {
2393
2479
  const resetManager = this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
2394
2480
  if (resetManager) {
@@ -2467,6 +2553,7 @@ class ReactNativeBleTransport {
2467
2553
  this.writeTimeoutCounts.delete(uuid);
2468
2554
  }
2469
2555
  catch (error) {
2556
+ this.iosPeerTermination.note(uuid, error);
2470
2557
  if (timedOut) {
2471
2558
  if (isCurrentOwner && !isCurrentOwner()) {
2472
2559
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.3",
3
+ "version": "1.2.4-alpha.1",
4
4
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -20,11 +20,11 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "dependencies": {
23
- "@onekeyfe/hd-core": "1.2.3",
24
- "@onekeyfe/hd-shared": "1.2.3",
25
- "@onekeyfe/hd-transport": "1.2.3",
23
+ "@onekeyfe/hd-core": "1.2.4-alpha.1",
24
+ "@onekeyfe/hd-shared": "1.2.4-alpha.1",
25
+ "@onekeyfe/hd-transport": "1.2.4-alpha.1",
26
26
  "@onekeyfe/react-native-ble-utils": "0.1.9",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "de8e0beb9d993464fbcac6231d7efe501d789c7d"
29
+ "gitHead": "711c2e0c33c1316257fce02bc01e7c9bbdd8ce48"
30
30
  }
@@ -0,0 +1,303 @@
1
+ import EventEmitter from 'events';
2
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+
4
+ import ReactNativeBleTransport from '../index';
5
+ import {
6
+ IOS_PEER_TERMINATION_LINK_WINDOW_MS,
7
+ IOS_PEER_TERMINATION_REPEAT_WINDOW_MS,
8
+ IosPeerTerminationTracker,
9
+ } from '../bleIosStaleBond';
10
+ import protocolV1Schema from './protocolV1SchemaFixture';
11
+
12
+ jest.mock(
13
+ 'react-native',
14
+ () => ({
15
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
16
+ PermissionsAndroid: {
17
+ PERMISSIONS: {},
18
+ RESULTS: {},
19
+ request: jest.fn(),
20
+ requestMultiple: jest.fn(),
21
+ },
22
+ }),
23
+ { virtual: true }
24
+ );
25
+
26
+ jest.mock('react-native-ble-plx', () => ({
27
+ BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
28
+ BleError: Error,
29
+ BleErrorCode: {
30
+ DeviceDisconnected: 201,
31
+ OperationStartFailed: 601,
32
+ DeviceMTUChangeFailed: 401,
33
+ OperationCancelled: 2,
34
+ OperationTimedOut: 3,
35
+ DeviceAlreadyConnected: 203,
36
+ },
37
+ BleManager: jest.fn(),
38
+ ScanMode: { LowLatency: 2 },
39
+ }));
40
+
41
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
42
+ __esModule: true,
43
+ default: {
44
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
45
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
46
+ pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
47
+ onDeviceBondState: jest.fn(),
48
+ },
49
+ }));
50
+
51
+ const UUID = '2146E74A-21CC-2EA6-2CE3-B4258520FD0F';
52
+
53
+ // What ble-plx rejects the in-flight operation with when the device ends the link. An iPhone 17
54
+ // reports a wiped device this way instead of CBErrorPeerRemovedPairingInformation.
55
+ const nativeDisconnect = (iosErrorCode: number, reason: string) =>
56
+ Object.assign(new Error(`Device ${UUID} was disconnected`), {
57
+ errorCode: 201,
58
+ iosErrorCode,
59
+ reason,
60
+ });
61
+ const peerEndedLink = () => nativeDisconnect(7, 'The specified device has disconnected from us.');
62
+ const linkTimedOut = () => nativeDisconnect(6, 'The connection has timed out unexpectedly.');
63
+
64
+ function createHarness() {
65
+ const writeCharacteristic = {
66
+ uuid: '00000002-0000-1000-8000-00805f9b34fb',
67
+ isWritableWithResponse: true,
68
+ writeWithResponse: jest.fn(() => Promise.resolve()),
69
+ };
70
+ const notifyCharacteristic = {
71
+ uuid: '00000003-0000-1000-8000-00805f9b34fb',
72
+ isNotifiable: true,
73
+ monitor: jest.fn(() => ({ remove: jest.fn() })),
74
+ };
75
+ const device = {
76
+ id: UUID,
77
+ name: 'Pro 2 48E9',
78
+ localName: 'Pro 2 48E9',
79
+ mtu: 247,
80
+ serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
81
+ isConnected: jest.fn(() => Promise.resolve(false)),
82
+ cancelConnection: jest.fn(() => Promise.resolve()),
83
+ connect: jest.fn(),
84
+ discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
85
+ characteristicsForService: jest.fn(() =>
86
+ Promise.resolve([writeCharacteristic, notifyCharacteristic])
87
+ ),
88
+ services: jest.fn(() => Promise.resolve([])),
89
+ onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
90
+ };
91
+ device.connect.mockImplementation(() => Promise.resolve(device));
92
+ const transport = new ReactNativeBleTransport({});
93
+ (transport as any).blePlxManager = {
94
+ devices: jest.fn(() => Promise.resolve([device])),
95
+ connectedDevices: jest.fn(() => Promise.resolve([])),
96
+ connectToDevice: jest.fn(() => Promise.resolve(device)),
97
+ cancelTransaction: jest.fn(() => Promise.resolve()),
98
+ cancelDeviceConnection: jest.fn(() => Promise.resolve()),
99
+ onStateChange: jest.fn((listener: (state: string) => void) => {
100
+ setImmediate(() => listener('PoweredOn'));
101
+ return { remove: jest.fn() };
102
+ }),
103
+ state: jest.fn(() => Promise.resolve('PoweredOn')),
104
+ };
105
+ transport.init(
106
+ { debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
107
+ new EventEmitter()
108
+ );
109
+ transport.configure(protocolV1Schema);
110
+ return { transport, device, writeCharacteristic, notifyCharacteristic };
111
+ }
112
+
113
+ type Outcome = { code?: unknown; params?: unknown; native?: unknown };
114
+
115
+ const flush = () =>
116
+ new Promise(resolve => {
117
+ setImmediate(resolve);
118
+ });
119
+
120
+ /** Runs one acquire to its end, moving the fake clock past the transport's own waits. */
121
+ async function acquire(
122
+ transport: ReactNativeBleTransport,
123
+ input: Record<string, unknown> = {}
124
+ ): Promise<Outcome> {
125
+ let outcome: Outcome | undefined;
126
+ transport.acquire({ uuid: UUID, ...input } as never).then(
127
+ () => {
128
+ outcome = {};
129
+ },
130
+ error => {
131
+ const { code, params } = ERRORS.serializeError({ error });
132
+ outcome = { code, params, native: (error as { iosErrorCode?: unknown }).iosErrorCode };
133
+ }
134
+ );
135
+ for (let i = 0; i < 400 && !outcome; i += 1) {
136
+ // eslint-disable-next-line no-await-in-loop
137
+ await flush();
138
+ if (!outcome) jest.advanceTimersByTime(100);
139
+ }
140
+ if (!outcome) throw new Error('acquire did not settle');
141
+ return outcome;
142
+ }
143
+
144
+ describe('iOS link ended by the device right after connecting', () => {
145
+ beforeEach(() => {
146
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance', 'nextTick'] });
147
+ });
148
+
149
+ afterEach(() => {
150
+ jest.clearAllTimers();
151
+ jest.useRealTimers();
152
+ });
153
+
154
+ test('keeps the first error so Core retries, then reports an invalid bond', async () => {
155
+ const { transport, device } = createHarness();
156
+ device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
157
+ Promise.reject(peerEndedLink())
158
+ );
159
+
160
+ // Core retries an error it cannot classify, which is what makes the second attempt.
161
+ await expect(acquire(transport)).resolves.toEqual({ native: 7 });
162
+ await expect(acquire(transport)).resolves.toEqual({
163
+ code: HardwareErrorCode.BleBondInvalid,
164
+ params: { phase: 'connect', reason: 'peer_disconnected' },
165
+ });
166
+ // The verdict consumes the evidence, so the next one needs two attempts again.
167
+ await expect(acquire(transport)).resolves.toEqual({ native: 7 });
168
+ });
169
+
170
+ test('counts a drop that interrupts the first write the same way', async () => {
171
+ const { transport, writeCharacteristic } = createHarness();
172
+ writeCharacteristic.writeWithResponse.mockImplementation(() => Promise.reject(peerEndedLink()));
173
+
174
+ await expect(acquire(transport)).resolves.toMatchObject({
175
+ code: HardwareErrorCode.BleDeviceNotBonded,
176
+ });
177
+ await expect(acquire(transport)).resolves.toMatchObject({
178
+ code: HardwareErrorCode.BleBondInvalid,
179
+ });
180
+ });
181
+
182
+ test('counts a drop that only the notification stream reports', async () => {
183
+ const { transport, notifyCharacteristic } = createHarness();
184
+ // The request went out before the link ended, so no write is there to fail.
185
+ notifyCharacteristic.monitor.mockImplementation(((
186
+ listener: (error: unknown, characteristic: unknown) => void
187
+ ) => {
188
+ setTimeout(() => listener(peerEndedLink(), null), 50);
189
+ return { remove: jest.fn() };
190
+ }) as never);
191
+
192
+ await expect(acquire(transport)).resolves.not.toMatchObject({
193
+ code: HardwareErrorCode.BleBondInvalid,
194
+ });
195
+ await expect(acquire(transport)).resolves.toMatchObject({
196
+ code: HardwareErrorCode.BleBondInvalid,
197
+ });
198
+ });
199
+
200
+ test('needs the second drop soon after the first', async () => {
201
+ const { transport, device } = createHarness();
202
+ device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
203
+ Promise.reject(peerEndedLink())
204
+ );
205
+
206
+ await acquire(transport);
207
+ jest.setSystemTime(Date.now() + IOS_PEER_TERMINATION_REPEAT_WINDOW_MS + 1);
208
+
209
+ await expect(acquire(transport)).resolves.toEqual({ native: 7 });
210
+ });
211
+
212
+ test('starts over after an attempt that failed for another reason', async () => {
213
+ const { transport, device } = createHarness();
214
+ device.discoverAllServicesAndCharacteristics
215
+ .mockImplementationOnce(() => Promise.reject(peerEndedLink()))
216
+ .mockImplementationOnce(() => Promise.reject(linkTimedOut()))
217
+ .mockImplementation(() => Promise.reject(peerEndedLink()));
218
+
219
+ await acquire(transport);
220
+ await expect(acquire(transport)).resolves.toEqual({ native: 6 });
221
+ await expect(acquire(transport)).resolves.toEqual({ native: 7 });
222
+ });
223
+
224
+ test('starts over once the device has answered', async () => {
225
+ const { transport, device } = createHarness();
226
+ (transport as any).detectProtocol = jest.fn(() => Promise.resolve('V1'));
227
+ device.discoverAllServicesAndCharacteristics
228
+ .mockImplementationOnce(() => Promise.reject(peerEndedLink()))
229
+ .mockImplementationOnce(() => Promise.resolve())
230
+ .mockImplementation(() => Promise.reject(peerEndedLink()));
231
+
232
+ await acquire(transport);
233
+ await expect(acquire(transport)).resolves.toEqual({});
234
+
235
+ await expect(acquire(transport)).resolves.toEqual({ native: 7 });
236
+ });
237
+
238
+ test('never classifies a firmware-install reconnect', async () => {
239
+ const { transport, device } = createHarness();
240
+ device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
241
+ Promise.reject(peerEndedLink())
242
+ );
243
+ const reconnect = { expectedProtocol: 'V2', skipProtocolProbe: true };
244
+
245
+ await expect(acquire(transport, reconnect)).resolves.toEqual({ native: 7 });
246
+ await expect(acquire(transport, reconnect)).resolves.toEqual({ native: 7 });
247
+ // The device was rebooting, so those drops are not evidence for a later attempt either.
248
+ await expect(acquire(transport)).resolves.toEqual({ native: 7 });
249
+ });
250
+
251
+ test('reports a lost bond that iOS names during GATT setup', async () => {
252
+ const { transport, device } = createHarness();
253
+ device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
254
+ Promise.reject(nativeDisconnect(14, 'Peer removed pairing information'))
255
+ );
256
+
257
+ await expect(acquire(transport)).resolves.toMatchObject({
258
+ code: HardwareErrorCode.BlePeerRemovedPairingInformation,
259
+ });
260
+ });
261
+ });
262
+
263
+ describe('IosPeerTerminationTracker', () => {
264
+ beforeEach(() => {
265
+ jest.useFakeTimers({ doNotFake: ['performance'] });
266
+ });
267
+
268
+ afterEach(() => {
269
+ jest.useRealTimers();
270
+ });
271
+
272
+ const dropLink = (tracker: IosPeerTerminationTracker, linkAgeMs = 0) => {
273
+ tracker.attemptStarted(UUID);
274
+ tracker.linkStarted(UUID);
275
+ jest.setSystemTime(Date.now() + linkAgeMs);
276
+ tracker.note(UUID, peerEndedLink());
277
+ return tracker.attemptFailed(UUID);
278
+ };
279
+
280
+ test('ignores a drop on a link that was reused or had been up for a while', () => {
281
+ const tracker = new IosPeerTerminationTracker();
282
+ expect(dropLink(tracker)).toBe(false);
283
+
284
+ tracker.attemptStarted(UUID);
285
+ tracker.note(UUID, peerEndedLink());
286
+ expect(tracker.attemptFailed(UUID)).toBe(false);
287
+
288
+ expect(dropLink(tracker)).toBe(false);
289
+ expect(dropLink(tracker, IOS_PEER_TERMINATION_LINK_WINDOW_MS + 1)).toBe(false);
290
+ });
291
+
292
+ test('ignores errors that do not say the peer ended the link', () => {
293
+ const tracker = new IosPeerTerminationTracker();
294
+ expect(dropLink(tracker)).toBe(false);
295
+
296
+ tracker.attemptStarted(UUID);
297
+ tracker.linkStarted(UUID);
298
+ tracker.note(UUID, linkTimedOut());
299
+ tracker.note(UUID, { errorCode: 201 });
300
+ tracker.note(UUID, undefined);
301
+ expect(tracker.attemptFailed(UUID)).toBe(false);
302
+ });
303
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * iOS reports a bond the device no longer holds as CBErrorPeerRemovedPairingInformation, but not
3
+ * on every iPhone. On the iPhone 17 family the device ends the link about a second after
4
+ * connecting instead (its firmware drops a link on the second key failure), and the app only sees
5
+ * CBErrorPeripheralDisconnected on whatever operation was in flight. The disconnect event carries
6
+ * no reason on iOS, so that error is the only place the code appears.
7
+ *
8
+ * A device that powers off or reboots right after connecting ends one link the same way, so a
9
+ * single drop proves nothing. iOS never re-pairs on its own, which makes a stale bond end every
10
+ * new link like that: two acquire attempts in a row, each dropped by the peer on a link it had
11
+ * just opened and before any response, is what this classifies as a stale bond.
12
+ */
13
+
14
+ /** CBError.peripheralDisconnected: the peer ended the link. A link timeout is a different code. */
15
+ const IOS_PERIPHERAL_DISCONNECTED = 7;
16
+
17
+ /** The device drops a stale bond about a second after connecting; later drops mean something else. */
18
+ export const IOS_PEER_TERMINATION_LINK_WINDOW_MS = 10_000;
19
+ /** How long one dropped link stays usable as evidence for the next attempt. */
20
+ export const IOS_PEER_TERMINATION_REPEAT_WINDOW_MS = 60_000;
21
+
22
+ export const isIosPeerTerminationError = (error: unknown): boolean =>
23
+ (error as { iosErrorCode?: unknown } | null | undefined)?.iosErrorCode ===
24
+ IOS_PERIPHERAL_DISCONNECTED;
25
+
26
+ export class IosPeerTerminationTracker {
27
+ /** When the current acquire attempt opened its link; unset when it reuses an older one. */
28
+ private linkStartedAt = new Map<string, number>();
29
+
30
+ /** When the peer ended the current attempt's link. */
31
+ private terminatedAt = new Map<string, number>();
32
+
33
+ /** When the previous attempt ended that way. */
34
+ private evidenceAt = new Map<string, number>();
35
+
36
+ attemptStarted(deviceId: string) {
37
+ this.linkStartedAt.delete(deviceId);
38
+ this.terminatedAt.delete(deviceId);
39
+ }
40
+
41
+ linkStarted(deviceId: string) {
42
+ this.linkStartedAt.set(deviceId, Date.now());
43
+ this.terminatedAt.delete(deviceId);
44
+ }
45
+
46
+ /** Reads the native error of a failed operation before it is mapped to a typed one. */
47
+ note(deviceId: string, error: unknown) {
48
+ if (!isIosPeerTerminationError(error)) return;
49
+ if (!this.terminatedAt.has(deviceId)) this.terminatedAt.set(deviceId, Date.now());
50
+ }
51
+
52
+ /** The device answered on this link, so its bond is valid. */
53
+ linkProven(deviceId: string) {
54
+ this.forget(deviceId);
55
+ }
56
+
57
+ forget(deviceId: string) {
58
+ this.linkStartedAt.delete(deviceId);
59
+ this.terminatedAt.delete(deviceId);
60
+ this.evidenceAt.delete(deviceId);
61
+ }
62
+
63
+ /** Returns true when this failed attempt is the second in a row the peer ended on a new link. */
64
+ attemptFailed(deviceId: string): boolean {
65
+ const startedAt = this.linkStartedAt.get(deviceId);
66
+ const terminatedAt = this.terminatedAt.get(deviceId);
67
+ const previous = this.evidenceAt.get(deviceId);
68
+ this.forget(deviceId);
69
+
70
+ if (
71
+ startedAt === undefined ||
72
+ terminatedAt === undefined ||
73
+ terminatedAt - startedAt > IOS_PEER_TERMINATION_LINK_WINDOW_MS
74
+ ) {
75
+ return false;
76
+ }
77
+ if (
78
+ previous !== undefined &&
79
+ terminatedAt - previous <= IOS_PEER_TERMINATION_REPEAT_WINDOW_MS
80
+ ) {
81
+ return true;
82
+ }
83
+ this.evidenceAt.set(deviceId, terminatedAt);
84
+ return false;
85
+ }
86
+
87
+ reset() {
88
+ this.linkStartedAt.clear();
89
+ this.terminatedAt.clear();
90
+ this.evidenceAt.clear();
91
+ }
92
+ }
package/src/index.ts CHANGED
@@ -59,6 +59,7 @@ import {
59
59
  stopBleEncryptionTracking,
60
60
  waitForAndroidLinkEncryption,
61
61
  } from './bleEncryption';
62
+ import { IosPeerTerminationTracker } from './bleIosStaleBond';
62
63
  import { isNativeBleDisconnectError, toBleDisconnectHardwareError } from './bleNativeDisconnect';
63
64
  import {
64
65
  isBleStaleBondHardwareError,
@@ -588,6 +589,9 @@ export default class ReactNativeBleTransport {
588
589
  /** When the current Android link attempt began; bounds which key-missing signals apply. */
589
590
  private androidLinkStartedAt: Map<string, number> = new Map();
590
591
 
592
+ /** iOS acquire attempts whose new link the peer ended; two in a row mean a stale bond. */
593
+ private iosPeerTermination = new IosPeerTerminationTracker();
594
+
591
595
  /** Strict or previously confirmed V2 target while acquire installs notifications. */
592
596
  private acquiringProtocolV2 = new Set<string>();
593
597
 
@@ -1112,7 +1116,7 @@ export default class ReactNativeBleTransport {
1112
1116
  try {
1113
1117
  return await this.acquireUnlocked(input);
1114
1118
  } catch (error) {
1115
- throw await this.resolveAndroidBondInvalid(uuid, error);
1119
+ throw this.resolveIosBondInvalid(input, await this.resolveAndroidBondInvalid(uuid, error));
1116
1120
  }
1117
1121
  });
1118
1122
  }
@@ -1120,6 +1124,7 @@ export default class ReactNativeBleTransport {
1120
1124
  private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
1121
1125
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1122
1126
  const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
1127
+ this.iosPeerTermination.attemptStarted(uuid);
1123
1128
  const shouldMapProtocolV2StaleBond = expectedProtocol
1124
1129
  ? expectedProtocol === 'V2'
1125
1130
  : this.confirmedProtocolV2.has(uuid);
@@ -1492,6 +1497,7 @@ export default class ReactNativeBleTransport {
1492
1497
  }
1493
1498
  );
1494
1499
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1500
+ this.iosPeerTermination.linkProven(uuid);
1495
1501
  const currentTransport = transportCache[uuid];
1496
1502
  if (!currentTransport) {
1497
1503
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
@@ -1524,6 +1530,7 @@ export default class ReactNativeBleTransport {
1524
1530
  const subscription = characteristic.monitor((error, c) => {
1525
1531
  const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
1526
1532
  if (error) {
1533
+ if (isCurrentMonitor) this.iosPeerTermination.note(uuid, error);
1527
1534
  Log?.debug(
1528
1535
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
1529
1536
  error as unknown as string
@@ -1810,6 +1817,31 @@ export default class ReactNativeBleTransport {
1810
1817
  });
1811
1818
  }
1812
1819
 
1820
+ /**
1821
+ * iPhones that do not report a lost bond only show the device ending each new link. One such
1822
+ * attempt keeps its error so Core retries; the second in a row is the stale bond.
1823
+ */
1824
+ private resolveIosBondInvalid(input: FirmwareInstallBleAcquireInput, error: unknown): unknown {
1825
+ if (Platform.OS !== 'ios') return error;
1826
+ const { uuid, skipProtocolProbe } = input;
1827
+ // A firmware-install reconnect expects the device to come and go.
1828
+ if (skipProtocolProbe || isBleStaleBondHardwareError(error)) {
1829
+ this.iosPeerTermination.forget(uuid);
1830
+ return error;
1831
+ }
1832
+ if (!this.iosPeerTermination.attemptFailed(uuid)) return error;
1833
+ Log?.debug(
1834
+ '[ReactNativeBleTransport] iOS peer ended two new links in a row, bond is invalid:',
1835
+ {
1836
+ connectIdSuffix: uuid.slice(-8),
1837
+ }
1838
+ );
1839
+ return ERRORS.TypedError(HardwareErrorCode.BleBondInvalid, undefined, {
1840
+ phase: 'connect',
1841
+ reason: 'peer_disconnected',
1842
+ });
1843
+ }
1844
+
1813
1845
  /**
1814
1846
  * Android 16+ encrypts a bonded link on its own right after connecting. Subscribing to
1815
1847
  * notifications before that finishes gets the CCCD write rejected, and the framework's retry
@@ -2109,6 +2141,7 @@ export default class ReactNativeBleTransport {
2109
2141
  this.androidPriorityResetTimers.clear();
2110
2142
  this.androidHighPriorityDevices.clear();
2111
2143
  this.androidLinkStartedAt.clear();
2144
+ this.iosPeerTermination.reset();
2112
2145
  stopBleKeyMissingTracking();
2113
2146
  stopBleEncryptionTracking();
2114
2147
  const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
@@ -2426,6 +2459,7 @@ export default class ReactNativeBleTransport {
2426
2459
  }),
2427
2460
  ]);
2428
2461
  succeeded = true;
2462
+ this.iosPeerTermination.linkStarted(uuid);
2429
2463
  return result;
2430
2464
  } catch (error) {
2431
2465
  if (timedOut || isNativeOperationTimeoutError(error)) {
@@ -2479,6 +2513,11 @@ export default class ReactNativeBleTransport {
2479
2513
  succeeded = true;
2480
2514
  return result;
2481
2515
  } catch (error) {
2516
+ this.iosPeerTermination.note(uuid, error);
2517
+ if (Platform.OS === 'ios' && isNativeBleStaleBondError(error)) {
2518
+ // iOS can report the lost bond on the first request instead of on the connect.
2519
+ throw toBleStaleBondHardwareError(error);
2520
+ }
2482
2521
  if (timedOut || isNativeOperationTimeoutError(error)) {
2483
2522
  const resetManager = this.abandonStalledConnection(
2484
2523
  uuid,
@@ -2596,6 +2635,7 @@ export default class ReactNativeBleTransport {
2596
2635
  ]);
2597
2636
  this.writeTimeoutCounts.delete(uuid);
2598
2637
  } catch (error) {
2638
+ this.iosPeerTermination.note(uuid, error);
2599
2639
  if (timedOut) {
2600
2640
  // A superseded call's late write must not tear down the link the current
2601
2641
  // call is using; only the owner of the transport may declare it dead.