Tracking issue for the remaining work from the two BitBox02 customer cases reported on 23.07. and the crash-reporting groundwork in #878. Four workstreams, ordered by customer impact.
1. USB connect: crash/hang when the BitBox02 re-enumerates during initial pairing
This is the crash/loop case explicitly declared out of scope in #866 ("The unexplained crash/loop reported alongside this customer case") — it now has a root-cause analysis and gets its tracked home here.
Symptoms (customer on a Samsung Galaxy Tab S4, BitBox02 Nova, current app):
- Tapping "BitBox hardware wallet" raises the system dialog "Open BitBoxApp?"; cancelling it crashes the app to the home screen.
- With the vendor BitBoxApp uninstalled, connecting still aborts/crashes.
- Best run: after "Tap this side", the logo spins forever and the password prompt never appears.
- The Android dialog "Allow RealUnit to access the USB device" never appeared in any attempt.
Verified root causes (code review of the app plus the pinned bitbox_flutter v0.0.10 plugin; the BitBox02 re-enumerates on the USB bus after firmware boot/unlock):
- Unstable device identifier. Devices are addressed by the Android
deviceName bus path (GetDevicesOperation.kt:17-19, key of UsbManager.getDeviceList()), which does not survive re-enumeration; BitboxManager.kt:18-22 then fails with "Device with identifier not found". GoDeviceInfo additionally holds the UsbDevice/connection from open() time — after re-attach the file descriptor is dead and read/write run into 10 s bulkTransfer timeouts (GoDeviceInfo.kt:35-55).
- No re-attach recovery during pairing. After starting the connect, the cubit only polls
getChannelHash() for 90 s against the old handle (connect_bitbox_cubit.dart:112-136) — no rescan, no new permission request. The only recovery observer starts after successful pairing (connect_bitbox_cubit.dart:232), i.e. after the exact point where the customer hangs. Result: endless spinner, then a silent restart of the same loop — symptom 3.
- Permission flow breaks silently on a stale identifier.
RequestPermissionOperation returns success(false) without ever showing a dialog when the identifier no longer resolves (explains symptom 4), and the Dart connect() ignores the requestPermission result and calls open() anyway (bitbox_manager.dart:15-20).
- Uncaught native error paths.
usbManager.openDevice() returns null without permission and the next line NPEs (GoDeviceInfo.kt:35-37); ConnectBitBoxOperation.kt:16-23 catches only BitBoxException and force-unwraps identifier!!; MethodCallRegistry.kt:16-28 has no try/catch; RequestPermissionOperation answers the Dart future from a BroadcastReceiver — if the broadcast never fires the future never completes (hang), if it fires twice a "Reply already submitted" crash threatens. Together these explain symptoms 1 and 2.
- No attach intent-filter of our own. Our manifest only declares
uses-feature usb.host; the "Open BitBoxApp?" resolver comes from the vendor app's USB_DEVICE_ATTACHED filter (VID 0x03eb / PID 0x2403). We never appear there and depend entirely on the runtime permission that (3) prevents.
Fix direction, in priority order (a–b are the substance, c–d are supporting):
a. Resolve devices by VID/PID (+ serial where available) instead of the bus path, and add re-attach recovery during pairing: on "device gone / identifier invalid", rescan, adopt the new device, request permission again — instead of polling a dead handle for 90 s.
b. Crash-guard the native path in the bitbox_flutter plugin (broad catch in the operations and registry, explicit openDevice() == null handling, evaluate the requestPermission result, single-reply/timeout guard in the permission receiver). Requires a plugin release + version bump here, since the plugin is pinned.
c. Register our own USB_DEVICE_ATTACHED intent-filter with a device filter for the BitBox VID/PID so attach events reach the app and the resolver offers it alongside the vendor app.
d. Concrete error states/texts in the connect flow instead of the generic failure snackbar (overlaps with the UX escalation already specced in #866).
Reproduction with real hardware is required before landing a fix — the re-enumeration is well evidenced in code, but which crash path fires on the customer device is timing-dependent.
2. Turn on crash reporting in releases (follow-up to #878)
#878 ships the reporter fully gated: without an injected DSN nothing starts. To make it effective:
3. Attach BitBox device context to crash reporting (follow-up to #878)
So hardware cases stop depending on asking the customer for device facts:
4. Registration signing on BitBox: rollout coordination for the chainId pair PRs
DFXswiss/api#4354 (verification accepts both EIP-712 domain variants) and #877 (BitBox signs with a chainId-extended domain) are drafts. Remaining before rollout:
Tracking issue for the remaining work from the two BitBox02 customer cases reported on 23.07. and the crash-reporting groundwork in #878. Four workstreams, ordered by customer impact.
1. USB connect: crash/hang when the BitBox02 re-enumerates during initial pairing
This is the crash/loop case explicitly declared out of scope in #866 ("The unexplained crash/loop reported alongside this customer case") — it now has a root-cause analysis and gets its tracked home here.
Symptoms (customer on a Samsung Galaxy Tab S4, BitBox02 Nova, current app):
Verified root causes (code review of the app plus the pinned
bitbox_flutterv0.0.10 plugin; the BitBox02 re-enumerates on the USB bus after firmware boot/unlock):deviceNamebus path (GetDevicesOperation.kt:17-19, key ofUsbManager.getDeviceList()), which does not survive re-enumeration;BitboxManager.kt:18-22then fails with "Device with identifier not found".GoDeviceInfoadditionally holds theUsbDevice/connection fromopen()time — after re-attach the file descriptor is dead andread/writerun into 10 sbulkTransfertimeouts (GoDeviceInfo.kt:35-55).getChannelHash()for 90 s against the old handle (connect_bitbox_cubit.dart:112-136) — no rescan, no new permission request. The only recovery observer starts after successful pairing (connect_bitbox_cubit.dart:232), i.e. after the exact point where the customer hangs. Result: endless spinner, then a silent restart of the same loop — symptom 3.RequestPermissionOperationreturnssuccess(false)without ever showing a dialog when the identifier no longer resolves (explains symptom 4), and the Dartconnect()ignores therequestPermissionresult and callsopen()anyway (bitbox_manager.dart:15-20).usbManager.openDevice()returnsnullwithout permission and the next line NPEs (GoDeviceInfo.kt:35-37);ConnectBitBoxOperation.kt:16-23catches onlyBitBoxExceptionand force-unwrapsidentifier!!;MethodCallRegistry.kt:16-28has no try/catch;RequestPermissionOperationanswers the Dart future from aBroadcastReceiver— if the broadcast never fires the future never completes (hang), if it fires twice a "Reply already submitted" crash threatens. Together these explain symptoms 1 and 2.uses-feature usb.host; the "Open BitBoxApp?" resolver comes from the vendor app'sUSB_DEVICE_ATTACHEDfilter (VID 0x03eb / PID 0x2403). We never appear there and depend entirely on the runtime permission that (3) prevents.Fix direction, in priority order (a–b are the substance, c–d are supporting):
a. Resolve devices by VID/PID (+ serial where available) instead of the bus path, and add re-attach recovery during pairing: on "device gone / identifier invalid", rescan, adopt the new device, request permission again — instead of polling a dead handle for 90 s.
b. Crash-guard the native path in the
bitbox_flutterplugin (broad catch in the operations and registry, explicitopenDevice() == nullhandling, evaluate therequestPermissionresult, single-reply/timeout guard in the permission receiver). Requires a plugin release + version bump here, since the plugin is pinned.c. Register our own
USB_DEVICE_ATTACHEDintent-filter with a device filter for the BitBox VID/PID so attach events reach the app and the resolver offers it alongside the vendor app.d. Concrete error states/texts in the connect flow instead of the generic failure snackbar (overlaps with the UX escalation already specced in #866).
Reproduction with real hardware is required before landing a fix — the re-enumeration is well evidenced in code, but which crash path fires on the customer device is timing-dependent.
fakeAsync) per CONTRIBUTING2. Turn on crash reporting in releases (follow-up to #878)
#878 ships the reporter fully gated: without an injected DSN nothing starts. To make it effective:
SENTRY_DSN(repo/environment secret +--dart-define), optionallySENTRY_ENVIRONMENTfor testnet builds3. Attach BitBox device context to crash reporting (follow-up to #878)
So hardware cases stop depending on asking the customer for device facts:
4. Registration signing on BitBox: rollout coordination for the chainId pair PRs
DFXswiss/api#4354 (verification accepts both EIP-712 domain variants) and #877 (BitBox signs with a chainId-extended domain) are drafts. Remaining before rollout: