diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index e9cb290b4..01bf6c30b 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -6,18 +6,20 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.HttpTimeoutConfig import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest -import io.ktor.client.plugins.logging.ANDROID import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.logging.LoggingConfig import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json +import to.bitkit.utils.Logger import javax.inject.Qualifier import javax.inject.Singleton +import io.ktor.client.plugins.logging.Logger as KtorLogger @Qualifier @Retention(AnnotationRetention.BINARY) @@ -26,18 +28,16 @@ annotation class ProtoClient @Module @InstallIn(SingletonComponent::class) object HttpModule { + @Provides @Singleton fun provideHttpClient(json: Json): HttpClient { return HttpClient { install(HttpTimeout) { - requestTimeoutMillis = 60_000 - connectTimeoutMillis = 30_000 - socketTimeoutMillis = 30_000 + this@install.defaultTimeoutConfig() } install(Logging) { - logger = Logger.ANDROID - level = LogLevel.INFO + this@install.defaultLoggingConfig() } install(ContentNegotiation) { json(json = json) @@ -54,14 +54,29 @@ object HttpModule { fun provideProtoHttpClient(): HttpClient { return HttpClient { install(HttpTimeout) { - requestTimeoutMillis = 60_000 - connectTimeoutMillis = 30_000 - socketTimeoutMillis = 30_000 + this@install.defaultTimeoutConfig() } install(Logging) { - logger = Logger.ANDROID - level = LogLevel.INFO + this@install.defaultLoggingConfig() } } } + + private fun HttpTimeoutConfig.defaultTimeoutConfig() { + requestTimeoutMillis = 60_000 + connectTimeoutMillis = 30_000 + socketTimeoutMillis = 30_000 + } + + private fun LoggingConfig.defaultLoggingConfig() { + logger = KtorLogger.APP + level = LogLevel.NONE + } } + +private val KtorLogger.Companion.APP + get() = object : KtorLogger { + override fun log(message: String) { + Logger.debug(message) + } + } diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index f594c5429..dfee9aab6 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -12,6 +12,7 @@ import android.content.ContextWrapper import android.content.pm.PackageManager.PERMISSION_GRANTED import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import to.bitkit.R import to.bitkit.utils.Logger import java.io.File import java.io.FileOutputStream @@ -63,7 +64,7 @@ fun Context.findActivity(): Activity? = } // Clipboard -fun Context.setClipboardText(text: String, label: String = "") { +fun Context.setClipboardText(text: String, label: String = getString(R.string.app_name)) { this.clipboardManager.setPrimaryClip( ClipData.newPlainText(label, text) ) diff --git a/app/src/main/java/to/bitkit/models/LnPeer.kt b/app/src/main/java/to/bitkit/models/LnPeer.kt index e4fe50a5c..1c523a732 100644 --- a/app/src/main/java/to/bitkit/models/LnPeer.kt +++ b/app/src/main/java/to/bitkit/models/LnPeer.kt @@ -26,15 +26,15 @@ data class LnPeer( address = address, ) - fun parseUri(string: String): Result { - val uri = string.split("@") - val nodeId = uri[0] + fun parseUri(uriString: String): Result { + val uriComponents = uriString.split("@") + val nodeId = uriComponents[0] - if (uri.size != 2) { + if (uriComponents.size != 2) { return Result.failure(Exception("Invalid peer uri")) } - val address = uri[1].split(":") + val address = uriComponents[1].split(":") if (address.size < 2) { return Result.failure(Exception("Invalid peer uri")) diff --git a/app/src/main/java/to/bitkit/services/BlocktankNotificationsService.kt b/app/src/main/java/to/bitkit/services/BlocktankNotificationsService.kt index 4a4b57b34..f4c80ec3b 100644 --- a/app/src/main/java/to/bitkit/services/BlocktankNotificationsService.kt +++ b/app/src/main/java/to/bitkit/services/BlocktankNotificationsService.kt @@ -1,10 +1,8 @@ package to.bitkit.services import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import to.bitkit.async.ServiceQueue -import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.data.keychain.Keychain.Key import to.bitkit.di.BgDispatcher @@ -24,7 +22,6 @@ class BlocktankNotificationsService @Inject constructor( private val lightningService: LightningService, private val keychain: Keychain, private val crypto: Crypto, - private val settingsStore: SettingsStore, ) { suspend fun registerDevice(deviceToken: String) = withContext(bgDispatcher) { diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index ec66ea53b..68336d992 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -207,7 +207,7 @@ fun ContentView( LaunchedEffect(appViewModel) { appViewModel.mainScreenEffect.collect { when (it) { - is MainScreenEffect.NavigateActivityDetail -> navController.navigate(Routes.ActivityDetail(it.activityId)) + is MainScreenEffect.Navigate -> navController.navigate(it.route) is MainScreenEffect.ProcessClipboardAutoRead -> { val isOnHome = navController.currentDestination?.hasRoute() == true if (!isOnHome) { @@ -436,7 +436,7 @@ private fun RootNavHost( navigation( startDestination = Routes.TransferIntro, ) { - composable { + composableWithDefaultTransitions { TransferIntroScreen( onContinueClick = { navController.navigateToTransferFunding() @@ -445,7 +445,7 @@ private fun RootNavHost( onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { SavingsIntroScreen( onContinueClick = { navController.navigate(Routes.SavingsAvailability) @@ -455,14 +455,14 @@ private fun RootNavHost( onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { SavingsAvailabilityScreen( onBackClick = { navController.popBackStack() }, onCancelClick = { navController.navigateToHome() }, onContinueClick = { navController.navigate(Routes.SavingsConfirm) }, ) } - composable { + composableWithDefaultTransitions { SavingsConfirmScreen( onConfirm = { navController.navigate(Routes.SavingsProgress) }, onAdvancedClick = { navController.navigate(Routes.SavingsAdvanced) }, @@ -470,20 +470,20 @@ private fun RootNavHost( onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { SavingsAdvancedScreen( onContinueClick = { navController.popBackStack(inclusive = false) }, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { SavingsProgressScreen( onContinueClick = { navController.popBackStack(inclusive = true) }, onCloseClick = { navController.popBackStack(inclusive = true) }, ) } - composable { + composableWithDefaultTransitions { SpendingIntroScreen( onContinueClick = { navController.navigate(Routes.SpendingAmount) @@ -493,7 +493,7 @@ private fun RootNavHost( onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { SpendingAmountScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, @@ -501,7 +501,7 @@ private fun RootNavHost( onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, ) } - composable { + composableWithDefaultTransitions { SpendingConfirmScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, @@ -511,7 +511,7 @@ private fun RootNavHost( onConfirm = { navController.navigate(Routes.SettingUp) }, ) } - composable { + composableWithDefaultTransitions { SpendingAdvancedScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, @@ -519,21 +519,21 @@ private fun RootNavHost( onOrderCreated = { navController.popBackStack(inclusive = false) }, ) } - composable { + composableWithDefaultTransitions { LiquidityScreen( onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onContinueClick = { navController.popBackStack() } ) } - composable { + composableWithDefaultTransitions { SettingUpScreen( viewModel = transferViewModel, onCloseClick = { navController.popBackStack(inclusive = true) }, onContinueClick = { navController.popBackStack(inclusive = true) }, ) } - composable { + composableWithDefaultTransitions { val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsState() FundingScreen( onTransfer = { @@ -553,32 +553,36 @@ private fun RootNavHost( }, onAdvanced = { navController.navigate(Routes.FundingAdvanced) }, onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateUp() }, + onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { FundingAdvancedScreen( - onLnUrl = { navController.navigateToQrScanner() }, + onLnUrl = { navController.navigateToScanner() }, onManual = { navController.navigate(Routes.ExternalNav) }, onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, + onCloseClick = { navController.navigateToHome() }, ) } navigation( - startDestination = Routes.ExternalConnection, + startDestination = Routes.ExternalConnection(), ) { - composable { + composableWithDefaultTransitions { val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } + val route = it.toRoute() val viewModel = hiltViewModel(parentEntry) ExternalConnectionScreen( + route = route, + savedStateHandle = it.savedStateHandle, viewModel = viewModel, onNodeConnected = { navController.navigate(Routes.ExternalAmount) }, + onScanClick = { navController.navigateToScanner(isCalledForResult = true) }, onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, + onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } val viewModel = hiltViewModel(parentEntry) @@ -586,10 +590,10 @@ private fun RootNavHost( viewModel = viewModel, onContinue = { navController.navigate(Routes.ExternalConfirm) }, onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, + onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } val viewModel = hiltViewModel(parentEntry) @@ -601,16 +605,16 @@ private fun RootNavHost( }, onNetworkFeeClick = { navController.navigate(Routes.ExternalFeeCustom) }, onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, + onCloseClick = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { ExternalSuccessScreen( onContinue = { navController.popBackStack(inclusive = true) }, - onClose = { navController.popBackStack(inclusive = true) }, + onClose = { navController.navigateToHome() }, ) } - composable { + composableWithDefaultTransitions { ExternalFeeCustomScreen( onBackClick = { navController.popBackStack() }, onCloseClick = { navController.popBackStack(inclusive = true) }, @@ -951,7 +955,6 @@ private fun NavGraphBuilder.qrScanner( exitTransition = { screenSlideOut }, ) { QrScanningScreen(navController = navController) { qrCode -> - navController.popBackStack() appViewModel.onScanSuccess( data = qrCode, onResultDelay = 650 // slight delay for nav transition before showing send sheet @@ -1324,7 +1327,7 @@ fun NavController.navigateToActivityExplore(id: String) = navigate( route = Routes.ActivityExplore(id), ) -fun NavController.navigateToQrScanner(isCalledForResult: Boolean = false) { +fun NavController.navigateToScanner(isCalledForResult: Boolean = false) { if (isCalledForResult) { currentBackStackEntry?.savedStateHandle?.set(SCAN_REQUEST_KEY, true) } @@ -1364,66 +1367,66 @@ fun NavController.navigateToAboutSettings() = navigate( ) // endregion -object Routes { +sealed interface Routes { @Serializable - data object Home + data object Home : Routes @Serializable - data object Settings + data object Settings : Routes @Serializable - data object NodeInfo + data object NodeInfo : Routes @Serializable - data object GeneralSettings + data object GeneralSettings : Routes @Serializable - data object TransactionSpeedSettings + data object TransactionSpeedSettings : Routes @Serializable - data object WidgetsSettings + data object WidgetsSettings : Routes @Serializable - data object TagsSettings + data object TagsSettings : Routes @Serializable - data object AdvancedSettings + data object AdvancedSettings : Routes @Serializable - data object CoinSelectPreference + data object CoinSelectPreference : Routes @Serializable - data object ElectrumConfig + data object ElectrumConfig : Routes @Serializable - data object RgsServer + data object RgsServer : Routes @Serializable - data object AddressViewer + data object AddressViewer : Routes @Serializable - data object AboutSettings + data object AboutSettings : Routes @Serializable - data object CustomFeeSettings + data object CustomFeeSettings : Routes @Serializable - data object SecuritySettings + data object SecuritySettings : Routes @Serializable - data object DisablePin + data object DisablePin : Routes @Serializable - data object ChangePin + data object ChangePin : Routes @Serializable - data object ChangePinNew + data object ChangePinNew : Routes @Serializable - data class ChangePinConfirm(val newPin: String) + data class ChangePinConfirm(val newPin: String) : Routes @Serializable - data object ChangePinResult + data object ChangePinResult : Routes @Serializable data class AuthCheck( @@ -1431,209 +1434,209 @@ object Routes { val requirePin: Boolean = false, val requireBiometrics: Boolean = false, val onSuccessActionId: String, - ) + ) : Routes @Serializable - data object DefaultUnitSettings + data object DefaultUnitSettings : Routes @Serializable - data object LocalCurrencySettings + data object LocalCurrencySettings : Routes @Serializable - data object BackupSettings + data object BackupSettings : Routes @Serializable - data object ResetAndRestoreSettings + data object ResetAndRestoreSettings : Routes @Serializable - data object ChannelOrdersSettings + data object ChannelOrdersSettings : Routes @Serializable - data object Logs + data object Logs : Routes @Serializable - data class LogDetail(val fileName: String) + data class LogDetail(val fileName: String) : Routes @Serializable - data class OrderDetail(val id: String) + data class OrderDetail(val id: String) : Routes @Serializable - data class CjitDetail(val id: String) + data class CjitDetail(val id: String) : Routes @Serializable - data object ConnectionsNav + data object ConnectionsNav : Routes @Serializable - data object LightningConnections + data object LightningConnections : Routes @Serializable - data object ChannelDetail + data object ChannelDetail : Routes @Serializable - data object CloseConnection + data object CloseConnection : Routes @Serializable - data object DevSettings + data object DevSettings : Routes @Serializable - data object RegtestSettings + data object RegtestSettings : Routes @Serializable - data object TransferRoot + data object TransferRoot : Routes @Serializable - data object TransferIntro + data object TransferIntro : Routes @Serializable - data object SpendingIntro + data object SpendingIntro : Routes @Serializable - data object SpendingAmount + data object SpendingAmount : Routes @Serializable - data object SpendingConfirm + data object SpendingConfirm : Routes @Serializable - data object SpendingAdvanced + data object SpendingAdvanced : Routes @Serializable - data object TransferLiquidity + data object TransferLiquidity : Routes @Serializable - data object SettingUp + data object SettingUp : Routes @Serializable - data object SavingsIntro + data object SavingsIntro : Routes @Serializable - data object SavingsAvailability + data object SavingsAvailability : Routes @Serializable - data object SavingsConfirm + data object SavingsConfirm : Routes @Serializable - data object SavingsAdvanced + data object SavingsAdvanced : Routes @Serializable - data object SavingsProgress + data object SavingsProgress : Routes @Serializable - data object Funding + data object Funding : Routes @Serializable - data object FundingAdvanced + data object FundingAdvanced : Routes @Serializable - data object ExternalNav + data object ExternalNav : Routes @Serializable - data object ExternalConnection + data class ExternalConnection(val scannedNodeUri: String? = null) : Routes @Serializable - data object ExternalAmount + data object ExternalAmount : Routes @Serializable - data object ExternalConfirm + data object ExternalConfirm : Routes @Serializable - data object ExternalSuccess + data object ExternalSuccess : Routes @Serializable - data object ExternalFeeCustom + data object ExternalFeeCustom : Routes @Serializable - data class ActivityDetail(val id: String) + data class ActivityDetail(val id: String) : Routes @Serializable - data class ActivityExplore(val id: String) + data class ActivityExplore(val id: String) : Routes @Serializable - data object QrScanner + data object QrScanner : Routes @Serializable - data object BuyIntro + data object BuyIntro : Routes @Serializable - data object Support + data object Support : Routes @Serializable - data object ReportIssue + data object ReportIssue : Routes @Serializable - data object ReportIssueSuccess + data object ReportIssueSuccess : Routes @Serializable - data object ReportIssueFailure + data object ReportIssueFailure : Routes @Serializable - data object QuickPayIntro + data object QuickPayIntro : Routes @Serializable - data object QuickPaySettings + data object QuickPaySettings : Routes @Serializable - data object ProfileIntro + data object ProfileIntro : Routes @Serializable - data object CreateProfile + data object CreateProfile : Routes @Serializable - data object ShopIntro + data object ShopIntro : Routes @Serializable - data object ShopDiscover + data object ShopDiscover : Routes @Serializable - data object WidgetsIntro + data object WidgetsIntro : Routes @Serializable - data object AddWidget + data object AddWidget : Routes @Serializable - data object Headlines + data object Headlines : Routes @Serializable - data object HeadlinesPreview + data object HeadlinesPreview : Routes @Serializable - data object HeadlinesEdit + data object HeadlinesEdit : Routes @Serializable - data object Facts + data object Facts : Routes @Serializable - data object FactsPreview + data object FactsPreview : Routes @Serializable - data object FactsEdit + data object FactsEdit : Routes @Serializable - data object Blocks + data object Blocks : Routes @Serializable - data object BlocksPreview + data object BlocksPreview : Routes @Serializable - data object BlocksEdit + data object BlocksEdit : Routes @Serializable - data object Weather + data object Weather : Routes @Serializable - data object WeatherPreview + data object WeatherPreview : Routes @Serializable - data object WeatherEdit + data object WeatherEdit : Routes @Serializable - data object Price + data object Price : Routes @Serializable - data object PricePreview + data object PricePreview : Routes @Serializable - data object PriceEdit + data object PriceEdit : Routes @Serializable - data object CalculatorPreview + data object CalculatorPreview : Routes } diff --git a/app/src/main/java/to/bitkit/ui/components/OutlinedColorButton.kt b/app/src/main/java/to/bitkit/ui/components/OutlinedColorButton.kt index 33715e7d6..166ee3d30 100644 --- a/app/src/main/java/to/bitkit/ui/components/OutlinedColorButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/OutlinedColorButton.kt @@ -35,7 +35,7 @@ fun OutlinedColorButton( disabledContentColor = color, ), enabled = enabled, - shape = AppShapes.smallButton, + shape = AppShapes.small, contentPadding = PaddingValues(8.dp, 4.dp), border = BorderStroke(1.dp, color), ) { diff --git a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt index 2d7d04cbe..66786b6e4 100644 --- a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt @@ -40,7 +40,7 @@ fun RectangleButton( containerColor = Colors.White10, ), enabled = enabled, - shape = AppShapes.smallButton, + shape = AppShapes.small, contentPadding = PaddingValues(24.dp), modifier = modifier .alpha(if (enabled) 1f else 0.5f) diff --git a/app/src/main/java/to/bitkit/ui/components/Text.kt b/app/src/main/java/to/bitkit/ui/components/Text.kt index 52056ac73..f1e7bfc54 100644 --- a/app/src/main/java/to/bitkit/ui/components/Text.kt +++ b/app/src/main/java/to/bitkit/ui/components/Text.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.InterFontFamily @@ -201,14 +202,8 @@ fun BodyMSB( ) { Text( text = text, - style = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 17.sp, - lineHeight = 22.sp, - letterSpacing = 0.4.sp, - fontFamily = InterFontFamily, + style = AppTextStyles.BodyMSB.merge( color = color, - textAlign = TextAlign.Start, ), maxLines = maxLines, overflow = overflow, @@ -305,14 +300,8 @@ fun BodySSB( ) { Text( text = text, - style = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 15.sp, - lineHeight = 20.sp, - letterSpacing = 0.4.sp, - fontFamily = InterFontFamily, + style = AppTextStyles.BodySSB.merge( color = color, - textAlign = TextAlign.Start, ), modifier = modifier, maxLines = maxLines, diff --git a/app/src/main/java/to/bitkit/ui/components/TextInput.kt b/app/src/main/java/to/bitkit/ui/components/TextInput.kt index d3a0fadef..c4ff4e96c 100644 --- a/app/src/main/java/to/bitkit/ui/components/TextInput.kt +++ b/app/src/main/java/to/bitkit/ui/components/TextInput.kt @@ -1,19 +1,21 @@ package to.bitkit.ui.components +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppTextFieldDefaults +import to.bitkit.ui.theme.AppTextStyles +import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.theme.InterFontFamily @Composable fun TextInput( @@ -36,21 +38,14 @@ fun TextInput( TextField( placeholder = { if (!placeholder.isNullOrEmpty()) { - BodyMSB( + BodySSB( placeholder, - color = Colors.White64 + color = Colors.White64, ) } else null }, isError = isError, - textStyle = LocalTextStyle.current.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 17.sp, - lineHeight = 22.sp, - letterSpacing = 0.4.sp, - fontFamily = InterFontFamily, - textAlign = TextAlign.Start, - ), + textStyle = AppTextStyles.BodySSB, value = value, onValueChange = onValueChange, maxLines = maxLines, @@ -65,6 +60,38 @@ fun TextInput( suffix = suffix, supportingText = supportingText, visualTransformation = visualTransformation, - modifier = modifier + modifier = modifier, ) } + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + ScreenColumn( + modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp) + ) { + TextInput( + value = "Input text value", + onValueChange = {}, + modifier = Modifier.fillMaxWidth(), + ) + + VerticalSpacer(12.dp) + TextInput( + value = "", + onValueChange = {}, + placeholder = "Placeholder text", + modifier = Modifier.fillMaxWidth(), + ) + + VerticalSpacer(12.dp) + TextInput( + value = "Error text", + onValueChange = {}, + isError = true, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index e9ddd9bb3..ca037014c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -17,7 +17,6 @@ import androidx.camera.core.ImageAnalysis import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView -import androidx.compose.animation.core.AnimationConstants import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -71,7 +70,6 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors import to.bitkit.utils.Logger import java.util.concurrent.Executors -import kotlin.time.Duration.Companion.milliseconds const val SCAN_REQUEST_KEY = "SCAN_REQUEST" const val SCAN_RESULT_KEY = "SCAN_RESULT" @@ -99,6 +97,7 @@ fun QrScanningScreen( navController.popBackStack() backStackEntry.savedStateHandle.remove(SCAN_REQUEST_KEY) } else { + navController.popBackStack() onScanSuccess(qrCode) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt index 36546083e..9682edaf6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt @@ -14,8 +14,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -24,7 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -32,20 +30,23 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.SavedStateHandle +import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R +import to.bitkit.ext.getClipboardText import to.bitkit.models.LnPeer -import to.bitkit.ui.appViewModel +import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.TextInput import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.theme.AppShapes -import to.bitkit.ui.theme.AppTextFieldDefaults +import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -55,15 +56,34 @@ import to.bitkit.viewmodels.ExternalNodeViewModel @Composable fun ExternalConnectionScreen( + route: Routes.ExternalConnection, + savedStateHandle: SavedStateHandle, viewModel: ExternalNodeViewModel, onNodeConnected: () -> Unit, + onScanClick: () -> Unit, onBackClick: () -> Unit, onCloseClick: () -> Unit, ) { - val app = appViewModel ?: return - val clipboard = LocalClipboardManager.current + val context = LocalContext.current val uiState by viewModel.uiState.collectAsState() + // Handle result from scanner opened from home + LaunchedEffect(route.scannedNodeUri) { + if (route.scannedNodeUri != null) { + viewModel.parseNodeUri(route.scannedNodeUri) + } + } + + // Handle result from scanner opened from this screen + LaunchedEffect(savedStateHandle) { + savedStateHandle.getStateFlow(SCAN_RESULT_KEY, null) + .filterNotNull() + .collect { scannedData -> + viewModel.parseNodeUri(scannedData) + savedStateHandle.remove(SCAN_RESULT_KEY) + } + } + LaunchedEffect(viewModel, onNodeConnected) { viewModel.effects.collect { when (it) { @@ -76,8 +96,8 @@ fun ExternalConnectionScreen( ExternalConnectionContent( uiState = uiState, onContinueClick = { peer -> viewModel.onConnectionContinue(peer) }, - onScanClick = { app.toast(Exception("Coming soon")) }, - onPasteClick = { viewModel.onConnectionPaste(clipboardText = clipboard.getText()?.text.orEmpty()) }, + onPasteClick = { viewModel.parseNodeUri(context.getClipboardText().orEmpty()) }, + onScanClick = onScanClick, onBackClick = onBackClick, onCloseClick = onCloseClick, ) @@ -120,13 +140,11 @@ private fun ExternalConnectionContent( Spacer(modifier = Modifier.height(16.dp)) Caption13Up(text = stringResource(R.string.lightning__external_manual__node_id), color = Colors.White64) Spacer(modifier = Modifier.height(8.dp)) - TextField( - placeholder = { Text("00000000000000000000000000000000000000000000000000000000000000") }, + TextInput( + placeholder = "00000000000000000000000000000000000000000000000000000000000000", value = nodeId, onValueChange = { nodeId = it }, singleLine = false, - colors = AppTextFieldDefaults.semiTransparent, - shape = AppShapes.smallInput, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, imeAction = ImeAction.Done, @@ -138,13 +156,11 @@ private fun ExternalConnectionContent( Spacer(modifier = Modifier.height(16.dp)) Caption13Up(text = stringResource(R.string.lightning__external_manual__host), color = Colors.White64) Spacer(modifier = Modifier.height(8.dp)) - TextField( - placeholder = { Text("00.00.00.00") }, + TextInput( + placeholder = "00.00.00.00", value = host, onValueChange = { host = it }, singleLine = true, - colors = AppTextFieldDefaults.semiTransparent, - shape = AppShapes.smallInput, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, imeAction = ImeAction.Done, @@ -156,13 +172,11 @@ private fun ExternalConnectionContent( Spacer(modifier = Modifier.height(16.dp)) Caption13Up(text = stringResource(R.string.lightning__external_manual__port), color = Colors.White64) Spacer(modifier = Modifier.height(8.dp)) - TextField( - placeholder = { Text("9735") }, + TextInput( + placeholder = "9735", value = port, onValueChange = { port = it }, singleLine = true, - colors = AppTextFieldDefaults.semiTransparent, - shape = AppShapes.smallInput, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, autoCorrectEnabled = false, 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 c7cde8613..3de63adab 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 @@ -88,7 +88,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.WalletBalanceView import to.bitkit.ui.currencyViewModel import to.bitkit.ui.navigateToActivityItem -import to.bitkit.ui.navigateToQrScanner +import to.bitkit.ui.navigateToScanner import to.bitkit.ui.navigateToSettings import to.bitkit.ui.navigateToTransferFunding import to.bitkit.ui.navigateToTransferIntro @@ -331,7 +331,7 @@ fun HomeScreen( TabBar( onSendClick = { appViewModel.showSheet(BottomSheetType.Send()) }, onReceiveClick = { appViewModel.showSheet(BottomSheetType.Receive) }, - onScanClick = { rootNavController.navigateToQrScanner() }, + onScanClick = { rootNavController.navigateToScanner() }, modifier = Modifier .align(Alignment.BottomCenter) .systemBarsPadding() diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 2cf178afa..c002ace73 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -38,7 +38,7 @@ import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue -import to.bitkit.ui.navigateToQrScanner +import to.bitkit.ui.navigateToScanner import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -91,7 +91,7 @@ fun ElectrumConfigScreen( Content( uiState = uiState, onBack = { navController.popBackStack() }, - onScan = { navController.navigateToQrScanner(isCalledForResult = true) }, + onScan = { navController.navigateToScanner(isCalledForResult = true) }, onChangeHost = viewModel::setHost, onChangePort = viewModel::setPort, onChangeProtocol = viewModel::setProtocol, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index c55fc2aed..5667c9975 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -32,7 +32,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer -import to.bitkit.ui.navigateToQrScanner +import to.bitkit.ui.navigateToScanner import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -83,7 +83,7 @@ fun RgsServerScreen( Content( uiState = uiState, onBack = { navController.popBackStack() }, - onScan = { navController.navigateToQrScanner(isCalledForResult = true) }, + onScan = { navController.navigateToScanner(isCalledForResult = true) }, onChangeUrl = viewModel::setRgsUrl, onClickReset = viewModel::resetToDefault, onClickConnect = viewModel::onClickConnect, diff --git a/app/src/main/java/to/bitkit/ui/theme/Shape.kt b/app/src/main/java/to/bitkit/ui/theme/Shape.kt index ccf0d656c..cad12ba8c 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Shape.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Shape.kt @@ -15,6 +15,4 @@ val Shapes = Shapes( object AppShapes { val sheet = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) val small = RoundedCornerShape(8.dp) - val smallButton = small - val smallInput = small } diff --git a/app/src/main/java/to/bitkit/ui/theme/Type.kt b/app/src/main/java/to/bitkit/ui/theme/Type.kt index 3ef2e8ae9..fd0b4223e 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Type.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Type.kt @@ -1,10 +1,12 @@ package to.bitkit.ui.theme import androidx.compose.material3.Typography +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp import to.bitkit.R @@ -47,3 +49,24 @@ val Typography = Typography( letterSpacing = 0.4.sp, ), ) + +object AppTextStyles { + val BodyMSB = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + lineHeight = 22.sp, + letterSpacing = 0.4.sp, + fontFamily = InterFontFamily, + textAlign = TextAlign.Start, + color = Color.Unspecified, + ) + val BodySSB = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 15.sp, + lineHeight = 20.sp, + letterSpacing = 0.4.sp, + fontFamily = InterFontFamily, + color = Color.Unspecified, + textAlign = TextAlign.Start, + ) +} diff --git a/app/src/main/java/to/bitkit/utils/ResourceProvider.kt b/app/src/main/java/to/bitkit/utils/ResourceProvider.kt deleted file mode 100644 index b1ba730f2..000000000 --- a/app/src/main/java/to/bitkit/utils/ResourceProvider.kt +++ /dev/null @@ -1,17 +0,0 @@ -package to.bitkit.utils - -import android.content.Context -import androidx.annotation.StringRes -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ResourceProvider @Inject constructor( - @ApplicationContext private val context: Context, -) { - - fun getString(@StringRes resId: Int): String { - return context.getString(resId) - } -} diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 64b143491..cfd9f18a2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -14,7 +14,6 @@ import com.synonym.bitkitcore.Scanner import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -54,6 +53,7 @@ import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.ScannerService import to.bitkit.services.hasLightingParam import to.bitkit.services.lightningParam +import to.bitkit.ui.Routes import to.bitkit.ui.components.BottomSheetType import to.bitkit.ui.screens.wallets.send.SendRoute import to.bitkit.ui.shared.toast.ToastEventBus @@ -493,6 +493,12 @@ class AppViewModel @Inject constructor( } } + is Scanner.NodeId -> { + hideSheet() // hide scan sheet if opened + val nextRoute = Routes.ExternalConnection(scan.url) + mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) + } + null -> { toast( type = Toast.ToastType.ERROR, @@ -644,7 +650,7 @@ class AppViewModel @Inject constructor( val filter = newTransaction.type.toActivityFilter() val paymentType = newTransaction.direction.toTxType() - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(bgDispatcher) { val activity = coreService.activity.get(filter = filter, txType = paymentType, limit = 1u).firstOrNull() if (activity == null) { @@ -652,7 +658,8 @@ class AppViewModel @Inject constructor( return@launch } - mainScreenEffect(MainScreenEffect.NavigateActivityDetail(activity.rawId())) + val nextRoute = Routes.ActivityDetail(activity.rawId()) + mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) } } @@ -933,7 +940,7 @@ sealed class SendEffect { } sealed class MainScreenEffect { - data class NavigateActivityDetail(val activityId: String) : MainScreenEffect() + data class Navigate(val route: Routes) : MainScreenEffect() data object WipeWallet : MainScreenEffect() data class ProcessClipboardAutoRead(val data: String) : MainScreenEffect() } diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index f9beb9391..5b4a1b12c 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -1,8 +1,10 @@ package to.bitkit.viewmodels +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -19,14 +21,13 @@ import to.bitkit.models.Toast import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService import to.bitkit.ui.shared.toast.ToastEventBus -import to.bitkit.utils.ResourceProvider import to.bitkit.viewmodels.ExternalNodeContract.SideEffect import to.bitkit.viewmodels.ExternalNodeContract.UiState import javax.inject.Inject @HiltViewModel class ExternalNodeViewModel @Inject constructor( - private val resourceProvider: ResourceProvider, + @ApplicationContext private val context: Context, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, ) : ViewModel() { @@ -51,23 +52,23 @@ class ExternalNodeViewModel @Inject constructor( } else { ToastEventBus.send( type = Toast.ToastType.ERROR, - title = resourceProvider.getString(R.string.lightning__error_add_title), - description = resourceProvider.getString(R.string.lightning__error_add), + title = context.getString(R.string.lightning__error_add_title), + description = context.getString(R.string.lightning__error_add), ) } } } - fun onConnectionPaste(clipboardText: String) { + fun parseNodeUri(uriString: String) { viewModelScope.launch { - val result = LnPeer.parseUri(clipboardText) + val result = LnPeer.parseUri(uriString) if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { ToastEventBus.send( type = Toast.ToastType.ERROR, - title = resourceProvider.getString(R.string.lightning__error_add_uri), + title = context.getString(R.string.lightning__error_add_uri), ) } } @@ -103,15 +104,13 @@ class ExternalNodeViewModel @Inject constructor( } } - private suspend fun failConfirm(errorMessage: String) { + private suspend fun failConfirm(error: String) { _uiState.update { it.copy(isLoading = false) } ToastEventBus.send( type = Toast.ToastType.ERROR, - title = resourceProvider.getString(R.string.lightning__error_channel_purchase), - description = resourceProvider - .getString(R.string.lightning__error_channel_setup_msg) - .replace("{raw}", errorMessage), + title = context.getString(R.string.lightning__error_channel_purchase), + description = context.getString(R.string.lightning__error_channel_setup_msg).replace("{raw}", error), ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 27b8a26e2..f6b1e5c66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanistPermissions = "0.36.0" activityCompose = "1.10.1" -agp = "8.11.0" +agp = "8.11.1" appcompat = "1.7.0" barcodeScanning = "17.3.0" biometric = "1.4.0-alpha02" @@ -81,7 +81,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.5.0" } # upstream +#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.6.1" } # upstream ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.6.1-rc.2" } # fork lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }