diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8f8e41c..0557c36 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,11 @@ name: Build # Verifies that the app compiles. -on: [workflow_dispatch, push, pull_request] +on: + push: + branches: [master] + pull_request: + workflow_dispatch: permissions: {} diff --git a/.github/workflows/format-check.yaml b/.github/workflows/format-check.yaml index e297ed3..b7b1368 100644 --- a/.github/workflows/format-check.yaml +++ b/.github/workflows/format-check.yaml @@ -1,6 +1,12 @@ name: Format Check -on: [pull_request, workflow_dispatch] +# Runs the ktfmt formatter and ensures the source files do not need formatting. + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: jobs: format_check: diff --git a/justfile b/justfile index ceaa9ea..da87ac9 100644 --- a/justfile +++ b/justfile @@ -28,3 +28,17 @@ format: exit 1 fi ktfmt --kotlinlang-style --enable-editorconfig src/ + +# Check formatting using ktfmt 0.64 without modifying files +format-check: + #!/usr/bin/env bash + set -euo pipefail + version=$(ktfmt --version | grep -oE '[0-9]+\.[0-9]+') + if [ "$version" != "0.64" ]; then + echo "Error: ktfmt 0.64 is required, but found $version" >&2 + exit 1 + fi + if ! ktfmt --kotlinlang-style --enable-editorconfig --dry-run --set-exit-if-changed src/ > /dev/null; then + echo "Error: formatting issues found. Run 'just format' to fix them." >&2 + exit 1 + fi diff --git a/src/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt b/src/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt index 8c3af68..827877f 100644 --- a/src/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt +++ b/src/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt @@ -138,6 +138,7 @@ fun AppNavigation( WalletHomeScreen( state = walletViewModel!!.state, onAction = walletViewModel::onAction, + snackbarMessages = walletViewModel.snackbarMessages, navController = navController, ) } diff --git a/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt b/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt index 2bf8f68..a205a23 100644 --- a/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt +++ b/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt @@ -53,10 +53,10 @@ fun OnboardingScreen(onFinishOnboarding: () -> Unit) { val messages = listOf( - "Easter egg #1: \uD83E\uDD5A", + "\uD83E\uDD5A\nEaster egg #1", "Welcome to the Devkit Wallet! This app is a playground for developers and bitcoin enthusiasts to experiment with bitcoin's test networks.", "It is developed with the Bitcoin Dev Kit, a powerful set of libraries produced and maintained by the Bitcoin Dev Kit Foundation.\n\nThis version of the app is using Compact Block Filters to sync its wallets.", - "The Foundation maintains this app as a way to showcase the capabilities of the Bitcoin Dev Kit and to provide a starting point for developers to build their own apps.\n\nIt is not a production application, and only works for testnet3, testnet4, signet, and regtest. Have fun!", + "The Foundation maintains this app as a way to showcase the capabilities of the Bitcoin Dev Kit and to provide a starting point for developers to build their own apps.\n\nIt is not a production application and only works for the Testnet3, Testnet4, Signet, and Regtest networks. Have fun!", ) Column( @@ -67,38 +67,25 @@ fun OnboardingScreen(onFinishOnboarding: () -> Unit) { // Logo Box( - modifier = - Modifier.size(100.dp) - .border( - width = 2.dp, - color = accent.copy(alpha = 0.20f), - shape = CircleShape, - ), + modifier = Modifier.size(200.dp), contentAlignment = Alignment.Center, ) { Image( painter = painterResource(id = R.drawable.bdk_logo), contentDescription = "Bitcoin Dev Kit logo", - modifier = Modifier.size(56.dp), + modifier = Modifier.size(120.dp), ) } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(32.dp)) Text( text = "Devkit Wallet", fontFamily = inter, - fontSize = 24.sp, + fontSize = 28.sp, fontWeight = FontWeight.Light, color = onSurface, ) - Text( - text = "BITCOIN DEVELOPMENT KIT", - fontFamily = inter, - fontSize = 11.sp, - color = subtle, - letterSpacing = 1.5.sp, - ) Spacer(Modifier.height(48.dp)) @@ -110,7 +97,7 @@ fun OnboardingScreen(onFinishOnboarding: () -> Unit) { Text( text = messages[page], fontFamily = inter, - fontSize = 15.sp, + fontSize = 16.sp, lineHeight = 24.sp, color = onSurface.copy(alpha = 0.85f), textAlign = TextAlign.Center, diff --git a/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt b/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt index faba035..b1aeb2f 100644 --- a/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt +++ b/src/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -60,8 +59,7 @@ import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Monitor import com.composables.icons.lucide.Settings import com.composables.icons.lucide.Shield -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit import org.bitcoindevkit.devkitwallet.domain.utils.formatInBtc import org.bitcoindevkit.devkitwallet.presentation.navigation.BlockchainClientScreen @@ -83,18 +81,24 @@ private const val TAG = "WalletHomeScreen" internal fun WalletHomeScreen( state: WalletScreenState, onAction: (WalletScreenAction) -> Unit, + snackbarMessages: Flow, navController: NavHostController, ) { val snackbarHostState = remember { SnackbarHostState() } val networkAvailable: Boolean = isOnline(LocalContext.current) val interactionSource = remember { MutableInteractionSource() } - val scope: CoroutineScope = rememberCoroutineScope() val colorScheme = MaterialTheme.colorScheme LaunchedEffect(Unit) { onAction(WalletScreenAction.UpdateBalance) } + LaunchedEffect(Unit) { + snackbarMessages.collect { message -> + snackbarHostState.showSnackbar(message) + } + } + Scaffold( topBar = { WalletAppBar(onSettingsClick = { navController.navigate(SettingsScreen) }) }, snackbarHost = { @@ -103,18 +107,6 @@ internal fun WalletHomeScreen( } }, ) { paddingValues -> - - // If a new snackbar has be triggered, show it - state.snackbarMessage?.let { message -> - Log.i("WalletHomeScreen", "Showing snackbar: $message") - LaunchedEffect(message) { - scope.launch { - snackbarHostState.showSnackbar(message) - onAction(WalletScreenAction.ClearSnackbar) - } - } - } - Column( modifier = Modifier.fillMaxSize() diff --git a/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt b/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt index 01dd42e..6532dcd 100644 --- a/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt +++ b/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt @@ -13,6 +13,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import org.bitcoindevkit.devkitwallet.data.Kyoto import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit @@ -31,24 +35,18 @@ internal class WalletViewModel(private val wallet: Wallet) : ViewModel() { private val kyotoCoroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) private var kyoto: Kyoto? = null + private val snackbarChannel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val snackbarMessages: Flow = snackbarChannel.receiveAsFlow() + fun onAction(action: WalletScreenAction) { when (action) { WalletScreenAction.SwitchUnit -> switchUnit() WalletScreenAction.UpdateBalance -> updateBalance() WalletScreenAction.ActivateCbfNode -> activateKyoto() WalletScreenAction.StopKyotoNode -> stopKyotoNode() - WalletScreenAction.ClearSnackbar -> clearSnackbar() } } - private fun showSnackbar(message: String) { - state = state.copy(snackbarMessage = message) - } - - private fun clearSnackbar() { - state = state.copy(snackbarMessage = null) - } - private fun switchUnit() { state = when (state.unit) { @@ -78,11 +76,19 @@ internal class WalletViewModel(private val wallet: Wallet) : ViewModel() { this.kyoto = Kyoto.create(wallet.wallet, dataDir, wallet.network) val updatesFlow = kyoto!!.start() kyotoCoroutineScope.launch { + var previousHeight: UInt = wallet.bestBlock() + updatesFlow.collect { Log.i(TAG, "Collecting a flow update") wallet.applyUpdate(it) updateBalance() updateBestBlock() + + val newHeight = state.bestBlockHeight + if (newHeight > previousHeight) { + snackbarChannel.send("New block: $newHeight") + } + previousHeight = newHeight } } kyoto!!.logToLogcat() diff --git a/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt b/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt index d2726a6..b1e6059 100644 --- a/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt +++ b/src/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt @@ -11,7 +11,6 @@ data class WalletScreenState( val balance: ULong = 0u, val unit: CurrencyUnit = CurrencyUnit.Bitcoin, val bestBlockHeight: UInt = 0u, - val snackbarMessage: String? = null, val kyotoNodeStatus: CbfNodeStatus = CbfNodeStatus.Stopped, ) @@ -23,8 +22,6 @@ sealed interface WalletScreenAction { data object ActivateCbfNode : WalletScreenAction data object StopKyotoNode : WalletScreenAction - - data object ClearSnackbar : WalletScreenAction } enum class CbfNodeStatus {