Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cad937c
feat: implement BalanceHeaderEditable WIP
jvsena42 Apr 3, 2025
9f77417
feat: implement BalanceHeaderEditable WIP
jvsena42 Apr 4, 2025
f88e578
feat: NumberPadTextField WIP
jvsena42 Apr 4, 2025
334356d
feat: add reversed option
jvsena42 Apr 4, 2025
37eacb7
refactor: clean code
jvsena42 Apr 4, 2025
14021ef
refactor: clean code
jvsena42 Apr 4, 2025
211e34e
fix: conversion
jvsena42 Apr 4, 2025
adf8d8b
fix: conversion
jvsena42 Apr 4, 2025
9176032
Merge branch 'master' into feat/edit-input-header
jvsena42 Apr 14, 2025
c6fd9a6
Merge branch 'master' into feat/edit-input-header
jvsena42 Apr 14, 2025
33b43b7
feat: format text
jvsena42 Apr 14, 2025
9ea4367
feat: fiat conversion
jvsena42 Apr 14, 2025
4f33cda
fix: decimal validation
jvsena42 Apr 14, 2025
2360c73
fix: placeholder length
jvsena42 Apr 14, 2025
048a7dc
feat. trigger amount send event
jvsena42 Apr 15, 2025
b3226b3
feat. trigger amount send event
jvsena42 Apr 15, 2025
c45312f
refactor: apply separation of concerns in SendAmountScreen
jvsena42 Apr 15, 2025
90377ba
refactor: improve code to display previews
jvsena42 Apr 15, 2025
e322a06
refactor: extract btc symbol
jvsena42 Apr 15, 2025
8becd64
fix: groupingSeparator
jvsena42 Apr 15, 2025
b320e16
feat: get currency symbol
jvsena42 Apr 15, 2025
474a099
feat: get currency symbol
jvsena42 Apr 15, 2025
84c207d
fix: remove space
jvsena42 Apr 15, 2025
289f2c1
fix: sats conversion
jvsena42 Apr 15, 2025
af26ec2
refactor: formatMoney method
jvsena42 Apr 15, 2025
55149b1
refactor: remove comments
jvsena42 Apr 15, 2025
4520b3b
test: add test tags
jvsena42 Apr 15, 2025
6fc5faa
test: SendAmountContentTest
jvsena42 Apr 15, 2025
f989c31
fix: remove ","
jvsena42 Apr 15, 2025
bae7890
Merge branch 'master' into feat/edit-input-header
jvsena42 Apr 16, 2025
e87e17f
refactor: remove not used method
jvsena42 Apr 16, 2025
bf8d7b1
fix: apply Display style
jvsena42 Apr 16, 2025
476a654
fix: apply Display style
jvsena42 Apr 16, 2025
10295c4
refactor: move states up
jvsena42 Apr 16, 2025
d5e2e7e
fix: placeholder glitching
jvsena42 Apr 16, 2025
bc550b6
fix: placeholder glitching
jvsena42 Apr 16, 2025
2aa0bf2
feat: add preview
jvsena42 Apr 16, 2025
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
@@ -0,0 +1,141 @@
package to.bitkit.ui.screens.wallets.send

import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.PrimaryDisplay
import to.bitkit.viewmodels.CurrencyUiState
import to.bitkit.viewmodels.MainUiState
import to.bitkit.viewmodels.SendEvent
import to.bitkit.viewmodels.SendMethod
import to.bitkit.viewmodels.SendUiState

class SendAmountContentTest {

@get:Rule
val composeTestRule = createComposeRule()

private val testUiState = SendUiState(
payMethod = SendMethod.LIGHTNING,
amountInput = "100",
isAmountInputValid = true,
isUnified = true
)

private val testWalletState = MainUiState(
nodeLifecycleState = NodeLifecycleState.Running
)

@Test
fun whenScreenLoaded_shouldShowAllComponents() {
composeTestRule.setContent {
SendAmountContent(
input = "100",
uiState = testUiState,
walletUiState = testWalletState,
currencyUiState = CurrencyUiState(primaryDisplay = PrimaryDisplay.BITCOIN),
onInputChanged = {},
onEvent = {},
onBack = {}
)
}

composeTestRule.onNodeWithTag("send_amount_screen").assertExists()
// composeTestRule.onNodeWithTag("amount_input_field").assertExists() doesn't displayed because of viewmodel injection
composeTestRule.onNodeWithTag("available_balance").assertExists()
composeTestRule.onNodeWithTag("payment_method_button").assertExists()
composeTestRule.onNodeWithTag("continue_button").assertExists()
composeTestRule.onNodeWithTag("amount_keyboard").assertExists()
}

@Test
fun whenNodeNotRunning_shouldShowSyncView() {
composeTestRule.setContent {
SendAmountContent(
input = "100",
uiState = testUiState,
walletUiState = MainUiState(
nodeLifecycleState = NodeLifecycleState.Initializing
),
currencyUiState = CurrencyUiState(),
onInputChanged = {},
onEvent = {},
onBack = {}
)
}

composeTestRule.onNodeWithTag("sync_node_view").assertExists()
composeTestRule.onNodeWithTag("amount_input_field").assertDoesNotExist()
}

@Test
fun whenPaymentMethodButtonClicked_shouldTriggerEvent() {
var eventTriggered = false
composeTestRule.setContent {
SendAmountContent(
input = "100",
uiState = testUiState,
walletUiState = testWalletState,
currencyUiState = CurrencyUiState(),
onInputChanged = {},
onEvent = { event ->
if (event is SendEvent.PaymentMethodSwitch) {
eventTriggered = true
}
},
onBack = {}
)
}

composeTestRule.onNodeWithTag("payment_method_button")
.performClick()

assert(eventTriggered)
}

@Test
fun whenContinueButtonClicked_shouldTriggerEvent() {
var eventTriggered = false
composeTestRule.setContent {
SendAmountContent(
input = "100",
uiState = testUiState.copy(isAmountInputValid = true),
walletUiState = testWalletState,
currencyUiState = CurrencyUiState(),
onInputChanged = {},
onEvent = { event ->
if (event is SendEvent.AmountContinue) {
eventTriggered = true
}
},
onBack = {}
)
}

composeTestRule.onNodeWithTag("continue_button")
.performClick()

assert(eventTriggered)
}

@Test
fun whenAmountInvalid_continueButtonShouldBeDisabled() {
composeTestRule.setContent {
SendAmountContent(
input = "100",
uiState = testUiState.copy(isAmountInputValid = false),
walletUiState = testWalletState,
currencyUiState = CurrencyUiState(),
onInputChanged = {},
onEvent = {},
onBack = {}
)
}

composeTestRule.onNodeWithTag("continue_button").assertIsNotEnabled()
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/to/bitkit/models/Currency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale

const val BITCOIN_SYMBOL = "₿"

@Serializable
data class FxRateResponse(
val tickers: List<FxRate>,
Expand Down Expand Up @@ -57,7 +59,7 @@ data class ConvertedAmount(
)

fun bitcoinDisplay(unit: BitcoinDisplayUnit): BitcoinDisplayComponents {
val symbol = "₿"
val symbol = BITCOIN_SYMBOL
val spaceSeparator = ' '
val formattedValue = when (unit) {
BitcoinDisplayUnit.MODERN -> {
Expand Down
14 changes: 2 additions & 12 deletions app/src/main/java/to/bitkit/services/CurrencyService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import to.bitkit.async.ServiceQueue
import to.bitkit.data.BlocktankHttpClient
import to.bitkit.models.ConvertedAmount
import to.bitkit.models.FxRate
import to.bitkit.ui.utils.formatCurrency
import to.bitkit.utils.AppError
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.pow
Expand Down Expand Up @@ -57,15 +55,7 @@ class CurrencyService @Inject constructor(
val btcAmount = BigDecimal(sats).divide(BigDecimal(100_000_000))
val value: BigDecimal = btcAmount.multiply(BigDecimal.valueOf(rate.rate))

val symbols = DecimalFormatSymbols(Locale.getDefault()).apply {
decimalSeparator = '.'
}
val formatter = DecimalFormat("#,##0.00", symbols).apply {
minimumFractionDigits = 2
maximumFractionDigits = 2
}

val formatted = runCatching { formatter.format(value) }.getOrNull() ?: return null
val formatted = value.formatCurrency() ?: return null

return ConvertedAmount(
value = value,
Expand Down
22 changes: 18 additions & 4 deletions app/src/main/java/to/bitkit/ui/components/Money.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ fun MoneyDisplay(
}

@Composable
fun MoneySSB(sats: Long) {
rememberMoneyText(sats)?.let { text ->
fun MoneySSB(
sats: Long,
reversed: Boolean = false,
) {
rememberMoneyText(sats = sats, reversed = reversed)?.let { text ->
BodySSB(text = text.withAccent(accentColor = Colors.White64))
}
}
Expand Down Expand Up @@ -62,16 +65,20 @@ fun MoneyCaptionB(
}
}



/**
* Generates a formatted representation of a monetary value based on the provided amount in satoshis
* and the current currency display settings. Can be either in bitcoin or fiat.
*
* @param sats The amount in satoshis to be formatted and displayed.
* @param reversed If true, swaps the primary and secondary display. Defaults to false.
* @return A formatted string representation of the monetary value, or null if it cannot be generated.
*/
@Composable
fun rememberMoneyText(
sats: Long,
reversed: Boolean = false,
): String? {
val isPreview = LocalInspectionMode.current
if (isPreview) {
Expand All @@ -81,10 +88,17 @@ fun rememberMoneyText(
val currency = currencyViewModel ?: return null
val currencies = LocalCurrencies.current

return remember(currencies, sats) {
return remember(currencies, sats, reversed) {
val converted = currency.convert(sats) ?: return@remember null

if (currencies.primaryDisplay == PrimaryDisplay.BITCOIN) {
val secondaryDisplay = when(currencies.primaryDisplay) {
PrimaryDisplay.BITCOIN -> PrimaryDisplay.FIAT
PrimaryDisplay.FIAT -> PrimaryDisplay.BITCOIN
}

val primary = if (reversed) secondaryDisplay else currencies.primaryDisplay

if (primary == PrimaryDisplay.BITCOIN) {
val btcComponents = converted.bitcoinDisplay(currencies.displayUnit)
"<accent>${btcComponents.symbol}</accent> ${btcComponents.value}"
} else {
Expand Down
Loading