Skip to content

Commit c11fb89

Browse files
authored
Merge pull request #94 from synonymdev/feat/edit-input-header
Implement editable BalanceHeader
2 parents fff2ba3 + 2aa0bf2 commit c11fb89

File tree

9 files changed

+724
-111
lines changed

9 files changed

+724
-111
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package to.bitkit.ui.screens.wallets.send
2+
3+
import androidx.compose.ui.test.assertIsNotEnabled
4+
import androidx.compose.ui.test.junit4.createComposeRule
5+
import androidx.compose.ui.test.onNodeWithTag
6+
import androidx.compose.ui.test.performClick
7+
import org.junit.Rule
8+
import org.junit.Test
9+
import to.bitkit.models.NodeLifecycleState
10+
import to.bitkit.models.PrimaryDisplay
11+
import to.bitkit.viewmodels.CurrencyUiState
12+
import to.bitkit.viewmodels.MainUiState
13+
import to.bitkit.viewmodels.SendEvent
14+
import to.bitkit.viewmodels.SendMethod
15+
import to.bitkit.viewmodels.SendUiState
16+
17+
class SendAmountContentTest {
18+
19+
@get:Rule
20+
val composeTestRule = createComposeRule()
21+
22+
private val testUiState = SendUiState(
23+
payMethod = SendMethod.LIGHTNING,
24+
amountInput = "100",
25+
isAmountInputValid = true,
26+
isUnified = true
27+
)
28+
29+
private val testWalletState = MainUiState(
30+
nodeLifecycleState = NodeLifecycleState.Running
31+
)
32+
33+
@Test
34+
fun whenScreenLoaded_shouldShowAllComponents() {
35+
composeTestRule.setContent {
36+
SendAmountContent(
37+
input = "100",
38+
uiState = testUiState,
39+
walletUiState = testWalletState,
40+
currencyUiState = CurrencyUiState(primaryDisplay = PrimaryDisplay.BITCOIN),
41+
onInputChanged = {},
42+
onEvent = {},
43+
onBack = {}
44+
)
45+
}
46+
47+
composeTestRule.onNodeWithTag("send_amount_screen").assertExists()
48+
// composeTestRule.onNodeWithTag("amount_input_field").assertExists() doesn't displayed because of viewmodel injection
49+
composeTestRule.onNodeWithTag("available_balance").assertExists()
50+
composeTestRule.onNodeWithTag("payment_method_button").assertExists()
51+
composeTestRule.onNodeWithTag("continue_button").assertExists()
52+
composeTestRule.onNodeWithTag("amount_keyboard").assertExists()
53+
}
54+
55+
@Test
56+
fun whenNodeNotRunning_shouldShowSyncView() {
57+
composeTestRule.setContent {
58+
SendAmountContent(
59+
input = "100",
60+
uiState = testUiState,
61+
walletUiState = MainUiState(
62+
nodeLifecycleState = NodeLifecycleState.Initializing
63+
),
64+
currencyUiState = CurrencyUiState(),
65+
onInputChanged = {},
66+
onEvent = {},
67+
onBack = {}
68+
)
69+
}
70+
71+
composeTestRule.onNodeWithTag("sync_node_view").assertExists()
72+
composeTestRule.onNodeWithTag("amount_input_field").assertDoesNotExist()
73+
}
74+
75+
@Test
76+
fun whenPaymentMethodButtonClicked_shouldTriggerEvent() {
77+
var eventTriggered = false
78+
composeTestRule.setContent {
79+
SendAmountContent(
80+
input = "100",
81+
uiState = testUiState,
82+
walletUiState = testWalletState,
83+
currencyUiState = CurrencyUiState(),
84+
onInputChanged = {},
85+
onEvent = { event ->
86+
if (event is SendEvent.PaymentMethodSwitch) {
87+
eventTriggered = true
88+
}
89+
},
90+
onBack = {}
91+
)
92+
}
93+
94+
composeTestRule.onNodeWithTag("payment_method_button")
95+
.performClick()
96+
97+
assert(eventTriggered)
98+
}
99+
100+
@Test
101+
fun whenContinueButtonClicked_shouldTriggerEvent() {
102+
var eventTriggered = false
103+
composeTestRule.setContent {
104+
SendAmountContent(
105+
input = "100",
106+
uiState = testUiState.copy(isAmountInputValid = true),
107+
walletUiState = testWalletState,
108+
currencyUiState = CurrencyUiState(),
109+
onInputChanged = {},
110+
onEvent = { event ->
111+
if (event is SendEvent.AmountContinue) {
112+
eventTriggered = true
113+
}
114+
},
115+
onBack = {}
116+
)
117+
}
118+
119+
composeTestRule.onNodeWithTag("continue_button")
120+
.performClick()
121+
122+
assert(eventTriggered)
123+
}
124+
125+
@Test
126+
fun whenAmountInvalid_continueButtonShouldBeDisabled() {
127+
composeTestRule.setContent {
128+
SendAmountContent(
129+
input = "100",
130+
uiState = testUiState.copy(isAmountInputValid = false),
131+
walletUiState = testWalletState,
132+
currencyUiState = CurrencyUiState(),
133+
onInputChanged = {},
134+
onEvent = {},
135+
onBack = {}
136+
)
137+
}
138+
139+
composeTestRule.onNodeWithTag("continue_button").assertIsNotEnabled()
140+
}
141+
}

app/src/main/java/to/bitkit/models/Currency.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import java.text.DecimalFormat
77
import java.text.DecimalFormatSymbols
88
import java.util.Locale
99

10+
const val BITCOIN_SYMBOL = ""
11+
1012
@Serializable
1113
data class FxRateResponse(
1214
val tickers: List<FxRate>,
@@ -57,7 +59,7 @@ data class ConvertedAmount(
5759
)
5860

5961
fun bitcoinDisplay(unit: BitcoinDisplayUnit): BitcoinDisplayComponents {
60-
val symbol = ""
62+
val symbol = BITCOIN_SYMBOL
6163
val spaceSeparator = ' '
6264
val formattedValue = when (unit) {
6365
BitcoinDisplayUnit.MODERN -> {

app/src/main/java/to/bitkit/services/CurrencyService.kt

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import to.bitkit.async.ServiceQueue
55
import to.bitkit.data.BlocktankHttpClient
66
import to.bitkit.models.ConvertedAmount
77
import to.bitkit.models.FxRate
8+
import to.bitkit.ui.utils.formatCurrency
89
import to.bitkit.utils.AppError
910
import java.math.BigDecimal
1011
import java.math.RoundingMode
11-
import java.text.DecimalFormat
12-
import java.text.DecimalFormatSymbols
13-
import java.util.Locale
1412
import javax.inject.Inject
1513
import javax.inject.Singleton
1614
import kotlin.math.pow
@@ -57,15 +55,7 @@ class CurrencyService @Inject constructor(
5755
val btcAmount = BigDecimal(sats).divide(BigDecimal(100_000_000))
5856
val value: BigDecimal = btcAmount.multiply(BigDecimal.valueOf(rate.rate))
5957

60-
val symbols = DecimalFormatSymbols(Locale.getDefault()).apply {
61-
decimalSeparator = '.'
62-
}
63-
val formatter = DecimalFormat("#,##0.00", symbols).apply {
64-
minimumFractionDigits = 2
65-
maximumFractionDigits = 2
66-
}
67-
68-
val formatted = runCatching { formatter.format(value) }.getOrNull() ?: return null
58+
val formatted = value.formatCurrency() ?: return null
6959

7060
return ConvertedAmount(
7161
value = value,

app/src/main/java/to/bitkit/ui/components/Money.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ fun MoneyDisplay(
2727
}
2828

2929
@Composable
30-
fun MoneySSB(sats: Long) {
31-
rememberMoneyText(sats)?.let { text ->
30+
fun MoneySSB(
31+
sats: Long,
32+
reversed: Boolean = false,
33+
) {
34+
rememberMoneyText(sats = sats, reversed = reversed)?.let { text ->
3235
BodySSB(text = text.withAccent(accentColor = Colors.White64))
3336
}
3437
}
@@ -62,16 +65,20 @@ fun MoneyCaptionB(
6265
}
6366
}
6467

68+
69+
6570
/**
6671
* Generates a formatted representation of a monetary value based on the provided amount in satoshis
6772
* and the current currency display settings. Can be either in bitcoin or fiat.
6873
*
6974
* @param sats The amount in satoshis to be formatted and displayed.
75+
* @param reversed If true, swaps the primary and secondary display. Defaults to false.
7076
* @return A formatted string representation of the monetary value, or null if it cannot be generated.
7177
*/
7278
@Composable
7379
fun rememberMoneyText(
7480
sats: Long,
81+
reversed: Boolean = false,
7582
): String? {
7683
val isPreview = LocalInspectionMode.current
7784
if (isPreview) {
@@ -81,10 +88,17 @@ fun rememberMoneyText(
8188
val currency = currencyViewModel ?: return null
8289
val currencies = LocalCurrencies.current
8390

84-
return remember(currencies, sats) {
91+
return remember(currencies, sats, reversed) {
8592
val converted = currency.convert(sats) ?: return@remember null
8693

87-
if (currencies.primaryDisplay == PrimaryDisplay.BITCOIN) {
94+
val secondaryDisplay = when(currencies.primaryDisplay) {
95+
PrimaryDisplay.BITCOIN -> PrimaryDisplay.FIAT
96+
PrimaryDisplay.FIAT -> PrimaryDisplay.BITCOIN
97+
}
98+
99+
val primary = if (reversed) secondaryDisplay else currencies.primaryDisplay
100+
101+
if (primary == PrimaryDisplay.BITCOIN) {
88102
val btcComponents = converted.bitcoinDisplay(currencies.displayUnit)
89103
"<accent>${btcComponents.symbol}</accent> ${btcComponents.value}"
90104
} else {

0 commit comments

Comments
 (0)