Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class PaymentRequestAmountLocaleTest {
}

@Test
fun commaDecimalEntryPreservesExactCentsAndTrailingZeros() {
fun dotDecimalEntryPreservesExactCentsInCommaRegions() {
var submitted: PaykitPaymentRequestDraft? = null
val amountInput = AmountInputViewModel(AmountInputHandler.stub())
composeTestRule.setContent {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class UsdtPaymentContentTest {
}

@Test
fun decimalKeyMatchesAmountLocaleWithoutChangingInputEncoding() {
fun decimalKeyUsesDotInCommaRegionsWithoutChangingInputEncoding() {
Locale.setDefault(Locale.GERMANY)
val amount = mutableStateOf("5")
composeTestRule.setContent {
Expand All @@ -97,13 +97,39 @@ 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 walletDistinguishesUnknownAndConfirmedBalancesSilentlyAndKeepsPrivacy() {
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("—").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 }
composeTestRule.onNodeWithText("1,234.57").assertDoesNotExist()
}

@Test
fun largeHeadersAndNetworkLabelsRemainReadable() {
composeTestRule.setContent {
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/to/bitkit/models/Currency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,22 @@ 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
}

val decimalPlacesString = "0".repeat(decimalPlaces)
val formatter = DecimalFormat("#,##0.$decimalPlacesString", symbols).apply {
minimumFractionDigits = decimalPlaces
this.minimumFractionDigits = minimumFractionDigits
maximumFractionDigits = decimalPlaces
this.roundingMode = roundingMode
isGroupingUsed = true
}

Expand Down
12 changes: 4 additions & 8 deletions app/src/main/java/to/bitkit/ui/components/PaykitAmountView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
29 changes: 12 additions & 17 deletions app/src/main/java/to/bitkit/ui/components/UsdtAmountHeader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -150,12 +148,16 @@ 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) {
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) {
Expand Down Expand Up @@ -204,7 +206,7 @@ internal fun PaymentRequestAmountContent(
)
} else {
NumberPadAmountText(
value = dollars.ifEmpty { "0" }.replace('.', decimalSeparator),
value = dollars.ifEmpty { "0" },
symbol = "$",
modifier = Modifier.testTag("PaymentRequestAmountField")
)
Expand Down Expand Up @@ -261,7 +263,6 @@ internal fun PaymentRequestAmountContent(
},
type = NumberPadType.DECIMAL,
availableHeight = availableHeight,
decimalSeparator = decimalSeparator.toString(),
modifier = Modifier.testTag("PaymentRequestNumberPad")
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,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
Expand Down Expand Up @@ -426,7 +425,6 @@ private fun SendAmountNodeRunning(
if (uiState.paykitUsesUsdt) {
NumberPad(
type = NumberPadType.DECIMAL,
decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString(),
availableHeight = maxHeight,
enabled = uiState.isAmountEditable,
onPress = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,14 @@ 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
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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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("") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -510,7 +509,6 @@ private fun UsdtAmountContent(
HorizontalDivider(color = Colors.White10)
NumberPad(
type = NumberPadType.DECIMAL,
decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString(),
availableHeight = maxHeight,
enabled = !state.busy && amountEditable,
onDeleteLongPress = { onAmountChange("") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
} ?: "—"
wallet.balance?.let { "$ " + usdtOverviewAmount(it) } ?: "—"
},
style = UsdtDetailStyles.HistoryTitle
)
Expand Down Expand Up @@ -184,7 +180,6 @@ fun UsdtWalletScreen(
} else {
UsdtWalletContent(
wallet = wallet,
error = state.refreshError,
hideBalance = settings.hideBalance,
onReceive = {
app?.requestUsdtEnable {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -355,10 +349,6 @@ private fun UsdtWalletContent(
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)
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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, {}, {}, {})
}
}
Loading
Loading