Skip to content

Commit 65db306

Browse files
authored
Merge pull request #1032 from synonymdev/feat/hw-wallet-settings
feat: hardware wallets settings
2 parents 7c960a9 + d53f2f9 commit 65db306

15 files changed

Lines changed: 568 additions & 90 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ Set up local env:
6060

6161
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.
6262

63+
### Trezor Bridge In Android Studio
64+
65+
When testing the Trezor Bridge emulator from bitkit-docker through Android Studio, add these gitignored local values to `local.properties`:
66+
67+
```properties
68+
TREZOR_BRIDGE=true
69+
TREZOR_BRIDGE_URL=http://10.0.2.2:21325
70+
```
71+
72+
CLI builds can still pass the same values as environment variables.
73+
6374
### Lint
6475

6576
This project uses detekt with default ktlint and compose-rules for android code linting.

app/build.gradle.kts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ val keystoreProperties by lazy {
4141
keystoreProperties
4242
}
4343

44+
val localProperties by lazy {
45+
Properties().apply {
46+
val localPropertiesFile = rootProject.file("local.properties")
47+
if (localPropertiesFile.exists()) {
48+
localPropertiesFile.inputStream().use { load(it) }
49+
}
50+
}
51+
}
52+
53+
fun localProp(key: String): String? {
54+
return System.getenv(key)
55+
?: providers.gradleProperty(key).orNull
56+
?: localProperties.getProperty(key)
57+
}
58+
4459
// Android resource qualifier format for androidResources.localeFilters
4560
val androidLocales = listOf(
4661
"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(
5166
)
5267
val e2eBackendEnv = System.getenv("E2E_BACKEND") ?: "local"
5368
val e2eHomegateUrlEnv = System.getenv("E2E_HOMEGATE_URL") ?: "http://127.0.0.1:6288"
54-
val trezorBridgeEnv = System.getenv("TREZOR_BRIDGE")?.toBoolean()?.toString() ?: "false"
55-
val trezorBridgeUrlEnv = System.getenv("TREZOR_BRIDGE_URL") ?: "http://10.0.2.2:21325"
69+
val trezorBridgeEnv = localProp("TREZOR_BRIDGE")?.toBoolean()?.toString() ?: "false"
70+
val trezorBridgeUrlEnv = localProp("TREZOR_BRIDGE_URL") ?: "http://10.0.2.2:21325"
5671
val requestedNdkVersion = System.getenv("NDK_VERSION")?.takeIf { it.isNotBlank() }
5772
val androidTestAnnotationPackage = "to.bitkit.test.annotations"
5873
val androidTestTaskPrefix = "connectedDevDebug"

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsIntroScreen
162162
import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsSettings
163163
import to.bitkit.ui.settings.backups.ResetAndRestoreScreen
164164
import to.bitkit.ui.settings.general.DefaultUnitSettingsScreen
165+
import to.bitkit.ui.settings.general.HardwareWalletsSettingsScreen
165166
import to.bitkit.ui.settings.general.LocalCurrencySettingsScreen
166167
import to.bitkit.ui.settings.general.TagsSettingsScreen
167168
import to.bitkit.ui.settings.general.WidgetsSettingsScreen
@@ -660,7 +661,7 @@ private fun RootNavHost(
660661
contacts(navController, settingsViewModel, appViewModel)
661662
profile(navController, settingsViewModel)
662663
shop(navController, settingsViewModel, appViewModel)
663-
generalSettingsSubScreens(navController, settingsViewModel)
664+
generalSettingsSubScreens(navController, appViewModel, settingsViewModel)
664665
advancedSettingsSubScreens(navController)
665666
transactionSpeedSettings(navController)
666667
pinManagement(navController)
@@ -1371,6 +1372,7 @@ private fun NavGraphBuilder.shop(
13711372

13721373
private fun NavGraphBuilder.generalSettingsSubScreens(
13731374
navController: NavHostController,
1375+
appViewModel: AppViewModel,
13741376
settingsViewModel: SettingsViewModel,
13751377
) {
13761378
composableWithDefaultTransitions<Routes.WidgetsSettings> {
@@ -1380,6 +1382,12 @@ private fun NavGraphBuilder.generalSettingsSubScreens(
13801382
composableWithDefaultTransitions<Routes.TagsSettings> {
13811383
TagsSettingsScreen(navController)
13821384
}
1385+
composableWithDefaultTransitions<Routes.HardwareWalletsSettings> {
1386+
HardwareWalletsSettingsScreen(
1387+
navController = navController,
1388+
onClickAdd = { appViewModel.showSheet(Sheet.Hardware()) },
1389+
)
1390+
}
13831391
composableWithDefaultTransitions<Routes.BackgroundPaymentsSettings> {
13841392
BackgroundPaymentsSettings(
13851393
onBack = { navController.popBackStack() },
@@ -1848,6 +1856,9 @@ sealed interface Routes {
18481856
@Serializable
18491857
data object TagsSettings : Routes
18501858

1859+
@Serializable
1860+
data object HardwareWalletsSettings : Routes
1861+
18511862
@Serializable
18521863
data object CoinSelectPreference : Routes
18531864

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package to.bitkit.ui.components
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.layout.BoxWithConstraints
5+
import androidx.compose.foundation.layout.BoxWithConstraintsScope
6+
import androidx.compose.foundation.layout.offset
7+
import androidx.compose.foundation.layout.size
8+
import androidx.compose.material3.Icon
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Alignment
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.draw.BlurredEdgeTreatment
13+
import androidx.compose.ui.draw.blur
14+
import androidx.compose.ui.res.painterResource
15+
import androidx.compose.ui.res.stringResource
16+
import androidx.compose.ui.unit.Dp
17+
import androidx.compose.ui.unit.dp
18+
import to.bitkit.R
19+
import to.bitkit.models.TransportType
20+
import to.bitkit.ui.theme.Colors
21+
22+
// Device illustration proportions, taken from the Figma hardware wallet frames.
23+
private const val HW_DEVICE_IMAGE_SIZE_RATIO = 256f / 375f
24+
private const val HW_DEVICE_TREZOR_BLEED_RATIO = 84f / 375f
25+
private const val HW_DEVICE_LEDGER_BLEED_RATIO = 53f / 375f
26+
private const val HW_DEVICE_STAGGER_RATIO = 12f / 375f
27+
28+
@Composable
29+
fun HwDeviceIllustrations(modifier: Modifier = Modifier) {
30+
BoxWithConstraints(modifier) {
31+
val imageSize = maxWidth * HW_DEVICE_IMAGE_SIZE_RATIO
32+
val staggerY = maxWidth * HW_DEVICE_STAGGER_RATIO
33+
TrezorImage(imageSize = imageSize, staggerY = staggerY)
34+
LedgerImage(
35+
imageSize = imageSize,
36+
staggerY = staggerY,
37+
modifier = Modifier.blur(16.dp, BlurredEdgeTreatment.Unbounded)
38+
)
39+
}
40+
}
41+
42+
@Composable
43+
fun HwWalletConnectionIcon(
44+
transportType: TransportType,
45+
isConnected: Boolean,
46+
modifier: Modifier = Modifier,
47+
) {
48+
val contentDescription = stringResource(
49+
id = when (transportType) {
50+
TransportType.BLUETOOTH -> if (isConnected) {
51+
R.string.hardware__connection_badge_connected_bluetooth
52+
} else {
53+
R.string.hardware__connection_badge_disconnected_bluetooth
54+
}
55+
TransportType.USB -> if (isConnected) {
56+
R.string.hardware__connection_badge_connected_usb
57+
} else {
58+
R.string.hardware__connection_badge_disconnected_usb
59+
}
60+
}
61+
)
62+
63+
Icon(
64+
painter = painterResource(
65+
id = when (transportType) {
66+
TransportType.BLUETOOTH -> R.drawable.ic_bluetooth_connected
67+
TransportType.USB -> R.drawable.ic_usb_connected
68+
}
69+
),
70+
contentDescription = contentDescription,
71+
tint = if (isConnected) Colors.Green else Colors.Gray1,
72+
modifier = modifier
73+
)
74+
}
75+
76+
@Composable
77+
private fun BoxWithConstraintsScope.TrezorImage(
78+
imageSize: Dp,
79+
staggerY: Dp,
80+
modifier: Modifier = Modifier,
81+
) {
82+
Image(
83+
painter = painterResource(R.drawable.trezor),
84+
contentDescription = null,
85+
modifier = modifier
86+
.size(imageSize)
87+
.align(Alignment.CenterStart)
88+
.offset(x = -maxWidth * HW_DEVICE_TREZOR_BLEED_RATIO, y = staggerY)
89+
)
90+
}
91+
92+
@Composable
93+
private fun BoxWithConstraintsScope.LedgerImage(
94+
imageSize: Dp,
95+
staggerY: Dp,
96+
modifier: Modifier = Modifier,
97+
) {
98+
Image(
99+
painter = painterResource(R.drawable.ledger),
100+
contentDescription = null,
101+
modifier = modifier
102+
.size(imageSize)
103+
.align(Alignment.CenterEnd)
104+
.offset(x = maxWidth * HW_DEVICE_LEDGER_BLEED_RATIO, y = -staggerY)
105+
)
106+
}

app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ fun HardwareWalletScreen(
7777
if (walletsLoaded && wallet == null) onBackClick()
7878
}
7979

80-
wallet?.let {
80+
wallet?.let { device ->
8181
HardwareWalletContent(
82-
wallet = it,
83-
showRemoveDialog = uiState.showRemoveDialog,
82+
wallet = device,
83+
showRemoveDialog = uiState.isPendingRemoval != null,
8484
onActivityItemClick = onActivityItemClick,
8585
onTransferToSpendingClick = onTransferToSpendingClick,
86-
onRemoveClick = viewModel::onRemoveClick,
86+
onRemoveClick = { viewModel.onRemoveClick(device) },
8787
onConfirmRemove = { viewModel.removeDevice(deviceId) },
8888
onDismissRemoveDialog = viewModel::onDismissRemoveDialog,
8989
onBackClick = onBackClick,

app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ import to.bitkit.ui.components.FillHeight
137137
import to.bitkit.ui.components.FillWidth
138138
import to.bitkit.ui.components.Headline24
139139
import to.bitkit.ui.components.HorizontalSpacer
140+
import to.bitkit.ui.components.HwWalletConnectionIcon
140141
import to.bitkit.ui.components.PubkyImage
141142
import to.bitkit.ui.components.Sheet
142143
import to.bitkit.ui.components.StatusBarSpacer
@@ -779,15 +780,9 @@ private fun RowScope.HwDeviceCell(
779780
.testTag("ActivityHardware")
780781
) {
781782
HorizontalSpacer(4.dp)
782-
Icon(
783-
painter = painterResource(
784-
id = when (wallet.transportType) {
785-
TransportType.BLUETOOTH -> R.drawable.ic_bluetooth_connected
786-
TransportType.USB -> R.drawable.ic_usb_connected
787-
}
788-
),
789-
contentDescription = null,
790-
tint = if (wallet.isConnected) Colors.Green else Colors.Gray1,
783+
HwWalletConnectionIcon(
784+
transportType = wallet.transportType,
785+
isConnected = wallet.isConnected,
791786
modifier = Modifier.size(16.dp)
792787
)
793788
}

app/src/main/java/to/bitkit/ui/screens/wallets/HwWalletViewModel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ class HwWalletViewModel @Inject constructor(
3131
private val _uiState = MutableStateFlow(HwWalletDetailUiState())
3232
val uiState: StateFlow<HwWalletDetailUiState> = _uiState.asStateFlow()
3333

34-
fun onRemoveClick() = _uiState.update { it.copy(showRemoveDialog = true) }
34+
fun onRemoveClick(wallet: HwWallet) = _uiState.update { it.copy(isPendingRemoval = wallet) }
3535

36-
fun onDismissRemoveDialog() = _uiState.update { it.copy(showRemoveDialog = false) }
36+
fun onDismissRemoveDialog() = _uiState.update { it.copy(isPendingRemoval = null) }
3737

3838
fun removeDevice(deviceId: String) {
3939
viewModelScope.launch {
40-
_uiState.update { it.copy(showRemoveDialog = false) }
40+
_uiState.update { it.copy(isPendingRemoval = null) }
4141
hwWalletRepo.removeDevice(deviceId).onFailure {
4242
ToastEventBus.send(
4343
type = Toast.ToastType.ERROR,
@@ -51,5 +51,5 @@ class HwWalletViewModel @Inject constructor(
5151

5252
@Immutable
5353
data class HwWalletDetailUiState(
54-
val showRemoveDialog: Boolean = false,
54+
val isPendingRemoval: HwWallet? = null,
5555
)

app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import to.bitkit.ui.scaffold.AppTopBar
5757
import to.bitkit.ui.scaffold.DrawerNavIcon
5858
import to.bitkit.ui.scaffold.PinnedTabsScaffold
5959
import to.bitkit.ui.scaffold.ScreenColumn
60+
import to.bitkit.ui.screens.wallets.HwWalletViewModel
6061
import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing
6162
import to.bitkit.ui.screens.wallets.activity.components.TabItem
6263
import to.bitkit.ui.settingsViewModel
@@ -79,6 +80,7 @@ fun SettingsScreen(
7980
navController: NavController,
8081
advancedViewModel: AdvancedSettingsViewModel = hiltViewModel(),
8182
languageViewModel: LanguageViewModel = hiltViewModel(),
83+
hwWalletViewModel: HwWalletViewModel = hiltViewModel(),
8284
) {
8385
val app = appViewModel ?: return
8486
val settings = settingsViewModel ?: return
@@ -94,6 +96,7 @@ fun SettingsScreen(
9496
val notificationsGranted by settings.notificationsGranted.collectAsStateWithLifecycle()
9597
val isPubkyAuthenticated by settings.isPubkyAuthenticated.collectAsStateWithLifecycle()
9698
val isPaykitEnabled by settings.isPaykitEnabled.collectAsStateWithLifecycle()
99+
val hardwareWallets by hwWalletViewModel.wallets.collectAsStateWithLifecycle()
97100
val languageUiState by languageViewModel.uiState.collectAsStateWithLifecycle()
98101

99102
// Security tab state
@@ -130,6 +133,7 @@ fun SettingsScreen(
130133
notificationsGranted = notificationsGranted,
131134
isPubkyAuthenticated = isPubkyAuthenticated,
132135
isPaykitEnabled = isPaykitEnabled,
136+
hardwareWalletCount = hardwareWallets.size,
133137
),
134138
securityState = SecurityTabState(
135139
isPinEnabled = isPinEnabled,
@@ -166,6 +170,7 @@ fun SettingsScreen(
166170
navController.navigateTo(Routes.BackgroundPaymentsIntro)
167171
}
168172
}
173+
SettingsEvent.HardwareWalletsClick -> navController.navigateTo(Routes.HardwareWalletsSettings)
169174
SettingsEvent.BackupWalletClick -> app.showSheet(Sheet.Backup())
170175
SettingsEvent.DataBackupsClick -> navController.navigateTo(Routes.BackupSettings)
171176
SettingsEvent.ResetWalletClick ->
@@ -365,6 +370,13 @@ private fun GeneralTabContent(
365370
onClick = { onEvent(SettingsEvent.BgPaymentsClick) },
366371
modifier = Modifier.testTag("BackgroundPaymentSettings")
367372
)
373+
SettingsButtonRow(
374+
title = stringResource(R.string.settings__hardware_wallets__nav_title),
375+
icon = { SettingsIcon(R.drawable.ic_device_mobile_speaker) },
376+
value = SettingsButtonValue.StringValue(state.hardwareWalletCount.toString()),
377+
onClick = { onEvent(SettingsEvent.HardwareWalletsClick) },
378+
modifier = Modifier.testTag("HardwareWalletsSettings")
379+
)
368380

369381
VerticalSpacer(32.dp)
370382
}
@@ -647,6 +659,7 @@ sealed interface SettingsEvent {
647659
data object PaymentPreferenceClick : SettingsEvent
648660
data object QuickPayClick : SettingsEvent
649661
data object BgPaymentsClick : SettingsEvent
662+
data object HardwareWalletsClick : SettingsEvent
650663

651664
// Security
652665
data object BackupWalletClick : SettingsEvent
@@ -689,6 +702,7 @@ data class GeneralTabState(
689702
val notificationsGranted: Boolean = false,
690703
val isPubkyAuthenticated: Boolean = false,
691704
val isPaykitEnabled: Boolean = false,
705+
val hardwareWalletCount: Int = 0,
692706
)
693707

694708
@Immutable

0 commit comments

Comments
 (0)