@onekeyfe/hd-transport-react-native 1.2.2-alpha.105 → 1.2.2-alpha.107

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.
@@ -4,7 +4,7 @@ import transportPackage, {
4
4
  ProtocolV2,
5
5
  TRANSPORT_EVENT,
6
6
  } from '@onekeyfe/hd-transport';
7
- import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
7
+ import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
8
8
 
9
9
  import ReactNativeBleTransport, {
10
10
  BLE_NATIVE_TEARDOWN_TIMEOUT_MS,
@@ -28,7 +28,7 @@ jest.mock('react-native-ble-plx', () => ({
28
28
  BleError: class BleError extends Error {},
29
29
  BleErrorCode: {
30
30
  DeviceAlreadyConnected: 203,
31
- DeviceDisconnected: 205,
31
+ DeviceDisconnected: 201,
32
32
  DeviceMTUChangeFailed: 206,
33
33
  OperationCancelled: 2,
34
34
  CharacteristicNotFound: 404,
@@ -360,32 +360,46 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
360
360
  expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
361
361
  });
362
362
 
363
- test('connects the Android GATT link before starting system bonding', async () => {
364
- setPlatformOS('android');
365
- const { transport, uuid, device } = createHarness();
366
- const operationOrder: string[] = [];
367
- const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
363
+ test.each(['V1', 'V2'] as const)(
364
+ 'waits for Android bonding before connecting the %s GATT link',
365
+ async protocol => {
366
+ setPlatformOS('android');
367
+ const { transport, uuid, device } = protocol === 'V1' ? createV1Harness() : createHarness();
368
+ const operationOrder: string[] = [];
369
+ const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
370
+ const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
371
+ const bonding = createDeferred<void>();
368
372
 
369
- device.isConnected.mockResolvedValueOnce(false);
370
- device.connect = jest.fn(() => {
371
- operationOrder.push('connect');
372
- return Promise.resolve(device);
373
- });
374
- pairDeviceMock.mockImplementationOnce(() => {
375
- operationOrder.push('bond');
376
- return Promise.resolve({ bonded: true, bonding: false });
377
- });
373
+ device.isConnected.mockResolvedValueOnce(false);
374
+ device.connect = jest.fn(() => {
375
+ operationOrder.push('connect');
376
+ return Promise.resolve(device);
377
+ });
378
+ pairDeviceMock.mockImplementationOnce(() => {
379
+ operationOrder.push('bond');
380
+ return Promise.resolve({ bonded: false, bonding: true });
381
+ });
382
+ bondStateMock.mockImplementationOnce(() => bonding.promise);
378
383
 
379
- await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
380
- uuid,
381
- protocolType: 'V2',
382
- });
384
+ const acquiring = transport.acquire({ uuid, expectedProtocol: protocol });
385
+ await new Promise(resolve => {
386
+ setImmediate(resolve);
387
+ });
388
+ expect(operationOrder).toEqual(['bond']);
389
+ expect(device.connect).not.toHaveBeenCalled();
383
390
 
384
- expect(operationOrder).toEqual(['connect', 'bond']);
385
- await transport.release(uuid, true);
386
- });
391
+ bonding.resolve();
392
+ await expect(acquiring).resolves.toEqual({
393
+ uuid,
394
+ protocolType: protocol,
395
+ });
387
396
 
388
- test('does not start Android bonding when both reconnect attempts fail', async () => {
397
+ expect(operationOrder).toEqual(['bond', 'connect']);
398
+ await transport.release(uuid, true);
399
+ }
400
+ );
401
+
402
+ test('preserves Android connect errors after bonding when both reconnect attempts fail', async () => {
389
403
  setPlatformOS('android');
390
404
  const { transport, uuid, device } = createHarness();
391
405
  const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
@@ -412,22 +426,35 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
412
426
  errorCode: HardwareErrorCode.BleConnectedError,
413
427
  });
414
428
  expect(device.connect).toHaveBeenCalledTimes(2);
415
- expect(pairDeviceMock).not.toHaveBeenCalled();
429
+ expect(pairDeviceMock).toHaveBeenCalledTimes(1);
416
430
  });
417
431
 
418
- test('checks that the Android GATT link is connected before bonding', async () => {
432
+ test('does not connect Android GATT when the system cannot start bonding', async () => {
419
433
  setPlatformOS('android');
420
434
  const { transport, uuid, device, bleManager } = createHarness();
421
435
  const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
422
436
 
423
- device.isConnected.mockResolvedValueOnce(false).mockResolvedValueOnce(false);
424
437
  device.connect = jest.fn().mockResolvedValue(device);
425
438
  pairDeviceMock.mockClear();
439
+ pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: false });
440
+
441
+ await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
442
+ errorCode: HardwareErrorCode.BleDeviceNotBonded,
443
+ });
444
+ expect(device.connect).not.toHaveBeenCalled();
445
+ expect(bleManager.devices).not.toHaveBeenCalled();
446
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
447
+ });
448
+
449
+ test('checks that Android GATT is connected after bonding and reconnecting', async () => {
450
+ setPlatformOS('android');
451
+ const { transport, uuid, device, bleManager } = createHarness();
452
+ device.isConnected.mockResolvedValueOnce(false).mockResolvedValueOnce(false);
453
+ device.connect = jest.fn().mockResolvedValue(device);
426
454
 
427
455
  await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
428
456
  errorCode: HardwareErrorCode.BleConnectedError,
429
457
  });
430
- expect(pairDeviceMock).not.toHaveBeenCalled();
431
458
  expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
432
459
  expect(device.cancelConnection).toHaveBeenCalledTimes(1);
433
460
  });
@@ -468,12 +495,12 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
468
495
  .mockRejectedValue(Object.assign(new BleErrorMock(nativeError.reason), nativeError));
469
496
 
470
497
  await expect(transport.acquire({ uuid })).rejects.toMatchObject({ errorCode });
471
- expect(pairDeviceMock).not.toHaveBeenCalled();
498
+ expect(pairDeviceMock).toHaveBeenCalledTimes(platform === 'android' ? 1 : 0);
472
499
  expect(transport.getProtocolType(uuid)).toBeUndefined();
473
500
  }
474
501
  );
475
502
 
476
- test('closes the Android GATT link when system bonding fails', async () => {
503
+ test('cleans up Android bonding failures without starting GATT', async () => {
477
504
  setPlatformOS('android');
478
505
  const { transport, uuid, device, bleManager } = createHarness();
479
506
  const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
@@ -485,7 +512,32 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
485
512
  );
486
513
 
487
514
  expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
488
- expect(device.cancelConnection).toHaveBeenCalledTimes(1);
515
+ expect(bleManager.devices).not.toHaveBeenCalled();
516
+ expect(device.isConnected).not.toHaveBeenCalled();
517
+ });
518
+
519
+ test('does not connect after stop while Android bonding is pending', async () => {
520
+ setPlatformOS('android');
521
+ const { transport, uuid, bleManager } = createHarness();
522
+ const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
523
+ const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
524
+ const bonding = createDeferred<void>();
525
+ pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: true });
526
+ bondStateMock.mockImplementationOnce(() => bonding.promise);
527
+
528
+ const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
529
+ const rejection = expect(acquiring).rejects.toMatchObject({
530
+ errorCode: HardwareErrorCode.BleDeviceDisconnected,
531
+ });
532
+ await new Promise(resolve => {
533
+ setImmediate(resolve);
534
+ });
535
+ const stopping = transport.stop();
536
+ bonding.resolve();
537
+
538
+ await rejection;
539
+ await stopping;
540
+ expect(bleManager.devices).not.toHaveBeenCalled();
489
541
  });
490
542
 
491
543
  test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
@@ -830,6 +882,116 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
830
882
  BLE_NATIVE_TEARDOWN_TIMEOUT_MS + 5_000
831
883
  );
832
884
 
885
+ test('preserves the unpaired error when iOS disconnects during the first V1 probe', async () => {
886
+ const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
887
+ writeCharacteristic.writeWithResponse.mockRejectedValueOnce({
888
+ errorCode: 201,
889
+ iosErrorCode: 7,
890
+ reason: 'The specified device has disconnected from us.',
891
+ });
892
+ const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
893
+
894
+ await expect(transport.acquire({ uuid })).rejects.toMatchObject({
895
+ errorCode: HardwareErrorCode.BleDeviceNotBonded,
896
+ });
897
+
898
+ expect(probeProtocolV2).not.toHaveBeenCalled();
899
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
900
+ expect(transport.getProtocolType(uuid)).toBeUndefined();
901
+ });
902
+
903
+ test('preserves a native iOS disconnect during an expected Protocol V2 probe', async () => {
904
+ const { transport, uuid, writeCharacteristic, bleManager } = createHarness({
905
+ deviceName: 'Neo Test',
906
+ });
907
+ const nativeDisconnect = {
908
+ errorCode: 201,
909
+ iosErrorCode: 7,
910
+ reason: 'The specified device has disconnected from us.',
911
+ };
912
+ writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce(nativeDisconnect);
913
+ const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
914
+
915
+ await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
916
+ errorCode: HardwareErrorCode.BleDeviceDisconnected,
917
+ });
918
+
919
+ expect(probeProtocolV1).not.toHaveBeenCalled();
920
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
921
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
922
+ expect(transport.getProtocolType(uuid)).toBeUndefined();
923
+ });
924
+
925
+ test('preserves a native iOS disconnect during a V2-first probe without falling back to V1', async () => {
926
+ const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
927
+ writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce({
928
+ errorCode: 201,
929
+ iosErrorCode: 7,
930
+ reason: 'The specified device has disconnected from us.',
931
+ });
932
+ const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
933
+
934
+ await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toMatchObject({
935
+ errorCode: HardwareErrorCode.BleDeviceDisconnected,
936
+ });
937
+
938
+ expect(probeProtocolV1).not.toHaveBeenCalled();
939
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
940
+ expect(transport.getProtocolType(uuid)).toBeUndefined();
941
+ });
942
+
943
+ test.each(['V1', 'V2'] as const)(
944
+ 'preserves terminal BLE failures from the %s probe without trying another protocol',
945
+ async protocol => {
946
+ for (const errorCode of [
947
+ HardwareErrorCode.BleDeviceNotBonded,
948
+ HardwareErrorCode.BleDeviceBondedCanceled,
949
+ HardwareErrorCode.BlePeerRemovedPairingInformation,
950
+ HardwareErrorCode.BleDeviceDisconnected,
951
+ HardwareErrorCode.BleCharacteristicNotifyError,
952
+ HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
953
+ HardwareErrorCode.BleWriteCharacteristicError,
954
+ ]) {
955
+ const { transport, uuid } = createHarness();
956
+ const error = ERRORS.TypedError(errorCode);
957
+ const call = jest
958
+ .spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
959
+ .mockRejectedValue(error);
960
+ const otherProbe = jest.spyOn(
961
+ transport as any,
962
+ protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1'
963
+ );
964
+
965
+ await expect(transport.acquire({ uuid, protocolHint: protocol })).rejects.toBe(error);
966
+
967
+ expect(call).toHaveBeenCalledTimes(1);
968
+ expect(otherProbe).not.toHaveBeenCalled();
969
+ expect(transport.getProtocolType(uuid)).toBeUndefined();
970
+ }
971
+ }
972
+ );
973
+
974
+ test.each(['V1', 'V2'] as const)(
975
+ 'still falls back after a silent %s probe timeout',
976
+ async protocol => {
977
+ const { transport, uuid } = createHarness();
978
+ jest
979
+ .spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
980
+ .mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError));
981
+ const otherProbe = jest
982
+ .spyOn(transport as any, protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1')
983
+ .mockResolvedValue(true);
984
+
985
+ await expect(transport.acquire({ uuid, protocolHint: protocol })).resolves.toEqual({
986
+ uuid,
987
+ protocolType: protocol === 'V1' ? 'V2' : 'V1',
988
+ });
989
+
990
+ expect(otherProbe).toHaveBeenCalledTimes(1);
991
+ await transport.release(uuid, true);
992
+ }
993
+ );
994
+
833
995
  test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
834
996
  const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
835
997
  const probeProtocolV1 = jest
@@ -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('orphan timer left behind by cancel() must not disconnect the transport', async () => {
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.BleTimeoutError);
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,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
+ };