You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up polish and parity items deferred from feat: home-screen hardware wallet (watch-only) #605. Android counterpart: synonymdev/bitkit-android#1030.
This issue tracks iOS-specific UX and connection-state gaps found during manual QA of #605. Scope grows as later stacked PRs land (#611, #612, settings screen, etc.).
Checklist
Settings entry placement — move Trezor Hardware Wallet from Settings → Advanced into Dev Settings (§ Settings entry placement)
Home connection indicator (QA 4a / 4b) — grey the Home BLE badge on disconnect / BT-off, green again on auto-reconnect (§ Home connection indicator)
Home connection icon placement — move HwWalletConnectionIcon from the amount row to the device-name row (Figma / Android parity) (§ Home connection icon placement)
Device busy / ping cadence while Trezor is locked — back off instead of reopening the transport during a locked-device THP handshake (needs bitkit-core / upstream work) (§ Device busy / ping cadence)
BLE power-off doesn't fail in-flight operations — resume pending continuations with an error in the power-off path (§ Deferred follow-ups from PR #612)
Connect-wizard passphrase (hidden wallet) step — Figma "Passphrase" button on Paired → "Enter Passphrase" screen to add a passphrase-protected hidden wallet as a separate watch-only balance; blocked on a known-device storage-identity change (§ Passphrase / hidden-wallet step)
Scope detail
Settings entry placement
Move Trezor Hardware Wallet from Settings → Advanced into Dev Settings, for consistency with the dev-gated entry point. Already agreed in #605 review; deferred to a polish PR rather than blocking the home-screen slice.
4b. Re-enable Bluetooth: indicator turns green again on its own (auto-reconnect, no pairing prompt).
Current behaviour (4a fails): the Home BLE badge stays green when phone Bluetooth is disabled or the device is out of range. Android greys it out on disconnect.
Root cause:HwWalletConnectionIcon already supports green vs grey, but HwWallet.isConnected follows trezorManager.connectedDevice, which is only cleared on explicit disconnect / connect failure. iOS lacks Android's TrezorRepo.observeExternalDisconnects() / externalDisconnect wiring from TrezorBLEManager.didDisconnectPeripheral (and BT-off state) → TrezorManager.
Expected fix direction:
On peripheral disconnect or bluetoothState != .poweredOn, clear live connection state for the Home badge (without suppressing auto-reconnect).
Verify 4b end-to-end after 4a fix: BT back on → auto-reconnect → badge green again.
Home connection icon placement (Figma / Android parity)
Splitting this out from the §1 observation in #612 QA: the green/grey glyph state is correct, but on Home it's placed on the wrong row.
Current (iOS): in HardwareWalletsGrid.HardwareWalletCell the HwWalletConnectionIcon sits on the amount row, next to MoneyText (Bitkit/Components/HardwareWalletsGrid.swift:67):
VStack(alignment: .leading) {
CaptionMText(wallet.name) // name row ← icon should live here
HStack { btc-circle-blue; MoneyText; HwWalletConnectionIcon } // amount row ← icon is here today
}
Expected (Figma 40364-130833): the glyph sits next to the device name, with the balance on its own row below.
Parity: Android Home renders it via WalletBalanceViewtitleTrailing — i.e. on the name row. This is a Home-only gap (from the #605 layout), not introduced by #612; Settings → Hardware Wallets already has the badge left of the name and matches.
Fix: move HwWalletConnectionIcon out of the amount HStack and onto the CaptionMText(wallet.name) row (name + trailing glyph), leaving the btc-circle-blue + MoneyText row unchanged. Small layout-only tweak to the shared home cell.
Note: distinct from the "Home connection indicator (QA 4a/4b)" grey/green state item above — that one is the disconnect-state wiring; this one is purely icon placement.
Behaviour: while connecting with the Trezor locked (waiting for the user to unlock on-device), Bitkit keeps reopening the transport instead of backing off, which can keep the device busy. Observed in session logs ~09:10:41–09:11:02 UTC:
Time (UTC)
Event
09:10:41
User-initiated connect while device locked
09:10:49
THP handshake → DeviceLocked
09:10:49–51
Handshake retry: closeDevice → wait 2000ms → openDevice again
Root cause / where it lives: the closeDevice → wait 2000ms → openDevice reopen loop and the DeviceLocked outcome are driven by the upstream trezor-connect-rs crate (CallbackTransport::acquire). iOS is pinned to bitkit-core 0.3.4 (includes trezor-connect-rs 0.3.3 and synonymdev/bitkit-core#106). Locally:
bitkit-coreconnect() does a single transport.acquire(...) with no retry wrapper.
The Swift transport callbacks openDevice/closeDevice (TrezorTransport.swift) are pass-throughs with no back-off — each crate-driven openDevice re-runs the full TrezorBLEManager.connect() 3-attempt (1s/2s) BLE loop underneath, so one crate reopen can spawn several BLE link attempts.
The Swift transport currently returns errorCode: nil for open/read/write/call failures, so core's structured DeviceBusy plumbing (feat: expose trezor lock state bitkit-core#104) cannot trigger from Swift-originated transport failures.
DeviceLocked is never surfaced as a distinct, retryable signal — it only arrives as the substring "THP handshake failed: DeviceLocked" after the crate's loop already failed, purely to produce a UI message.
Expected fix direction (one or both):
upstream/core: make THP DeviceLocked during CallbackTransport.acquire(...) a non-retryable / back-off-aware state instead of continuing the close/open handshake retry loop. Add a bitkit-core issue and check with @coreyphillips.
iOS: wire the new core primitives (TrezorFeatures.unlocked, trezor_refresh_features(), TrezorError.DeviceBusy, TrezorTransportErrorCode.DeviceBusy from feat: expose trezor lock state bitkit-core#104) into the transport/manager layer so the app can back off and avoid amplifying retries while the device is locked/busy (e.g. not re-running the 3-attempt BLE connect loop on every crate-driven openDevice, and not auto-reconnecting while a locked-device handshake is in flight).
Distinct from the wrong/cancelled-PIN failure typing already fixed by synonymdev/bitkit-core#106 — this is locked-device THP handshake churn during session acquisition, before a usable session/features result exists.
Minor items surfaced while reviewing PR #612 (Hardware Wallets settings screen), intentionally left out of that PR:
BLE power-off doesn't fail in-flight operations — TrezorBLEManager.centralManagerDidUpdateState (the power-off block, ~TrezorBLEManager.swift:571) clears connection state but does not resume pending connect / service-discovery / notification continuations, unlike didDisconnectPeripheral (~lines 633–636). It's bounded by the per-op timeouts (30s / 10s / 5s), so it's a multi-second stall rather than a hang. Fix: resume those continuations with an error in the power-off path too.
"0" count badge in General Settings — the Hardware Wallets row (GeneralSettingsView.swift:~118) always renders String(hwWalletManager.wallets.count), showing a literal 0 when no devices are paired. Show the count only when > 0.
Duplicated device-removal teardown — HardwareWalletsSettingsScreen.remove(_:) (~:467) duplicates the removeDevice + forgetDevice-loop sequence that also lives in HardwareWalletScreen. Consolidate onto HwWalletManager so removal semantics live in one place.
Two related bugs from the same review were already fixed in PR #612 (see checklist): a stale expectedDisconnectPaths marker that could suppress a genuine BLE disconnect (phantom "connected" session), and the rename sheet blocking reset-to-default.
The Paired step gains a Passphrase button (next to Finish) that opens an Enter Passphrase screen (shield illustration, passphrase field, Back / Continue). Entering a passphrase should add the funds of the passphrase-protected (hidden) wallet as well, as watch-only.
Not implemented on iOS or bitkit-android master — both HwPairedView / HwPairedSheet have only Finish; no reference port exists.
Already present (reusable):
Passphrase mechanics on TrezorManager: submitPassphrase(_:) / setWalletMode(.passphraseHost:), requestPassphraseWallet(), the on-device-vs-host chooser, passphraseEntryCapable; TrezorUiHandler mode plumbing; TrezorPassphraseSheet (dev-screen UI reference); the shield-figure illustration asset.
HwWalletManager already renders any distinct-xpub entry as its own tile / balance (grouping via HwWalletId.derive), so a separate hidden entry would surface as a second watch-only wallet with no extra engine work.
Blocker (why deferred):saveCurrentDeviceAsKnown() (TrezorManager.swift:485) keys the stored TrezorKnownDevice by the physical device.id, and TrezorKnownDeviceStorage.save() replaces same-id — so opening a hidden session overwrites / merges into the standard entry instead of persisting a second wallet. Making the hidden wallet an additional balance needs a composite storage identity (device.id + mode / xpub-signature) and a non-clobbering save path, which touches the reconnect / forget / rename identity shared across the already-merged hardware-wallet work.
Open question: any passphrase opens a valid (possibly empty) hidden wallet — there is no "wrong passphrase" to reject — so a rule is needed for whether / when to persist an empty hidden wallet (e.g. only after a watcher reports funds, or always + removable from Settings).
Part of #589.
Follow-up polish and parity items deferred from feat: home-screen hardware wallet (watch-only) #605. Android counterpart: synonymdev/bitkit-android#1030.
This issue tracks iOS-specific UX and connection-state gaps found during manual QA of #605. Scope grows as later stacked PRs land (#611, #612, settings screen, etc.).
Checklist
HwWalletConnectionIconfrom the amount row to the device-name row (Figma / Android parity) (§ Home connection icon placement)> 0(§ Deferred follow-ups from PR #612)removeDevice+forgetDeviceontoHwWalletManager(§ Deferred follow-ups from PR #612)expectedDisconnectPathsmarker — suppressed a genuine BLE disconnect (phantom "connected" session) — fixed in feat: hardware wallets settings screen #612Scope detail
Settings entry placement
Move Trezor Hardware Wallet from Settings → Advanced into Dev Settings, for consistency with the dev-gated entry point. Already agreed in #605 review; deferred to a polish PR rather than blocking the home-screen slice.
Home connection indicator (QA 4a / 4b)
From #605 QA notes (manual tests 4a / 4b):
Current behaviour (4a fails): the Home BLE badge stays green when phone Bluetooth is disabled or the device is out of range. Android greys it out on disconnect.
Root cause:
HwWalletConnectionIconalready supports green vs grey, butHwWallet.isConnectedfollowstrezorManager.connectedDevice, which is only cleared on explicit disconnect / connect failure. iOS lacks Android'sTrezorRepo.observeExternalDisconnects()/externalDisconnectwiring fromTrezorBLEManager.didDisconnectPeripheral(and BT-off state) →TrezorManager.Expected fix direction:
bluetoothState != .poweredOn, clear live connection state for the Home badge (without suppressing auto-reconnect).Home connection icon placement (Figma / Android parity)
Splitting this out from the §1 observation in #612 QA: the green/grey glyph state is correct, but on Home it's placed on the wrong row.
Current (iOS): in
HardwareWalletsGrid.HardwareWalletCelltheHwWalletConnectionIconsits on the amount row, next toMoneyText(Bitkit/Components/HardwareWalletsGrid.swift:67):Expected (Figma 40364-130833): the glyph sits next to the device name, with the balance on its own row below.
Parity: Android Home renders it via
WalletBalanceViewtitleTrailing— i.e. on the name row. This is a Home-only gap (from the #605 layout), not introduced by #612; Settings → Hardware Wallets already has the badge left of the name and matches.Fix: move
HwWalletConnectionIconout of the amountHStackand onto theCaptionMText(wallet.name)row (name + trailing glyph), leaving thebtc-circle-blue+MoneyTextrow unchanged. Small layout-only tweak to the shared home cell.Device busy / ping cadence while Trezor is locked
Deferred from #611 QA (comment). Android counterpart: synonymdev/bitkit-android#1030 (device-state / ping-cadence).
Behaviour: while connecting with the Trezor locked (waiting for the user to unlock on-device), Bitkit keeps reopening the transport instead of backing off, which can keep the device busy. Observed in session logs ~09:10:41–09:11:02 UTC:
DeviceLockedcloseDevice→ wait 2000ms →openDeviceagainopenDeviceattempts on same pathTHP handshake failed: … DeviceLockedRoot cause / where it lives: the
closeDevice → wait 2000ms → openDevicereopen loop and theDeviceLockedoutcome are driven by the upstreamtrezor-connect-rscrate (CallbackTransport::acquire). iOS is pinned tobitkit-core 0.3.4(includestrezor-connect-rs 0.3.3and synonymdev/bitkit-core#106). Locally:bitkit-coreconnect()does a singletransport.acquire(...)with no retry wrapper.openDevice/closeDevice(TrezorTransport.swift) are pass-throughs with no back-off — each crate-drivenopenDevicere-runs the fullTrezorBLEManager.connect()3-attempt (1s/2s) BLE loop underneath, so one crate reopen can spawn several BLE link attempts.errorCode: nilfor open/read/write/call failures, so core's structuredDeviceBusyplumbing (feat: expose trezor lock state bitkit-core#104) cannot trigger from Swift-originated transport failures.DeviceLockedis never surfaced as a distinct, retryable signal — it only arrives as the substring"THP handshake failed: DeviceLocked"after the crate's loop already failed, purely to produce a UI message.Expected fix direction (one or both):
DeviceLockedduringCallbackTransport.acquire(...)a non-retryable / back-off-aware state instead of continuing the close/open handshake retry loop. Add a bitkit-core issue and check with @coreyphillips.TrezorFeatures.unlocked,trezor_refresh_features(),TrezorError.DeviceBusy,TrezorTransportErrorCode.DeviceBusyfrom feat: expose trezor lock state bitkit-core#104) into the transport/manager layer so the app can back off and avoid amplifying retries while the device is locked/busy (e.g. not re-running the 3-attempt BLE connect loop on every crate-drivenopenDevice, and not auto-reconnecting while a locked-device handshake is in flight).Deferred follow-ups from PR #612 review
Minor items surfaced while reviewing PR #612 (Hardware Wallets settings screen), intentionally left out of that PR:
TrezorBLEManager.centralManagerDidUpdateState(the power-off block, ~TrezorBLEManager.swift:571) clears connection state but does not resume pendingconnect/ service-discovery / notification continuations, unlikedidDisconnectPeripheral(~lines 633–636). It's bounded by the per-op timeouts (30s / 10s / 5s), so it's a multi-second stall rather than a hang. Fix: resume those continuations with an error in the power-off path too.GeneralSettingsView.swift:~118) always rendersString(hwWalletManager.wallets.count), showing a literal0when no devices are paired. Show the count only when> 0.HardwareWalletsSettingsScreen.remove(_:)(~:467) duplicates theremoveDevice+forgetDevice-loop sequence that also lives inHardwareWalletScreen. Consolidate ontoHwWalletManagerso removal semantics live in one place.Two related bugs from the same review were already fixed in PR #612 (see checklist): a stale
expectedDisconnectPathsmarker that could suppress a genuine BLE disconnect (phantom "connected" session), and the rename sheet blocking reset-to-default.Passphrase / hidden-wallet step
Figma: Paired w/ Passphrase button 40364-133759, Enter Passphrase 46312-107873. Deferred from the connect-flow PR (#614).
The Paired step gains a Passphrase button (next to Finish) that opens an Enter Passphrase screen (shield illustration, passphrase field, Back / Continue). Entering a passphrase should add the funds of the passphrase-protected (hidden) wallet as well, as watch-only.
Not implemented on iOS or bitkit-android master — both
HwPairedView/HwPairedSheethave only Finish; no reference port exists.Already present (reusable):
TrezorManager:submitPassphrase(_:)/setWalletMode(.passphraseHost:),requestPassphraseWallet(), the on-device-vs-host chooser,passphraseEntryCapable;TrezorUiHandlermode plumbing;TrezorPassphraseSheet(dev-screen UI reference); theshield-figureillustration asset.HwWalletManageralready renders any distinct-xpub entry as its own tile / balance (grouping viaHwWalletId.derive), so a separate hidden entry would surface as a second watch-only wallet with no extra engine work.Blocker (why deferred):
saveCurrentDeviceAsKnown()(TrezorManager.swift:485) keys the storedTrezorKnownDeviceby the physicaldevice.id, andTrezorKnownDeviceStorage.save()replaces same-id — so opening a hidden session overwrites / merges into the standard entry instead of persisting a second wallet. Making the hidden wallet an additional balance needs a composite storage identity (device.id+ mode / xpub-signature) and a non-clobbering save path, which touches the reconnect / forget / rename identity shared across the already-merged hardware-wallet work.Open question: any passphrase opens a valid (possibly empty) hidden wallet — there is no "wrong passphrase" to reject — so a rule is needed for whether / when to persist an empty hidden wallet (e.g. only after a watcher reports funds, or always + removable from Settings).
Related work