Skip to content

feat: connect hardware flow#614

Merged
jvsena42 merged 31 commits into
masterfrom
feat/hw-wallet-connect
Jul 6, 2026
Merged

feat: connect hardware flow#614
jvsena42 merged 31 commits into
masterfrom
feat/hw-wallet-connect

Conversation

@jvsena42

@jvsena42 jvsena42 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Refs #589

This PR adds the guided Connect Hardware flow for pairing a Trezor as a watch-only wallet over Bluetooth, reachable from the Home hardware suggestion card and Hardware Wallets settings. It completes the "later PR" the intro sheet was deferring to and is an iOS port of bitkit-android's connect hardware flow (synonymdev/bitkit-android#1033).

Description

  • Turns the Hardware intro sheet into a guided in-sheet wizard: Intro → Searching → Found → Paired, driving device discovery, connection, balance display, an editable Label Funds field, and Finish.
  • Discovers a nearby Trezor over Bluetooth, connects and pairs it as a watch-only device, then shows its balance and lets you label the funds before finishing.
  • Surfaces the one-time pairing code inline within the flow when a device asks for it, with the code characters collapsing into the submit spinner while pairing completes.
  • Adds the Figma dashed-ring searching animation (counter-rotating rings and arrows) and the paired coins illustration.
  • Gates Bluetooth: when Bluetooth is off, unauthorized, or unsupported, Continue shows a recovery alert with an Open Settings action instead of starting a scan.
  • After Finish the flow returns to Home so the paired hardware-wallet tile is visible immediately.
  • Keeps the flow sheet-native: cancel or back at any step dismisses the sheet rather than stepping backward, and the app-wide pairing-code sheet no longer replaces the wizard while it is open.

This builds on the hardware-wallet backend already on the branch (device scan/connect, known-device storage, funds label, name resolution, watch-only balances). Because iOS is Bluetooth-only, the Android USB attach handling, USB device identifiers, scan-before-connect, and runtime permission dialog are intentionally omitted; Bluetooth availability is handled via CoreBluetooth state and a Settings deep-link.

QA Notes

OBS: couldn't test some flows on emulator

Manual Tests

  • 1. Settings → General → Payments → Hardware Wallets → Add Hardware Wallet → Continue: in-sheet nav runs Intro → Searching with the dashed-ring loader, then advances to Found when a Trezor is discovered.
  • 2. Home → Hardware suggestion card → Continue: the same connect flow opens and pairs a Trezor. → Navigate to home
  • 3. Found → Connect → Paired: shows the device balance and editable Label Funds field; Finish returns to Home and the Home tile / Settings list show the entered label.
  • 4. First-ever pair → enter the 6-digit code: the code row collapses into the spinner, then the flow advances to Paired.
  • 5a. regression: back or Cancel from Intro / Searching / Found / Paired: dismisses the sheet instead of stepping back through completed steps.
    • 5b. regression: reconnect of a known device that requests a pairing code while the wizard is closed: the app-wide Pair Device sheet still appears.
  • 6. Bluetooth off or unauthorized → Continue: a Bluetooth alert appears with an Open Settings action, and no scan starts.

Automated Checks

  • Unit tests added: BitkitTests/HwConnectViewModelTests.swift (9 cases) cover Intro→Searching discovery, search failure surfacing, Connect→Paired, connect-failure return to Found, inline pairing-code navigation (and its guard when not connecting), connected-wallet balance update, label 50-char cap, and Finish persisting the label.
  • Local build + tests: xcodebuild build succeeded and HwConnectViewModelTests passed 9/9 on the iPhone 16 simulator with ONLY_ACTIVE_ARCH=YES; SwiftFormat reports no changes.

Linked Issues/Tasks

Screenshot / Video

home.mov
hw-settings.mov

@jvsena42 jvsena42 self-assigned this Jul 2, 2026
@jvsena42 jvsena42 mentioned this pull request Jul 2, 2026
8 tasks
@jvsena42 jvsena42 added this to the 2.4.0 milestone Jul 2, 2026
@jvsena42

jvsena42 commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Passphrase flow deferred to another PR, It touches too many files
#613 (comment)

@jvsena42 jvsena42 marked this pull request as ready for review July 3, 2026 13:09
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the stub Hardware Intro sheet with a fully functional guided wizard (Intro → Searching → Found → Paired) that discovers a nearby Trezor over Bluetooth, pairs it as a watch-only device, surfaces a one-time inline pairing code when required, and returns to Home after Finish — completing the connect flow deferred by the earlier intro-sheet PR.

  • ViewModel + service protocol: HwConnectViewModel drives all phase transitions and task lifecycle through the HwConnectServicing seam, keeping BLE logic out of the ViewModel and enabling the 9-case unit-test suite with a FakeHwConnectService.
  • Sheet wizard: HardwareConnectSheet wires Bluetooth state guards (showing a recovery alert for off/unauthorized/unsupported), the inline pairing-code interception flag that suppresses the app-wide pair-device sheet while the wizard is active, and phase-keyed push transitions without a NavigationStack.
  • TrezorManager / TrezorTransport fixes: Adds a Task.isCancelled disconnect guard after connect completes, drains stale semaphore signals before each new pairing request, and maps two new error strings (code-verification failure, device-busy) to user-facing localised messages.

Confidence Score: 5/5

The connect flow is well-isolated behind a protocol seam, task cancellation is handled correctly at all exit paths, and the pairing-code interception guard prevents the app-wide sheet from racing with the wizard.

No functional defects were found in the new connect flow, BLE task lifecycle, or error-handling paths. The two findings are visual quality concerns (animation foreground-re-entry and missing @2x image assets) that do not affect correctness or data integrity.

HwSearchingAnimation.swift (animation restart on foreground return) and the four new 1x-only imageset Contents.json files.

Important Files Changed

Filename Overview
Bitkit/ViewModels/Trezor/HwConnectViewModel.swift New ViewModel driving the Intro→Searching→Found→Paired wizard; service protocol cleanly separates BLE logic for testability. Task cancellation is handled correctly at all early-exit paths.
Bitkit/Views/Sheets/HardwareConnectSheet.swift Sheet host wiring Bluetooth state guards, the pairing-code interception flag, and phase-keyed push transitions. onFinished is assigned inside .task{}, but this is a pre-existing open thread item.
Bitkit/Views/Sheets/HardwareConnect/HwSearchingAnimation.swift Counter-rotating ring animation using repeatForever; onAppear starts it but there is no foreground-re-entry hook to restart the animation after backgrounding.
Bitkit/Assets.xcassets/Illustrations/hw-searching-ring.imageset/Contents.json New ring/arrows/coin-stack imagesets use a single scaleless universal asset; other project illustrations use explicit 1x/2x entries, so these may render blurry on Retina devices.
Bitkit/Managers/TrezorManager.swift Adds a Task.isCancelled guard after connect completes (disconnects cleanly) and two new error-message translations for code-verification failure and device-busy states.
Bitkit/Services/Trezor/TrezorTransport.swift Drains stale semaphore signals before each new pairing-code request, preventing a prior cancelled/submitted code from immediately short-circuiting the new handshake.
BitkitTests/HwConnectViewModelTests.swift 9 test cases covering the full connect flow via a fake service; waitUntil silently passes on timeout (pre-existing flagged item).
Bitkit/ViewModels/SheetViewModel.swift Renames hardwareIntro sheet ID to hardwareConnect and adds the hardwareConnectHandlesPairing flag used to suppress the app-wide pairing sheet while the wizard handles it inline.
Bitkit/Views/Sheets/HardwareConnect/HwPairCodeView.swift Inline 6-digit pairing code entry that submits on the sixth digit and collapses digits into a spinner; handles delete key correctly until submit.
Bitkit/Views/Sheets/HardwareConnect/HwPairedView.swift Paired step showing device balance and editable label field over a coins illustration; layout is correct and Finish passes through to ViewModel.
Bitkit/MainNavView.swift Swaps HardwareIntroSheet for HardwareConnectSheet and adds the guard that suppresses the app-wide pairing sheet while the connect wizard is active.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User taps Add Hardware Wallet\nor Home suggestion card] --> B{Bluetooth usable?}
    B -- No --> C[Show Bluetooth alert\nwith Open Settings]
    B -- Yes --> D[Phase: .searching\nstartSearching loop]
    D -- scan error --> D
    D -- device found --> E[Phase: .found\nHwFoundView]
    E -- Cancel --> Z[Dismiss sheet\nreset]
    E -- Connect --> F{connect task}
    F -- device requests\npairing code --> G[Phase: .pairCode\nHwPairCodeView\nhardwareConnectHandlesPairing=true]
    G -- 6 digits entered --> H[submitPairingCode\nresumes connect task]
    H --> F
    F -- connect failed --> E
    F -- success --> I[Phase: .paired\nHwPairedView\nshow balance + label]
    I -- Finish --> J[setDeviceLabel\nonFinished\nhideSheet + reset nav\nHome page 0]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[User taps Add Hardware Wallet\nor Home suggestion card] --> B{Bluetooth usable?}
    B -- No --> C[Show Bluetooth alert\nwith Open Settings]
    B -- Yes --> D[Phase: .searching\nstartSearching loop]
    D -- scan error --> D
    D -- device found --> E[Phase: .found\nHwFoundView]
    E -- Cancel --> Z[Dismiss sheet\nreset]
    E -- Connect --> F{connect task}
    F -- device requests\npairing code --> G[Phase: .pairCode\nHwPairCodeView\nhardwareConnectHandlesPairing=true]
    G -- 6 digits entered --> H[submitPairingCode\nresumes connect task]
    H --> F
    F -- connect failed --> E
    F -- success --> I[Phase: .paired\nHwPairedView\nshow balance + label]
    I -- Finish --> J[setDeviceLabel\nonFinished\nhideSheet + reset nav\nHome page 0]
Loading

Reviews (2): Last reviewed commit: "fix: error parsing" | Re-trigger Greptile

Comment thread Bitkit/ViewModels/Trezor/HwConnectViewModel.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d366be64eb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Bitkit/ViewModels/Trezor/HwConnectViewModel.swift Outdated
Comment thread Bitkit/MainNavView.swift Outdated
@jvsena42 jvsena42 requested a review from piotr-iohk July 3, 2026 15:04
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Manual QA — Connect Hardware flow
Trezor Safe 7, physical iPhone, regtest, #614 latest HEAD

Logs:
bitkit_logs_2026-07-06_07-52-45.zip


Verdict: partial pass — not merge-ready ❌

Connect flow can complete end-to-end after clearing stale iOS Bluetooth bond, and BT toggle reconnect on Home works. Re-pair after Remove Trezor is broken: phone/Trezor THP state desyncs, pairing PIN loops, wrong-PIN path spins without recovery.


What I tested (one session, ~05:56–07:52 UTC)

Log bundle is one continuous session (foreground logs roll over at app restart: 05-56-56, 07-43-39, 07-48-27).

Phase What happened Result
1. Initial connect attempts Every Connect tap failed with stale iOS BLE bond (Peer removed pairing information); UI showed generic "Check that it is unlocked"
2. iOS Bluetooth forget Settings → Bluetooth → Forget Trezor Safe 7 prerequisite
3. First successful pair Found → Connect → pairing code → THP complete → Connected to Trezor (ble:F75048C0…)
4. Receive + BT toggle On-chain receive notification; BT off → grey on Home; BT on → auto-reconnect → green (~44s)
5. Remove Trezor Settings → Remove Trezor Safe 7 (Forgot device, THP credential deleted) ✅ removal itself
6. Re-pair after remove Trezor cycles connect/PIN/connect with new PIN; Bitkit spinner or generic error; wrong PIN → long spinner, no recovery
7. App restart (same session) Killed/relaunched app to keep testing — paired successfully once more
8. Remove + re-pair again Same desync/retry loop as phase 6; no successful connect before I stopped (~07:52)

What worked ✅

  • Intro → Searching → Found
  • Connect → inline pairing code → Paired → Finish (after iOS BT forget)
  • On-chain receive notification while paired
  • BT off → grey → BT on → green on Home

What failed ❌

1. Stale iOS bond + misleading errors

Before forgetting in iPhone Settings, logs show Peer removed pairing information / Stale Bluetooth pairing… on every attempt. UI always showed generic unlock error — not actionable.

614.mp4

2. Re-pair after Remove Trezor (primary blocker)

getPairingCode: waiting for user input...
getPairingCode: cancelled/empty          ← handshake retry aborts active pairing
[HANDSHAKE] Attempt N FAILED: Pairing cancelled by user / TransportBusy
submitPairingCode                        ← user code applied to wrong/restarted handshake

What I saw: Trezor shows Connect to Bitkitenter PIN → back to Connect with a new PIN while Bitkit still shows pair-code/connect spinner. Phone and device get out of sync.

Likely cause: THP handshake retries inside one connect() call cancel in-flight pairing (getPairingCode: cancelled/empty) while the user is entering the code.

3. Wrong PIN → spinner with no recovery

Log: Pairing failed: Code verification failed @ 07:47:20 — UI stayed on spinner ~23s, then generic failure. No “incorrect pairing code”, no fast return to Found.

4. Generic errors in connect UI

HwConnectViewModel.onConnectFailed() always sets hardware__connect_error, discarding real errors (Code verification failed, TransportBusy, Pairing cancelled by user, stale bond).


Suggested fixes (#614)

  • Propagate real errors to Found-step UI
  • Single pairing session — no handshake retry while awaiting PIN
  • Wrong PIN → fail fast with dedicated message
  • Post-remove guidance (iOS BT forget + Trezor pairing mode)
  • Disable Connect while isConnecting

Likely also needs #613 / core THP retry work (TransportBusy, pairing-mode handshake).


QA checklist

  • 1. Intro → Searching → Found
  • 3–4. Connect → pairing code → Paired → Finish (after iOS BT forget only)
  • 3–4. After Remove + re-pair
  • 5a. Cancel/back dismisses sheet
  • 5b. App-wide pairing sheet (not exercised)
  • 6. BT off alert in connect flow (Home BT toggle tested instead — pass)

Summary: Happy path works once stale iOS bond is cleared; BT reconnect on Home is solid. Re-pair after Remove and THP desync block merge; error UX should be fixed in #614 regardless.

@jvsena42 jvsena42 marked this pull request as draft July 6, 2026 09:14
@jvsena42

jvsena42 commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

Draft for check comment

@jvsena42 jvsena42 marked this pull request as ready for review July 6, 2026 11:30
@jvsena42

jvsena42 commented Jul 6, 2026

Copy link
Copy Markdown
Member Author
Simulator.Screen.Recording.-.iPhone.17.-.2026-07-06.at.08.30.04.mov

Emulator basic tests after fixes for #614 (comment)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16293c21b5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Bitkit/Views/Sheets/HardwareConnectSheet.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Manual QA — Connect Hardware flow (retest)
Trezor Safe 7, physical iPhone, regtest, #614 latest HEAD

Attachments: bitkit_logs_2026-07-06_13-43-52/, screenshot (stale bond error), screen recording
bitkit_logs_2026-07-06_13-43-52.zip


Verdict: approve with one fix ✅ / ⚠️

Looks good overall. Stale iOS bond now shows the correct actionable error on Found ✅ (screenshot). Connect → pair code → Paired → Finish works, including re-pair after Remove ✅.

File (1)

One remaining issue: wrong pairing code leaves the inline pair-code step on a spinner with no recovery while Trezor already shows a new code. Logs show Code verification failed then a second getPairingCode: waiting…, but Bitkit stays on the submitted/spinner state. Android resets input on retry — please match that before merge. No need for a long error message if the input resets immediately.

614-2.mov

Log highlights

Check Result
First pair (after wrong code + retry) Connected @ 13:33:38
Re-pair after Remove ✅ @ 13:35:09
Stale iOS bond error surfaced ✅ @ 13:37:36 (matches screenshot)
Pair after BT forget ✅ @ 13:39:25
Wrong code → second getPairingCode Backend OK, UI stuck on spinner @ 13:41:21
Recovery via dismiss + reconnect ✅ @ 13:43:30 (workaround only)

Wrong-PIN pattern:

13:41:11 submitPairingCode
13:41:12 Code verification failed
13:41:21 getPairingCode: waiting…   ← Trezor new code; Bitkit still spinner

Suggested fix

Reset HwPairCodeView when a new pairing prompt arrives mid-handshake (e.g. .id(attempt) or clear submitted/code on retry). onPairingCodeRequested() alone may not fire again if showPairingCode is already true.


Summary: Main stale-connection blocker is fixed ✅. Only blocker left is wrong-code input reset (Android parity).

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Manual QA — Connect Hardware flow
Trezor Safe 7, physical iPhone, regtest, #614 latest HEAD

Attachments:
bitkit_logs_2026-07-06_15-04-13.zip


Verdict: approve

Retested on latest HEAD — all good.

  • Stale iOS bond — actionable Found-step message ✅
  • Connect → pair code → Paired → Finish
  • Wrong pairing code — input resets on retry (Android parity) ✅
  • Re-pair after Remove
  • BT off on intro Continue — alert shown, does not proceed to scan ✅
    Logs: BT state: 4 @ 15:02:13, connect sheet @ 15:02:25, no scan until BT state: 5 @ 15:02:47 → connected @ 15:03:47

Not exercised: app-wide pairing sheet when connect wizard dismissed mid-pair (5b).

Recommendation: merge #614.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

@jvsena42 jvsena42 enabled auto-merge July 6, 2026 16:41
@jvsena42 jvsena42 merged commit 844d3fc into master Jul 6, 2026
16 of 19 checks passed
@jvsena42 jvsena42 deleted the feat/hw-wallet-connect branch July 6, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants