Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 17 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1371,6 +1372,7 @@ private fun NavGraphBuilder.shop(

private fun NavGraphBuilder.generalSettingsSubScreens(
navController: NavHostController,
appViewModel: AppViewModel,
settingsViewModel: SettingsViewModel,
) {
composableWithDefaultTransitions<Routes.WidgetsSettings> {
Expand All @@ -1380,6 +1382,12 @@ private fun NavGraphBuilder.generalSettingsSubScreens(
composableWithDefaultTransitions<Routes.TagsSettings> {
TagsSettingsScreen(navController)
}
composableWithDefaultTransitions<Routes.HardwareWalletsSettings> {
HardwareWalletsSettingsScreen(
navController = navController,
onClickAdd = { appViewModel.showSheet(Sheet.Hardware()) },
)
}
composableWithDefaultTransitions<Routes.BackgroundPaymentsSettings> {
BackgroundPaymentsSettings(
onBack = { navController.popBackStack() },
Expand Down Expand Up @@ -1848,6 +1856,9 @@ sealed interface Routes {
@Serializable
data object TagsSettings : Routes

@Serializable
data object HardwareWalletsSettings : Routes

@Serializable
data object CoinSelectPreference : Routes

Expand Down
106 changes: 106 additions & 0 deletions app/src/main/java/to/bitkit/ui/components/HwWalletComponents.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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.material3.Icon
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.res.stringResource
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
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
fun HwWalletConnectionIcon(
transportType: TransportType,
isConnected: Boolean,
modifier: Modifier = Modifier,
) {
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) {
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,
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)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 4 additions & 9 deletions app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ class HwWalletViewModel @Inject constructor(
private val _uiState = MutableStateFlow(HwWalletDetailUiState())
val uiState: StateFlow<HwWalletDetailUiState> = _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,
Expand All @@ -51,5 +51,5 @@ class HwWalletViewModel @Inject constructor(

@Immutable
data class HwWalletDetailUiState(
val showRemoveDialog: Boolean = false,
val isPendingRemoval: HwWallet? = null,
)
14 changes: 14 additions & 0 deletions app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -130,6 +133,7 @@ fun SettingsScreen(
notificationsGranted = notificationsGranted,
isPubkyAuthenticated = isPubkyAuthenticated,
isPaykitEnabled = isPaykitEnabled,
hardwareWalletCount = hardwareWallets.size,
),
securityState = SecurityTabState(
isPinEnabled = isPinEnabled,
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -689,6 +702,7 @@ data class GeneralTabState(
val notificationsGranted: Boolean = false,
val isPubkyAuthenticated: Boolean = false,
val isPaykitEnabled: Boolean = false,
val hardwareWalletCount: Int = 0,
)

@Immutable
Expand Down
Loading
Loading