From a952badabf7d4fa2709e37125a41bb30bc680a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20Nehasil?= Date: Sat, 14 Mar 2026 15:45:04 +0100 Subject: [PATCH] Binance UX improvements: fiat precision, lot size info & network error handling - Fix fiat amount display: show 2 decimal places (4.36 instead of 4) - Add Binance lot size info note per crypto in plan form and plan details - Add localized network error message for credential validation (CS/EN) - Add fiatWhole() formatter for chart axis labels (no decimals) Co-Authored-By: Claude Opus 4.6 --- .../com/accbot/dca/domain/model/Models.kt | 14 +++- .../ValidateAndSaveCredentialsUseCase.kt | 6 ++ .../credentials/CredentialFormDelegate.kt | 48 ++++++++++--- .../dca/presentation/plan/PlanFormContent.kt | 26 +++++++ .../dca/presentation/screens/AddPlanScreen.kt | 1 + .../presentation/screens/AddPlanViewModel.kt | 5 ++ .../screens/exchanges/AddExchangeScreen.kt | 3 +- .../screens/exchanges/ExchangeDetailScreen.kt | 3 +- .../screens/onboarding/ExchangeSetupScreen.kt | 3 +- .../screens/onboarding/FirstPlanScreen.kt | 1 + .../screens/plans/EditPlanScreen.kt | 1 + .../screens/plans/EditPlanViewModel.kt | 3 + .../screens/plans/PlanDetailsScreen.kt | 71 +++++++++++++++++-- .../presentation/utils/NumberFormatters.kt | 19 +++-- .../app/src/main/res/values-cs/strings.xml | 2 + .../app/src/main/res/values/strings.xml | 2 + 16 files changed, 186 insertions(+), 22 deletions(-) diff --git a/accbot-android/app/src/main/java/com/accbot/dca/domain/model/Models.kt b/accbot-android/app/src/main/java/com/accbot/dca/domain/model/Models.kt index 1f5dd84..1838ae8 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/domain/model/Models.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/domain/model/Models.kt @@ -82,7 +82,19 @@ enum class Exchange( supportedCryptos = listOf("BTC", "ETH", "SOL", "ADA"), minOrderSize = mapOf("EUR" to BigDecimal("1"), "USD" to BigDecimal("1")), sandboxSupport = SandboxSupport.FULL - ) + ); + + companion object { + /** Binance LOT_SIZE step sizes per crypto (from /api/v3/exchangeInfo). */ + val binanceLotStepSize = mapOf( + "BTC" to "0.00001", + "ETH" to "0.0001", + "BNB" to "0.001", + "SOL" to "0.001", + "ADA" to "0.1", + "DOT" to "0.01" + ) + } } /** diff --git a/accbot-android/app/src/main/java/com/accbot/dca/domain/usecase/ValidateAndSaveCredentialsUseCase.kt b/accbot-android/app/src/main/java/com/accbot/dca/domain/usecase/ValidateAndSaveCredentialsUseCase.kt index 5ead9ab..29866cc 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/domain/usecase/ValidateAndSaveCredentialsUseCase.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/domain/usecase/ValidateAndSaveCredentialsUseCase.kt @@ -5,6 +5,7 @@ import com.accbot.dca.data.local.UserPreferences import com.accbot.dca.domain.model.Exchange import com.accbot.dca.domain.model.ExchangeCredentials import com.accbot.dca.exchange.ExchangeApiFactory +import java.net.UnknownHostException import javax.inject.Inject /** @@ -13,6 +14,7 @@ import javax.inject.Inject sealed class CredentialValidationResult { data object Success : CredentialValidationResult() data class Error(val message: String) : CredentialValidationResult() + data object NetworkError : CredentialValidationResult() } /** @@ -82,6 +84,10 @@ class ValidateAndSaveCredentialsUseCase @Inject constructor( } else "" CredentialValidationResult.Error("Invalid API credentials.$hint") } + } catch (e: UnknownHostException) { + CredentialValidationResult.NetworkError + } catch (e: java.io.IOException) { + CredentialValidationResult.NetworkError } catch (e: Exception) { val isSandbox = userPreferences.isSandboxMode() val hint = if (isSandbox) { diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/credentials/CredentialFormDelegate.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/credentials/CredentialFormDelegate.kt index 6734f85..b750701 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/credentials/CredentialFormDelegate.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/credentials/CredentialFormDelegate.kt @@ -1,6 +1,10 @@ package com.accbot.dca.presentation.credentials +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.stringResource +import com.accbot.dca.R import com.accbot.dca.data.local.CredentialsStore import com.accbot.dca.data.local.UserPreferences import com.accbot.dca.domain.model.Exchange @@ -29,10 +33,21 @@ data class CredentialFormState( val isValidatingCredentials: Boolean = false, val credentialsValid: Boolean = false, val credentialsError: String? = null, + @StringRes val credentialsErrorRes: Int = 0, val isSandboxMode: Boolean = false, val availableExchanges: List = emptyList(), val showExperimental: Boolean = false -) +) { + val hasCredentialsError: Boolean + get() = credentialsError != null || credentialsErrorRes != 0 +} + +/** Resolve the credentials error to a localized string. */ +val CredentialFormState.resolvedCredentialsError: String? + @Composable get() = when { + credentialsErrorRes != 0 -> stringResource(credentialsErrorRes) + else -> credentialsError + } /** * Shared delegate for credential form state and logic. @@ -76,7 +91,7 @@ class CredentialFormDelegate( apiSecret = credentials?.apiSecret ?: "", passphrase = credentials?.passphrase ?: "", clientId = credentials?.clientId ?: "", - credentialsError = null + credentialsError = null, credentialsErrorRes = 0 ) } } @@ -95,25 +110,25 @@ class CredentialFormDelegate( apiKey = "", apiSecret = "", passphrase = "", - credentialsError = null + credentialsError = null, credentialsErrorRes = 0 ) } } fun setClientId(value: String) { - _state.update { it.copy(clientId = value, credentialsError = null) } + _state.update { it.copy(clientId = value, credentialsError = null, credentialsErrorRes = 0) } } fun setApiKey(value: String) { - _state.update { it.copy(apiKey = value, credentialsError = null) } + _state.update { it.copy(apiKey = value, credentialsError = null, credentialsErrorRes = 0) } } fun setApiSecret(value: String) { - _state.update { it.copy(apiSecret = value, credentialsError = null) } + _state.update { it.copy(apiSecret = value, credentialsError = null, credentialsErrorRes = 0) } } fun setPassphrase(value: String) { - _state.update { it.copy(passphrase = value, credentialsError = null) } + _state.update { it.copy(passphrase = value, credentialsError = null, credentialsErrorRes = 0) } } fun validateAndSaveCredentials(onSuccess: () -> Unit) { @@ -123,7 +138,7 @@ class CredentialFormDelegate( val exchange = state.selectedExchange ?: return coroutineScope.launch { - _state.update { it.copy(isValidatingCredentials = true, credentialsError = null) } + _state.update { it.copy(isValidatingCredentials = true, credentialsError = null, credentialsErrorRes = 0) } val result = validateAndSaveCredentialsUseCase.execute( exchange = exchange, @@ -152,10 +167,27 @@ class CredentialFormDelegate( ) } } + is CredentialValidationResult.NetworkError -> { + _state.update { + it.copy( + isValidatingCredentials = false, + credentialsErrorRes = R.string.error_no_internet + ) + } + } } } } + fun notifyNetworkError() { + _state.update { + it.copy( + isValidatingCredentials = false, + credentialsErrorRes = R.string.error_no_internet + ) + } + } + fun setExperimentalExchangesEnabled(enabled: Boolean) { userPreferences.setExperimentalExchangesEnabled(enabled) val isSandbox = _state.value.isSandboxMode diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/plan/PlanFormContent.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/plan/PlanFormContent.kt index 2984cde..7b25cf9 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/plan/PlanFormContent.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/plan/PlanFormContent.kt @@ -3,6 +3,8 @@ package com.accbot.dca.presentation.plan import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -18,6 +20,7 @@ import androidx.compose.ui.unit.dp import com.accbot.dca.R import com.accbot.dca.domain.model.DcaFrequency import com.accbot.dca.domain.model.DcaStrategy +import com.accbot.dca.domain.model.Exchange import com.accbot.dca.presentation.components.AmountInputWithPresets import com.accbot.dca.presentation.components.ChipGroup import com.accbot.dca.presentation.components.FrequencyDropdown @@ -52,6 +55,7 @@ fun PlanFormContent( onWithdrawalAddressChanged: (String) -> Unit, onTargetAmountChanged: (String) -> Unit, modifier: Modifier = Modifier, + exchange: Exchange? = null, showCryptoFiatSelection: Boolean = true, errorMessage: String? = null ) { @@ -93,6 +97,28 @@ fun PlanFormContent( minOrderSize = state.minOrderSize, amountBelowMinimum = state.amountBelowMinimum ) + if (exchange == Exchange.BINANCE) { + val stepSize = Exchange.binanceLotStepSize[state.selectedCrypto] + if (stepSize != null) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + Text( + text = stringResource(R.string.binance_lot_size_note, state.selectedCrypto, stepSize), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } } // Frequency selection diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanScreen.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanScreen.kt index 1414c73..847b977 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanScreen.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanScreen.kt @@ -213,6 +213,7 @@ fun AddPlanScreen( onWithdrawalEnabledChanged = viewModel.planForm::setWithdrawalEnabled, onWithdrawalAddressChanged = viewModel.planForm::setWithdrawalAddress, onTargetAmountChanged = viewModel.planForm::setTargetAmount, + exchange = cred.selectedExchange, errorMessage = uiState.errorMessage ) diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanViewModel.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanViewModel.kt index a3e3388..06e89aa 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanViewModel.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/AddPlanViewModel.kt @@ -111,6 +111,11 @@ class AddPlanViewModel @Inject constructor( } return@launch } + is CredentialValidationResult.NetworkError -> { + credentialForm.notifyNetworkError() + _localState.update { it.copy(isLoading = false) } + return@launch + } is CredentialValidationResult.Success -> { // Credentials validated and saved, continue with plan creation } diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/AddExchangeScreen.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/AddExchangeScreen.kt index 9662159..c12c369 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/AddExchangeScreen.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/AddExchangeScreen.kt @@ -40,6 +40,7 @@ import com.accbot.dca.domain.usecase.ApiImportResultState import com.accbot.dca.presentation.components.AccBotTopAppBar import com.accbot.dca.presentation.components.ApiImportResultDialog import com.accbot.dca.presentation.components.CredentialsInputCard +import com.accbot.dca.presentation.credentials.resolvedCredentialsError import com.accbot.dca.presentation.components.ExchangeSelectionTile import com.accbot.dca.presentation.components.ExperimentalExchangeDisclaimer import com.accbot.dca.presentation.components.RequestExchangeTile @@ -135,7 +136,7 @@ fun AddExchangeScreen( apiSecret = uiState.credentialForm.apiSecret, passphrase = uiState.credentialForm.passphrase, isValidating = uiState.credentialForm.isValidatingCredentials, - error = uiState.credentialForm.credentialsError, + error = uiState.credentialForm.resolvedCredentialsError, onClientIdChange = viewModel.credentialForm::setClientId, onApiKeyChange = viewModel.credentialForm::setApiKey, onApiSecretChange = viewModel.credentialForm::setApiSecret, diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/ExchangeDetailScreen.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/ExchangeDetailScreen.kt index 3847994..d51a323 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/ExchangeDetailScreen.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/exchanges/ExchangeDetailScreen.kt @@ -29,6 +29,7 @@ import com.accbot.dca.domain.model.supportsApiImport import com.accbot.dca.presentation.components.AccBotTopAppBar import com.accbot.dca.presentation.components.ApiImportResultDialog import com.accbot.dca.presentation.components.CredentialsInputCard +import com.accbot.dca.presentation.credentials.resolvedCredentialsError import com.accbot.dca.presentation.components.ExchangeAvatar import com.accbot.dca.presentation.components.ImportConfigDialog import com.accbot.dca.presentation.ui.theme.Error @@ -251,7 +252,7 @@ fun ExchangeDetailScreen( onApiKeyChange = viewModel.credentialForm::setApiKey, onApiSecretChange = viewModel.credentialForm::setApiSecret, onPassphraseChange = viewModel.credentialForm::setPassphrase, - errorMessage = uiState.credentialForm.credentialsError, + errorMessage = uiState.credentialForm.resolvedCredentialsError, isValidating = uiState.credentialForm.isValidatingCredentials ) diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/ExchangeSetupScreen.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/ExchangeSetupScreen.kt index de54572..6d12f87 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/ExchangeSetupScreen.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/ExchangeSetupScreen.kt @@ -26,6 +26,7 @@ import com.accbot.dca.domain.model.Exchange import com.accbot.dca.domain.model.isStable import com.accbot.dca.presentation.components.AccBotTopAppBar import com.accbot.dca.presentation.components.CredentialsInputCard +import com.accbot.dca.presentation.credentials.resolvedCredentialsError import com.accbot.dca.presentation.components.ExchangeSelectionGrid import com.accbot.dca.presentation.components.ExchangeInstructionsCard import com.accbot.dca.presentation.components.ExperimentalExchangeDisclaimer @@ -191,7 +192,7 @@ fun ExchangeSetupScreen( onApiKeyChange = viewModel.credentialForm::setApiKey, onApiSecretChange = viewModel.credentialForm::setApiSecret, onPassphraseChange = viewModel.credentialForm::setPassphrase, - errorMessage = cred.credentialsError, + errorMessage = cred.resolvedCredentialsError, isValidating = cred.isValidatingCredentials ) } diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/FirstPlanScreen.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/FirstPlanScreen.kt index ad8f95d..7c6a4d7 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/FirstPlanScreen.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/onboarding/FirstPlanScreen.kt @@ -102,6 +102,7 @@ fun FirstPlanScreen( onWithdrawalEnabledChanged = viewModel.planForm::setWithdrawalEnabled, onWithdrawalAddressChanged = viewModel.planForm::setWithdrawalAddress, onTargetAmountChanged = viewModel.planForm::setTargetAmount, + exchange = uiState.credentialForm.selectedExchange, errorMessage = uiState.error ) diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanScreen.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanScreen.kt index 99af2d9..5ca28cd 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanScreen.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanScreen.kt @@ -127,6 +127,7 @@ fun EditPlanScreen( onWithdrawalEnabledChanged = viewModel.planForm::setWithdrawalEnabled, onWithdrawalAddressChanged = viewModel.planForm::setWithdrawalAddress, onTargetAmountChanged = viewModel.planForm::setTargetAmount, + exchange = uiState.exchange, errorMessage = if (uiState.isSaving) uiState.error else null ) } diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanViewModel.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanViewModel.kt index b9813d0..58b41a2 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanViewModel.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/EditPlanViewModel.kt @@ -6,6 +6,7 @@ import com.accbot.dca.data.local.DcaPlanDao import com.accbot.dca.data.local.DcaPlanEntity import com.accbot.dca.domain.model.DcaFrequency import com.accbot.dca.domain.model.DcaStrategy +import com.accbot.dca.domain.model.Exchange import com.accbot.dca.domain.usecase.CalculateMonthlyCostUseCase import com.accbot.dca.domain.util.CronUtils import com.accbot.dca.data.local.UserPreferences @@ -26,6 +27,7 @@ data class EditPlanUiState( val planId: Long = 0, val crypto: String = "", val fiat: String = "", + val exchange: Exchange? = null, val exchangeName: String = "", // Plan form (from delegate) @@ -79,6 +81,7 @@ class EditPlanViewModel @Inject constructor( planId = plan.id, crypto = plan.crypto, fiat = plan.fiat, + exchange = plan.exchange, exchangeName = plan.exchange.displayName, isLoading = false ) diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/PlanDetailsScreen.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/PlanDetailsScreen.kt index 4b2429f..f51122f 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/PlanDetailsScreen.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/screens/plans/PlanDetailsScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.accbot.dca.R import com.accbot.dca.domain.model.DcaStrategy +import com.accbot.dca.domain.model.Exchange import com.accbot.dca.domain.model.supportsApiImport import com.accbot.dca.presentation.components.* import androidx.compose.foundation.text.KeyboardOptions @@ -53,6 +54,7 @@ fun PlanDetailsScreen( var showDeleteDialog by rememberSaveable { mutableStateOf(false) } var deletePlanConfirmText by rememberSaveable { mutableStateOf("") } var showStrategyInfo by rememberSaveable { mutableStateOf(false) } + var showLotSizeInfo by rememberSaveable { mutableStateOf(false) } var showDeleteTransactionsDialog by rememberSaveable { mutableStateOf(false) } var deleteTransactionsConfirmText by rememberSaveable { mutableStateOf("") } var dangerZoneExpanded by rememberSaveable { mutableStateOf(false) } @@ -433,11 +435,47 @@ fun PlanDetailsScreen( modifier = Modifier.semantics { heading() } ) - PlanConfigRow( - icon = Icons.Default.AttachMoney, - label = stringResource(R.string.plan_details_amount), - value = "${plan.amount} ${plan.fiat}" - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.AttachMoney, + contentDescription = null, + tint = accentColor(), + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = stringResource(R.string.plan_details_amount), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "${plan.amount} ${plan.fiat}", + fontWeight = FontWeight.Medium + ) + } + } + if (plan.exchange == Exchange.BINANCE) { + IconButton( + onClick = { showLotSizeInfo = true } + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = accentColor(), + modifier = Modifier.size(20.dp) + ) + } + } + } val frequencyDisplayText = if (plan.frequency == com.accbot.dca.domain.model.DcaFrequency.CUSTOM && plan.cronExpression != null) { com.accbot.dca.domain.util.CronUtils.describeCron(plan.cronExpression) ?: stringResource(plan.frequency.displayNameRes) @@ -521,6 +559,29 @@ fun PlanDetailsScreen( onDismiss = { showStrategyInfo = false } ) } + + // Binance lot size info dialog + if (showLotSizeInfo) { + AlertDialog( + onDismissRequest = { showLotSizeInfo = false }, + confirmButton = { + TextButton(onClick = { showLotSizeInfo = false }) { + Text(stringResource(R.string.common_done)) + } + }, + icon = { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = accentColor() + ) + }, + text = { + val stepSize = Exchange.binanceLotStepSize[plan.crypto] ?: "0.00001" + Text(stringResource(R.string.binance_lot_size_note, plan.crypto, stepSize)) + } + ) + } } // 4. Exchange Balance card (with heading) diff --git a/accbot-android/app/src/main/java/com/accbot/dca/presentation/utils/NumberFormatters.kt b/accbot-android/app/src/main/java/com/accbot/dca/presentation/utils/NumberFormatters.kt index f13502c..8620c28 100644 --- a/accbot-android/app/src/main/java/com/accbot/dca/presentation/utils/NumberFormatters.kt +++ b/accbot-android/app/src/main/java/com/accbot/dca/presentation/utils/NumberFormatters.kt @@ -12,13 +12,13 @@ import java.util.Locale */ object NumberFormatters { - /** Format fiat amounts: whole numbers, with grouping (e.g. 1,235 or 1 235) */ + /** Format fiat amounts: 2 decimal places, with grouping (e.g. 1,235.00 or 1 235,00) */ fun fiat(value: BigDecimal): String { val nf = NumberFormat.getInstance(Locale.getDefault()) - nf.minimumFractionDigits = 0 - nf.maximumFractionDigits = 0 + nf.minimumFractionDigits = 2 + nf.maximumFractionDigits = 2 nf.isGroupingUsed = true - return nf.format(value.setScale(0, RoundingMode.HALF_UP)) + return nf.format(value.setScale(2, RoundingMode.HALF_UP)) } /** Format fee amounts: 2 decimal places, with grouping (e.g. 1.50 or 0.45) */ @@ -30,6 +30,15 @@ object NumberFormatters { return nf.format(value.setScale(2, RoundingMode.HALF_UP)) } + /** Format fiat as whole number: no decimal places, with grouping (e.g. 1,235 or 1 235). Used for chart axis labels. */ + fun fiatWhole(value: BigDecimal): String { + val nf = NumberFormat.getInstance(Locale.getDefault()) + nf.minimumFractionDigits = 0 + nf.maximumFractionDigits = 0 + nf.isGroupingUsed = true + return nf.format(value.setScale(0, RoundingMode.HALF_UP)) + } + /** Compact fiat for Y-axis labels: 1800000 → "1.8M", 50000 → "50K", 800 → "800" */ fun compactFiat(value: BigDecimal): String { val abs = value.abs() @@ -42,7 +51,7 @@ object NumberFormatters { val k = value.divide(BigDecimal(1_000), 1, RoundingMode.HALF_UP) "${stripTrailingDecimalZero(k)}K" } - else -> fiat(value) + else -> fiatWhole(value) } } diff --git a/accbot-android/app/src/main/res/values-cs/strings.xml b/accbot-android/app/src/main/res/values-cs/strings.xml index 09e0428..cc24392 100644 --- a/accbot-android/app/src/main/res/values-cs/strings.xml +++ b/accbot-android/app/src/main/res/values-cs/strings.xml @@ -410,6 +410,7 @@ Skrýt heslo Vyžadováno pro %1$s Přihlašovací údaje uloženy lokálně se šifrováním + Žádné připojení k internetu. Zkontrolujte síť a zkuste to znovu. Šifrováno lokálně pomocí AES-256 @@ -496,6 +497,7 @@ Měna Vlastní částka Minimum: %1$s %2$s + Binance zaokrouhluje nákup %1$s na %2$s %1$s. Skutečná částka nákupu může být o něco nižší, než je nastaveno. Jak často Doporučeno pro začátečníky Nižší frekvence, větší nákupy diff --git a/accbot-android/app/src/main/res/values/strings.xml b/accbot-android/app/src/main/res/values/strings.xml index 8184e05..3423123 100644 --- a/accbot-android/app/src/main/res/values/strings.xml +++ b/accbot-android/app/src/main/res/values/strings.xml @@ -409,6 +409,7 @@ Hide password Required for %1$s Credentials stored locally with encryption + No internet connection. Please check your network and try again. Encrypted locally with AES-256 @@ -495,6 +496,7 @@ Currency Custom amount Minimum: %1$s %2$s + Binance rounds %1$s purchases to %2$s %1$s. The actual purchase amount may be slightly less than configured. How often Recommended for beginners Lower frequency, larger purchases