Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
72b10cb
feat: home screen hardware wallet ui
ovitrif Jun 9, 2026
51b3898
chore: rename changelog fragment
ovitrif Jun 9, 2026
477575a
fix: hw wallets 2-column grid on home
ovitrif Jun 9, 2026
f8e39e6
fix: watch only monitored hw address types
ovitrif Jun 9, 2026
e3ea871
fix: saturate total-with-hardware balance sum
ovitrif Jun 9, 2026
a4dbd43
docs: reference test files by name in pr notes
ovitrif Jun 9, 2026
e6cef83
refactor: address hw wallet review feedback
ovitrif Jun 9, 2026
69cfc72
fix: open hw wallet activity detail from home
ovitrif Jun 9, 2026
44044dc
fix: hi-res trezor suggestion card image
ovitrif Jun 9, 2026
2c3157a
fix: sync suggestion cards with v61 design
ovitrif Jun 9, 2026
658d6b0
feat: show received sheet for new hw wallet txs
ovitrif Jun 9, 2026
d8366ea
fix: use green tint for invite suggestion card
ovitrif Jun 9, 2026
79115ee
fix: vendor-prefixed hw device name on home tile
ovitrif Jun 10, 2026
a47a295
fix: dedupe hw wallet paired over multiple transports
ovitrif Jun 10, 2026
a61bfc7
fix: observe trezor disconnects for app lifetime
ovitrif Jun 10, 2026
7fa31e7
fix: include hw wallet activity in all activity list
ovitrif Jun 10, 2026
c4b69ca
refactor: rename hardware sheet and transport type
ovitrif Jun 10, 2026
6ef4a7b
fix: add gradient bg and preview to hardware sheet
ovitrif Jun 10, 2026
14a75cc
refactor: rename trezor repo scope for consistency
ovitrif Jun 10, 2026
a4a85de
fix: localize hardware sheet title
ovitrif Jun 10, 2026
8572ef3
fix: use localized cancel in hardware sheet
ovitrif Jun 10, 2026
097f00d
fix: hardware sheet intro title per figma
ovitrif Jun 10, 2026
281ace4
feat: auto-reconnect hw device on transport restore
ovitrif Jun 10, 2026
7b78b46
feat: implement hardware connect intro screen
ovitrif Jun 10, 2026
d6c0321
fix: center hw intro visuals and pad bottom buttons
ovitrif Jun 10, 2026
0d3ae64
fix: retry stale watcher stop and drop intro back arrow
ovitrif Jun 10, 2026
9a16f69
fix: proportional sizing for hw intro visuals
ovitrif Jun 10, 2026
08e8257
fix: keep home toolbar visible above sheets
ovitrif Jun 10, 2026
f06572c
fix: retry hw auto-reconnect and reset stale session
ovitrif Jun 10, 2026
f1f09bc
feat: reconnect hw device on usb attach intent
ovitrif Jun 10, 2026
e60caf5
feat: add pair device sheet for hw pairing code
ovitrif Jun 10, 2026
ff9d4b9
fix: use sync usb transfers to avoid native crash
ovitrif Jun 10, 2026
5088aad
fix: skip auto-reconnect during live hw handshake
ovitrif Jun 10, 2026
7436803
fix: serialize hw reconnect triggers into one loop
ovitrif Jun 10, 2026
39ee64f
fix: stable pairing code cells and numpad focus
ovitrif Jun 10, 2026
a0e5afd
feat: prefer restored transport on hw reconnect
ovitrif Jun 10, 2026
b301a77
chore: fix import ordering
ovitrif Jun 10, 2026
5dd9ed0
test: add hw wallet ai journeys for bridge emulator
ovitrif Jun 11, 2026
be3eb93
docs: drop orbstack mention from journeys
ovitrif Jun 11, 2026
f6be482
refactor: split hardware sheet screens into files
ovitrif Jun 12, 2026
b6ddb24
chore: add detekt suppressions at source
ovitrif Jun 12, 2026
b06d461
refactor: wire hw sheet screens like send sheet
ovitrif Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .agents/commands/pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ When the user provides custom instructions after `--`:
#### Automated Checks
```
- Keep local verification commands, Gradle tasks, detekt, lint, unit tests, build passes, cargo test, cargo clippy, npm test, typecheck, CI coverage, or similar automated checks out of `#### Manual Tests`; summarize them under `#### Automated Checks` when they add useful context.
- Use `#### Automated Checks` to summarize automated verification evidence, prioritizing coverage added, modified, or removed with file paths and a short explanation.
- Use `#### Automated Checks` to summarize automated verification evidence, prioritizing coverage added, modified, or removed, each with the test file name and a short explanation.
- Reference test files by bare file name only (e.g. `HwWalletRepoTest.kt`), never the full path. Only when two referenced test files share the same name, prefix the shortest leading path segment(s) that disambiguate them (e.g. `repositories/FooTest.kt` vs `viewmodels/FooTest.kt`).
- For removed automated coverage, state why it was removed.
- Do not list standard CI or PR bot commands as checkbox items just because they run for every PR. If standard CI coverage is worth mentioning, summarize it in one sentence.
- List raw commands only when they were run locally, are non-standard, use special flags or environment values, validate workflow behavior, or explain a meaningful verification gap.
Expand Down Expand Up @@ -184,9 +185,9 @@ Concrete style target:
- [ ] **5b.** back: returns to Connections List.
- [ ] **6.** `regression:` Channel Detail → tap Close Connection: works.
#### Automated Checks
- Unit tests added: cover invoice timeout handling in `app/src/test/.../SendInvoiceTest.kt`.
- Unit tests modified: update channel navigation assertions in `app/src/test/.../ChannelDetailTest.kt`.
- Test coverage removed: delete stale mock-only assertions from `app/src/test/.../OldFlowTest.kt` because the flow no longer exists.
- Unit tests added: cover invoice timeout handling in `SendInvoiceTest.kt`.
- Unit tests modified: update channel navigation assertions in `ChannelDetailTest.kt`.
- Test coverage removed: delete stale mock-only assertions from `OldFlowTest.kt` because the flow no longer exists.
- CI: standard compile, unit test, and detekt checks run by the PR bot.
```

Expand Down
62 changes: 62 additions & 0 deletions .agents/journeys/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Hardware Wallet Journeys

AI-driven UI test journeys for the home-screen hardware wallet features, designed to run
against the deterministic Trezor emulator from `synonymdev/bitkit-docker` — no physical
Trezor required. Journeys follow the `android` CLI journey XML format: natural-language
`<action>` steps evaluated sequentially against the running app; any failed step fails
the journey.

## What the Bridge emulator does and does not simulate

The Bridge transport is HTTP (`TrezorBridgeTransport` → `http://127.0.0.1:21325` through
`adb reverse`), so it bypasses the Android USB stack entirely. Calibrate expectations:

- **Reliably simulated**: the device itself (deterministic seed and label), the full
wallet protocol (scan, connect, features, xpubs, watchers, signing), and therefore all
home-screen UI behavior: tiles, balances, activity, indicators, sheets.
- **Partially simulated**: the USB attach → auto-reconnect chain. The OS-level attach
intent can be injected with `am start -a android.hardware.usb.action.USB_DEVICE_ATTACHED`,
which drives the full in-app path (MainActivity → AppViewModel → reconnect loop), with
the Bridge standing in for the transport.
- **Not simulated**: kernel/libusbhost behavior, USB enumeration timing, permission
grants, the OS app picker, and THP pairing (the Pair Device sheet). Those need a
physical device.

Journey steps that start with `adb:` are device commands the runner executes verbatim
instead of UI interactions.

## Prerequisites

1. Docker running with the `bitkit-docker` stack up:
```sh
cd ../bitkit-docker && docker compose up -d
```
2. Deterministic Trezor User Env started (Bridge on `21325`, T2T1 emulator, seed
`all all ...`, label `Bitkit Test Trezor`):
```sh
../bitkit-docker/scripts/trezor-emulator start
```
3. For a physical phone, reverse the Bridge port and install with Bridge enabled:
```sh
../bitkit-docker/scripts/trezor-emulator adb
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./gradlew installDevDebug
```
For an Android emulator use `TREZOR_BRIDGE_URL=http://10.0.2.2:21325`.
4. A wallet must exist in the app (onboarding completed) on regtest (dev flavor).

## Journeys

Run in this order — `hw-connect-home-tile` pairs the emulator that the later journeys rely
on, and `hw-suggestion-intro-sheet` ends by re-pairing after a forget.

| Journey | Covers |
| - | - |
| `hw-connect-home-tile.journey.xml` | Dev-screen connect, home tile, indicator, balance, overview toast |
| `hw-activity-blue-icons.journey.xml` | Hardware activity merge, blue icons, All Activity filters, detail fallback |
| `hw-usb-reconnect.journey.xml` | Disconnect indicator, injected USB attach intent → silent auto-reconnect |
| `hw-suggestion-intro-sheet.journey.xml` | Forget device, Hardware suggestion card, connect intro sheet |

To exercise the received-money sheet (not covered by a journey because it needs an
out-of-band transfer), fund the emulator wallet on regtest from `bitkit-docker`, e.g.
send to an address generated via Dev Settings → Trezor → Get Address, then mine a block
with `./bitcoin-cli`.
38 changes: 38 additions & 0 deletions .agents/journeys/hw-activity-blue-icons.journey.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<journey name="HW Activity Blue Icons">
<description>
Verifies hardware wallet on-chain activity merged into the home list and the All
Activity screen with blue icon variants, filter behavior, and the activity detail
fallback. Requires a paired Bridge emulator whose wallet has at least one on-chain
transaction (run hw-connect-home-tile.journey.xml first; fund per README.md if the
deterministic wallet has no history).
</description>
<actions>
<action>
Launch the Bitkit app and go to the wallet home screen
</action>
<action>
Verify the recent activity list contains at least one item whose circular icon is blue instead of orange
</action>
<action>
Tap the first activity item with a blue icon
</action>
<action>
Verify an activity detail screen opens showing a blue icon and an on-chain amount
</action>
<action>
Navigate back, then tap "Show All" beneath the activity list
</action>
<action>
Verify the All Activity list also contains items with blue icons
</action>
<action>
Tap the "Sent" tab and verify no blue-icon item with a received (downward) arrow is listed
</action>
<action>
Tap the "Received" tab and verify blue-icon items with received arrows are listed, assuming the hardware wallet has incoming transactions
</action>
<action>
Apply any tag filter if a tag exists, and verify blue-icon hardware items disappear from the filtered list; skip this step if no tags exist
</action>
</actions>
</journey>
46 changes: 46 additions & 0 deletions .agents/journeys/hw-connect-home-tile.journey.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<journey name="HW Connect and Home Tile">
<description>
Pairs the Bridge Trezor emulator through the dev Trezor screen, then verifies the
home-screen hardware wallet tile: name, connection indicator, balance, and the
not-yet-implemented overview notice. Requires the bitkit-docker Trezor User Env
running and a Bridge-enabled build (see README.md).
</description>
<actions>
<action>
Launch the Bitkit app and dismiss any onboarding or authentication prompts if a wallet already exists
</action>
<action>
Open the menu via the avatar or header, then navigate to Settings, then Dev Settings
</action>
<action>
Tap the "Trezor" row under the HARDWARE WALLET section
</action>
<action>
Tap the "Scan" button
</action>
<action>
Verify a device named "Bitkit Test Trezor" or "Trezor" appears in the device list within 10 seconds
</action>
<action>
Tap the listed device to connect
</action>
<action>
Verify a toast or status shows the device as connected within 15 seconds, with no PIN or pairing prompt
</action>
<action>
Navigate back to the wallet home screen
</action>
<action>
Verify a hardware wallet tile is shown beneath the SAVINGS and SPENDING tiles, labelled with the device name in caps (e.g. "BITKIT TEST TREZOR")
</action>
<action>
Verify the hardware tile shows a green connection indicator icon next to its name and a blue bitcoin icon with a sats amount
</action>
<action>
Tap the hardware wallet tile
</action>
<action>
Verify a notice appears saying "Hardware wallet overview not yet implemented."
</action>
</actions>
</journey>
46 changes: 46 additions & 0 deletions .agents/journeys/hw-suggestion-intro-sheet.journey.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<journey name="HW Suggestion Card and Intro Sheet">
<description>
Verifies the no-device home state: forgetting the paired device removes the
hardware tile, the Hardware suggestion card appears, and tapping it opens the
connect intro sheet with disabled Continue. Re-pairs the emulator at the end so
other journeys can run afterwards. Requires a paired Bridge emulator.
</description>
<actions>
<action>
Launch the Bitkit app, open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row
</action>
<action>
Tap the forget-device (trash) icon next to the known device, confirming any dialog
</action>
<action>
Navigate back to the wallet home screen
</action>
<action>
Verify no hardware wallet tile is shown beneath the SAVINGS and SPENDING tiles
</action>
<action>
Scroll the suggestion cards horizontally until a card titled "Hardware" with the text "Connect device" is visible
</action>
<action>
Tap the "Hardware" suggestion card
</action>
<action>
Verify a bottom sheet opens titled "Hardware Wallet" showing Trezor and Ledger device images
</action>
<action>
Verify the headline reads "Add your hardware wallet" with the words "hardware wallet" in blue
</action>
<action>
Verify the "Continue" button appears disabled and tapping it does nothing
</action>
<action>
Tap the "Cancel" button and verify the sheet closes back to the home screen
</action>
<action>
Open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row, tap "Scan", and tap the discovered device to re-pair it
</action>
<action>
Verify the device connects within 15 seconds
</action>
</actions>
</journey>
32 changes: 32 additions & 0 deletions .agents/journeys/hw-usb-reconnect.journey.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<journey name="HW USB Reconnect">
<description>
Verifies the disconnect indicator and the USB-attach auto-reconnect chain by
injecting the OS attach intent, with the Bridge emulator standing in for the
transport. Covers MainActivity intent handling, the serialized reconnect retry
loop, and the prompt-free reconnect. Requires a paired Bridge emulator (run
hw-connect-home-tile.journey.xml first).
</description>
<actions>
<action>
Launch the Bitkit app, open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row
</action>
<action>
Verify the connected device is shown, then tap the "Disconnect" button
</action>
<action>
Navigate back to the wallet home screen
</action>
<action>
Verify the hardware wallet tile still shows its name and balance but the connection indicator icon is grey
</action>
<action>
adb: adb shell am start -a android.hardware.usb.action.USB_DEVICE_ATTACHED -n to.bitkit.dev/to.bitkit.ui.MainActivity
</action>
<action>
Verify the app comes to the foreground on the wallet home screen
</action>
<action>
Verify the hardware tile connection indicator turns green within 15 seconds, with no PIN or pairing prompt shown
</action>
</actions>
</journey>
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
- ALWAYS add new localizable string resources in alphabetical order in `strings.xml`
- NEVER add string resources for strings used only in dev settings screens and previews and never localize acronyms
- ALWAYS use template in `.github/pull_request_template.md` for PR descriptions
- ALWAYS reference test files in PR descriptions/QA Notes by bare file name only (e.g. `HwWalletRepoTest.kt`), NEVER the full path; only when two referenced test files share the same name, prefix the shortest leading path segment(s) that disambiguate them (e.g. `repositories/FooTest.kt` vs `viewmodels/FooTest.kt`)
- ALWAYS wrap `ULong` numbers with `USat` in arithmetic operations, to guard against overflows
- PREFER to use one-liners with `run {}` when applicable, e.g. `override fun someCall(value: String) = run { this.value = value }`
- ALWAYS add imports instead of inline fully-qualified names
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,25 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import to.bitkit.data.serializers.TrezorDataSerializer
import to.bitkit.data.serializers.HwWalletDataSerializer
import to.bitkit.di.IoDispatcher
import to.bitkit.repositories.KnownDevice
import javax.inject.Inject
import javax.inject.Singleton

private val Context.trezorDataStore: DataStore<TrezorData> by dataStore(
private val Context.hwWalletDataStore: DataStore<HwWalletData> by dataStore(
fileName = "trezor_device.json",
serializer = TrezorDataSerializer
serializer = HwWalletDataSerializer
)

@Singleton
class TrezorStore @Inject constructor(
class HwWalletStore @Inject constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {
private val store = context.trezorDataStore
private val store = context.hwWalletDataStore

val data: Flow<TrezorData> = store.data
val data: Flow<HwWalletData> = store.data

suspend fun loadKnownDevices(): List<KnownDevice> = withContext(ioDispatcher) {
store.data.first().knownDevices
Expand All @@ -39,12 +39,12 @@ class TrezorStore @Inject constructor(
}

suspend fun reset() = withContext(ioDispatcher) {
store.updateData { TrezorData() }
store.updateData { HwWalletData() }
Unit
}
}

@Serializable
data class TrezorData(
data class HwWalletData(
val knownDevices: List<KnownDevice> = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ package to.bitkit.data.serializers

import androidx.datastore.core.Serializer
import kotlinx.serialization.SerializationException
import to.bitkit.data.TrezorData
import to.bitkit.data.HwWalletData
import to.bitkit.di.json
import to.bitkit.utils.Logger
import java.io.InputStream
import java.io.OutputStream

object TrezorDataSerializer : Serializer<TrezorData> {
private const val TAG = "TrezorDataSerializer"
object HwWalletDataSerializer : Serializer<HwWalletData> {
private const val TAG = "HwWalletDataSerializer"

override val defaultValue: TrezorData = TrezorData()
override val defaultValue: HwWalletData = HwWalletData()

override suspend fun readFrom(input: InputStream): TrezorData {
override suspend fun readFrom(input: InputStream): HwWalletData {
return try {
json.decodeFromString(input.readBytes().decodeToString())
} catch (e: SerializationException) {
Logger.error("Deserialize Trezor data failed", e, context = TAG)
Logger.error("Deserialize hardware wallet data failed", e, context = TAG)
defaultValue
}
}

override suspend fun writeTo(t: TrezorData, output: OutputStream) {
override suspend fun writeTo(t: HwWalletData, output: OutputStream) {
output.write(json.encodeToString(t).encodeToByteArray())
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/to/bitkit/models/AddressType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package to.bitkit.models

import com.synonym.bitkitcore.AccountType
import com.synonym.bitkitcore.AddressType
import org.lightningdevkit.ldknode.Network
import to.bitkit.env.Env
Expand Down Expand Up @@ -99,6 +100,14 @@ fun AddressType.toAccountDerivationPath(network: Network = Env.network): String
}
}

fun AddressType.toAccountType(): AccountType = when (this) {
AddressType.P2TR -> AccountType.TAPROOT
AddressType.P2WPKH -> AccountType.NATIVE_SEGWIT
AddressType.P2SH -> AccountType.WRAPPED_SEGWIT
AddressType.P2PKH -> AccountType.LEGACY
else -> AccountType.NATIVE_SEGWIT
}

fun AddressType.toSettingsString(): String = when (this) {
AddressType.P2TR -> "taproot"
AddressType.P2WPKH -> "nativeSegwit"
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/to/bitkit/models/BalanceState.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package to.bitkit.models

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import kotlinx.serialization.Serializable

@Immutable
@Stable
@Serializable
data class BalanceState(
val totalOnchainSats: ULong = 0uL,
Expand All @@ -13,6 +13,11 @@ data class BalanceState(
val maxSendOnchainSats: ULong = 0uL,
val balanceInTransferToSavings: ULong = 0uL,
val balanceInTransferToSpending: ULong = 0uL,
val hardwareWallets: List<HwWalletBalance> = emptyList(),
) {
val totalSats get() = totalOnchainSats + totalLightningSats

val totalHardwareSats get() = hardwareWallets.fold(0uL) { acc, wallet -> acc.safe() + wallet.sats.safe() }

val totalWithHardwareSats get() = totalSats.safe() + totalHardwareSats.safe()
}
Loading
Loading