@onekeyfe/hd-transport-react-native 1.2.4-alpha.0 → 1.2.4-alpha.2

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/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,
@@ -233,55 +234,6 @@ const isAndroidLinkLossError = (error: unknown): boolean => {
233
234
  code === HardwareErrorCode.BleConnectedError
234
235
  );
235
236
  };
236
-
237
- const logIosBleBondDiagnostic = (
238
- phase: string,
239
- error: unknown,
240
- outcome: 'fallback' | 'rethrow'
241
- ) => {
242
- if (Platform.OS !== 'ios') return;
243
-
244
- const nativeError = (error ?? {}) as {
245
- name?: unknown;
246
- errorCode?: unknown;
247
- attErrorCode?: unknown;
248
- iosErrorCode?: unknown;
249
- androidErrorCode?: unknown;
250
- };
251
- const scalar = (value: unknown) =>
252
- typeof value === 'string' || typeof value === 'number' ? value : undefined;
253
-
254
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] probe error', {
255
- phase,
256
- outcome,
257
- errorName: scalar(nativeError.name),
258
- errorCode: scalar(nativeError.errorCode),
259
- attErrorCode: scalar(nativeError.attErrorCode),
260
- iosErrorCode: scalar(nativeError.iosErrorCode),
261
- androidErrorCode: scalar(nativeError.androidErrorCode),
262
- nativeStaleBond: isNativeBleStaleBondError(error),
263
- hardwareStaleBond: isBleStaleBondHardwareError(error),
264
- nativeDisconnect: isNativeBleDisconnectError(error),
265
- });
266
- };
267
-
268
- type IosProbeDisconnectEvidence = {
269
- phase: 'protocol-v1-get-features' | 'protocol-v2-ping';
270
- errorCode?: string | number;
271
- recordedAt: number;
272
- };
273
-
274
- const IOS_PROBE_DISCONNECT_EVIDENCE_TTL_MS = 5 * 60_000;
275
-
276
- const isIosProbeDisconnectError = (error: unknown): boolean => {
277
- if (Platform.OS !== 'ios') return false;
278
- const code = (error as { errorCode?: unknown })?.errorCode;
279
- return (
280
- isNativeBleDisconnectError(error) ||
281
- code === HardwareErrorCode.BleDeviceNotBonded ||
282
- code === HardwareErrorCode.BleDeviceDisconnected
283
- );
284
- };
285
237
  /** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
286
238
  export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
287
239
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
@@ -624,14 +576,6 @@ export default class ReactNativeBleTransport {
624
576
  /** Endpoints whose last detection got no answer; 'woken' once their Initialize wake is spent. */
625
577
  private silentDetections = new Map<string, 'silent' | 'woken'>();
626
578
 
627
- /**
628
- * iOS can report a reset Pro2 as a generic peripheral disconnect instead of
629
- * CBErrorPeerRemovedPairingInformation. Keep that first probe failure across
630
- * the automatic cold reconnect and require a second failed probe before
631
- * classifying the OS bond as invalid.
632
- */
633
- private iosProbeDisconnectEvidence = new Map<string, IosProbeDisconnectEvidence>();
634
-
635
579
  /** Android endpoints whose cached GATT table is suspect; the next connect refreshes it. */
636
580
  private androidGattCacheRefreshes = new Set<string>();
637
581
 
@@ -645,6 +589,9 @@ export default class ReactNativeBleTransport {
645
589
  /** When the current Android link attempt began; bounds which key-missing signals apply. */
646
590
  private androidLinkStartedAt: Map<string, number> = new Map();
647
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
+
648
595
  /** Strict or previously confirmed V2 target while acquire installs notifications. */
649
596
  private acquiringProtocolV2 = new Set<string>();
650
597
 
@@ -1169,7 +1116,7 @@ export default class ReactNativeBleTransport {
1169
1116
  try {
1170
1117
  return await this.acquireUnlocked(input);
1171
1118
  } catch (error) {
1172
- throw await this.resolveAndroidBondInvalid(uuid, error);
1119
+ throw this.resolveIosBondInvalid(input, await this.resolveAndroidBondInvalid(uuid, error));
1173
1120
  }
1174
1121
  });
1175
1122
  }
@@ -1177,6 +1124,7 @@ export default class ReactNativeBleTransport {
1177
1124
  private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
1178
1125
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1179
1126
  const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
1127
+ this.iosPeerTermination.attemptStarted(uuid);
1180
1128
  const shouldMapProtocolV2StaleBond = expectedProtocol
1181
1129
  ? expectedProtocol === 'V2'
1182
1130
  : this.confirmedProtocolV2.has(uuid);
@@ -1549,6 +1497,7 @@ export default class ReactNativeBleTransport {
1549
1497
  }
1550
1498
  );
1551
1499
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1500
+ this.iosPeerTermination.linkProven(uuid);
1552
1501
  const currentTransport = transportCache[uuid];
1553
1502
  if (!currentTransport) {
1554
1503
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
@@ -1581,6 +1530,7 @@ export default class ReactNativeBleTransport {
1581
1530
  const subscription = characteristic.monitor((error, c) => {
1582
1531
  const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
1583
1532
  if (error) {
1533
+ if (isCurrentMonitor) this.noteIosLinkError(uuid, 'notify', error);
1584
1534
  Log?.debug(
1585
1535
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
1586
1536
  error as unknown as string
@@ -1867,6 +1817,56 @@ export default class ReactNativeBleTransport {
1867
1817
  });
1868
1818
  }
1869
1819
 
1820
+ /**
1821
+ * iOS says why a link ended only on the operation it interrupted, so the native codes are read
1822
+ * and logged here. Codes only: the reason text is localized and cannot be matched or searched.
1823
+ */
1824
+ private noteIosLinkError(uuid: string, stage: 'gatt-setup' | 'notify' | 'write', error: unknown) {
1825
+ if (Platform.OS !== 'ios') return;
1826
+ this.iosPeerTermination.note(uuid, error);
1827
+ // Writes and notifications also fail for reasons that say nothing about the link.
1828
+ if (stage !== 'gatt-setup' && !isNativeBleDisconnectError(error)) return;
1829
+ const native = (error ?? {}) as {
1830
+ errorCode?: unknown;
1831
+ iosErrorCode?: unknown;
1832
+ attErrorCode?: unknown;
1833
+ };
1834
+ const code = (value: unknown) =>
1835
+ typeof value === 'number' || typeof value === 'string' ? value : undefined;
1836
+ Log?.debug('[ReactNativeBleTransport] iOS operation failed', {
1837
+ connectIdSuffix: uuid.slice(-8),
1838
+ stage,
1839
+ errorCode: code(native.errorCode),
1840
+ iosErrorCode: code(native.iosErrorCode),
1841
+ attErrorCode: code(native.attErrorCode),
1842
+ });
1843
+ }
1844
+
1845
+ /**
1846
+ * iPhones that do not report a lost bond only show the device ending each new link. One such
1847
+ * attempt keeps its error so Core retries; the second in a row is the stale bond.
1848
+ */
1849
+ private resolveIosBondInvalid(input: FirmwareInstallBleAcquireInput, error: unknown): unknown {
1850
+ if (Platform.OS !== 'ios') return error;
1851
+ const { uuid, skipProtocolProbe } = input;
1852
+ // A firmware-install reconnect expects the device to come and go.
1853
+ if (skipProtocolProbe || isBleStaleBondHardwareError(error)) {
1854
+ this.iosPeerTermination.forget(uuid);
1855
+ return error;
1856
+ }
1857
+ if (!this.iosPeerTermination.attemptFailed(uuid)) return error;
1858
+ Log?.debug(
1859
+ '[ReactNativeBleTransport] iOS peer ended two new links in a row, bond is invalid:',
1860
+ {
1861
+ connectIdSuffix: uuid.slice(-8),
1862
+ }
1863
+ );
1864
+ return ERRORS.TypedError(HardwareErrorCode.BleBondInvalid, undefined, {
1865
+ phase: 'connect',
1866
+ reason: 'peer_disconnected',
1867
+ });
1868
+ }
1869
+
1870
1870
  /**
1871
1871
  * Android 16+ encrypts a bonded link on its own right after connecting. Subscribing to
1872
1872
  * notifications before that finishes gets the CCCD write rejected, and the framework's retry
@@ -2166,9 +2166,9 @@ export default class ReactNativeBleTransport {
2166
2166
  this.androidPriorityResetTimers.clear();
2167
2167
  this.androidHighPriorityDevices.clear();
2168
2168
  this.androidLinkStartedAt.clear();
2169
+ this.iosPeerTermination.reset();
2169
2170
  stopBleKeyMissingTracking();
2170
2171
  stopBleEncryptionTracking();
2171
- this.iosProbeDisconnectEvidence.clear();
2172
2172
  const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
2173
2173
  this.runPromise?.reject(error);
2174
2174
  this.runPromise = null;
@@ -2484,6 +2484,7 @@ export default class ReactNativeBleTransport {
2484
2484
  }),
2485
2485
  ]);
2486
2486
  succeeded = true;
2487
+ this.iosPeerTermination.linkStarted(uuid);
2487
2488
  return result;
2488
2489
  } catch (error) {
2489
2490
  if (timedOut || isNativeOperationTimeoutError(error)) {
@@ -2537,6 +2538,11 @@ export default class ReactNativeBleTransport {
2537
2538
  succeeded = true;
2538
2539
  return result;
2539
2540
  } catch (error) {
2541
+ this.noteIosLinkError(uuid, 'gatt-setup', error);
2542
+ if (Platform.OS === 'ios' && isNativeBleStaleBondError(error)) {
2543
+ // iOS can report the lost bond on the first request instead of on the connect.
2544
+ throw toBleStaleBondHardwareError(error);
2545
+ }
2540
2546
  if (timedOut || isNativeOperationTimeoutError(error)) {
2541
2547
  const resetManager = this.abandonStalledConnection(
2542
2548
  uuid,
@@ -2654,6 +2660,7 @@ export default class ReactNativeBleTransport {
2654
2660
  ]);
2655
2661
  this.writeTimeoutCounts.delete(uuid);
2656
2662
  } catch (error) {
2663
+ this.noteIosLinkError(uuid, 'write', error);
2657
2664
  if (timedOut) {
2658
2665
  // A superseded call's late write must not tear down the link the current
2659
2666
  // call is using; only the owner of the transport may declare it dead.
@@ -2788,64 +2795,6 @@ export default class ReactNativeBleTransport {
2788
2795
  );
2789
2796
  }
2790
2797
 
2791
- private rememberIosProbeDisconnect(
2792
- uuid: string,
2793
- phase: IosProbeDisconnectEvidence['phase'],
2794
- error: unknown
2795
- ): Error | undefined {
2796
- if (!isIosProbeDisconnectError(error)) return undefined;
2797
-
2798
- const previous = this.getIosProbeDisconnectEvidence(uuid);
2799
- const errorCode = (error as { errorCode?: unknown })?.errorCode;
2800
- const evidence: IosProbeDisconnectEvidence = {
2801
- phase,
2802
- ...(typeof errorCode === 'string' || typeof errorCode === 'number' ? { errorCode } : {}),
2803
- recordedAt: Date.now(),
2804
- };
2805
- this.iosProbeDisconnectEvidence.set(uuid, evidence);
2806
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] probe disconnect recorded', {
2807
- phase,
2808
- errorCode: evidence.errorCode,
2809
- repeated: Boolean(previous),
2810
- });
2811
-
2812
- if (!previous) return undefined;
2813
- return this.confirmIosProbeDisconnect(uuid, 'repeated-probe-disconnect');
2814
- }
2815
-
2816
- private getIosProbeDisconnectEvidence(uuid: string): IosProbeDisconnectEvidence | undefined {
2817
- const evidence = this.iosProbeDisconnectEvidence.get(uuid);
2818
- if (!evidence) return undefined;
2819
-
2820
- const ageMs = Date.now() - evidence.recordedAt;
2821
- if (ageMs <= IOS_PROBE_DISCONNECT_EVIDENCE_TTL_MS) return evidence;
2822
-
2823
- this.iosProbeDisconnectEvidence.delete(uuid);
2824
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] probe disconnect expired', {
2825
- triggerPhase: evidence.phase,
2826
- triggerErrorCode: evidence.errorCode,
2827
- ageMs,
2828
- });
2829
- return undefined;
2830
- }
2831
-
2832
- private confirmIosProbeDisconnect(
2833
- uuid: string,
2834
- confirmation: 'repeated-probe-disconnect' | 'follow-up-no-response'
2835
- ): Error | undefined {
2836
- const evidence = this.getIosProbeDisconnectEvidence(uuid);
2837
- if (!evidence) return undefined;
2838
-
2839
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] stale bond confirmed', {
2840
- confirmation,
2841
- triggerPhase: evidence.phase,
2842
- triggerErrorCode: evidence.errorCode,
2843
- evidenceAgeMs: Date.now() - evidence.recordedAt,
2844
- finalErrorCode: HardwareErrorCode.BleBondInvalid,
2845
- });
2846
- return ERRORS.TypedError(HardwareErrorCode.BleBondInvalid);
2847
- }
2848
-
2849
2798
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
2850
2799
  if (this.probingProtocols.get(uuid) === protocol) {
2851
2800
  this.probingProtocols.delete(uuid);
@@ -2877,7 +2826,6 @@ export default class ReactNativeBleTransport {
2877
2826
  // received no response. skipProtocolProbe does not need it either — its only
2878
2827
  // caller is the V2 firmware-install reconnect.
2879
2828
  if (expectedProtocol === 'V1') {
2880
- this.iosProbeDisconnectEvidence.delete(uuid);
2881
2829
  this.deviceProtocol.set(uuid, 'V1');
2882
2830
  Log?.debug('[ReactNativeBleTransport] protocol selected', {
2883
2831
  deviceId: uuid,
@@ -2893,7 +2841,6 @@ export default class ReactNativeBleTransport {
2893
2841
 
2894
2842
  if (expectedProtocol === 'V2') {
2895
2843
  if (await this.probeProtocolV2(uuid)) {
2896
- this.iosProbeDisconnectEvidence.delete(uuid);
2897
2844
  this.deviceProtocol.set(uuid, 'V2');
2898
2845
  this.sessionProtocols.set(uuid, 'V2');
2899
2846
  this.confirmedProtocolV2.add(uuid);
@@ -2904,18 +2851,6 @@ export default class ReactNativeBleTransport {
2904
2851
  });
2905
2852
  return 'V2';
2906
2853
  }
2907
- const staleBondError = this.confirmIosProbeDisconnect(uuid, 'follow-up-no-response');
2908
- if (Platform.OS === 'ios') {
2909
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] protocol detection ended', {
2910
- outcome: 'expected-v2-no-response',
2911
- expectedProtocol,
2912
- protocolHint: protocolHint ?? 'none',
2913
- finalErrorCode: staleBondError
2914
- ? HardwareErrorCode.BleBondInvalid
2915
- : HardwareErrorCode.RuntimeError,
2916
- });
2917
- }
2918
- if (staleBondError) throw staleBondError;
2919
2854
  throw this.createProtocolMismatchError(expectedProtocol);
2920
2855
  }
2921
2856
 
@@ -2951,7 +2886,6 @@ export default class ReactNativeBleTransport {
2951
2886
  const detected =
2952
2887
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
2953
2888
  if (detected) {
2954
- this.iosProbeDisconnectEvidence.delete(uuid);
2955
2889
  this.deviceProtocol.set(uuid, protocol);
2956
2890
  this.sessionProtocols.set(uuid, protocol);
2957
2891
  if (protocol === 'V2') {
@@ -2981,25 +2915,6 @@ export default class ReactNativeBleTransport {
2981
2915
 
2982
2916
  this.deviceProtocol.delete(uuid);
2983
2917
  this.probingProtocols.delete(uuid);
2984
- const staleBondError = this.confirmIosProbeDisconnect(uuid, 'follow-up-no-response');
2985
- if (Platform.OS === 'ios') {
2986
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] protocol detection ended', {
2987
- outcome: 'all-probes-no-response',
2988
- expectedProtocol: expectedProtocol ?? 'none',
2989
- protocolHint: protocolHint ?? 'none',
2990
- sessionProtocol: sessionProtocol ?? 'none',
2991
- probeOrder: probeOrder.join(','),
2992
- trustSessionProtocol,
2993
- reprobeFailures,
2994
- transportCached: Boolean(transportCache[uuid]),
2995
- notifyActive: Boolean(transportCache[uuid]?.notifySubscription),
2996
- staleBondRecorded: this.iosProbeDisconnectEvidence.has(uuid),
2997
- finalErrorCode: staleBondError
2998
- ? HardwareErrorCode.BleBondInvalid
2999
- : HardwareErrorCode.BleTimeoutError,
3000
- });
3001
- }
3002
- if (staleBondError) throw staleBondError;
3003
2918
  throw this.createProtocolDetectionError();
3004
2919
  }
3005
2920
 
@@ -3098,19 +3013,7 @@ export default class ReactNativeBleTransport {
3098
3013
  } catch (error) {
3099
3014
  this.clearProbeProtocol(uuid, 'V1');
3100
3015
  Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
3101
- const shouldRethrow = shouldRethrowProtocolProbeError(error);
3102
- const staleBondError = this.rememberIosProbeDisconnect(
3103
- uuid,
3104
- 'protocol-v1-get-features',
3105
- error
3106
- );
3107
- logIosBleBondDiagnostic(
3108
- 'protocol-v1-get-features',
3109
- error,
3110
- shouldRethrow ? 'rethrow' : 'fallback'
3111
- );
3112
- if (staleBondError) throw staleBondError;
3113
- if (shouldRethrow) {
3016
+ if (shouldRethrowProtocolProbeError(error)) {
3114
3017
  throw error;
3115
3018
  }
3116
3019
  return false;
@@ -3125,26 +3028,18 @@ export default class ReactNativeBleTransport {
3125
3028
  this.probingProtocols.set(uuid, 'V2');
3126
3029
  this.protocolV2Assemblers.get(uuid)?.reset();
3127
3030
  this.throwIfStaleBondError(uuid);
3128
- let detected: boolean;
3129
- try {
3130
- detected = await probeProtocolV2Helper({
3131
- call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
3132
- this.callProtocolV2(uuid, name, data, options),
3133
- timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
3134
- logger: Log,
3135
- logPrefix: 'ProtocolV2 RN-BLE',
3136
- onProbeFailed: () => {
3137
- this.protocolV2Assemblers.get(uuid)?.reset();
3138
- this.resetProtocolV2Frames(uuid);
3139
- },
3140
- shouldRethrow: shouldRethrowProtocolProbeError,
3141
- });
3142
- } catch (error) {
3143
- const staleBondError = this.rememberIosProbeDisconnect(uuid, 'protocol-v2-ping', error);
3144
- logIosBleBondDiagnostic('protocol-v2-ping', error, 'rethrow');
3145
- if (staleBondError) throw staleBondError;
3146
- throw error;
3147
- }
3031
+ const detected = await probeProtocolV2Helper({
3032
+ call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
3033
+ this.callProtocolV2(uuid, name, data, options),
3034
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
3035
+ logger: Log,
3036
+ logPrefix: 'ProtocolV2 RN-BLE',
3037
+ onProbeFailed: () => {
3038
+ this.protocolV2Assemblers.get(uuid)?.reset();
3039
+ this.resetProtocolV2Frames(uuid);
3040
+ },
3041
+ shouldRethrow: shouldRethrowProtocolProbeError,
3042
+ });
3148
3043
  if (!detected) {
3149
3044
  this.clearProbeProtocol(uuid, 'V2');
3150
3045
  } else {