From c86990cdb4f028ab0a23af0670290fe96a534716 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 06:04:20 -0600 Subject: [PATCH 1/4] fix: unify usdt balance and amount presentation --- .../PaymentRequestAmountLocaleTest.kt | 8 ++--- .../wallets/usdt/UsdtPaymentContentTest.kt | 29 +++++++++++++++-- .../main/java/to/bitkit/models/Currency.kt | 10 ++++-- .../bitkit/ui/components/PaykitAmountView.kt | 12 +++---- .../bitkit/ui/components/UsdtAmountHeader.kt | 29 +++++++---------- .../CreatePaymentRequestScreen.kt | 7 +---- .../screens/wallets/send/SendAmountScreen.kt | 2 -- .../wallets/usdt/UsdtDepositReceiveScreen.kt | 6 ++-- .../wallets/usdt/UsdtReceiveAmountContent.kt | 2 -- .../screens/wallets/usdt/UsdtSendContent.kt | 2 -- .../screens/wallets/usdt/UsdtWalletScreen.kt | 27 +++------------- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-b+es+419/strings.xml | 1 - app/src/main/res/values-ca/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-es-rES/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - .../ui/components/PaykitAmountViewTest.kt | 27 ++++++++++------ .../bitkit/ui/components/UsdtDisplayTest.kt | 31 ++++++++++--------- .../wallets/usdt/UsdtDepositViewModelTest.kt | 4 +-- changelog.d/next/1655.fixed.md | 1 + journeys/README.md | 2 +- journeys/usdt-number-format.xml | 12 +++++++ journeys/usdt-wallet-navigation.xml | 3 +- 34 files changed, 113 insertions(+), 117 deletions(-) create mode 100644 changelog.d/next/1655.fixed.md create mode 100644 journeys/usdt-number-format.xml diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt index 5a54a6b2a3..d7b3503b08 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt @@ -45,7 +45,7 @@ class PaymentRequestAmountLocaleTest { } @Test - fun commaDecimalEntryPreservesExactCentsAndTrailingZeros() { + fun dotDecimalEntryPreservesExactCentsInCommaRegions() { var submitted: PaykitPaymentRequestDraft? = null val amountInput = AmountInputViewModel(AmountInputHandler.stub()) composeTestRule.setContent { @@ -67,15 +67,15 @@ class PaymentRequestAmountLocaleTest { } } - composeTestRule.onNodeWithText(",", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText(".", useUnmergedTree = true).assertIsDisplayed() listOf("N1", "N5", "N0", "NDecimal").forEach { composeTestRule.onNodeWithTag(it).performClick() } - composeTestRule.onNodeWithText("150,", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText("150.", useUnmergedTree = true).assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestAmountContinue").assertIsEnabled() composeTestRule.onNodeWithTag("N5").performClick() composeTestRule.onNodeWithTag("N0").performClick() - composeTestRule.onNodeWithText("150,50", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText("150.50", useUnmergedTree = true).assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestAmountContinue").performClick() assertEquals(PaykitAmount(PaykitAsset.USD, 15_050uL), submitted?.amount) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt index abefd7e155..b51360f4bf 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.test.assertDoesNotExist import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -79,7 +80,7 @@ class UsdtPaymentContentTest { } @Test - fun decimalKeyMatchesAmountLocaleWithoutChangingInputEncoding() { + fun decimalKeyUsesDotInCommaRegionsWithoutChangingInputEncoding() { Locale.setDefault(Locale.GERMANY) val amount = mutableStateOf("5") composeTestRule.setContent { @@ -97,13 +98,35 @@ class UsdtPaymentContentTest { } } } - composeTestRule.onNodeWithText(",").assertIsDisplayed() + composeTestRule.onNodeWithText(".").assertIsDisplayed() composeTestRule.onNodeWithTag("NDecimal").performClick() composeTestRule.onNodeWithTag("N5").performClick() - composeTestRule.onNodeWithText("5,5").assertIsDisplayed() + composeTestRule.onNodeWithText("5.5").assertIsDisplayed() composeTestRule.runOnIdle { assertEquals("5.5", amount.value) } } + @Test + fun walletShowsZeroSilentlyUntilBalanceArrivesAndKeepsBalancePrivacy() { + Locale.setDefault(Locale.GERMANY) + val wallet = mutableStateOf(UsdtWalletState()) + val hidden = mutableStateOf(false) + composeTestRule.setContent { + AppThemeSurface { + UsdtWalletContent( + wallet = wallet.value, + hideBalance = hidden.value, + onReceive = {}, onSend = {}, onBack = {}, + ) + } + } + composeTestRule.onNodeWithText("0.00").assertIsDisplayed() + composeTestRule.onNodeWithTag("UsdtHistorySync").assertDoesNotExist() + composeTestRule.runOnIdle { wallet.value = wallet.value.copy(balance = 1_234_567_890uL) } + composeTestRule.onNodeWithText("1,234.57").assertIsDisplayed() + composeTestRule.runOnIdle { hidden.value = true } + composeTestRule.onNodeWithText("1,234.57").assertDoesNotExist() + } + @Test fun largeHeadersAndNetworkLabelsRemainReadable() { composeTestRule.setContent { diff --git a/app/src/main/java/to/bitkit/models/Currency.kt b/app/src/main/java/to/bitkit/models/Currency.kt index c74930db7e..0f47af0bc1 100644 --- a/app/src/main/java/to/bitkit/models/Currency.kt +++ b/app/src/main/java/to/bitkit/models/Currency.kt @@ -139,7 +139,12 @@ fun Long.formatToClassicDisplay(locale: Locale = Locale.getDefault()): String { return formatter.format(asBtc()) } -fun BigDecimal.formatCurrency(decimalPlaces: Int = FIAT_DECIMALS, locale: Locale = Locale.getDefault()): String? { +fun BigDecimal.formatCurrency( + decimalPlaces: Int = FIAT_DECIMALS, + locale: Locale = Locale.getDefault(), + minimumFractionDigits: Int = decimalPlaces, + roundingMode: RoundingMode = RoundingMode.HALF_EVEN, +): String? { val symbols = DecimalFormatSymbols(locale).apply { decimalSeparator = DECIMAL_SEPARATOR groupingSeparator = FIAT_GROUPING_SEPARATOR @@ -147,8 +152,9 @@ fun BigDecimal.formatCurrency(decimalPlaces: Int = FIAT_DECIMALS, locale: Locale val decimalPlacesString = "0".repeat(decimalPlaces) val formatter = DecimalFormat("#,##0.$decimalPlacesString", symbols).apply { - minimumFractionDigits = decimalPlaces + this.minimumFractionDigits = minimumFractionDigits maximumFractionDigits = decimalPlaces + this.roundingMode = roundingMode isGroupingUsed = true } diff --git a/app/src/main/java/to/bitkit/ui/components/PaykitAmountView.kt b/app/src/main/java/to/bitkit/ui/components/PaykitAmountView.kt index 7623cd1de5..0871457dbc 100644 --- a/app/src/main/java/to/bitkit/ui/components/PaykitAmountView.kt +++ b/app/src/main/java/to/bitkit/ui/components/PaykitAmountView.kt @@ -8,9 +8,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import to.bitkit.models.PaykitAmount import to.bitkit.models.PaykitAsset +import to.bitkit.models.formatCurrency import to.bitkit.ui.theme.Colors import java.math.BigDecimal -import java.text.NumberFormat import java.util.Locale @Composable @@ -69,10 +69,6 @@ fun PaykitAmountCell( } } -internal fun usdDisplayAmount(cents: ULong, locale: Locale = Locale.getDefault()): String { - val format = NumberFormat.getNumberInstance(locale).apply { - minimumFractionDigits = PaykitAsset.USD.decimals - maximumFractionDigits = PaykitAsset.USD.decimals - } - return format.format(BigDecimal(cents.toString()).movePointLeft(PaykitAsset.USD.decimals)) -} +internal fun usdDisplayAmount(cents: ULong, locale: Locale = Locale.getDefault()): String = checkNotNull( + BigDecimal(cents.toString()).movePointLeft(PaykitAsset.USD.decimals).formatCurrency(locale = locale) +) diff --git a/app/src/main/java/to/bitkit/ui/components/UsdtAmountHeader.kt b/app/src/main/java/to/bitkit/ui/components/UsdtAmountHeader.kt index aa306ca4be..1f0f6df6cd 100644 --- a/app/src/main/java/to/bitkit/ui/components/UsdtAmountHeader.kt +++ b/app/src/main/java/to/bitkit/ui/components/UsdtAmountHeader.kt @@ -8,13 +8,12 @@ import to.bitkit.ext.nowMillis import to.bitkit.models.BITCOIN_SYMBOL import to.bitkit.models.PaykitAsset import to.bitkit.models.PaykitExchangeRate +import to.bitkit.models.formatCurrency import to.bitkit.models.formatMoney import to.bitkit.repositories.paykitRate import to.bitkit.ui.LocalCurrencies import java.math.BigDecimal import java.math.RoundingMode -import java.text.DecimalFormatSymbols -import java.text.NumberFormat import java.util.Locale @Composable @@ -34,7 +33,7 @@ internal fun UsdtAmountHeader( isBitcoinPrimary = false, smallRowText = btc ?: "USDT", smallRowSymbol = if (btc != null) BITCOIN_SYMBOL else null, - largeRowText = displayAmount ?: usdtInputAmount(amount), + largeRowText = displayAmount ?: amount, largeRowSymbol = "$", largeRowPrefix = prefix, showSymbol = true, @@ -46,26 +45,22 @@ internal fun UsdtAmountHeader( ) } -internal fun usdtInputAmount(amount: String, locale: Locale = Locale.getDefault()): String = - amount.replace('.', DecimalFormatSymbols.getInstance(locale).decimalSeparator) - private const val USDT_CENT = 10_000uL internal fun usdtOverviewAmount(amount: ULong, locale: Locale = Locale.getDefault()): String { - val format = NumberFormat.getNumberInstance(locale).apply { - minimumFractionDigits = 2 - maximumFractionDigits = 2 - roundingMode = RoundingMode.HALF_UP + if (amount > 0uL && amount < USDT_CENT) { + return "<" + checkNotNull(BigDecimal("0.01").formatCurrency(locale = locale)) } - if (amount > 0uL && amount < USDT_CENT) return "<" + format.format(BigDecimal("0.01")) - return format.format(BigDecimal(amount.toString()).movePointLeft(PaykitAsset.USDT.decimals)) + return checkNotNull( + BigDecimal(amount.toString()).movePointLeft(PaykitAsset.USDT.decimals) + .formatCurrency(locale = locale, roundingMode = RoundingMode.HALF_UP) + ) } -internal fun usdtExactAmount(amount: ULong, locale: Locale = Locale.getDefault()): String = - NumberFormat.getNumberInstance(locale).apply { - minimumFractionDigits = 2 - maximumFractionDigits = PaykitAsset.USDT.decimals - }.format(BigDecimal(amount.toString()).movePointLeft(PaykitAsset.USDT.decimals)) +internal fun usdtExactAmount(amount: ULong, locale: Locale = Locale.getDefault()): String = checkNotNull( + BigDecimal(amount.toString()).movePointLeft(PaykitAsset.USDT.decimals) + .formatCurrency(decimalPlaces = PaykitAsset.USDT.decimals, locale = locale, minimumFractionDigits = 2) +) internal fun usdtDisplaySats(amount: String, rate: PaykitExchangeRate?, now: Long = nowMillis()): Long? { val dollars = amount.toBigDecimalOrNull()?.takeIf { it.signum() >= 0 } ?: return null diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 6a5bc97dce..9b844440c5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -95,7 +94,6 @@ import to.bitkit.ui.utils.NumberPadInputHandler import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.AppViewModel -import java.text.DecimalFormatSymbols import kotlin.math.abs import kotlin.time.Clock import kotlin.time.Duration @@ -150,8 +148,6 @@ internal fun PaymentRequestAmountContent( ) { val currencies = LocalCurrencies.current val amountState by amountInputViewModel.uiState.collectAsStateWithLifecycle() - val locale = LocalConfiguration.current.locales[0] - val decimalSeparator = remember(locale) { DecimalFormatSymbols.getInstance(locale).decimalSeparator } var asset by remember(initialDraft.amount.asset) { mutableStateOf(initialDraft.amount.asset) } var dollars by remember(initialDraft.amount) { @@ -204,7 +200,7 @@ internal fun PaymentRequestAmountContent( ) } else { NumberPadAmountText( - value = dollars.ifEmpty { "0" }.replace('.', decimalSeparator), + value = dollars.ifEmpty { "0" }, symbol = "$", modifier = Modifier.testTag("PaymentRequestAmountField") ) @@ -261,7 +257,6 @@ internal fun PaymentRequestAmountContent( }, type = NumberPadType.DECIMAL, availableHeight = availableHeight, - decimalSeparator = decimalSeparator.toString(), modifier = Modifier.testTag("PaymentRequestNumberPad") ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 45eabb6bb7..95e846e58c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -103,7 +103,6 @@ import to.bitkit.viewmodels.SendFundingSource import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState import to.bitkit.viewmodels.previewAmountInputViewModel -import java.text.DecimalFormatSymbols /** Opacity of unavailable payment sources in the balance selector. */ private const val DISABLED_FUNDING_SOURCE_ALPHA = 0.30f @@ -419,7 +418,6 @@ private fun SendAmountNodeRunning( ) { NumberPad( type = NumberPadType.DECIMAL, - decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString(), availableHeight = this.maxHeight, enabled = uiState.isAmountEditable, onPress = { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt index 8562cdd6bb..31beddc78f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt @@ -49,6 +49,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SendCell import to.bitkit.ui.components.UsdtAmountHeader import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.components.usdDisplayAmount import to.bitkit.ui.components.usdtOverviewAmount import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing @@ -56,8 +57,6 @@ import to.bitkit.ui.screens.wallets.receive.ReceiveTab import to.bitkit.ui.shared.modifiers.swipeToChangeTab import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.Colors -import java.text.DecimalFormatSymbols -import java.text.NumberFormat import java.util.Locale @Composable @@ -316,8 +315,7 @@ internal val UsdtDepositNetwork.label: String internal fun String.depositNetwork(): UsdtDepositNetwork? = UsdtDepositNetwork.entries.firstOrNull { it.name.equals(this, ignoreCase = true) } internal fun String.usdCents(locale: Locale = Locale.getDefault()): String = toULongOrNull()?.let { - val whole = NumberFormat.getIntegerInstance(locale).format((it / 100u).toLong()) - "$$whole${DecimalFormatSymbols.getInstance(locale).decimalSeparator}${(it % 100u).toString().padStart(2, '0')}" + "$" + usdDisplayAmount(it, locale) } ?: "—" internal fun depositStatus(status: String, code: String?): Int = when (status) { "completed" -> R.string.usdt__confirmed diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt index a9e70c7e34..2ae1e189b1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt @@ -27,7 +27,6 @@ import to.bitkit.ui.components.UsdtAmountHeader import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.NumberPadInputHandler -import java.text.DecimalFormatSymbols @Composable internal fun ColumnScope.UsdtReceiveAmountContent( @@ -50,7 +49,6 @@ internal fun ColumnScope.UsdtReceiveAmountContent( BoxWithConstraints(contentAlignment = Alignment.BottomCenter, modifier = Modifier.weight(1f).fillMaxWidth()) { NumberPad( type = NumberPadType.DECIMAL, - decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString(), availableHeight = maxHeight, enabled = !busy, onDeleteLongPress = { onAmount("") }, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt index ed5c1a4bfb..853cc43643 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt @@ -97,7 +97,6 @@ import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.NumberPadInputHandler -import java.text.DecimalFormatSymbols import kotlin.time.Duration.Companion.seconds @Composable @@ -509,7 +508,6 @@ private fun UsdtAmountContent( BoxWithConstraints(contentAlignment = Alignment.Center, modifier = Modifier.weight(1f).fillMaxWidth()) { NumberPad( type = NumberPadType.DECIMAL, - decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString(), availableHeight = maxHeight, enabled = !state.busy && amountEditable, onDeleteLongPress = { onAmountChange("") }, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt index 6d30faeb66..6f22d1da5e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset @@ -60,7 +59,6 @@ import to.bitkit.repositories.UsdtWalletState import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BiometricsView import to.bitkit.ui.components.BodyMSB -import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BottomSheet import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Sheet @@ -105,9 +103,7 @@ fun RowScope.UsdtWalletBalance(onClick: () -> Unit, viewModel: UsdtViewModel = h if (settings.hideBalance) { UiConstants.HIDE_BALANCE_SHORT } else { - wallet.balance?.let { - usdtOverviewAmount(it) - } ?: "—" + "$ " + usdtOverviewAmount(wallet.balance ?: 0u) }, style = UsdtDetailStyles.HistoryTitle ) @@ -184,7 +180,6 @@ fun UsdtWalletScreen( } else { UsdtWalletContent( wallet = wallet, - error = state.refreshError, hideBalance = settings.hideBalance, onReceive = { app?.requestUsdtEnable { @@ -317,9 +312,8 @@ internal fun rememberUsdtPage(initialRecipient: String, state: UsdtSendState): M } @Composable -private fun UsdtWalletContent( +internal fun UsdtWalletContent( wallet: UsdtWalletState, - error: Int?, hideBalance: Boolean, onReceive: () -> Unit, onSend: () -> Unit, @@ -348,17 +342,13 @@ private fun UsdtWalletContent( item { VerticalSpacer(16.dp) UsdtAmountHeader( - amount = wallet.balance?.let { usdtFormatAmount(it) } ?: "—", - displayAmount = wallet.balance?.let { usdtOverviewAmount(it) }, + amount = usdtFormatAmount(wallet.balance ?: 0u), + displayAmount = usdtOverviewAmount(wallet.balance ?: 0u), hideBalance = hideBalance, onToggleHide = if (allowSwipe) ({ settings?.setHideBalance(!hideBalance) }) else null, modifier = Modifier.testTag("UsdtBalance") ) VerticalSpacer(32.dp) - error?.let { - BodyS(stringResource(it), color = Colors.Brand) - VerticalSpacer(16.dp) - } } itemsIndexed(wallet.transfers, key = { _, transfer -> transfer.id }) { index, transfer -> val heading = activityGroupTitleResource(transfer.timestamp) @@ -373,13 +363,6 @@ private fun UsdtWalletContent( VerticalSpacer(16.dp) } item { - if (!wallet.historyComplete) { - BodyS( - stringResource(R.string.usdt__history_sync), - color = Colors.White64, - modifier = Modifier.fillMaxWidth().background(Colors.Black).testTag("UsdtHistorySync") - ) - } VerticalSpacer(120.dp) } } @@ -483,6 +466,6 @@ internal enum class UsdtPage { @Composable private fun Preview() { AppThemeSurface { - UsdtWalletContent(UsdtWalletState(balance = 100_000_000u), null, false, {}, {}, {}) + UsdtWalletContent(UsdtWalletState(balance = 100_000_000u), false, {}, {}, {}) } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a3729f4157..ec95b839aa 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -817,7 +817,6 @@ تُدفع جميع الرسوم بـ USDT. قد تكون الرسوم النهائية أقل. من العنوان إخفاء التفاصيل - لا تزال مزامنة النشاط الأقدم جارية. رصيدك محدَّث عند توفر الاتصال. لا، شكرًا Bitcoin في الصميم. ودولارات عندما تحتاجها. فعّل USDT (Tether) لإرسال الدولارات الرقمية واستلامها باستخدام Bitkit. <accent>دولارات؟</accent>\nالقرار لك diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 0a7214189c..f1b6a1a159 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -834,7 +834,6 @@ Todas las comisiones se pagan en USDT. La comisión final puede ser menor. Origen Ocultar detalles - La actividad anterior aún se está sincronizando. Su saldo estará actualizado cuando haya conexión. No, gracias Bitcoin en esencia. Dólares cuando los necesite. Active USDT (Tether) para enviar y recibir dólares digitales con Bitkit. <accent>¿DÓLARES?</accent>\nUSTED DECIDE diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 12877bfec5..b6bf1a35e1 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -827,7 +827,6 @@ Totes les tarifes es paguen en USDT. La tarifa final pot ser inferior. Adreça d\'origen Amagar detalls - L\'activitat anterior encara s\'està sincronitzant. El teu saldo està al dia quan hi ha connexió. No, gràcies Bitcoin de cor. Dòlars quan els necessitis. Activa USDT (Tether) per enviar i rebre dòlars digitals amb Bitkit. <accent>DÒLARS?</accent>\nTU DECIDEIXES diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 057ac4e371..7bbe06dd9b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -831,7 +831,6 @@ Všechny poplatky se platí v USDT. Konečný poplatek může být nižší. Z adresy Skrýt podrobnosti - Starší aktivita se stále synchronizuje. Když je připojení k dispozici, je váš zůstatek aktuální. Ne, děkuji V srdci bitcoin. Dolary, když je potřebujete. Zapněte USDT (Tether) a posílejte a přijímejte digitální dolary s Bitkit. <accent>DOLARY?</accent>\nVAŠE VOLBA diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 76f55ebcce..885e098133 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -829,7 +829,6 @@ Alle Gebühren werden in USDT bezahlt. Die endgültige Gebühr kann niedriger sein. Absenderadresse Details ausblenden - Ältere Aktivitäten werden noch synchronisiert. Dein Guthaben ist aktuell, sobald eine Verbindung besteht. Nein danke Im Herzen Bitcoin. Dollar, wenn du sie brauchst. Aktiviere USDT (Tether), um mit Bitkit digitale Dollar zu senden und zu empfangen. <accent>DOLLAR?</accent>\nDU ENTSCHEIDEST diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 7716ce9422..2bed7847fb 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -819,7 +819,6 @@ Όλα τα τέλη πληρώνονται σε USDT. Το τελικό τέλος μπορεί να είναι χαμηλότερο. Από διεύθυνση Απόκρυψη λεπτομερειών - Η παλαιότερη δραστηριότητα συγχρονίζεται ακόμα. Το υπόλοιπό σου είναι ενημερωμένο όταν υπάρχει σύνδεση. Όχι, ευχαριστώ Bitcoin στην καρδιά. Δολάρια όταν τα χρειάζεσαι. Ενεργοποίησε το USDT (Tether) για να στέλνεις και να λαμβάνεις ψηφιακά δολάρια με το Bitkit. <accent>ΔΟΛΑΡΙΑ;</accent>\nΔΙΚΗ ΣΟΥ ΕΠΙΛΟΓΗ diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 625c9643ea..6f9980c923 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -830,7 +830,6 @@ Todas las comisiones se pagan en USDT. La comisión final puede ser menor. Origen Ocultar detalles - La actividad anterior aún se está sincronizando. Tu saldo estará actualizado cuando haya conexión. No, gracias Bitcoin en esencia. Dólares cuando los necesites. Activa USDT (Tether) para enviar y recibir dólares digitales con Bitkit. <accent>¿DÓLARES?</accent>\nTÚ DECIDES diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d8e7343e9a..d62f30be15 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -827,7 +827,6 @@ Todas las comisiones se pagan en USDT. La comisión final puede ser menor. Origen Ocultar detalles - La actividad anterior aún se está sincronizando. Su saldo estará actualizado cuando haya conexión. No, gracias Bitcoin en esencia. Dólares cuando los necesite. Active USDT (Tether) para enviar y recibir dólares digitales con Bitkit. <accent>¿DÓLARES?</accent>\nUSTED DECIDE diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fb5734cbb9..f57f281ab6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -831,7 +831,6 @@ Tous les frais sont payés en USDT. Les frais finaux peuvent être inférieurs. Adresse d\'envoi Masquer les détails - L\'activité plus ancienne est encore en cours de synchronisation. Votre solde est à jour lorsque la connexion est disponible. Non merci Bitcoin dans l\'âme. Des dollars quand vous en avez besoin. Activez USDT (Tether) pour envoyer et recevoir des dollars numériques avec Bitkit. <accent>DES DOLLARS ?</accent>\nÀ VOUS DE VOIR diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c3b3b545e1..9c82db26b4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -830,7 +830,6 @@ Tutte le commissioni sono pagate in USDT. La commissione finale potrebbe essere inferiore. Indirizzo mittente Nascondi dettagli - Le attività meno recenti sono ancora in sincronizzazione. Il tuo saldo è aggiornato quando la connessione è disponibile. No Grazie Bitcoin nel cuore. Dollari quando ti servono. Attiva USDT (Tether) per inviare e ricevere dollari digitali con Bitkit. <accent>DOLLARI?</accent>\nDECIDI TU diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 54dae1eec1..9587bad195 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -826,7 +826,6 @@ Alle vergoedingen worden in USDT betaald. De uiteindelijke vergoeding kan lager zijn. Van adres Details verbergen - Oudere activiteit wordt nog gesynchroniseerd. Uw saldo is actueel zodra er verbinding is. Nee, bedankt Bitcoin in hart en nieren. Dollars wanneer u ze nodig heeft. Schakel USDT (Tether) in om digitale dollars te verzenden en ontvangen met Bitkit. <accent>DOLLARS?</accent>\nU BESLIST diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 05f2d1755e..51ff677b2e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -833,7 +833,6 @@ Wszystkie opłaty są płacone w USDT. Ostateczna opłata może być niższa. Z adresu Ukryj szczegóły - Starsza aktywność wciąż się synchronizuje. Saldo jest aktualne, gdy połączenie jest dostępne. Nie, dzięki Bitcoin w sercu. Dolary, gdy ich potrzebujesz. Włącz USDT (Tether), aby wysyłać i otrzymywać cyfrowe dolary w Bitkit. <accent>DOLARY?</accent>\nTWÓJ WYBÓR diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fddc8c369f..8d22ff4284 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -834,7 +834,6 @@ Todas as taxas são pagas em USDT. A taxa final pode ser menor. Endereço de origem Ocultar detalhes - A atividade mais antiga ainda está sincronizando. Seu saldo fica atualizado sempre que houver conexão. Não, Obrigado Bitcoin no coração. Dólares quando você precisar. Ative o USDT (Tether) para enviar e receber dólares digitais com a Bitkit. <accent>DÓLARES?</accent>\nVOCÊ DECIDE diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 275913a53f..5b5ed1664c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1040,7 +1040,6 @@ Todas as taxas são pagas em USDT. A taxa final pode ser inferior. Endereço de origem Ocultar detalhes - A atividade mais antiga ainda está a sincronizar. O seu saldo está atualizado sempre que houver ligação. Não, obrigado Bitcoin no coração. Dólares quando precisar. Ative o USDT (Tether) para enviar e receber dólares digitais com a Bitkit. <accent>DÓLARES?</accent>\nA ESCOLHA É SUA diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1eac79d7d3..5d63ae1ec4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -852,7 +852,6 @@ Все комиссии оплачиваются в USDT. Итоговая комиссия может быть ниже. С адреса Скрыть детали - Более ранняя активность ещё синхронизируется. Ваш баланс актуален, когда есть подключение. Нет, Спасибо Биткоин в душе. Доллары, когда они нужны. Включите USDT (Tether), чтобы отправлять и получать цифровые доллары в Bitkit. <accent>ДОЛЛАРЫ?</accent>\nРЕШАТЬ ВАМ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d940bcf647..98a4da0afb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1237,7 +1237,6 @@ All fees are paid in USDT. Final fee may be lower. From address Hide details - Older activity is still syncing. Your balance is up to date when the connection is available. No Thanks Bitcoin at heart. Dollars when you need them. Enable USDT (Tether) to send and receive digital dollars with Bitkit. <accent>DOLLARS?</accent>\nYOUR CALL diff --git a/app/src/test/java/to/bitkit/ui/components/PaykitAmountViewTest.kt b/app/src/test/java/to/bitkit/ui/components/PaykitAmountViewTest.kt index af1b23370c..e1ace12947 100644 --- a/app/src/test/java/to/bitkit/ui/components/PaykitAmountViewTest.kt +++ b/app/src/test/java/to/bitkit/ui/components/PaykitAmountViewTest.kt @@ -1,22 +1,29 @@ package to.bitkit.ui.components import org.junit.Test +import to.bitkit.models.formatCurrency +import java.math.BigDecimal import java.util.Locale import kotlin.test.assertEquals class PaykitAmountViewTest { @Test - fun `usd display preserves cents and uses locale grouping`() { + fun `usd display preserves cents and matches fiat separators across regions`() { listOf( - Triple(0uL, "0.00", "0,00"), - Triple(1uL, "0.01", "0,01"), - Triple(100uL, "1.00", "1,00"), - Triple(15050uL, "150.50", "150,50"), - Triple(123456uL, "1,234.56", "1.234,56"), - Triple(ULong.MAX_VALUE, "184,467,440,737,095,516.15", "184.467.440.737.095.516,15"), - ).forEach { (cents, us, de) -> - assertEquals(us, usdDisplayAmount(cents, Locale.US), "cents: $cents") - assertEquals(de, usdDisplayAmount(cents, Locale.GERMANY), "cents: $cents") + 0uL to "0.00", + 1uL to "0.01", + 100uL to "1.00", + 15050uL to "150.50", + 123456uL to "1,234.56", + ULong.MAX_VALUE to "184,467,440,737,095,516.15", + ).forEach { (cents, expected) -> + listOf(Locale.US, Locale.GERMANY, Locale.forLanguageTag("nl-NL"), Locale.FRANCE).forEach { locale -> + assertEquals(expected, usdDisplayAmount(cents, locale), locale.toString()) + assertEquals( + BigDecimal(cents.toString()).movePointLeft(2).formatCurrency(locale = locale), + usdDisplayAmount(cents, locale), + ) + } } } } diff --git a/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt b/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt index 04a3958d14..8dcc3fc5fc 100644 --- a/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt +++ b/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt @@ -8,18 +8,19 @@ import kotlin.test.assertNull class UsdtDisplayTest { @Test - fun `amount input uses the local separator and preserves precision and trailing zeros`() { - assertEquals("150,50", usdtInputAmount("150.50", Locale.GERMANY)) - assertEquals("0,000001", usdtInputAmount("0.000001", Locale.GERMANY)) - assertEquals("150.50", usdtInputAmount("150.50", Locale.US)) - assertEquals("1,", usdtInputAmount("1.", Locale.GERMANY)) - } - - @Test - fun `exact amounts preserve token precision and locale separators`() { - assertEquals("4.996", usdtExactAmount(4_996_000u, Locale.US)) - assertEquals("1,234567", usdtExactAmount(1_234_567u, Locale.GERMANY)) - assertEquals("3,50", usdtExactAmount(3_500_000u, Locale.GERMANY)) + fun `exact amounts preserve token precision across regions`() { + listOf(Locale.US, Locale.GERMANY, Locale.forLanguageTag("nl-NL"), Locale.FRANCE).forEach { locale -> + listOf( + 0uL to "0.00", + 1uL to "0.000001", + 4_996_000uL to "4.996", + 1_234_567uL to "1.234567", + 3_500_000uL to "3.50", + ULong.MAX_VALUE to "18,446,744,073,709.551615", + ).forEach { (amount, expected) -> + assertEquals(expected, usdtExactAmount(amount, locale), locale.toString()) + } + } } @Test @@ -46,9 +47,9 @@ class UsdtDisplayTest { 1025000uL to "1.03", 1234567890uL to "1,234.57", ).forEach { (amount, expected) -> - assertEquals(expected, usdtOverviewAmount(amount, Locale.US), "amount: $amount") + listOf(Locale.US, Locale.GERMANY, Locale.forLanguageTag("nl-NL"), Locale.FRANCE).forEach { locale -> + assertEquals(expected, usdtOverviewAmount(amount, locale), locale.toString()) + } } - assertEquals("<0,01", usdtOverviewAmount(1uL, Locale.GERMANY)) - assertEquals("1.234,57", usdtOverviewAmount(1234567890uL, Locale.GERMANY)) } } diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositViewModelTest.kt index 488ea77a9c..83499766d0 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositViewModelTest.kt @@ -56,8 +56,8 @@ class UsdtDepositViewModelTest : BaseUnitTest() { @Test fun `USD cents respect locale grouping and decimal separators`() { assertEquals("$10,000.00", "1000000".usdCents(Locale.US)) - assertEquals("$10.000,00", "1000000".usdCents(Locale.GERMANY)) - assertEquals("$0,05", "5".usdCents(Locale.GERMANY)) + assertEquals("$10,000.00", "1000000".usdCents(Locale.GERMANY)) + assertEquals("$0.05", "5".usdCents(Locale.GERMANY)) } @Test diff --git a/changelog.d/next/1655.fixed.md b/changelog.d/next/1655.fixed.md new file mode 100644 index 0000000000..a5ed912baf --- /dev/null +++ b/changelog.d/next/1655.fixed.md @@ -0,0 +1 @@ +Show USDT balances quietly from zero with consistent decimal formatting and a currency symbol on Home. diff --git a/journeys/README.md b/journeys/README.md index 059e46249e..4cc564cfb9 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -123,7 +123,7 @@ journey PR, which is what made this file conflict on every merge. | Capability | Provided by | | --- | --- | -| USDT wallet navigation without funding | an onboarded build with USDT configured; provider errors may remain visible while offline — [USDT QA](../docs/usdt-qa.md) | +| USDT wallet navigation without funding | an onboarded build with USDT configured; wallet syncing remains silent while offline; payment-action errors remain available — [USDT QA](../docs/usdt-qa.md) | | Controlled USDT quotes, provider history, funded submission and recovery | an explicitly configured local USDT gateway/RPC/bundler fixture with owned test accounts and funds; absent from the standard journey environment. Report these steps as blocked unless that setup and transfer authorization are supplied — [USDT QA](../docs/usdt-qa.md) | | On-chain funds and blocks on regtest | `./lsp` deposit and mine against the staging LSP — [Backend preconditions](#backend-preconditions) | | Several separate on-chain UTXOs to choose between | three or more `./lsp` deposits, each mined, so manual coin selection has inputs to list — [Backend preconditions](#backend-preconditions) | diff --git a/journeys/usdt-number-format.xml b/journeys/usdt-number-format.xml new file mode 100644 index 0000000000..489e74ce42 --- /dev/null +++ b/journeys/usdt-number-format.xml @@ -0,0 +1,12 @@ + + Use an onboarded unfunded test wallet with USDT enabled. Set the system region to Germany or the Netherlands, keep the app language English, and restart the app. Use the same region on both platforms. No payment is submitted; a configured offline USDT provider is sufficient for empty balances and amount entry. + + Set the wallet display unit to dollars. On Home, verify Savings, Spending and USDT all use a dot decimal separator, and the USDT tile shows $ 0.00 for an empty wallet. + Open USDT. Verify the balance shows $ 0.00, no refresh-error or history-sync copy appears underneath, and Send and Receive remain available. + Return Home and hide balances. Verify the USDT tile is masked. Open USDT and verify its balance is masked. Restore visible balances. + Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the displayed request is 3.50 with a dot. + Open Show Details and verify the exact amount remains 3.50. Return and edit the amount to 0.000001. Verify all six decimals remain visible while editing, the QR payment URI encodes one atomic unit (uint256=1), and the receive overview shows <0.01 rather than 0.00. + Return Home. Open Send, Enter Manually, and enter 0x1111111111111111111111111111111111111111. Continue to the USDT amount screen. Verify its decimal key is a dot and entering 1.25 displays $ 1.25. Close without submitting. + Repeat the empty balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. + + diff --git a/journeys/usdt-wallet-navigation.xml b/journeys/usdt-wallet-navigation.xml index a742c3cb99..e61965445f 100644 --- a/journeys/usdt-wallet-navigation.xml +++ b/journeys/usdt-wallet-navigation.xml @@ -1,9 +1,10 @@ - Use an onboarded wallet in an E2E build with USDT RPC and bundler URLs configured. No chain funding or live provider is required. A network error may be visible when services are offline; no payment is submitted. + Use an onboarded wallet in an E2E build with USDT RPC and bundler URLs configured. No chain funding or live provider is required. Wallet syncing stays silent when services are offline; no payment is submitted. Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. On Home, verify the Savings/Spending and USDT balance dividers align vertically, including any hardware wallet rows. With an odd number of paired hardware wallets, verify USDT fills the last row’s empty cell; with an even number, it starts the next row. Verify USDT balances and activity amounts show at most two decimals; a nonzero amount below one cent shows <0.01. Open USDT from Home and verify the wallet screen uses the same balance, action buttons and activity layout as the Bitcoin wallet, with its Tether coin illustration anchored at the upper right. + With an empty wallet, verify Home and the USDT wallet show $ 0.00, including before the first successful refresh. Verify no red refresh error or grey history-sync copy appears below the balance while the provider is offline. Open Receive. Verify the network is Arbitrum One. Open details, verify the address starts with 0x, and verify Details Copy contains only the address. Return to the QR and verify Copy and tapping the QR both copy only the same address, while the QR still contains the full USDT payment URI including the network and token. On Android, system Back from Details returns to the USDT QR; Back from the QR closes Receive without switching to Savings. Close Receive, open Send and manual entry. Enter an invalid Bitcoin address and verify the address error prevents continuing. Replace it with an owned valid EVM address. Continue to Amount, enter six decimal places and try a seventh; verify the amount remains limited to six. Close before requesting a quote. From be661d7ef46bcc3130cc6dc1f81ef54ff19204df Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 06:12:10 -0600 Subject: [PATCH 2/4] test: correct compose assertion import --- .../to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt index b51360f4bf..dd850c3924 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.SemanticsActions -import androidx.compose.ui.test.assertDoesNotExist import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag From 9bb64a38b52116c9793a2559aa15b608752a71e2 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 06:50:19 -0600 Subject: [PATCH 3/4] fix: allow entering new USD request amounts --- .../screens/paymentrequests/CreatePaymentRequestScreen.kt | 8 +++++++- journeys/usdt-number-format.xml | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 9b844440c5..bb58556ba5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -151,7 +151,13 @@ internal fun PaymentRequestAmountContent( var asset by remember(initialDraft.amount.asset) { mutableStateOf(initialDraft.amount.asset) } var dollars by remember(initialDraft.amount) { - mutableStateOf(if (initialDraft.amount.asset != PaykitAsset.BTC) initialDraft.amount.inputValue else "") + mutableStateOf( + if (initialDraft.amount.asset != PaykitAsset.BTC && initialDraft.amount.atomic != 0uL) { + initialDraft.amount.inputValue + } else { + "" + } + ) } var conversionError by remember { mutableStateOf(false) } val entered = if (asset == PaykitAsset.BTC) { diff --git a/journeys/usdt-number-format.xml b/journeys/usdt-number-format.xml index 489e74ce42..5a1ae4fb33 100644 --- a/journeys/usdt-number-format.xml +++ b/journeys/usdt-number-format.xml @@ -4,8 +4,8 @@ Set the wallet display unit to dollars. On Home, verify Savings, Spending and USDT all use a dot decimal separator, and the USDT tile shows $ 0.00 for an empty wallet. Open USDT. Verify the balance shows $ 0.00, no refresh-error or history-sync copy appears underneath, and Send and Receive remain available. Return Home and hide balances. Verify the USDT tile is masked. Open USDT and verify its balance is masked. Restore visible balances. - Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the displayed request is 3.50 with a dot. - Open Show Details and verify the exact amount remains 3.50. Return and edit the amount to 0.000001. Verify all six decimals remain visible while editing, the QR payment URI encodes one atomic unit (uint256=1), and the receive overview shows <0.01 rather than 0.00. + Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the QR payment URI encodes 3500000 atomic units (uint256=3500000). + Open Show Details and verify the receiving address matches the QR recipient. Return to the QR and edit the amount to 0.000001. Verify all six decimals remain visible while editing and the QR payment URI encodes one atomic unit (uint256=1). Return Home. Open Send, Enter Manually, and enter 0x1111111111111111111111111111111111111111. Continue to the USDT amount screen. Verify its decimal key is a dot and entering 1.25 displays $ 1.25. Close without submitting. Repeat the empty balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. From de87006c89ed4d217d596aaa1aedd6f59624d8bf Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 08:27:12 -0600 Subject: [PATCH 4/4] fix: distinguish unknown usdt balances --- .../ui/screens/wallets/usdt/UsdtPaymentContentTest.kt | 8 ++++++-- .../to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt | 6 +++--- .../test/java/to/bitkit/ui/components/UsdtDisplayTest.kt | 1 + changelog.d/next/1655.fixed.md | 2 +- journeys/usdt-bridge-details.xml | 2 +- journeys/usdt-number-format.xml | 8 ++++---- journeys/usdt-wallet-navigation.xml | 2 +- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt index dd850c3924..b38b94b04a 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt @@ -105,7 +105,7 @@ class UsdtPaymentContentTest { } @Test - fun walletShowsZeroSilentlyUntilBalanceArrivesAndKeepsBalancePrivacy() { + fun walletDistinguishesUnknownAndConfirmedBalancesSilentlyAndKeepsPrivacy() { Locale.setDefault(Locale.GERMANY) val wallet = mutableStateOf(UsdtWalletState()) val hidden = mutableStateOf(false) @@ -118,8 +118,12 @@ class UsdtPaymentContentTest { ) } } - composeTestRule.onNodeWithText("0.00").assertIsDisplayed() + composeTestRule.onNodeWithText("—").assertIsDisplayed() + composeTestRule.onNodeWithText("0.00").assertDoesNotExist() composeTestRule.onNodeWithTag("UsdtHistorySync").assertDoesNotExist() + composeTestRule.runOnIdle { wallet.value = wallet.value.copy(balance = 0uL) } + composeTestRule.onNodeWithText("0.00").assertIsDisplayed() + composeTestRule.onNodeWithText("—").assertDoesNotExist() composeTestRule.runOnIdle { wallet.value = wallet.value.copy(balance = 1_234_567_890uL) } composeTestRule.onNodeWithText("1,234.57").assertIsDisplayed() composeTestRule.runOnIdle { hidden.value = true } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt index 6f22d1da5e..07f16c65a1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt @@ -103,7 +103,7 @@ fun RowScope.UsdtWalletBalance(onClick: () -> Unit, viewModel: UsdtViewModel = h if (settings.hideBalance) { UiConstants.HIDE_BALANCE_SHORT } else { - "$ " + usdtOverviewAmount(wallet.balance ?: 0u) + wallet.balance?.let { "$ " + usdtOverviewAmount(it) } ?: "—" }, style = UsdtDetailStyles.HistoryTitle ) @@ -342,8 +342,8 @@ internal fun UsdtWalletContent( item { VerticalSpacer(16.dp) UsdtAmountHeader( - amount = usdtFormatAmount(wallet.balance ?: 0u), - displayAmount = usdtOverviewAmount(wallet.balance ?: 0u), + amount = wallet.balance?.let { usdtFormatAmount(it) } ?: "—", + displayAmount = wallet.balance?.let { usdtOverviewAmount(it) }, hideBalance = hideBalance, onToggleHide = if (allowSwipe) ({ settings?.setHideBalance(!hideBalance) }) else null, modifier = Modifier.testTag("UsdtBalance") diff --git a/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt b/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt index 8dcc3fc5fc..49f02ce6cc 100644 --- a/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt +++ b/app/src/test/java/to/bitkit/ui/components/UsdtDisplayTest.kt @@ -31,6 +31,7 @@ class UsdtDisplayTest { assertEquals(0L, usdtDisplaySats("0.000001", rate, now)) assertNull(usdtDisplaySats("3.5", null, now)) assertNull(usdtDisplaySats("3.5", rate.copy(timestampMillis = now - 601_000), now)) + assertNull(usdtDisplaySats("—", rate, now)) assertNull(usdtDisplaySats("-1", rate, now)) assertNull(usdtDisplaySats("999999999999999999999999", rate, now)) } diff --git a/changelog.d/next/1655.fixed.md b/changelog.d/next/1655.fixed.md index a5ed912baf..3b3e2a9794 100644 --- a/changelog.d/next/1655.fixed.md +++ b/changelog.d/next/1655.fixed.md @@ -1 +1 @@ -Show USDT balances quietly from zero with consistent decimal formatting and a currency symbol on Home. +Show USDT balances quietly with consistent decimals, a currency symbol on Home and an unknown marker until the balance loads. diff --git a/journeys/usdt-bridge-details.xml b/journeys/usdt-bridge-details.xml index 17557d9cf4..d03f1d10a4 100644 --- a/journeys/usdt-bridge-details.xml +++ b/journeys/usdt-bridge-details.xml @@ -5,7 +5,7 @@ Tap Details (id "UsdtActivityDetails"). Verify the real wallet transaction and available addresses are shown without inventing a provider deposit reference or refund eligibility. Open Cross-Network Deposits (id "UsdtDepositHistory"). Verify the Provider Status note says the spendable balance updates when USDT arrives on Arbitrum, and each row shows its source network, provider status and available amount. Open a supported deposit that needs attention (id "UsdtDepositRow-{depositId}"). Verify the summary appears first with the provider status and receiving network. Do not invent a deposit date or individual fee when the provider does not supply one. Tap Details (id "UsdtDepositDetails"). Verify its own source transaction and deposit reference are shown. Verify the refund instructions and address placeholder name that deposit's source network. - Enter an owned test refund address (id "UsdtRefundAddress") slowly across two background refreshes. Verify the field stays enabled and focused, the keyboard remains open, and every typed character is retained while the provider status updates. Then tap Request refund (id "UsdtDepositRefund"). Verify Confirm Refund shows the exact deposit amount with up to six decimals, network, full refund address and refund fee note. In a comma locale, a 3.5 USDT deposit must display 3,50 USDT. Hidden balances must remain masked, and an unknown amount must remain a dash. Cancel without submitting. + Enter an owned test refund address (id "UsdtRefundAddress") slowly across two background refreshes. Verify the field stays enabled and focused, the keyboard remains open, and every typed character is retained while the provider status updates. Then tap Request refund (id "UsdtDepositRefund"). Verify Confirm Refund shows the exact deposit amount with up to six decimals, network, full refund address and refund fee note. In a comma locale, a 3.5 USDT deposit must display 3.50 USDT. Hidden balances must remain masked, and an unknown amount must remain a dash. Cancel without submitting. Tap Hide details (id "UsdtDepositDetails"), then Details again. Verify the provider status remains separate from wallet activity and the entered refund address is retained. Open completed, refunded and refund-in-progress deposit fixtures. Verify Request Refund is unavailable and the current provider status and available refund transaction are shown. An unsupported asset or network must show support guidance instead of a refund form. While Cross-Network Deposits is open, inject a confirmed incoming USDT transfer through the isolated fixture. Verify no received sheet interrupts deposit details or refund review. Return to the USDT wallet and verify the received celebration appears once after any active sheet is dismissed. Dismiss it and revisit deposit details, then return to the wallet; verify the same receipt does not appear again. diff --git a/journeys/usdt-number-format.xml b/journeys/usdt-number-format.xml index 5a1ae4fb33..71d6167191 100644 --- a/journeys/usdt-number-format.xml +++ b/journeys/usdt-number-format.xml @@ -1,12 +1,12 @@ - Use an onboarded unfunded test wallet with USDT enabled. Set the system region to Germany or the Netherlands, keep the app language English, and restart the app. Use the same region on both platforms. No payment is submitted; a configured offline USDT provider is sufficient for empty balances and amount entry. + Use an onboarded unfunded test wallet with USDT enabled. Set the system region to Germany or the Netherlands, keep the app language English, and restart the app. Use the same region on both platforms. No payment is submitted; a configured offline USDT provider is sufficient for unknown balances and amount entry. Confirmed empty-balance checks require a successful read from an owned local test provider. - Set the wallet display unit to dollars. On Home, verify Savings, Spending and USDT all use a dot decimal separator, and the USDT tile shows $ 0.00 for an empty wallet. - Open USDT. Verify the balance shows $ 0.00, no refresh-error or history-sync copy appears underneath, and Send and Receive remain available. + Set the wallet display unit to dollars. On Home with the provider offline after relaunch, verify the USDT tile shows — rather than zero. After a successful balance read, verify a confirmed empty wallet shows $ 0.00 and loaded USDT balances use the same dot decimal separator as Savings and Spending. + Open USDT. Verify an unknown balance shows $ — and a confirmed empty balance shows $ 0.00. No refresh-error or history-sync copy appears underneath, and Send and Receive remain available. Return Home and hide balances. Verify the USDT tile is masked. Open USDT and verify its balance is masked. Restore visible balances. Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the QR payment URI encodes 3500000 atomic units (uint256=3500000). Open Show Details and verify the receiving address matches the QR recipient. Return to the QR and edit the amount to 0.000001. Verify all six decimals remain visible while editing and the QR payment URI encodes one atomic unit (uint256=1). Return Home. Open Send, Enter Manually, and enter 0x1111111111111111111111111111111111111111. Continue to the USDT amount screen. Verify its decimal key is a dot and entering 1.25 displays $ 1.25. Close without submitting. - Repeat the empty balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. + Repeat the unknown and confirmed empty-balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. diff --git a/journeys/usdt-wallet-navigation.xml b/journeys/usdt-wallet-navigation.xml index e61965445f..1332329977 100644 --- a/journeys/usdt-wallet-navigation.xml +++ b/journeys/usdt-wallet-navigation.xml @@ -4,7 +4,7 @@ Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. On Home, verify the Savings/Spending and USDT balance dividers align vertically, including any hardware wallet rows. With an odd number of paired hardware wallets, verify USDT fills the last row’s empty cell; with an even number, it starts the next row. Verify USDT balances and activity amounts show at most two decimals; a nonzero amount below one cent shows <0.01. Open USDT from Home and verify the wallet screen uses the same balance, action buttons and activity layout as the Bitcoin wallet, with its Tether coin illustration anchored at the upper right. - With an empty wallet, verify Home and the USDT wallet show $ 0.00, including before the first successful refresh. Verify no red refresh error or grey history-sync copy appears below the balance while the provider is offline. + Before the first successful balance read, verify Home shows — and the USDT wallet shows $ —. A confirmed empty balance must show $ 0.00. A known nonzero balance must remain visible if a later refresh fails; after relaunching offline it must show unknown rather than zero. Verify no red refresh error or grey history-sync copy appears below the balance while the provider is offline. Open Receive. Verify the network is Arbitrum One. Open details, verify the address starts with 0x, and verify Details Copy contains only the address. Return to the QR and verify Copy and tapping the QR both copy only the same address, while the QR still contains the full USDT payment URI including the network and token. On Android, system Back from Details returns to the USDT QR; Back from the QR closes Receive without switching to Savings. Close Receive, open Send and manual entry. Enter an invalid Bitcoin address and verify the address error prevents continuing. Replace it with an owned valid EVM address. Continue to Amount, enter six decimal places and try a seventh; verify the amount remains limited to six. Close before requesting a quote.