diff --git a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart index 60170bb6..3ad9b0d3 100644 --- a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart +++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart @@ -64,7 +64,13 @@ class ConnectBitboxCubit extends Cubit { } Future connectToBitbox(sdk.BitboxDevice device) async { - if (state is BitboxConnecting) return; + // Coalesce overlapping scan ticks onto ONE connect attempt: the state + // guard alone is defeated once the flow moves past BitboxConnecting + // (e.g. BitboxCheckHash) while the init future is still pending — a late + // tick then started a second init() on the shared SDK manager and wedged + // pairing (issue #657 P7 F1). `_pendingInit` covers that whole window; it + // is cleared on failure, so a genuine retry still passes. + if (state is BitboxConnecting || _pendingInit != null) return; emit(BitboxConnecting(device)); try { // Snapshot any hash from a prior pairing on the same BitboxService diff --git a/test/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit_test.dart b/test/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit_test.dart index aa0420f2..6c9cd320 100644 --- a/test/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit_test.dart +++ b/test/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit_test.dart @@ -116,6 +116,36 @@ void main() { verify(() => authService.ensureSignatureFor(any())).called(1); }); + test('a late scan tick does not start a second init while one is pending ' + '(issue #657 P7 F1)', () async { + final initCompleter = Completer(); + var pollCount = 0; + when(() => service.getAllUsbDevices()).thenAnswer((_) async => [device]); + when(() => service.init(any())).thenAnswer((_) => initCompleter.future); + when(() => service.getChannelHash()).thenAnswer((_) async { + pollCount++; + return pollCount < 2 ? '' : 'HASH-1'; + }); + + final cubit = makeCubit(); + addTearDown(cubit.close); + + // First connect runs init() once and settles in BitboxCheckHash — state + // is now past BitboxConnecting while the init future is still pending. + await waitForState(cubit); + verify(() => service.init(any())).called(1); + + // An overlapping/late scan tick re-invokes connectToBitbox. + await cubit.connectToBitbox(device); + + // It must coalesce onto the in-flight init, NOT start a second init on + // the shared SDK manager (which used to wedge pairing). + verifyNever(() => service.init(any())); + expect(cubit.state, isA()); + + initCompleter.complete(true); + }); + test('emits BitboxSignatureFailed when the signature capture throws', () async { var pollCount = 0; when(() => service.getAllUsbDevices()).thenAnswer((_) async => [device]);