Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
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
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()
}
36 changes: 36 additions & 0 deletions app/src/main/java/to/bitkit/models/HwWallet.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package to.bitkit.models

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.synonym.bitkitcore.Activity
import kotlinx.collections.immutable.ImmutableList
import kotlinx.serialization.Serializable

/** A paired hardware wallet tracked as a watch-only balance. */
@Stable
data class HwWallet(
val id: String,
val name: String,
val model: String?,
val transportType: TransportType,
val isConnected: Boolean,
val balanceSats: ULong,
val activities: ImmutableList<Activity>,
)

/** Serializable per-device balance snapshot carried by [BalanceState]. */
@Immutable
@Serializable
data class HwWalletBalance(
val id: String,
val sats: ULong,
)

/** A newly detected inbound transaction to a watched hardware wallet. */
@Immutable
data class HwWalletReceivedTx(
val txid: String,
val sats: ULong,
)

fun HwWallet.toBalance() = HwWalletBalance(id = id, sats = balanceSats)
10 changes: 8 additions & 2 deletions app/src/main/java/to/bitkit/models/Suggestion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ enum class Suggestion(
color = Colors.Brand24,
icon = R.drawable.b_emboss,
),
HARDWARE(
title = R.string.cards__hardware__title,
description = R.string.cards__hardware__description,
color = Colors.Blue24,
icon = R.drawable.trezor,
),
LIGHTNING(
title = R.string.cards__lightning__title,
description = R.string.cards__lightning__description,
Expand Down Expand Up @@ -46,13 +52,13 @@ enum class Suggestion(
INVITE(
title = R.string.cards__invite__title,
description = R.string.cards__invite__description,
color = Colors.Blue24,
color = Colors.Green24,
icon = R.drawable.group
),
PROFILE(
title = R.string.cards__slashtagsProfile__title,
description = R.string.cards__slashtagsProfile__description,
color = Colors.PubkyGreen24,
color = Colors.Brand24,
icon = R.drawable.crown,
),
SHOP(
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/to/bitkit/models/TransportType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package to.bitkit.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/** Transport a hardware-wallet device is paired over. */
@Serializable
enum class TransportType {
@SerialName("bluetooth")
BLUETOOTH,

@SerialName("usb")
USB,
}
Loading
Loading