Skip to content

Commit 1d1c91a

Browse files
committed
Merge branch 'master' into feat/biometrics-setup
2 parents 54cbc25 + ac731cd commit 1d1c91a

File tree

13 files changed

+874
-126
lines changed

13 files changed

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

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<uses-permission android:name="android.permission.READ_CLIPBOARD" />
1111
<uses-permission android:name="android.permission.INTERNET" />
1212
<uses-permission android:name="android.permission.CAMERA" />
13+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
1314

1415
<application
1516
android:name=".App"

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)