diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index 5d226cad4..b84ac0aad 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -9,9 +9,11 @@ import androidx.datastore.preferences.preferencesDataStore import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import to.bitkit.di.json import to.bitkit.ext.enumValueOfOrNull import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.PrimaryDisplay +import to.bitkit.models.TransactionSpeed import to.bitkit.utils.Logger import javax.inject.Inject import javax.inject.Singleton @@ -48,6 +50,20 @@ class SettingsStore @Inject constructor( store.edit { it[SELECTED_CURRENCY_KEY] = currency } } + val defaultTransactionSpeed: Flow = store.data + .map { + it[DEFAULT_TX_SPEED]?.let { x -> runCatching { json.decodeFromString(x) }.getOrNull() } + ?: TransactionSpeed.Medium + } + + suspend fun setDefaultTransactionSpeed(speed: TransactionSpeed) { + val encoded = runCatching { json.encodeToString(speed) }.getOrElse { + Logger.error("Failed to encode default transaction speed: $it") + return + } + store.edit { it[DEFAULT_TX_SPEED] = encoded } + } + val showEmptyState: Flow = store.data.map { it[SHOW_EMPTY_STATE] == true } suspend fun setShowEmptyState(show: Boolean) { store.edit { it[SHOW_EMPTY_STATE] = show } @@ -92,6 +108,7 @@ class SettingsStore @Inject constructor( private val PRIMARY_DISPLAY_UNIT_KEY = stringPreferencesKey("primary_display_unit") private val BTC_DISPLAY_UNIT_KEY = stringPreferencesKey("btc_display_unit") private val SELECTED_CURRENCY_KEY = stringPreferencesKey("selected_currency") + private val DEFAULT_TX_SPEED = stringPreferencesKey("default_tx_speed") private val SHOW_EMPTY_STATE = booleanPreferencesKey("show_empty_state") private val HAS_SEEN_SPENDING_INTRO = booleanPreferencesKey("has_seen_spending_intro") private val HAS_SEEN_SAVINGS_INTRO = booleanPreferencesKey("has_seen_savings_intro") diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt index 12b259bf1..54d4bac16 100644 --- a/app/src/main/java/to/bitkit/env/Env.kt +++ b/app/src/main/java/to/bitkit/env/Env.kt @@ -5,7 +5,7 @@ import org.lightningdevkit.ldknode.Network import to.bitkit.BuildConfig import to.bitkit.ext.ensureDir import to.bitkit.models.LnPeer -import to.bitkit.models.blocktank.BlocktankNotificationType +import to.bitkit.models.BlocktankNotificationType import to.bitkit.utils.Logger import java.io.File import kotlin.io.path.Path diff --git a/app/src/main/java/to/bitkit/ext/FeeRates.kt b/app/src/main/java/to/bitkit/ext/FeeRates.kt new file mode 100644 index 000000000..bfaafe0b6 --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/FeeRates.kt @@ -0,0 +1,13 @@ +package to.bitkit.ext + +import to.bitkit.models.TransactionSpeed +import uniffi.bitkitcore.FeeRates + +fun FeeRates.getSatsPerVByteFor(speed: TransactionSpeed): UInt { + return when (speed) { + is TransactionSpeed.Fast -> fast + is TransactionSpeed.Medium -> mid + is TransactionSpeed.Slow -> slow + is TransactionSpeed.Custom -> speed.satsPerVByte + } +} diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index ea5e5d24d..09fff79d7 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -17,7 +17,7 @@ import to.bitkit.di.json import to.bitkit.env.Env.DERIVATION_NAME import to.bitkit.ext.fromBase64 import to.bitkit.ext.fromHex -import to.bitkit.models.blocktank.BlocktankNotificationType +import to.bitkit.models.BlocktankNotificationType import to.bitkit.ui.pushNotification import to.bitkit.utils.Crypto import to.bitkit.utils.Logger diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 8b5376b7d..edbace2fd 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -18,12 +18,12 @@ import to.bitkit.di.json import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType -import to.bitkit.models.blocktank.BlocktankNotificationType -import to.bitkit.models.blocktank.BlocktankNotificationType.cjitPaymentArrived -import to.bitkit.models.blocktank.BlocktankNotificationType.incomingHtlc -import to.bitkit.models.blocktank.BlocktankNotificationType.mutualClose -import to.bitkit.models.blocktank.BlocktankNotificationType.orderPaymentConfirmed -import to.bitkit.models.blocktank.BlocktankNotificationType.wakeToTimeout +import to.bitkit.models.BlocktankNotificationType +import to.bitkit.models.BlocktankNotificationType.cjitPaymentArrived +import to.bitkit.models.BlocktankNotificationType.incomingHtlc +import to.bitkit.models.BlocktankNotificationType.mutualClose +import to.bitkit.models.BlocktankNotificationType.orderPaymentConfirmed +import to.bitkit.models.BlocktankNotificationType.wakeToTimeout import to.bitkit.repositories.LightningRepo import to.bitkit.services.CoreService import to.bitkit.ui.pushNotification diff --git a/app/src/main/java/to/bitkit/models/blocktank/BlocktankNotificationType.kt b/app/src/main/java/to/bitkit/models/BlocktankNotificationType.kt similarity index 89% rename from app/src/main/java/to/bitkit/models/blocktank/BlocktankNotificationType.kt rename to app/src/main/java/to/bitkit/models/BlocktankNotificationType.kt index 7eecf9084..162645d53 100644 --- a/app/src/main/java/to/bitkit/models/blocktank/BlocktankNotificationType.kt +++ b/app/src/main/java/to/bitkit/models/BlocktankNotificationType.kt @@ -1,4 +1,4 @@ -package to.bitkit.models.blocktank +package to.bitkit.models import kotlinx.serialization.Serializable diff --git a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt new file mode 100644 index 000000000..e8fa67e79 --- /dev/null +++ b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt @@ -0,0 +1,53 @@ +package to.bitkit.models + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable(with = TransactionSpeedSerializer::class) +sealed class TransactionSpeed { + object Fast : TransactionSpeed() + object Medium : TransactionSpeed() + object Slow : TransactionSpeed() + data class Custom(val satsPerVByte: UInt) : TransactionSpeed() + + fun serialized(): String = when (this) { + is Fast -> "fast" + is Medium -> "medium" + is Slow -> "slow" + is Custom -> "custom_$satsPerVByte" + } + + companion object { + fun fromString(value: String): TransactionSpeed = when { + value == "fast" -> Fast + value == "medium" -> Medium + value == "slow" -> Slow + value.matches(Regex("custom_\\d+")) -> { + value.substringAfter("custom_") + .toUIntOrNull() + ?.let { Custom(it) } + ?: Medium + } + + else -> Medium + } + } +} + +private object TransactionSpeedSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("TransactionSpeed", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: TransactionSpeed) { + encoder.encodeString(value.serialized()) + } + + override fun deserialize(decoder: Decoder): TransactionSpeed { + return TransactionSpeed.fromString(decoder.decodeString()) + } +} diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 2bb2b6dfd..eaf857902 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -16,9 +16,13 @@ import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PaymentId import org.lightningdevkit.ldknode.Txid import org.lightningdevkit.ldknode.UserChannelId +import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher +import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.LnPeer import to.bitkit.models.NodeLifecycleState +import to.bitkit.models.TransactionSpeed +import to.bitkit.services.CoreService import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService import to.bitkit.services.NodeEventHandler @@ -34,7 +38,9 @@ class LightningRepo @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, - private val addressChecker: AddressChecker + private val addressChecker: AddressChecker, + private val settingsStore: SettingsStore, + private val coreService: CoreService, ) { private val _nodeLifecycleState: MutableStateFlow = MutableStateFlow(NodeLifecycleState.Stopped) val nodeLifecycleState = _nodeLifecycleState.asStateFlow() @@ -221,9 +227,23 @@ class LightningRepo @Inject constructor( Result.success(paymentId) } - suspend fun sendOnChain(address: Address, sats: ULong): Result = + /** + * Sends bitcoin to an on-chain address + * + * @param address The bitcoin address to send to + * @param sats The amount in satoshis to send + * @param speed The desired transaction speed determining the fee rate. If null, the user's default speed is used. + * @return A `Result` with the `Txid` of sent transaction, or an error if the transaction fails + * or the fee rate cannot be retrieved. + */ + suspend fun sendOnChain(address: Address, sats: ULong, speed: TransactionSpeed? = null): Result = executeWhenNodeRunning("Send on-chain") { - val txId = lightningService.send(address = address, sats = sats) + val transactionSpeed = speed ?: settingsStore.defaultTransactionSpeed.first() + + var fees = coreService.blocktank.getFees().getOrThrow() + var satsPerVByte = fees.getSatsPerVByteFor(transactionSpeed) + + val txId = lightningService.send(address = address, sats = sats, satsPerVByte = satsPerVByte) Result.success(txId) } diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 45c1f543d..15f62d40b 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -21,6 +21,7 @@ import uniffi.bitkitcore.BtOrderState2 import uniffi.bitkitcore.CJitStateEnum import uniffi.bitkitcore.CreateCjitOptions import uniffi.bitkitcore.CreateOrderOptions +import uniffi.bitkitcore.FeeRates import uniffi.bitkitcore.IBtEstimateFeeResponse2 import uniffi.bitkitcore.IBtInfo import uniffi.bitkitcore.IBtOrder @@ -435,12 +436,29 @@ class BlocktankService( private val coreService: CoreService, private val lightningService: LightningService, ) { - suspend fun info(refresh: Boolean = false): IBtInfo? { + suspend fun info(refresh: Boolean = true): IBtInfo? { return ServiceQueue.CORE.background { getInfo(refresh = refresh) } } + private suspend fun fees(refresh: Boolean = true) : FeeRates? { + return info(refresh)?.onchain?.feeRates + } + + suspend fun getFees() : Result { + var fees = fees(refresh = true) + if (fees == null) { + Logger.warn("Failed to fetch fresh fee rate, using cached rate.") + fees = fees(refresh = false) + } + if (fees == null) { + return Result.failure(AppError("Fees unavailable from bitkit-core")) + } + + return Result.success(fees) + } + suspend fun createCjit( channelSizeSat: ULong, invoiceSat: ULong, diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 962750f68..e4012fbb0 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -361,17 +361,16 @@ class LightningService @Inject constructor( return true } - // TODO: get feeRate from real source - suspend fun send(address: Address, sats: ULong, satKwu: ULong = 250uL * 5uL): Txid { + suspend fun send(address: Address, sats: ULong, satsPerVByte: UInt): Txid { val node = this.node ?: throw ServiceError.NodeNotSetup - Logger.info("Sending $sats sats to $address") + Logger.info("Sending $sats sats to $address with satsPerVByte=$satsPerVByte") return ServiceQueue.LDK.background { node.onchainPayment().sendToAddress( address = address, amountSats = sats, - feeRate = FeeRate.fromSatPerKwu(satKwu) + feeRate = convertVByteToKwu(satsPerVByte) ) } } @@ -508,4 +507,11 @@ fun List.filterOpen(): List { return this.filter { it.isChannelReady } } + +private fun convertVByteToKwu(satsPerVByte: UInt): FeeRate { + // 1 vbyte = 4 weight units, so 1 sats/vbyte = 250 sats/kwu + val satPerKwu = satsPerVByte.toULong() * 250u + // Ensure we're above the minimum relay fee + return FeeRate.fromSatPerKwu(maxOf(satPerKwu, 253u)) // FEERATE_FLOOR_SATS_PER_KW is 253 in LDK +} // endregion diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 61dc068ca..6a4dca38b 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -81,6 +81,7 @@ import to.bitkit.ui.settings.LogsScreen import to.bitkit.ui.settings.OrderDetailScreen import to.bitkit.ui.settings.SecuritySettingsScreen import to.bitkit.ui.settings.SettingsScreen +import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen import to.bitkit.ui.settings.backups.BackupWalletScreen import to.bitkit.ui.settings.backups.RestoreWalletScreen import to.bitkit.ui.settings.pin.ChangePinConfirmScreen @@ -88,6 +89,7 @@ import to.bitkit.ui.settings.pin.ChangePinNewScreen import to.bitkit.ui.settings.pin.ChangePinResultScreen import to.bitkit.ui.settings.pin.ChangePinScreen import to.bitkit.ui.settings.pin.DisablePinScreen +import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen import to.bitkit.ui.utils.screenScaleIn import to.bitkit.ui.utils.screenScaleOut import to.bitkit.ui.utils.screenSlideIn @@ -243,6 +245,7 @@ fun ContentView( settings(walletViewModel, navController) nodeState(walletViewModel, navController) generalSettings(navController) + transactionSpeedSettings(navController) securitySettings(navController) disablePin(navController) changePin(navController) @@ -495,11 +498,18 @@ private fun NavGraphBuilder.generalSettings(navController: NavHostController) { } } +private fun NavGraphBuilder.transactionSpeedSettings(navController: NavHostController) { + composableWithDefaultTransitions { + TransactionSpeedSettingsScreen(navController) + } + composableWithDefaultTransitions { + CustomFeeSettingsScreen(navController) + } +} + private fun NavGraphBuilder.securitySettings(navController: NavHostController) { composableWithDefaultTransitions { - SecuritySettingsScreen( - navController = navController, - ) + SecuritySettingsScreen(navController = navController) } } @@ -876,6 +886,14 @@ fun NavController.navigateToLogs() = navigate( fun NavController.navigateToLogDetail(fileName: String) = navigate( route = Routes.LogDetail(fileName), ) + +fun NavController.navigateToTransactionSpeedSettings() = navigate( + route = Routes.TransactionSpeedSettings, +) + +fun NavController.navigateToCustomFeeSettings() = navigate( + route = Routes.CustomFeeSettings, +) // endregion object Routes { @@ -891,6 +909,12 @@ object Routes { @Serializable data object GeneralSettings + @Serializable + data object TransactionSpeedSettings + + @Serializable + data object CustomFeeSettings + @Serializable data object SecuritySettings diff --git a/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt b/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt index c1b834154..abe0540aa 100644 --- a/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt +++ b/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt @@ -197,7 +197,7 @@ private fun PinPad( pin = pin, modifier = Modifier.padding(vertical = 16.dp), ) - PinNumberPad( + NumberPadSimple( modifier = Modifier.height(310.dp), onPress = { key -> if (key == KEY_DELETE) { diff --git a/app/src/main/java/to/bitkit/ui/components/PinNumberPad.kt b/app/src/main/java/to/bitkit/ui/components/NumberPadSimple.kt similarity index 98% rename from app/src/main/java/to/bitkit/ui/components/PinNumberPad.kt rename to app/src/main/java/to/bitkit/ui/components/NumberPadSimple.kt index 8cef870df..0be884a15 100644 --- a/app/src/main/java/to/bitkit/ui/components/PinNumberPad.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPadSimple.kt @@ -55,7 +55,7 @@ private fun NumberButton( } @Composable -fun PinNumberPad( +fun NumberPadSimple( onPress: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -123,7 +123,7 @@ fun PinNumberPad( @Composable private fun Preview() { AppThemeSurface { - PinNumberPad( + NumberPadSimple( onPress = {}, modifier = Modifier.height(310.dp) ) 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 10c15df95..d2e213f4c 100644 --- a/app/src/main/java/to/bitkit/ui/components/Text.kt +++ b/app/src/main/java/to/bitkit/ui/components/Text.kt @@ -367,7 +367,7 @@ fun Caption13Up( fontWeight = FontWeight.Medium, fontSize = 13.sp, lineHeight = 18.sp, - letterSpacing = 0.8.sp, + letterSpacing = 0.4.sp, fontFamily = InterFontFamily, color = color, textAlign = TextAlign.Start, diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt b/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt index 21f5166b6..1f5a12e21 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt @@ -1,5 +1,6 @@ package to.bitkit.ui.components.settings +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,56 +10,131 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider 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.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyMSB +import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.BodySSB import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +sealed class SettingsButtonValue { + data class BooleanValue(val checked: Boolean) : SettingsButtonValue() + data class StringValue(val value: String) : SettingsButtonValue() + data object None : SettingsButtonValue() +} + @Composable fun SettingsButtonRow( title: String, - onClick: () -> Unit, modifier: Modifier = Modifier, - value: String? = null, + subtitle: String? = null, + value: SettingsButtonValue = SettingsButtonValue.None, + description: String? = null, + iconRes: Int? = null, + iconTint: Color = Color.Unspecified, + enabled: Boolean = true, + loading: Boolean = false, + onClick: () -> Unit, ) { Column( - modifier = Modifier.height(52.dp) + modifier = modifier + .then(if (!enabled) Modifier.alpha(0.5f) else Modifier) ) { Row( - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = Modifier .fillMaxWidth() + .clickableAlpha(onClick = if (enabled) onClick else null) .padding(vertical = 16.dp) - .clickableAlpha { onClick() } ) { - BodyM(text = title, color = Colors.White) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - value?.let { - BodyM(text = it, color = Colors.White) - Spacer(modifier = Modifier.width(4.dp)) - } + if (iconRes != null) { Icon( - painter = painterResource(R.drawable.ic_chevron_right), + painter = painterResource(iconRes), contentDescription = null, - tint = Colors.White64, - modifier = Modifier.size(24.dp) + tint = iconTint, + modifier = Modifier.size(32.dp), ) + Spacer(modifier = Modifier.width(10.dp)) } + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .weight(1f) + .padding(end = 16.dp) + ) { + if (subtitle != null) { + BodyMSB(text = title) + Spacer(modifier = Modifier.height(4.dp)) + BodySSB(text = subtitle, color = Colors.White64) + } else { + BodyM(text = title) + } + } + + when (value) { + is SettingsButtonValue.BooleanValue -> { + Crossfade(targetState = loading to value.checked) { (isLoading, isChecked) -> + when { + isLoading && isChecked -> CircularProgressIndicator( + color = Colors.White, + strokeWidth = 2.dp, + modifier = Modifier.size(32.dp), + ) + + isChecked -> Icon( + painter = painterResource(R.drawable.ic_checkmark), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(32.dp), + ) + + else -> Unit + } + } + } + + is SettingsButtonValue.StringValue -> { + BodyM(text = value.value) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + tint = Colors.White64, + modifier = Modifier.size(24.dp), + ) + } + + SettingsButtonValue.None -> { + Icon( + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + tint = Colors.White64, + modifier = Modifier.size(24.dp), + ) + } + } + } + if (description != null) { + BodyS( + text = description, + color = Colors.White64, + modifier = Modifier.padding(top = 4.dp, bottom = 4.dp), + ) } - HorizontalDivider(color = Colors.White10) + HorizontalDivider() } } @@ -66,14 +142,46 @@ fun SettingsButtonRow( @Composable private fun Preview() { AppThemeSurface { - Column(modifier = Modifier.padding(16.dp)) { + Column { + SettingsButtonRow( + title = "Selected", + subtitle = "Subtitle for selected", + value = SettingsButtonValue.BooleanValue(true), + iconRes = R.drawable.ic_speed_fast, + iconTint = Colors.Brand, + onClick = {}, + ) + SettingsButtonRow( + title = "Not Selected", + subtitle = "Subtitle of item", + value = SettingsButtonValue.BooleanValue(false), + iconRes = R.drawable.ic_speed_normal, + iconTint = Colors.Brand, + onClick = {}, + ) + SettingsButtonRow( + title = "String Value", + value = SettingsButtonValue.StringValue("USD"), + iconRes = R.drawable.ic_settings, + iconTint = Colors.White, + onClick = {}, + ) + SettingsButtonRow( + title = "No Value", + iconRes = R.drawable.ic_users, + onClick = {}, + ) SettingsButtonRow( - title = "Setting Button", - value = "Enabled", + title = "Loading", + iconRes = R.drawable.ic_copy, + loading = true, + value = SettingsButtonValue.BooleanValue(true), onClick = {}, ) SettingsButtonRow( - title = "Setting Button Without Value", + title = "Disabled With Description", + enabled = false, + description = "This is a description.", onClick = {}, ) } diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt b/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt index 790e827e3..0034bfd47 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt @@ -27,12 +27,12 @@ fun SettingsSwitchRow( modifier: Modifier = Modifier, ) { Column( - modifier = Modifier.height(52.dp) + modifier = modifier.height(52.dp) ) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = Modifier .fillMaxWidth() .clickableAlpha { onClick() } .padding(vertical = 16.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/PinCheckScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/PinCheckScreen.kt index d7dfd2dfd..d2db71869 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/PinCheckScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/PinCheckScreen.kt @@ -28,7 +28,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots -import to.bitkit.ui.components.PinNumberPad +import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground @@ -134,7 +134,7 @@ private fun PinCheckContent( Spacer(modifier = Modifier.weight(1f)) - PinNumberPad( + NumberPadSimple( onPress = onKeyPress, modifier = Modifier .height(350.dp) diff --git a/app/src/main/java/to/bitkit/ui/settings/DefaultUnitSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/DefaultUnitSettingsScreen.kt index 9e957c3e3..76323cf6c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/DefaultUnitSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/DefaultUnitSettingsScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.filled.Language -import androidx.compose.material3.DividerDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -32,13 +31,15 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.BitcoinDisplayUnit +import to.bitkit.models.PrimaryDisplay import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.components.LabelText +import to.bitkit.ui.components.Caption +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.display +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.display import to.bitkit.viewmodels.CurrencyViewModel -import to.bitkit.models.PrimaryDisplay @Composable fun DefaultUnitSettingsScreen( @@ -52,30 +53,30 @@ fun DefaultUnitSettingsScreen( .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) ) { - val (_, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current + val (_, _, _, selectedCurrency, displayUnit, primaryDisplay) = LocalCurrencies.current SectionTitle(title = "DISPLAY AMOUNTS IN") CurrencyOptionRow( leadingIcon = Icons.Default.CurrencyBitcoin, - label = "Bitcoin", + label = stringResource(R.string.settings__general__unit_bitcoin), isSelected = primaryDisplay == PrimaryDisplay.BITCOIN, onClick = { currencyViewModel.setPrimaryDisplayUnit(PrimaryDisplay.BITCOIN) }, ) - val convertedCurrency = currencyViewModel.convert(1)?.currency ?: "fiat" CurrencyOptionRow( leadingIcon = Icons.Default.Language, - label = convertedCurrency, + label = selectedCurrency, isSelected = primaryDisplay == PrimaryDisplay.FIAT, onClick = { currencyViewModel.setPrimaryDisplayUnit(PrimaryDisplay.FIAT) }, ) - Spacer(modifier = Modifier.height(4.dp)) - LabelText( - text = "Tip: Quickly toggle between Bitcoin and $convertedCurrency by tapping on your wallet balance.", + Spacer(modifier = Modifier.height(8.dp)) + Caption( + text = stringResource(R.string.settings__general__unit_note).replace("{currency}", selectedCurrency), + color = Colors.White64, ) Spacer(modifier = Modifier.height(16.dp)) @@ -96,7 +97,7 @@ fun DefaultUnitSettingsScreen( @Composable private fun SectionTitle(title: String) { - LabelText( + Caption13Up( text = title, modifier = Modifier.padding(vertical = 8.dp) ) @@ -141,7 +142,7 @@ private fun CurrencyOptionRow( ) } } - HorizontalDivider(color = DividerDefaults.color.copy(alpha = 0.25f)) + HorizontalDivider() } } @@ -165,6 +166,6 @@ private fun BitcoinDenominationRow(unit: BitcoinDisplayUnit, isSelected: Boolean ) } } - HorizontalDivider(color = DividerDefaults.color.copy(alpha = 0.25f)) + HorizontalDivider() } } diff --git a/app/src/main/java/to/bitkit/ui/settings/GeneralSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/GeneralSettingsScreen.kt index adf307adc..8e4e211fd 100644 --- a/app/src/main/java/to/bitkit/ui/settings/GeneralSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/GeneralSettingsScreen.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.settings -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -8,29 +7,99 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.components.NavButton +import to.bitkit.models.PrimaryDisplay +import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.appViewModel +import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.navigateToDefaultUnitSettings import to.bitkit.ui.navigateToLocalCurrencySettings +import to.bitkit.ui.navigateToTransactionSpeedSettings import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.utils.displayText +import to.bitkit.models.TransactionSpeed +import to.bitkit.ui.components.settings.SettingsButtonValue +import to.bitkit.ui.navigateToHome +import to.bitkit.ui.scaffold.CloseNavIcon +import to.bitkit.ui.theme.AppThemeSurface @Composable fun GeneralSettingsScreen( navController: NavController, ) { - ScreenColumn { - AppTopBar(stringResource(R.string.settings__general_title), onBackClick = { navController.popBackStack() }) + val app = appViewModel ?: return + val currencies = LocalCurrencies.current + val defaultTransactionSpeed = app.defaultTransactionSpeed.collectAsStateWithLifecycle() + + GeneralSettingsContent( + selectedCurrency = currencies.selectedCurrency, + primaryDisplay = currencies.primaryDisplay, + defaultTransactionSpeed = defaultTransactionSpeed.value, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + onLocalCurrencyClick = { navController.navigateToLocalCurrencySettings() }, + onDefaultUnitClick = { navController.navigateToDefaultUnitSettings() }, + onTransactionSpeedClick = { navController.navigateToTransactionSpeedSettings() }, + ) +} + +@Composable +private fun GeneralSettingsContent( + selectedCurrency: String, + primaryDisplay: PrimaryDisplay, + defaultTransactionSpeed: TransactionSpeed, + onBackClick: () -> Unit = {}, + onCloseClick: () -> Unit = {}, + onLocalCurrencyClick: () -> Unit = {}, + onDefaultUnitClick: () -> Unit = {}, + onTransactionSpeedClick: () -> Unit = {}, +) { + ScreenColumn( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + AppTopBar( + titleText = stringResource(R.string.settings__general_title), + onBackClick = onBackClick, + actions = { CloseNavIcon(onClick = onCloseClick) }, + ) Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()) + modifier = Modifier.padding(horizontal = 16.dp) ) { - NavButton(stringResource(R.string.default_unit)) { navController.navigateToDefaultUnitSettings() } - NavButton(stringResource(R.string.local_currency)) { navController.navigateToLocalCurrencySettings() } + SettingsButtonRow( + title = stringResource(R.string.local_currency), + value = SettingsButtonValue.StringValue(selectedCurrency), + onClick = onLocalCurrencyClick, + ) + SettingsButtonRow( + title = stringResource(R.string.default_unit), + value = SettingsButtonValue.StringValue(when (primaryDisplay) { + PrimaryDisplay.BITCOIN -> stringResource(R.string.settings__general__unit_bitcoin) + PrimaryDisplay.FIAT -> selectedCurrency + }), + onClick = onDefaultUnitClick, + ) + SettingsButtonRow( + title = stringResource(R.string.settings__general__speed), + value = SettingsButtonValue.StringValue(defaultTransactionSpeed.displayText), + onClick = onTransactionSpeedClick, + ) } } } + +@Preview(showBackground = true) +@Composable +private fun Preview() { + AppThemeSurface { + GeneralSettingsContent( + selectedCurrency = "USD", + primaryDisplay = PrimaryDisplay.BITCOIN, + defaultTransactionSpeed = TransactionSpeed.Medium, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/LocalCurrencySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/LocalCurrencySettingsScreen.kt index d74c928f0..1a344e9a6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/LocalCurrencySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/LocalCurrencySettingsScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.DividerDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -33,10 +32,11 @@ import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.FxRate import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.components.LabelText +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppTextFieldDefaults +import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.CurrencyViewModel @Composable @@ -45,7 +45,10 @@ fun LocalCurrencySettingsScreen( navController: NavController, ) { ScreenColumn { - AppTopBar(stringResource(R.string.local_currency), onBackClick = { navController.popBackStack() }) + AppTopBar( + titleText = stringResource(R.string.settings__general__currency_local_title), + onBackClick = { navController.popBackStack() } + ) Column( modifier = Modifier .padding(horizontal = 16.dp) @@ -85,7 +88,13 @@ fun LocalCurrencySettingsScreen( LazyColumn { if (mostUsedRates.isNotEmpty()) { - item { LabelText(text = "MOST USED", modifier = Modifier.padding(vertical = 8.dp)) } + item { + Caption13Up( + text = stringResource(R.string.settings__general__currency_most_used), + modifier = Modifier.padding(vertical = 8.dp), + color = Colors.White64, + ) + } items(mostUsedRates) { rate -> CurrencyRow( rate = rate, @@ -97,7 +106,13 @@ fun LocalCurrencySettingsScreen( } item { Spacer(modifier = Modifier.height(16.dp)) } - item { LabelText(text = "OTHER CURRENCIES", modifier = Modifier.padding(vertical = 8.dp)) } + item { + Caption13Up( + text = stringResource(R.string.settings__general__currency_other), + modifier = Modifier.padding(vertical = 8.dp), + color = Colors.White64, + ) + } items(otherCurrencies) { rate -> CurrencyRow( @@ -137,6 +152,6 @@ private fun CurrencyRow( ) } } - HorizontalDivider(color = DividerDefaults.color.copy(alpha = 0.25f)) + HorizontalDivider() } } diff --git a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt index ab6106c2b..efd8215f7 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt @@ -20,6 +20,7 @@ import to.bitkit.ui.appViewModel import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.settings.SettingsButtonRow +import to.bitkit.ui.components.settings.SettingsButtonValue import to.bitkit.ui.components.settings.SettingsSwitchRow import to.bitkit.ui.navigateToAuthCheck import to.bitkit.ui.navigateToChangePin @@ -125,8 +126,10 @@ private fun SecuritySettingsContent( ) { SettingsButtonRow( title = stringResource(R.string.settings__security__pin), - value = stringResource( - if (isPinEnabled) R.string.settings__security__pin_enabled else R.string.settings__security__pin_disabled + value = SettingsButtonValue.StringValue( + stringResource( + if (isPinEnabled) R.string.settings__security__pin_enabled else R.string.settings__security__pin_disabled + ) ), onClick = onPinClick, ) @@ -177,7 +180,7 @@ private fun SecuritySettingsContent( @Preview @Composable -fun Preview() { +private fun Preview() { AppThemeSurface { SecuritySettingsContent( isPinEnabled = true, diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt index 9095d3b38..95feb97c1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt @@ -27,7 +27,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots -import to.bitkit.ui.components.PinNumberPad +import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.navigateToHome import to.bitkit.ui.navigateToChangePinResult import to.bitkit.ui.scaffold.AppTopBar @@ -117,7 +117,7 @@ private fun ChangePinConfirmContent( Spacer(modifier = Modifier.weight(1f)) - PinNumberPad( + NumberPadSimple( modifier = Modifier.height(350.dp), onPress = onKeyPress, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt index 24573e559..f8adda82a 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt @@ -21,7 +21,7 @@ import to.bitkit.env.Env import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots -import to.bitkit.ui.components.PinNumberPad +import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.navigateToChangePinConfirm import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppTopBar @@ -90,7 +90,7 @@ private fun ChangePinNewContent( Spacer(modifier = Modifier.weight(1f)) - PinNumberPad( + NumberPadSimple( modifier = Modifier.height(350.dp), onPress = onKeyPress, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt index e4be5e272..3803b85fc 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt @@ -26,7 +26,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots -import to.bitkit.ui.components.PinNumberPad +import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.navigateToChangePinNew import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppTopBar @@ -129,7 +129,7 @@ private fun ChangePinContent( Spacer(modifier = Modifier.weight(1f)) - PinNumberPad( + NumberPadSimple( modifier = Modifier.height(350.dp), onPress = onKeyPress, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChoosePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChoosePinScreen.kt index fd78e9b38..a7491faa3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChoosePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChoosePinScreen.kt @@ -21,7 +21,7 @@ import to.bitkit.env.Env import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots -import to.bitkit.ui.components.PinNumberPad +import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface @@ -58,7 +58,7 @@ fun ChoosePinScreen( Spacer(modifier = Modifier.height(32.dp)) - PinNumberPad( + NumberPadSimple( onPress = { key -> if (key == KEY_DELETE) { if (pin.isNotEmpty()) { diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ConfirmPinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ConfirmPinScreen.kt index 18695c515..234f62469 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ConfirmPinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ConfirmPinScreen.kt @@ -27,7 +27,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots -import to.bitkit.ui.components.PinNumberPad +import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface @@ -119,7 +119,7 @@ private fun ConfirmPinContent( Spacer(modifier = Modifier.height(32.dp)) - PinNumberPad( + NumberPadSimple( onPress = onKeyPress, modifier = Modifier .height(350.dp) diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt new file mode 100644 index 000000000..1f82cbeec --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt @@ -0,0 +1,156 @@ +package to.bitkit.ui.settings.transactionSpeed + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import to.bitkit.R +import to.bitkit.env.Env +import to.bitkit.models.BITCOIN_SYMBOL +import to.bitkit.models.ConvertedAmount +import to.bitkit.models.TransactionSpeed +import to.bitkit.ui.appViewModel +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.KEY_DELETE +import to.bitkit.ui.components.LargeRow +import to.bitkit.ui.components.NumberPadSimple +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.currencyViewModel +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.CloseNavIcon +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +@Composable +fun CustomFeeSettingsScreen( + navController: NavController, +) { + val app = appViewModel ?: return + val customFeeRate = app.defaultTransactionSpeed.collectAsStateWithLifecycle() + var input by remember { + mutableStateOf((customFeeRate.value as? TransactionSpeed.Custom)?.satsPerVByte?.toString() ?: "") + } + val currency = currencyViewModel ?: return + val totalFee = Env.TransactionDefaults.recommendedBaseFee * (input.toUIntOrNull() ?: 0u) + var converted: ConvertedAmount? by remember { mutableStateOf(null) } + + LaunchedEffect(input) { + val inputNum = input.toLongOrNull() ?: 0 + converted = if (inputNum == 0L) { + null + } else { + currency.convert(totalFee.toLong()) + } + } + + val totalFeeText = converted + ?.let { + stringResource(R.string.settings__general__speed_fee_total_fiat) + .replace("{feeSats}", "$totalFee") + .replace("{fiatSymbol}", it.symbol) + .replace("{fiatFormatted}", it.formatted) + } + ?: stringResource(R.string.settings__general__speed_fee_total) + .replace("{feeSats}", "$totalFee") + + CustomFeeSettingsContent( + input = input, + totalFeeText = totalFeeText, + onKeyPress = { key -> + when (key) { + KEY_DELETE -> input = if (input.isNotEmpty()) input.dropLast(1) else "" + else -> if (input.length < 3) input = (input + key).trimStart('0') + } + }, + onContinue = { + val feeRate = input.toUIntOrNull() ?: 0u + app.setDefaultTransactionSpeed(TransactionSpeed.Custom(feeRate)) + navController.popBackStack() + }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.popBackStack() }, + ) +} + +@Composable +private fun CustomFeeSettingsContent( + input: String, + totalFeeText: String, + onKeyPress: (String) -> Unit = {}, + onContinue: () -> Unit = {}, + onBackClick: () -> Unit = {}, + onCloseClick: () -> Unit = {}, +) { + val feeRate = input.toUIntOrNull() ?: 0u + val isValid = feeRate != 0u + + ScreenColumn( + modifier = Modifier.navigationBarsPadding() + ) { + AppTopBar( + titleText = stringResource(R.string.settings__general__speed_fee_custom), + onBackClick = onBackClick, + actions = { CloseNavIcon(onClick = onCloseClick) }, + ) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize() + ) { + Caption13Up(text = stringResource(R.string.common__sat_vbyte), color = Colors.White64) + + Spacer(modifier = Modifier.height(16.dp)) + LargeRow( + prefix = null, + text = if (input.isEmpty()) "0" else input, + symbol = BITCOIN_SYMBOL, + showSymbol = true, + ) + + if (isValid) { + Spacer(modifier = Modifier.height(8.dp)) + BodyM(totalFeeText, color = Colors.White64) + } + + Spacer(modifier = Modifier.weight(1f)) + + NumberPadSimple( + onPress = onKeyPress, + modifier = Modifier.height(350.dp) + ) + PrimaryButton( + onClick = onContinue, + enabled = isValid, + text = stringResource(R.string.common__continue), + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + CustomFeeSettingsContent( + input = "5", + totalFeeText = "₿ 256 for average transaction ($0.25)" + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt new file mode 100644 index 000000000..6530c7c18 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt @@ -0,0 +1,113 @@ +package to.bitkit.ui.settings.transactionSpeed + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import to.bitkit.R +import to.bitkit.models.TransactionSpeed +import to.bitkit.ui.appViewModel +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.settings.SettingsButtonRow +import to.bitkit.ui.components.settings.SettingsButtonValue +import to.bitkit.ui.navigateToCustomFeeSettings +import to.bitkit.ui.navigateToHome +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.CloseNavIcon +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +@Composable +fun TransactionSpeedSettingsScreen( + navController: NavController, +) { + val app = appViewModel ?: return + val defaultTransactionSpeed = app.defaultTransactionSpeed.collectAsStateWithLifecycle() + + TransactionSpeedSettingsContent( + selectedSpeed = defaultTransactionSpeed.value, + onSpeedSelected = { app.setDefaultTransactionSpeed(it) }, + onCustomFeeClick = { navController.navigateToCustomFeeSettings() }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + ) +} + +@Composable +private fun TransactionSpeedSettingsContent( + selectedSpeed: TransactionSpeed, + onSpeedSelected: (TransactionSpeed) -> Unit = {}, + onCustomFeeClick: () -> Unit = {}, + onBackClick: () -> Unit = {}, + onCloseClick: () -> Unit = {}, +) { + ScreenColumn( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + AppTopBar( + titleText = stringResource(R.string.settings__general__speed_title), + onBackClick = onBackClick, + actions = { CloseNavIcon(onClick = onCloseClick) }, + ) + Column( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + Caption13Up(text = stringResource(R.string.settings__general__speed_default), color = Colors.White64) + Spacer(modifier = Modifier.height(16.dp)) + + SettingsButtonRow( + title = stringResource(R.string.settings__fee__fast__label), + subtitle = stringResource(R.string.settings__fee__fast__description), + iconRes = R.drawable.ic_speed_fast, + iconTint = Colors.Brand, + value = SettingsButtonValue.BooleanValue(selectedSpeed is TransactionSpeed.Fast), + onClick = { onSpeedSelected(TransactionSpeed.Fast) }, + ) + SettingsButtonRow( + title = stringResource(R.string.settings__fee__normal__label), + subtitle = stringResource(R.string.settings__fee__normal__description), + iconRes = R.drawable.ic_speed_normal, + iconTint = Colors.Brand, + value = SettingsButtonValue.BooleanValue(selectedSpeed is TransactionSpeed.Medium), + onClick = { onSpeedSelected(TransactionSpeed.Medium) }, + ) + SettingsButtonRow( + title = stringResource(R.string.settings__fee__slow__label), + subtitle = stringResource(R.string.settings__fee__slow__description), + iconRes = R.drawable.ic_speed_slow, + iconTint = Colors.Brand, + value = SettingsButtonValue.BooleanValue(selectedSpeed is TransactionSpeed.Slow), + onClick = { onSpeedSelected(TransactionSpeed.Slow) }, + ) + SettingsButtonRow( + title = stringResource(R.string.settings__fee__custom__label), + subtitle = stringResource(R.string.settings__fee__custom__description), + iconRes = R.drawable.ic_settings, + iconTint = Colors.White, + value = SettingsButtonValue.BooleanValue(selectedSpeed is TransactionSpeed.Custom), + onClick = onCustomFeeClick, + ) + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + TransactionSpeedSettingsContent( + selectedSpeed = TransactionSpeed.Medium, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/shared/util/Localizables.kt b/app/src/main/java/to/bitkit/ui/shared/util/Localizables.kt deleted file mode 100644 index 326d445a1..000000000 --- a/app/src/main/java/to/bitkit/ui/shared/util/Localizables.kt +++ /dev/null @@ -1,15 +0,0 @@ -package to.bitkit.ui.shared.util - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import to.bitkit.R -import to.bitkit.models.BitcoinDisplayUnit - -val BitcoinDisplayUnit.display: String - @Composable - get() { - return when (this) { - BitcoinDisplayUnit.MODERN -> stringResource(R.string.bitcoin_display_modern) - BitcoinDisplayUnit.CLASSIC -> stringResource(R.string.bitcoin_display_classic) - } - } diff --git a/app/src/main/java/to/bitkit/ui/utils/Localizables.kt b/app/src/main/java/to/bitkit/ui/utils/Localizables.kt new file mode 100644 index 000000000..8b4580d51 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/utils/Localizables.kt @@ -0,0 +1,27 @@ +package to.bitkit.ui.utils + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import to.bitkit.R +import to.bitkit.models.BitcoinDisplayUnit +import to.bitkit.models.TransactionSpeed + +val BitcoinDisplayUnit.display: String + @Composable + get() { + return when (this) { + BitcoinDisplayUnit.MODERN -> stringResource(R.string.bitcoin_display_modern) + BitcoinDisplayUnit.CLASSIC -> stringResource(R.string.bitcoin_display_classic) + } + } + +val TransactionSpeed.displayText: String + @Composable + get() { + return when (this) { + is TransactionSpeed.Fast -> stringResource(R.string.settings__fee__fast__value) + is TransactionSpeed.Medium -> stringResource(R.string.settings__fee__normal__value) + is TransactionSpeed.Slow -> stringResource(R.string.settings__fee__slow__value) + is TransactionSpeed.Custom -> stringResource(R.string.settings__fee__custom__value) + } + } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 99856a89a..3243b62a8 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -35,6 +35,7 @@ import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Toast +import to.bitkit.models.TransactionSpeed import to.bitkit.models.toActivityFilter import to.bitkit.models.toTxType import to.bitkit.repositories.LightningRepo @@ -161,6 +162,15 @@ class AppViewModel @Inject constructor( } } + val defaultTransactionSpeed = settingsStore.defaultTransactionSpeed + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), TransactionSpeed.Medium) + + fun setDefaultTransactionSpeed(speed: TransactionSpeed) { + viewModelScope.launch { + settingsStore.setDefaultTransactionSpeed(speed) + } + } + private val _isAuthenticated = MutableStateFlow(false) val isAuthenticated = _isAuthenticated.asStateFlow() diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index edafeb20b..e84d60bf8 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.data.SettingsStore +import to.bitkit.models.TransactionSpeed import to.bitkit.repositories.LightningRepo import to.bitkit.services.CoreService import to.bitkit.services.CurrencyService @@ -66,15 +67,16 @@ class TransferViewModel @Inject constructor( } /** Pays for the order and start watching it for state updates */ - fun onTransferToSpendingConfirm(order: IBtOrder) { + fun onTransferToSpendingConfirm(order: IBtOrder, speed: TransactionSpeed? = null) { viewModelScope.launch { - try { - lightningRepo.sendOnChain(address = order.payment.onchain.address, sats = order.feeSat) - settingsStore.setLightningSetupStep(0) - watchOrder(order.id) - } catch (e: Throwable) { - ToastEventBus.send(e) - } + lightningRepo.sendOnChain(address = order.payment.onchain.address, sats = order.feeSat, speed = speed) + .onSuccess { + settingsStore.setLightningSetupStep(0) + watchOrder(order.id) + } + .onFailure { error -> + ToastEventBus.send(error) + } } } diff --git a/app/src/main/res/drawable/ic_checkmark.xml b/app/src/main/res/drawable/ic_checkmark.xml new file mode 100644 index 000000000..ae73574fa --- /dev/null +++ b/app/src/main/res/drawable/ic_checkmark.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 000000000..c9eaadb5a --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,19 @@ + + + + +