Skip to content

Commit 01ac18d

Browse files
committed
refactor(transfer): move spending advanced screen logic to viewmodel
1 parent 8b07f97 commit 01ac18d

File tree

3 files changed

+194
-60
lines changed

3 files changed

+194
-60
lines changed

app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt

Lines changed: 135 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,21 @@ import androidx.compose.runtime.Composable
1616
import androidx.compose.runtime.LaunchedEffect
1717
import androidx.compose.runtime.collectAsState
1818
import androidx.compose.runtime.getValue
19-
import androidx.compose.runtime.mutableLongStateOf
2019
import androidx.compose.runtime.mutableStateOf
2120
import androidx.compose.runtime.remember
22-
import androidx.compose.runtime.rememberCoroutineScope
23-
import androidx.compose.runtime.saveable.rememberSaveable
2421
import androidx.compose.runtime.setValue
2522
import androidx.compose.ui.Alignment
2623
import androidx.compose.ui.Modifier
2724
import androidx.compose.ui.platform.testTag
2825
import androidx.compose.ui.res.stringResource
26+
import androidx.compose.ui.tooling.preview.Devices.NEXUS_5
27+
import androidx.compose.ui.tooling.preview.Preview
2928
import androidx.compose.ui.unit.dp
3029
import androidx.lifecycle.compose.collectAsStateWithLifecycle
31-
import kotlinx.coroutines.launch
3230
import to.bitkit.R
31+
import to.bitkit.ext.mockOrder
3332
import to.bitkit.ui.LocalCurrencies
3433
import to.bitkit.ui.appViewModel
35-
import to.bitkit.ui.blocktankViewModel
3634
import to.bitkit.ui.components.AmountInput
3735
import to.bitkit.ui.components.Caption13Up
3836
import to.bitkit.ui.components.Display
@@ -42,8 +40,12 @@ import to.bitkit.ui.components.PrimaryButton
4240
import to.bitkit.ui.scaffold.AppTopBar
4341
import to.bitkit.ui.scaffold.CloseNavIcon
4442
import to.bitkit.ui.scaffold.ScreenColumn
43+
import to.bitkit.ui.theme.AppThemeSurface
4544
import to.bitkit.ui.theme.Colors
4645
import to.bitkit.ui.utils.withAccent
46+
import to.bitkit.viewmodels.TransferEffect
47+
import to.bitkit.viewmodels.TransferToSpendingUiState
48+
import to.bitkit.viewmodels.TransferValues
4749
import to.bitkit.viewmodels.TransferViewModel
4850

4951
@Composable
@@ -53,18 +55,64 @@ fun SpendingAdvancedScreen(
5355
onCloseClick: () -> Unit = {},
5456
onOrderCreated: () -> Unit = {},
5557
) {
56-
val scope = rememberCoroutineScope()
5758
val app = appViewModel ?: return
58-
val blocktank = blocktankViewModel ?: return
59-
val currencies = LocalCurrencies.current
6059
val state by viewModel.spendingUiState.collectAsStateWithLifecycle()
6160
val order = state.order ?: return
61+
val transferValues by viewModel.transferValues.collectAsState()
62+
63+
LaunchedEffect(order.clientBalanceSat) {
64+
viewModel.updateTransferValues(order.clientBalanceSat)
65+
}
66+
67+
LaunchedEffect(Unit) {
68+
viewModel.transferEffects.collect { effect ->
69+
when (effect) {
70+
TransferEffect.OnOrderCreated -> onOrderCreated()
71+
is TransferEffect.ToastException -> app.toast(effect.e)
72+
is TransferEffect.ToastError -> app.toast(
73+
type = to.bitkit.models.Toast.ToastType.ERROR,
74+
title = effect.title,
75+
description = effect.description,
76+
)
77+
}
78+
}
79+
}
80+
81+
val isValid = transferValues.let {
82+
val isAboveMin = state.receivingAmount.toULong() >= it.minLspBalance
83+
val isBelowMax = state.receivingAmount.toULong() <= it.maxLspBalance
84+
state.receivingAmount > 0 && isAboveMin && isBelowMax
85+
}
86+
87+
Content(
88+
uiState = state,
89+
transferValues = transferValues,
90+
isValid = isValid,
91+
onBack = onBackClick,
92+
onClose = onCloseClick,
93+
onAmountChange = viewModel::onReceivingAmountChange,
94+
onContinue = viewModel::onSpendingAdvancedContinue,
95+
)
96+
}
97+
98+
@Composable
99+
private fun Content(
100+
uiState: TransferToSpendingUiState,
101+
transferValues: TransferValues,
102+
isValid: Boolean,
103+
onBack: () -> Unit,
104+
onClose: () -> Unit,
105+
onAmountChange: (Long) -> Unit,
106+
onContinue: () -> Unit,
107+
) {
108+
val currencies = LocalCurrencies.current
109+
uiState.order ?: return
62110

63111
ScreenColumn {
64112
AppTopBar(
65113
titleText = stringResource(R.string.lightning__transfer__nav_title),
66-
onBackClick = onBackClick,
67-
actions = { CloseNavIcon(onCloseClick) },
114+
onBackClick = onBack,
115+
actions = { CloseNavIcon(onClose) },
68116
)
69117
Column(
70118
modifier = Modifier
@@ -73,38 +121,9 @@ fun SpendingAdvancedScreen(
73121
.imePadding()
74122
.testTag("SpendingAdvanced")
75123
) {
76-
var receivingSatsAmount by rememberSaveable { mutableLongStateOf(0) }
77124
var overrideSats: Long? by remember { mutableStateOf(null) }
78-
79-
val clientBalance = order.clientBalanceSat
80-
var feeEstimate: Long? by remember { mutableStateOf(null) }
81125
var isLoading by remember { mutableStateOf(false) }
82126

83-
val transferValues by viewModel.transferValues.collectAsState()
84-
85-
LaunchedEffect(clientBalance) {
86-
viewModel.updateTransferValues(clientBalance)
87-
}
88-
89-
val isValid = transferValues.let {
90-
val isAboveMin = receivingSatsAmount.toULong() >= it.minLspBalance
91-
val isBelowMax = receivingSatsAmount.toULong() <= it.maxLspBalance
92-
isAboveMin && isBelowMax
93-
}
94-
95-
// Update feeEstimate
96-
LaunchedEffect(receivingSatsAmount, transferValues) {
97-
feeEstimate = null
98-
if (!isValid) return@LaunchedEffect
99-
runCatching {
100-
val estimate = blocktank.estimateOrderFee(
101-
spendingBalanceSats = clientBalance,
102-
receivingBalanceSats = receivingSatsAmount.toULong(),
103-
)
104-
feeEstimate = estimate.feeSat.toLong()
105-
}
106-
}
107-
108127
Spacer(modifier = Modifier.height(32.dp))
109128
Display(
110129
text = stringResource(R.string.lightning__spending_advanced__title)
@@ -113,10 +132,11 @@ fun SpendingAdvancedScreen(
113132
Spacer(modifier = Modifier.height(32.dp))
114133

115134
AmountInput(
135+
defaultValue = uiState.receivingAmount,
116136
primaryDisplay = currencies.primaryDisplay,
117137
overrideSats = overrideSats,
118138
onSatsChange = { sats ->
119-
receivingSatsAmount = sats
139+
onAmountChange(sats)
120140
overrideSats = null
121141
},
122142
modifier = Modifier.testTag("SpendingAdvancedNumberField")
@@ -132,7 +152,7 @@ fun SpendingAdvancedScreen(
132152
color = Colors.White64,
133153
)
134154
Spacer(modifier = Modifier.width(4.dp))
135-
feeEstimate?.let {
155+
uiState.feeEstimate?.let {
136156
MoneySSB(it)
137157
} ?: run {
138158
Caption13Up(text = "", color = Colors.White64)
@@ -184,20 +204,7 @@ fun SpendingAdvancedScreen(
184204
text = stringResource(R.string.common__continue),
185205
onClick = {
186206
isLoading = true
187-
scope.launch {
188-
try {
189-
val newOrder = blocktank.createOrder(
190-
spendingBalanceSats = clientBalance,
191-
receivingBalanceSats = receivingSatsAmount.toULong(),
192-
)
193-
viewModel.onAdvancedOrderCreated(newOrder)
194-
onOrderCreated()
195-
} catch (e: Throwable) {
196-
app.toast(e)
197-
} finally {
198-
isLoading = false
199-
}
200-
}
207+
onContinue()
201208
},
202209
enabled = !isLoading && isValid,
203210
isLoading = isLoading,
@@ -208,3 +215,76 @@ fun SpendingAdvancedScreen(
208215
}
209216
}
210217
}
218+
219+
@Preview(showSystemUi = true)
220+
@Composable
221+
private fun Preview() {
222+
AppThemeSurface {
223+
Content(
224+
uiState = TransferToSpendingUiState(
225+
order = mockOrder().copy(clientBalanceSat = 100_000u),
226+
receivingAmount = 55_000L,
227+
feeEstimate = 2_500L,
228+
),
229+
transferValues = TransferValues(
230+
defaultLspBalance = 50_000u,
231+
minLspBalance = 10_000u,
232+
maxLspBalance = 90_000u,
233+
),
234+
isValid = true,
235+
onBack = {},
236+
onClose = {},
237+
onAmountChange = {},
238+
onContinue = {},
239+
)
240+
}
241+
}
242+
243+
@Preview(showSystemUi = true, device = NEXUS_5)
244+
@Composable
245+
private fun PreviewSmall() {
246+
AppThemeSurface {
247+
Content(
248+
uiState = TransferToSpendingUiState(
249+
order = mockOrder().copy(clientBalanceSat = 50_000u),
250+
receivingAmount = 120_521L,
251+
feeEstimate = 12_461L,
252+
),
253+
transferValues = TransferValues(
254+
defaultLspBalance = 50_000u,
255+
minLspBalance = 10_000u,
256+
maxLspBalance = 90_000u,
257+
),
258+
isValid = true,
259+
onBack = {},
260+
onClose = {},
261+
onAmountChange = {},
262+
onContinue = {},
263+
)
264+
}
265+
}
266+
267+
@Preview(showSystemUi = true)
268+
@Composable
269+
private fun PreviewLoading() {
270+
AppThemeSurface {
271+
Content(
272+
uiState = TransferToSpendingUiState(
273+
order = mockOrder().copy(clientBalanceSat = 50_000u),
274+
receivingAmount = 20_000L,
275+
feeEstimate = null,
276+
isLoading = true,
277+
),
278+
transferValues = TransferValues(
279+
defaultLspBalance = 25_000u,
280+
minLspBalance = 10_000u,
281+
maxLspBalance = 40_000u,
282+
),
283+
isValid = true,
284+
onBack = {},
285+
onClose = {},
286+
onAmountChange = {},
287+
onContinue = {},
288+
)
289+
}
290+
}

app/src/main/java/to/bitkit/viewmodels/BlocktankViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ class BlocktankViewModel @Inject constructor(
6161
suspend fun estimateOrderFee(
6262
spendingBalanceSats: ULong,
6363
receivingBalanceSats: ULong,
64-
): IBtEstimateFeeResponse2 {
65-
return blocktankRepo.estimateOrderFee(spendingBalanceSats, receivingBalanceSats).getOrThrow()
64+
): Result<IBtEstimateFeeResponse2> {
65+
return blocktankRepo.estimateOrderFee(spendingBalanceSats, receivingBalanceSats)
6666
}
6767

6868
suspend fun openChannel(orderId: String): IBtOrder {

app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,61 @@ class TransferViewModel @Inject constructor(
133133
updateAvailableAmount()
134134
}
135135

136-
fun onAdvancedOrderCreated(order: IBtOrder) {
137-
val defaultOrder = _spendingUiState.value.order
138-
_spendingUiState.update { it.copy(order = order, defaultOrder = defaultOrder, isAdvanced = true) }
136+
fun onReceivingAmountChange(amount: Long) {
137+
viewModelScope.launch {
138+
_spendingUiState.update { it.copy(receivingAmount = amount, feeEstimate = null) }
139+
140+
if (amount == 0L) return@launch
141+
142+
val transferValues = _transferValues.value
143+
if (transferValues.minLspBalance == 0uL) return@launch
144+
145+
val isValid = amount.toULong() >= transferValues.minLspBalance &&
146+
amount.toULong() <= transferValues.maxLspBalance
147+
148+
if (!isValid) return@launch
149+
150+
val result = blocktankRepo.estimateOrderFee(
151+
spendingBalanceSats = _spendingUiState.value.order?.clientBalanceSat ?: 0u,
152+
receivingBalanceSats = amount.toULong(),
153+
)
154+
155+
result.fold(
156+
onSuccess = { response ->
157+
_spendingUiState.update {
158+
it.copy(feeEstimate = response.feeSat.toLong())
159+
}
160+
},
161+
onFailure = { error ->
162+
Logger.error("Failed to estimate fee", error, context = TAG)
163+
_spendingUiState.update {
164+
it.copy(feeEstimate = null)
165+
}
166+
}
167+
)
168+
}
169+
}
170+
171+
fun onSpendingAdvancedContinue() {
172+
viewModelScope.launch {
173+
runCatching {
174+
val oldOrder = _spendingUiState.value.order ?: return@launch
175+
val newOrder = blocktankRepo.createOrder(
176+
spendingBalanceSats = oldOrder.clientBalanceSat,
177+
receivingBalanceSats = _spendingUiState.value.receivingAmount.toULong(),
178+
).getOrThrow()
179+
_spendingUiState.update {
180+
it.copy(
181+
order = newOrder,
182+
defaultOrder = oldOrder,
183+
isAdvanced = true,
184+
)
185+
}
186+
setTransferEffect(TransferEffect.OnOrderCreated)
187+
}.onFailure { e ->
188+
setTransferEffect(TransferEffect.ToastException(e))
189+
}
190+
}
139191
}
140192

141193
/** Pays for the order and start watching it for state updates */
@@ -492,6 +544,8 @@ data class TransferToSpendingUiState(
492544
val maxAllowedToSend: Long = 0,
493545
val balanceAfterFee: Long = 0,
494546
val isLoading: Boolean = false,
547+
val receivingAmount: Long = 0,
548+
val feeEstimate: Long? = null,
495549
) {
496550
fun balanceAfterFeeQuarter() = (balanceAfterFee.toDouble() * 0.25).roundToLong()
497551
}

0 commit comments

Comments
 (0)