From 7aa839e2fb17cf192407b2227cb0fe7997b9dbb5 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 18 Jun 2026 15:37:53 +0200 Subject: [PATCH 1/6] feat: add hardware wallets settings screen --- app/src/main/java/to/bitkit/ui/ContentView.kt | 13 +- .../ui/components/HwWalletComponents.kt | 68 +++++ .../screens/wallets/HardwareWalletScreen.kt | 8 +- .../ui/screens/wallets/HwWalletViewModel.kt | 8 +- .../to/bitkit/ui/settings/SettingsScreen.kt | 14 + .../general/HardwareWalletsSettingsScreen.kt | 284 ++++++++++++++++++ .../bitkit/ui/sheets/hardware/HwIntroSheet.kt | 58 +--- app/src/main/res/values/strings.xml | 3 + .../screens/wallets/HwWalletViewModelTest.kt | 40 ++- changelog.d/next/1026.added.md | 1 + journeys/hardware-wallet/README.md | 6 +- .../settings-hardware-wallets.xml | 60 ++++ 12 files changed, 487 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt create mode 100644 changelog.d/next/1026.added.md create mode 100644 journeys/hardware-wallet/settings-hardware-wallets.xml diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 62bde56803..1714718528 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -162,6 +162,7 @@ import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsIntroScreen import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsSettings import to.bitkit.ui.settings.backups.ResetAndRestoreScreen import to.bitkit.ui.settings.general.DefaultUnitSettingsScreen +import to.bitkit.ui.settings.general.HardwareWalletsSettingsScreen import to.bitkit.ui.settings.general.LocalCurrencySettingsScreen import to.bitkit.ui.settings.general.TagsSettingsScreen import to.bitkit.ui.settings.general.WidgetsSettingsScreen @@ -660,7 +661,7 @@ private fun RootNavHost( contacts(navController, settingsViewModel, appViewModel) profile(navController, settingsViewModel) shop(navController, settingsViewModel, appViewModel) - generalSettingsSubScreens(navController, settingsViewModel) + generalSettingsSubScreens(navController, appViewModel, settingsViewModel) advancedSettingsSubScreens(navController) transactionSpeedSettings(navController) pinManagement(navController) @@ -1371,6 +1372,7 @@ private fun NavGraphBuilder.shop( private fun NavGraphBuilder.generalSettingsSubScreens( navController: NavHostController, + appViewModel: AppViewModel, settingsViewModel: SettingsViewModel, ) { composableWithDefaultTransitions { @@ -1380,6 +1382,12 @@ private fun NavGraphBuilder.generalSettingsSubScreens( composableWithDefaultTransitions { TagsSettingsScreen(navController) } + composableWithDefaultTransitions { + HardwareWalletsSettingsScreen( + navController = navController, + onClickAdd = { appViewModel.showSheet(Sheet.Hardware()) }, + ) + } composableWithDefaultTransitions { BackgroundPaymentsSettings( onBack = { navController.popBackStack() }, @@ -1848,6 +1856,9 @@ sealed interface Routes { @Serializable data object TagsSettings : Routes + @Serializable + data object HardwareWalletsSettings : Routes + @Serializable data object CoinSelectPreference : Routes diff --git a/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt b/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt new file mode 100644 index 0000000000..8f18dd407c --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt @@ -0,0 +1,68 @@ +package to.bitkit.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import to.bitkit.R + +// Device illustration proportions, taken from the Figma hardware wallet frames. +private const val HW_DEVICE_IMAGE_SIZE_RATIO = 256f / 375f +private const val HW_DEVICE_TREZOR_BLEED_RATIO = 84f / 375f +private const val HW_DEVICE_LEDGER_BLEED_RATIO = 53f / 375f +private const val HW_DEVICE_STAGGER_RATIO = 12f / 375f + +@Composable +fun HwDeviceIllustrations(modifier: Modifier = Modifier) { + BoxWithConstraints(modifier) { + val imageSize = maxWidth * HW_DEVICE_IMAGE_SIZE_RATIO + val staggerY = maxWidth * HW_DEVICE_STAGGER_RATIO + TrezorImage(imageSize = imageSize, staggerY = staggerY) + LedgerImage( + imageSize = imageSize, + staggerY = staggerY, + modifier = Modifier.blur(16.dp, BlurredEdgeTreatment.Unbounded) + ) + } +} + +@Composable +private fun BoxWithConstraintsScope.TrezorImage( + imageSize: Dp, + staggerY: Dp, + modifier: Modifier = Modifier, +) { + Image( + painter = painterResource(R.drawable.trezor), + contentDescription = null, + modifier = modifier + .size(imageSize) + .align(Alignment.CenterStart) + .offset(x = -maxWidth * HW_DEVICE_TREZOR_BLEED_RATIO, y = staggerY) + ) +} + +@Composable +private fun BoxWithConstraintsScope.LedgerImage( + imageSize: Dp, + staggerY: Dp, + modifier: Modifier = Modifier, +) { + Image( + painter = painterResource(R.drawable.ledger), + contentDescription = null, + modifier = modifier + .size(imageSize) + .align(Alignment.CenterEnd) + .offset(x = maxWidth * HW_DEVICE_LEDGER_BLEED_RATIO, y = -staggerY) + ) +} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt index 91178520d2..c45d40ced5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt @@ -77,13 +77,13 @@ fun HardwareWalletScreen( if (walletsLoaded && wallet == null) onBackClick() } - wallet?.let { + wallet?.let { device -> HardwareWalletContent( - wallet = it, - showRemoveDialog = uiState.showRemoveDialog, + wallet = device, + showRemoveDialog = uiState.isPendingRemoval != null, onActivityItemClick = onActivityItemClick, onTransferToSpendingClick = onTransferToSpendingClick, - onRemoveClick = viewModel::onRemoveClick, + onRemoveClick = { viewModel.onRemoveClick(device) }, onConfirmRemove = { viewModel.removeDevice(deviceId) }, onDismissRemoveDialog = viewModel::onDismissRemoveDialog, onBackClick = onBackClick, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HwWalletViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HwWalletViewModel.kt index 1bef49065e..ed8c2bf239 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HwWalletViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HwWalletViewModel.kt @@ -31,13 +31,13 @@ class HwWalletViewModel @Inject constructor( private val _uiState = MutableStateFlow(HwWalletDetailUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun onRemoveClick() = _uiState.update { it.copy(showRemoveDialog = true) } + fun onRemoveClick(wallet: HwWallet) = _uiState.update { it.copy(isPendingRemoval = wallet) } - fun onDismissRemoveDialog() = _uiState.update { it.copy(showRemoveDialog = false) } + fun onDismissRemoveDialog() = _uiState.update { it.copy(isPendingRemoval = null) } fun removeDevice(deviceId: String) { viewModelScope.launch { - _uiState.update { it.copy(showRemoveDialog = false) } + _uiState.update { it.copy(isPendingRemoval = null) } hwWalletRepo.removeDevice(deviceId).onFailure { ToastEventBus.send( type = Toast.ToastType.ERROR, @@ -51,5 +51,5 @@ class HwWalletViewModel @Inject constructor( @Immutable data class HwWalletDetailUiState( - val showRemoveDialog: Boolean = false, + val isPendingRemoval: HwWallet? = null, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index 47816bcda4..195194b578 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -57,6 +57,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.PinnedTabsScaffold import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.screens.wallets.HwWalletViewModel import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing import to.bitkit.ui.screens.wallets.activity.components.TabItem import to.bitkit.ui.settingsViewModel @@ -79,6 +80,7 @@ fun SettingsScreen( navController: NavController, advancedViewModel: AdvancedSettingsViewModel = hiltViewModel(), languageViewModel: LanguageViewModel = hiltViewModel(), + hwWalletViewModel: HwWalletViewModel = hiltViewModel(), ) { val app = appViewModel ?: return val settings = settingsViewModel ?: return @@ -94,6 +96,7 @@ fun SettingsScreen( val notificationsGranted by settings.notificationsGranted.collectAsStateWithLifecycle() val isPubkyAuthenticated by settings.isPubkyAuthenticated.collectAsStateWithLifecycle() val isPaykitEnabled by settings.isPaykitEnabled.collectAsStateWithLifecycle() + val hardwareWallets by hwWalletViewModel.wallets.collectAsStateWithLifecycle() val languageUiState by languageViewModel.uiState.collectAsStateWithLifecycle() // Security tab state @@ -130,6 +133,7 @@ fun SettingsScreen( notificationsGranted = notificationsGranted, isPubkyAuthenticated = isPubkyAuthenticated, isPaykitEnabled = isPaykitEnabled, + hardwareWalletCount = hardwareWallets.size, ), securityState = SecurityTabState( isPinEnabled = isPinEnabled, @@ -166,6 +170,7 @@ fun SettingsScreen( navController.navigateTo(Routes.BackgroundPaymentsIntro) } } + SettingsEvent.HardwareWalletsClick -> navController.navigateTo(Routes.HardwareWalletsSettings) SettingsEvent.BackupWalletClick -> app.showSheet(Sheet.Backup()) SettingsEvent.DataBackupsClick -> navController.navigateTo(Routes.BackupSettings) SettingsEvent.ResetWalletClick -> @@ -365,6 +370,13 @@ private fun GeneralTabContent( onClick = { onEvent(SettingsEvent.BgPaymentsClick) }, modifier = Modifier.testTag("BackgroundPaymentSettings") ) + SettingsButtonRow( + title = stringResource(R.string.settings__hardware_wallets__nav_title), + icon = { SettingsIcon(R.drawable.ic_device_mobile_speaker) }, + value = SettingsButtonValue.StringValue(state.hardwareWalletCount.toString()), + onClick = { onEvent(SettingsEvent.HardwareWalletsClick) }, + modifier = Modifier.testTag("HardwareWalletsSettings") + ) VerticalSpacer(32.dp) } @@ -647,6 +659,7 @@ sealed interface SettingsEvent { data object PaymentPreferenceClick : SettingsEvent data object QuickPayClick : SettingsEvent data object BgPaymentsClick : SettingsEvent + data object HardwareWalletsClick : SettingsEvent // Security data object BackupWalletClick : SettingsEvent @@ -689,6 +702,7 @@ data class GeneralTabState( val notificationsGranted: Boolean = false, val isPubkyAuthenticated: Boolean = false, val isPaykitEnabled: Boolean = false, + val hardwareWalletCount: Int = 0, ) @Immutable diff --git a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt new file mode 100644 index 0000000000..7ab81a01ca --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt @@ -0,0 +1,284 @@ +package to.bitkit.ui.settings.general + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import to.bitkit.R +import to.bitkit.models.HwWallet +import to.bitkit.models.TransportType +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.Display +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.HorizontalSpacer +import to.bitkit.ui.components.HwDeviceIllustrations +import to.bitkit.ui.components.MoneySSB +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.scaffold.AppAlertDialog +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.DrawerNavIcon +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.screens.wallets.HwWalletViewModel +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +private const val ILLUSTRATIONS_HEIGHT_FRACTION = 0.8f + +@Composable +fun HardwareWalletsSettingsScreen( + navController: NavController, + onClickAdd: () -> Unit, + viewModel: HwWalletViewModel = hiltViewModel(), +) { + val wallets by viewModel.wallets.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + Content( + wallets = wallets, + isPendingRemoval = uiState.isPendingRemoval, + onBack = { navController.popBackStack() }, + onClickAdd = onClickAdd, + onRemoveClick = viewModel::onRemoveClick, + onConfirmRemove = { viewModel.removeDevice(it.id) }, + onDismissRemoveDialog = viewModel::onDismissRemoveDialog, + ) +} + +@Composable +private fun Content( + wallets: ImmutableList, + isPendingRemoval: HwWallet?, + onBack: () -> Unit = {}, + onClickAdd: () -> Unit = {}, + onRemoveClick: (HwWallet) -> Unit = {}, + onConfirmRemove: (HwWallet) -> Unit = {}, + onDismissRemoveDialog: () -> Unit = {}, +) { + ScreenColumn( + modifier = Modifier.testTag("HardwareWalletsScreen") + ) { + AppTopBar( + titleText = stringResource(R.string.settings__hardware_wallets__nav_title), + onBackClick = onBack, + actions = { DrawerNavIcon() }, + ) + + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + HwDeviceIllustrations( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .fillMaxHeight(ILLUSTRATIONS_HEIGHT_FRACTION) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + if (wallets.isEmpty()) { + EmptyState(modifier = Modifier.weight(1f)) + } else { + wallets.forEach { wallet -> + HwWalletRow(wallet = wallet, onRemoveClick = onRemoveClick) + HorizontalDivider(color = Colors.White10) + } + FillHeight() + } + + PrimaryButton( + text = stringResource(R.string.settings__hardware_wallets__add_button), + onClick = onClickAdd, + modifier = Modifier + .fillMaxWidth() + .testTag("AddHardwareWallet") + ) + VerticalSpacer(16.dp) + } + } + } + + isPendingRemoval?.let { wallet -> + AppAlertDialog( + title = stringResource(R.string.hardware__remove_dialog_title, wallet.name), + text = stringResource(R.string.hardware__remove_dialog_text), + confirmText = stringResource(R.string.common__remove), + dismissText = stringResource(R.string.common__cancel), + onConfirm = { onConfirmRemove(wallet) }, + onDismiss = onDismissRemoveDialog, + ) + } +} + +@Composable +private fun EmptyState(modifier: Modifier = Modifier) { + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier.fillMaxWidth() + ) { + Display(text = stringResource(R.string.settings__hardware_wallets__nav_title)) + VerticalSpacer(8.dp) + BodyM( + text = stringResource(R.string.settings__hardware_wallets__empty_text), + color = Colors.White64, + ) + } +} + +@Composable +private fun HwWalletRow( + wallet: HwWallet, + onRemoveClick: (HwWallet) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .height(52.dp) + .testTag("HardwareWalletRow") + ) { + HwConnectionBadge(transportType = wallet.transportType, isConnected = wallet.isConnected) + HorizontalSpacer(12.dp) + BodyM( + text = wallet.name, + maxLines = 1, + modifier = Modifier.weight(1f) + ) + HorizontalSpacer(8.dp) + MoneySSB( + sats = wallet.balanceSats.toLong(), + color = Colors.White64, + accent = Colors.White64, + showSymbol = true, + ) + IconButton( + onClick = { onRemoveClick(wallet) }, + modifier = Modifier.testTag("HardwareWalletRowDelete") + ) { + Icon( + painter = painterResource(R.drawable.ic_trash), + contentDescription = stringResource(R.string.common__remove), + tint = Colors.White64, + modifier = Modifier.size(24.dp) + ) + } + } +} + +@Composable +private fun HwConnectionBadge( + transportType: TransportType, + isConnected: Boolean, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .size(32.dp) + .clip(CircleShape) + .background(if (isConnected) Colors.Green16 else Colors.White16) + ) { + Icon( + painter = painterResource( + id = when (transportType) { + TransportType.BLUETOOTH -> R.drawable.ic_bluetooth_connected + TransportType.USB -> R.drawable.ic_usb_connected + } + ), + contentDescription = null, + tint = if (isConnected) Colors.Green else Colors.White64, + modifier = Modifier.size(16.dp) + ) + } +} + +private fun previewWallet( + id: String = "dev1", + name: String = "Trezor Safe 3", + transportType: TransportType = TransportType.BLUETOOTH, + isConnected: Boolean = true, + balanceSats: ULong = 10_562_411uL, +) = HwWallet( + id = id, + name = name, + model = name.removePrefix("Trezor ").ifEmpty { null }, + transportType = transportType, + isConnected = isConnected, + balanceSats = balanceSats, + activities = persistentListOf(), +) + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + Content( + wallets = listOf( + previewWallet(), + previewWallet( + id = "dev2", + name = "Ledger Nano X", + transportType = TransportType.USB, + isConnected = false, + balanceSats = 2_735_180uL, + ), + ).toImmutableList(), + isPendingRemoval = null, + ) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewEmpty() { + AppThemeSurface { + Content( + wallets = persistentListOf(), + isPendingRemoval = null, + ) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewRemoveDialog() { + AppThemeSurface { + Content( + wallets = persistentListOf(previewWallet()), + isPendingRemoval = previewWallet(), + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt index c1a71b904b..8f2f3fbee0 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt @@ -1,32 +1,23 @@ package to.bitkit.ui.sheets.hardware -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.BlurredEdgeTreatment -import androidx.compose.ui.draw.blur import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Display +import to.bitkit.ui.components.HwDeviceIllustrations import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer @@ -36,12 +27,6 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -// Proportions from Figma v61 frame -private const val INTRO_IMAGE_SIZE_RATIO = 256f / 375f -private const val INTRO_TREZOR_BLEED_RATIO = 84f / 375f -private const val INTRO_LEDGER_BLEED_RATIO = 53f / 375f -private const val INTRO_IMAGE_STAGGER_RATIO = 12f / 375f - @Composable fun HwIntroSheet( modifier: Modifier = Modifier, @@ -66,16 +51,11 @@ private fun Content( .testTag("hw_intro_screen") ) { SheetTopBar(titleText = stringResource(R.string.hardware__intro_title)) - BoxWithConstraints( + HwDeviceIllustrations( modifier = Modifier .fillMaxWidth() .weight(1f) - ) { - val imageSize = maxWidth * INTRO_IMAGE_SIZE_RATIO - val staggerY = maxWidth * INTRO_IMAGE_STAGGER_RATIO - TrezorImage(imageSize, staggerY) - LedgerImage(imageSize, staggerY, modifier = Modifier.blur(16.dp, BlurredEdgeTreatment.Unbounded)) - } + ) Column( modifier = Modifier .fillMaxWidth() @@ -106,38 +86,6 @@ private fun Content( } } -@Composable -private fun BoxWithConstraintsScope.TrezorImage( - imageSize: Dp, - staggerY: Dp, - modifier: Modifier = Modifier, -) { - Image( - painter = painterResource(R.drawable.trezor), - contentDescription = null, - modifier = modifier - .size(imageSize) - .align(Alignment.CenterStart) - .offset(x = -maxWidth * INTRO_TREZOR_BLEED_RATIO, y = staggerY) - ) -} - -@Composable -private fun BoxWithConstraintsScope.LedgerImage( - imageSize: Dp, - staggerY: Dp, - modifier: Modifier = Modifier, -) { - Image( - painter = painterResource(R.drawable.ledger), - contentDescription = null, - modifier = modifier - .size(imageSize) - .align(Alignment.CenterEnd) - .offset(x = maxWidth * INTRO_LEDGER_BLEED_RATIO, y = -staggerY) - ) -} - @Preview(showSystemUi = true) @Composable private fun PreviewIntro() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c53b4dedd..dcef4345d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -860,6 +860,9 @@ Tip: Quickly toggle between Bitcoin and {currency} by tapping on your wallet balance. Default Unit General + Add Hardware Wallet + Pair a hardware device to watch its balance and activity in Bitkit. + Hardware Wallets System Settings Language Payments from contacts diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt index 4636deb149..565227e3de 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt @@ -1,6 +1,7 @@ package to.bitkit.ui.screens.wallets import android.content.Context +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -20,8 +21,7 @@ import to.bitkit.test.BaseUnitTest import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.AppError import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.assertNull class HwWalletViewModelTest : BaseUnitTest() { @@ -38,7 +38,17 @@ class HwWalletViewModelTest : BaseUnitTest() { activities = persistentListOf(), ) - private lateinit var wallets: MutableStateFlow> + private val otherWallet = HwWallet( + id = "dev2", + name = "Trezor Safe 5", + model = "Safe 5", + transportType = TransportType.BLUETOOTH, + isConnected = false, + balanceSats = 2_735_180uL, + activities = persistentListOf(), + ) + + private lateinit var wallets: MutableStateFlow> private lateinit var walletsLoaded: MutableStateFlow @Before @@ -62,27 +72,37 @@ class HwWalletViewModelTest : BaseUnitTest() { } @Test - fun `onRemoveClick shows the dialog and onDismiss hides it`() = test { + fun `onRemoveClick sets the pending device and onDismiss clears it`() = test { val sut = createSut() - sut.onRemoveClick() - assertTrue(sut.uiState.value.showRemoveDialog) + sut.onRemoveClick(wallet) + assertEquals(wallet, sut.uiState.value.isPendingRemoval) sut.onDismissRemoveDialog() - assertFalse(sut.uiState.value.showRemoveDialog) + assertNull(sut.uiState.value.isPendingRemoval) + } + + @Test + fun `onRemoveClick stores the clicked device when multiple are paired`() = test { + wallets.value = listOf(wallet, otherWallet).toImmutableList() + val sut = createSut() + + sut.onRemoveClick(otherWallet) + + assertEquals(otherWallet, sut.uiState.value.isPendingRemoval) } @Test - fun `removeDevice delegates to the repo and hides the dialog`() = test { + fun `removeDevice delegates to the repo and clears the pending device`() = test { whenever { hwWalletRepo.removeDevice("dev1") }.thenReturn(Result.success(Unit)) val sut = createSut() - sut.onRemoveClick() + sut.onRemoveClick(wallet) sut.removeDevice("dev1") advanceUntilIdle() verify(hwWalletRepo).removeDevice("dev1") - assertFalse(sut.uiState.value.showRemoveDialog) + assertNull(sut.uiState.value.isPendingRemoval) } @Test diff --git a/changelog.d/next/1026.added.md b/changelog.d/next/1026.added.md new file mode 100644 index 0000000000..d655682a75 --- /dev/null +++ b/changelog.d/next/1026.added.md @@ -0,0 +1 @@ +Added a Hardware Wallets settings screen to view and remove paired devices, reachable from Settings under General › Payments. diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index def199d577..5ea4f3f69f 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -47,8 +47,9 @@ instead of UI interactions. ## Journeys Run in this order — `connect-home-tile.xml` pairs the emulator that the later journeys -rely on, `suggestion-intro-sheet.xml` ends by re-pairing after a forget, and -`detail-overview.xml` runs last because its final Remove step forgets the device. +rely on, `suggestion-intro-sheet.xml` and `settings-hardware-wallets.xml` each end by +re-pairing after a forget, and `detail-overview.xml` runs last because its final Remove step +forgets the device. | Journey | Covers | | - | - | @@ -56,6 +57,7 @@ rely on, `suggestion-intro-sheet.xml` ends by re-pairing after a forget, and | `activity-blue-icons.xml` | Hardware activity merge, blue icons, All Activity filters, detail fallback | | `usb-reconnect.xml` | Disconnect indicator, injected USB attach intent → silent auto-reconnect | | `suggestion-intro-sheet.xml` | Forget device, Hardware suggestion card, connect intro sheet | +| `settings-hardware-wallets.xml` | Payments count row, Hardware Wallets screen list, Add button sheet, per-row delete confirm + re-pair | | `detail-overview.xml` | Detail screen overview, Transfer placeholder, activity, Remove confirm + forget | To exercise the received-money sheet (not covered by a journey because it needs an diff --git a/journeys/hardware-wallet/settings-hardware-wallets.xml b/journeys/hardware-wallet/settings-hardware-wallets.xml new file mode 100644 index 0000000000..1b15a95dae --- /dev/null +++ b/journeys/hardware-wallet/settings-hardware-wallets.xml @@ -0,0 +1,60 @@ + + + Verifies the Hardware Wallets settings surface: the Payments row with the paired-device + count, the Hardware Wallets screen listing the paired device (name, balance, connection + indicator), the Add Hardware Wallet button opening the connect intro sheet, and the + per-row delete confirm dialog. The final Remove forgets the device, so this re-pairs the + emulator at the end so other journeys can run afterwards. Requires a paired Bridge + emulator (run connect-home-tile.xml first). + + + + Launch the Bitkit app, open the menu, and navigate to Settings + + + Ensure the "General" tab is selected and scroll down to the "Payments" section + + + Verify a "Hardware Wallets" row is shown under Payments with a numeric value of at least 1 + + + Tap the "Hardware Wallets" row + + + Verify the Hardware Wallets screen opens with the top bar titled "Hardware Wallets" + + + Verify the paired device is listed with its name, a bitcoin balance prefixed with the ₿ symbol, and a green connection indicator on the left + + + Tap the "Add Hardware Wallet" button near the bottom + + + Verify a bottom sheet opens titled "Hardware Wallet" showing Trezor and Ledger device images, then tap "Cancel" to close it + + + Tap the trash (delete) icon on the device row + + + Verify a confirm dialog appears explaining that your funds are safe and your coins won't be deleted + + + Tap "Cancel" and verify the device is still listed on the Hardware Wallets screen + + + Tap the trash (delete) icon again, then tap "Remove" in the confirm dialog + + + Verify the device is removed: the list no longer shows it and an empty state is displayed + + + Navigate back to Settings and verify the "Hardware Wallets" row value decreased (or the row shows 0) + + + 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 + + + Verify the device connects within 15 seconds + + + From af4e8e03cc995701bf26c4a8c938357cab73dc37 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 18 Jun 2026 15:39:37 +0200 Subject: [PATCH 2/6] chore: rename changelog fragment --- changelog.d/next/{1026.added.md => 1032.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{1026.added.md => 1032.added.md} (100%) diff --git a/changelog.d/next/1026.added.md b/changelog.d/next/1032.added.md similarity index 100% rename from changelog.d/next/1026.added.md rename to changelog.d/next/1032.added.md From 1d987320c57eace2ffa88b280464685ed9cc8b2c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 18 Jun 2026 15:55:06 +0200 Subject: [PATCH 3/6] fix: address hw settings review --- .../general/HardwareWalletsSettingsScreen.kt | 42 +++++++++++++++---- app/src/main/res/values/strings.xml | 4 ++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt index 7ab81a01ca..c19e5596a0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -36,7 +38,6 @@ import to.bitkit.models.HwWallet import to.bitkit.models.TransportType import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display -import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.HorizontalSpacer import to.bitkit.ui.components.HwDeviceIllustrations import to.bitkit.ui.components.MoneySSB @@ -111,11 +112,19 @@ private fun Content( if (wallets.isEmpty()) { EmptyState(modifier = Modifier.weight(1f)) } else { - wallets.forEach { wallet -> - HwWalletRow(wallet = wallet, onRemoveClick = onRemoveClick) - HorizontalDivider(color = Colors.White10) + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + items( + items = wallets, + key = { it.id }, + ) { wallet -> + HwWalletRow(wallet = wallet, onRemoveClick = onRemoveClick) + HorizontalDivider(color = Colors.White10) + } } - FillHeight() } PrimaryButton( @@ -168,7 +177,7 @@ private fun HwWalletRow( modifier = modifier .fillMaxWidth() .height(52.dp) - .testTag("HardwareWalletRow") + .testTag("HardwareWalletRow_${wallet.id}") ) { HwConnectionBadge(transportType = wallet.transportType, isConnected = wallet.isConnected) HorizontalSpacer(12.dp) @@ -186,7 +195,7 @@ private fun HwWalletRow( ) IconButton( onClick = { onRemoveClick(wallet) }, - modifier = Modifier.testTag("HardwareWalletRowDelete") + modifier = Modifier.testTag("HardwareWalletRowDelete_${wallet.id}") ) { Icon( painter = painterResource(R.drawable.ic_trash), @@ -204,6 +213,23 @@ private fun HwConnectionBadge( isConnected: Boolean, modifier: Modifier = Modifier, ) { + val contentDescription = when (transportType) { + TransportType.BLUETOOTH -> { + if (isConnected) { + stringResource(R.string.hardware__connection_badge_connected_bluetooth) + } else { + stringResource(R.string.hardware__connection_badge_disconnected_bluetooth) + } + } + TransportType.USB -> { + if (isConnected) { + stringResource(R.string.hardware__connection_badge_connected_usb) + } else { + stringResource(R.string.hardware__connection_badge_disconnected_usb) + } + } + } + Box( contentAlignment = Alignment.Center, modifier = modifier @@ -218,7 +244,7 @@ private fun HwConnectionBadge( TransportType.USB -> R.drawable.ic_usb_connected } ), - contentDescription = null, + contentDescription = contentDescription, tint = if (isConnected) Colors.Green else Colors.White64, modifier = Modifier.size(16.dp) ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dcef4345d5..96a239a800 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -164,6 +164,10 @@ ±1-2 hours ±1h Slow + Connected via Bluetooth + Connected via USB + Disconnected via Bluetooth + Disconnected via USB Add your <accent>hardware wallet</accent> Connect your hardware device to watch or manage your long-term funds. Hardware Wallet From cd799cefc96ccdcb392850da6a7cf810e580641c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 18 Jun 2026 17:15:35 +0200 Subject: [PATCH 4/6] chore: cleanup --- README.md | 11 +++++++++++ app/build.gradle.kts | 19 +++++++++++++++++-- .../general/HardwareWalletsSettingsScreen.kt | 2 +- .../screens/wallets/HwWalletViewModelTest.kt | 6 +++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b4c4a17829..dbeb5cc44c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,17 @@ Set up local env: Run `just list` to see available commands. The common ones are `just init`, `just compile`, `just run`, `just build`, `just release`, `just test`, `just lint`, and `just translations pull`. `just run` prefers a physical device and falls back to an emulator. +### Trezor Bridge In Android Studio + +When testing the Trezor Bridge emulator from bitkit-docker through Android Studio, add these gitignored local values to `local.properties`: + +```properties +TREZOR_BRIDGE=true +TREZOR_BRIDGE_URL=http://10.0.2.2:21325 +``` + +CLI builds can still pass the same values as environment variables. + ### Lint This project uses detekt with default ktlint and compose-rules for android code linting. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e5e7f31133..2f798e6949 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,6 +41,21 @@ val keystoreProperties by lazy { keystoreProperties } +val localProperties by lazy { + Properties().apply { + val localPropertiesFile = rootProject.file("local.properties") + if (localPropertiesFile.exists()) { + localPropertiesFile.inputStream().use { load(it) } + } + } +} + +fun localProp(key: String): String? { + return System.getenv(key) + ?: providers.gradleProperty(key).orNull + ?: localProperties.getProperty(key) +} + // Android resource qualifier format for androidResources.localeFilters val androidLocales = listOf( "en", "ar", "b+es+419", "ca", "cs", "de", "el", "es", "es-rES", "fr", "it", "nl", "pl", "pt", "pt-rBR", "ru" @@ -51,8 +66,8 @@ val bcp47Locales = listOf( ) val e2eBackendEnv = System.getenv("E2E_BACKEND") ?: "local" val e2eHomegateUrlEnv = System.getenv("E2E_HOMEGATE_URL") ?: "http://127.0.0.1:6288" -val trezorBridgeEnv = System.getenv("TREZOR_BRIDGE")?.toBoolean()?.toString() ?: "false" -val trezorBridgeUrlEnv = System.getenv("TREZOR_BRIDGE_URL") ?: "http://10.0.2.2:21325" +val trezorBridgeEnv = localProp("TREZOR_BRIDGE")?.toBoolean()?.toString() ?: "false" +val trezorBridgeUrlEnv = localProp("TREZOR_BRIDGE_URL") ?: "http://10.0.2.2:21325" val requestedNdkVersion = System.getenv("NDK_VERSION")?.takeIf { it.isNotBlank() } val androidTestAnnotationPackage = "to.bitkit.test.annotations" val androidTestTaskPrefix = "connectedDevDebug" diff --git a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt index c19e5596a0..d21b88ba8f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt @@ -161,7 +161,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { VerticalSpacer(8.dp) BodyM( text = stringResource(R.string.settings__hardware_wallets__empty_text), - color = Colors.White64, + color = Colors.White80, ) } } diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt index 565227e3de..d3e8e6102c 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/HwWalletViewModelTest.kt @@ -4,6 +4,7 @@ import android.content.Context import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceUntilIdle @@ -23,6 +24,7 @@ import to.bitkit.utils.AppError import kotlin.test.assertEquals import kotlin.test.assertNull +@OptIn(ExperimentalCoroutinesApi::class) class HwWalletViewModelTest : BaseUnitTest() { private val context: Context = mock() @@ -49,14 +51,12 @@ class HwWalletViewModelTest : BaseUnitTest() { ) private lateinit var wallets: MutableStateFlow> - private lateinit var walletsLoaded: MutableStateFlow @Before fun setUp() { wallets = MutableStateFlow(listOf(wallet).toImmutableList()) - walletsLoaded = MutableStateFlow(true) whenever(hwWalletRepo.wallets).thenReturn(wallets) - whenever(hwWalletRepo.walletsLoaded).thenReturn(walletsLoaded) + whenever(hwWalletRepo.walletsLoaded).thenReturn(MutableStateFlow(true)) whenever(context.getString(R.string.common__error)).thenReturn("Error") whenever(context.getString(R.string.hardware__remove_error)).thenReturn("Could not remove") } From c334b3d6fe382fb9263d82952385ef7e7241d6da Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 18 Jun 2026 17:32:07 +0200 Subject: [PATCH 5/6] refactor: share hw wallet icon --- .../ui/components/HwWalletComponents.kt | 23 +++++++++++++++++++ .../bitkit/ui/screens/wallets/HomeScreen.kt | 13 ++++------- .../general/HardwareWalletsSettingsScreen.kt | 12 ++++------ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt b/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt index 8f18dd407c..7c91b2c19d 100644 --- a/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt +++ b/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,6 +15,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.models.TransportType +import to.bitkit.ui.theme.Colors // Device illustration proportions, taken from the Figma hardware wallet frames. private const val HW_DEVICE_IMAGE_SIZE_RATIO = 256f / 375f @@ -35,6 +38,26 @@ fun HwDeviceIllustrations(modifier: Modifier = Modifier) { } } +@Composable +fun HwWalletConnectionIcon( + transportType: TransportType, + isConnected: Boolean, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { + Icon( + painter = painterResource( + id = when (transportType) { + TransportType.BLUETOOTH -> R.drawable.ic_bluetooth_connected + TransportType.USB -> R.drawable.ic_usb_connected + } + ), + contentDescription = contentDescription, + tint = if (isConnected) Colors.Green else Colors.Gray1, + modifier = modifier + ) +} + @Composable private fun BoxWithConstraintsScope.TrezorImage( imageSize: Dp, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index c829639789..0abf58af0b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -137,6 +137,7 @@ import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth import to.bitkit.ui.components.Headline24 import to.bitkit.ui.components.HorizontalSpacer +import to.bitkit.ui.components.HwWalletConnectionIcon import to.bitkit.ui.components.PubkyImage import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.StatusBarSpacer @@ -779,15 +780,9 @@ private fun RowScope.HwDeviceCell( .testTag("ActivityHardware") ) { HorizontalSpacer(4.dp) - Icon( - painter = painterResource( - id = when (wallet.transportType) { - TransportType.BLUETOOTH -> R.drawable.ic_bluetooth_connected - TransportType.USB -> R.drawable.ic_usb_connected - } - ), - contentDescription = null, - tint = if (wallet.isConnected) Colors.Green else Colors.Gray1, + HwWalletConnectionIcon( + transportType = wallet.transportType, + isConnected = wallet.isConnected, modifier = Modifier.size(16.dp) ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt index d21b88ba8f..60cb9bf3f1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt @@ -40,6 +40,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.HorizontalSpacer import to.bitkit.ui.components.HwDeviceIllustrations +import to.bitkit.ui.components.HwWalletConnectionIcon import to.bitkit.ui.components.MoneySSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.VerticalSpacer @@ -237,15 +238,10 @@ private fun HwConnectionBadge( .clip(CircleShape) .background(if (isConnected) Colors.Green16 else Colors.White16) ) { - Icon( - painter = painterResource( - id = when (transportType) { - TransportType.BLUETOOTH -> R.drawable.ic_bluetooth_connected - TransportType.USB -> R.drawable.ic_usb_connected - } - ), + HwWalletConnectionIcon( + transportType = transportType, + isConnected = isConnected, contentDescription = contentDescription, - tint = if (isConnected) Colors.Green else Colors.White64, modifier = Modifier.size(16.dp) ) } From d53f2f91559c38b39805c4bb771501ca42bced13 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 18 Jun 2026 17:40:38 +0200 Subject: [PATCH 6/6] refactor: centralize hw icon labels --- .../bitkit/ui/components/HwWalletComponents.kt | 17 ++++++++++++++++- .../general/HardwareWalletsSettingsScreen.kt | 18 ------------------ 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt b/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt index 7c91b2c19d..75c268af21 100644 --- a/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt +++ b/app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.BlurredEdgeTreatment import androidx.compose.ui.draw.blur import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.R @@ -43,8 +44,22 @@ fun HwWalletConnectionIcon( transportType: TransportType, isConnected: Boolean, modifier: Modifier = Modifier, - contentDescription: String? = null, ) { + val contentDescription = stringResource( + id = when (transportType) { + TransportType.BLUETOOTH -> if (isConnected) { + R.string.hardware__connection_badge_connected_bluetooth + } else { + R.string.hardware__connection_badge_disconnected_bluetooth + } + TransportType.USB -> if (isConnected) { + R.string.hardware__connection_badge_connected_usb + } else { + R.string.hardware__connection_badge_disconnected_usb + } + } + ) + Icon( painter = painterResource( id = when (transportType) { diff --git a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt index 60cb9bf3f1..7aa93153e7 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt @@ -214,23 +214,6 @@ private fun HwConnectionBadge( isConnected: Boolean, modifier: Modifier = Modifier, ) { - val contentDescription = when (transportType) { - TransportType.BLUETOOTH -> { - if (isConnected) { - stringResource(R.string.hardware__connection_badge_connected_bluetooth) - } else { - stringResource(R.string.hardware__connection_badge_disconnected_bluetooth) - } - } - TransportType.USB -> { - if (isConnected) { - stringResource(R.string.hardware__connection_badge_connected_usb) - } else { - stringResource(R.string.hardware__connection_badge_disconnected_usb) - } - } - } - Box( contentAlignment = Alignment.Center, modifier = modifier @@ -241,7 +224,6 @@ private fun HwConnectionBadge( HwWalletConnectionIcon( transportType = transportType, isConnected = isConnected, - contentDescription = contentDescription, modifier = Modifier.size(16.dp) ) }