@@ -8,13 +8,16 @@ import com.flipcash.app.core.ui.CurrencyHolder
88import com.flipcash.app.tokens.TokenCoordinator
99import com.flipcash.features.cash.R
1010import com.flipcash.libs.coroutines.DispatcherProvider
11+ import com.flipcash.shared.amountentry.AmountEntryAction
12+ import com.flipcash.shared.amountentry.AmountEntryConfig
13+ import com.flipcash.shared.amountentry.AmountEntryDelegate
14+ import com.flipcash.shared.amountentry.AmountEntryHint
1115import com.getcode.manager.BottomBarAction
1216import com.getcode.manager.BottomBarManager
1317import com.getcode.opencode.controllers.TransactionOperations
1418import com.getcode.opencode.exchange.Exchange
1519import com.getcode.opencode.exchange.VerifiedFiatCalculator
1620import com.getcode.opencode.model.core.errors.ComputeVerifiedFiatError
17- import com.getcode.opencode.model.financial.Currency
1821import com.getcode.opencode.model.financial.CurrencyCode
1922import com.getcode.opencode.model.financial.Fiat
2023import com.getcode.opencode.model.financial.Limits
@@ -23,13 +26,12 @@ import com.getcode.opencode.model.financial.SendLimit
2326import com.getcode.opencode.model.financial.TokenWithLocalizedBalance
2427import com.getcode.opencode.model.financial.minus
2528import com.getcode.solana.keys.Mint
26- import com.getcode.ui.components.text.AmountAnimatedInputUiModel
27- import com.getcode.ui.components.text.NumberInputHelper
2829import com.getcode.util.resources.ResourceHelper
2930import com.getcode.view.BaseViewModel
3031import com.getcode.view.LoadingSuccessState
3132import dagger.hilt.android.lifecycle.HiltViewModel
3233import kotlinx.coroutines.CompletableDeferred
34+ import kotlinx.coroutines.flow.SharingStarted
3335import kotlinx.coroutines.flow.combine
3436import kotlinx.coroutines.flow.distinctUntilChanged
3537import kotlinx.coroutines.flow.drop
@@ -41,6 +43,7 @@ import kotlinx.coroutines.flow.launchIn
4143import kotlinx.coroutines.flow.map
4244import kotlinx.coroutines.flow.mapNotNull
4345import kotlinx.coroutines.flow.onEach
46+ import kotlinx.coroutines.flow.stateIn
4447import kotlinx.coroutines.flow.take
4548import kotlinx.coroutines.launch
4649import javax.inject.Inject
@@ -60,59 +63,31 @@ internal class CashScreenViewModel @Inject constructor(
6063 defaultDispatcher = dispatchers.Default ,
6164) {
6265
63- private val numberInputHelper = NumberInputHelper ( )
66+ val amountDelegate = AmountEntryDelegate (exchange, viewModelScope )
6467 private val tokenInitialized = CompletableDeferred <Mint ?>()
6568
6669 internal data class State (
6770 val selectedTokenAddress : Mint ? = null ,
6871 val token : TokenWithLocalizedBalance ? = null ,
6972 val currencyModel : CurrencyHolder = CurrencyHolder (),
70- val amountAnimatedModel : AmountAnimatedInputUiModel = AmountAnimatedInputUiModel (),
7173 val limits : Limits ? = null ,
7274 val maxForGive : Pair <Double , CurrencyCode >? = null ,
7375 val generatingBill : LoadingSuccessState = LoadingSuccessState (),
7476 ) {
75- val canGive: Boolean
76- get() = (amountAnimatedModel.amountData.amount) > 0.00
77-
7877 val maxAvailableForGive: String
7978 get() = maxForGive?.let { Fiat (it.first, it.second).formatted() }.orEmpty()
80-
81-
82- val isError: Boolean
83- get() {
84- if (amountAnimatedModel.amountData.isEmpty()) return false
85- if (maxForGive != null ) {
86- val enteredAmount = Fiat (
87- fiat = amountAnimatedModel.amountData.amount,
88- currencyCode = maxForGive.second
89- )
90- val limit = Fiat (maxForGive.first, maxForGive.second)
91- if (enteredAmount.valueLessThanOrEqualTo(limit)) {
92- return false
93- }
94- }
95-
96- return true
97- }
9879 }
9980
10081 sealed interface Event {
10182 data class InitializeToken (val mint : Mint ? ) : Event
10283 data class OnTokenSelected (val address : Mint ) : Event
10384 data class OnTokenUpdated (val token : TokenWithLocalizedBalance ) : Event
104- data class OnNumberPressed (val number : Int ) : Event
105- data object OnDecimalPressed : Event
106- data object OnBackspace : Event
107- data class OnEnteredNumberChanged (val backspace : Boolean = false ) : Event
108- data class OnAmountChanged (val amountAnimatedModel : AmountAnimatedInputUiModel ) : Event
109- data class OnCurrencyChanged (val model : Currency ) : Event
85+ data class OnCurrencyChanged (val model : com.getcode.opencode.model.financial.Currency ) : Event
11086 data class OnMaxDetermined (val max : Double , val currencyCode : CurrencyCode ) : Event
11187 data class OnLimitsChanged (val limits : Limits ? ) : Event
11288 data object OnGive : Event
11389 data class PresentBill (val bill : Bill .Cash ) : Event
11490
115-
11691 data class AddCashToWallet (val amount : Fiat ) : Event
11792 data class UpdateLoadingState (val loading : Boolean = false , val success : Boolean = false ) :
11893 Event
@@ -123,7 +98,7 @@ internal class CashScreenViewModel @Inject constructor(
12398 val checkBalanceLimit: () -> Boolean = {
12499 // this balance check differs from withdrawal due to the fact this is a localized check
125100 // whereas withdrawal is USD locked
126- val amount = stateFlow. value.amountAnimatedModel.amountData.amount
101+ val amount = amountDelegate.state. value.enteredAmount
127102 val enteredAmount = Fiat (
128103 fiat = amount,
129104 currencyCode = stateFlow.value.currencyModel.code ? : CurrencyCode .USD
@@ -170,7 +145,7 @@ internal class CashScreenViewModel @Inject constructor(
170145 isOverBalance
171146 }
172147 val checkSendLimit: () -> Boolean = {
173- val amount = stateFlow. value.amountAnimatedModel.amountData.amount
148+ val amount = amountDelegate.state. value.enteredAmount
174149 val currency = stateFlow.value.currencyModel
175150 val sendLimit =
176151 currency.code?.let { stateFlow.value.limits?.sendLimitFor(it) } ? : SendLimit .Zero
@@ -185,8 +160,6 @@ internal class CashScreenViewModel @Inject constructor(
185160 }
186161
187162 init {
188- numberInputHelper.reset()
189-
190163 eventFlow
191164 .filterIsInstance<Event .InitializeToken >()
192165 .take(1 )
@@ -230,68 +203,13 @@ internal class CashScreenViewModel @Inject constructor(
230203 exchange.getCurrency(balance.rate.currency.name)
231204 }.onEach {
232205 dispatchEvent(Event .OnCurrencyChanged (it))
233- }.launchIn(viewModelScope)
234-
235- exchange.observePreferredRate()
236- .onEach {
237- // reset when entry rate changes
238- numberInputHelper.reset()
239- dispatchEvent(Event .OnAmountChanged (AmountAnimatedInputUiModel ()))
206+ amountDelegate.onCurrencyChanged(it)
240207 }.launchIn(viewModelScope)
241208
242209 transactionController.limits
243210 .onEach { dispatchEvent(Event .OnLimitsChanged (it)) }
244211 .launchIn(viewModelScope)
245212
246- eventFlow
247- .filterIsInstance<Event .OnCurrencyChanged >()
248- .map { it.model }
249- .onEach {
250- numberInputHelper.fractionUnits = it.fractionUnits
251- }.launchIn(viewModelScope)
252-
253- eventFlow
254- .filterIsInstance<Event .OnNumberPressed >()
255- .map { it.number }
256- .onEach { number ->
257- numberInputHelper.fractionUnits = stateFlow.value.currencyModel.fractionUnits
258- numberInputHelper.maxLength = 10 // 1 billion dollars
259- numberInputHelper.onNumber(number)
260- dispatchEvent(Event .OnEnteredNumberChanged ())
261- }.launchIn(viewModelScope)
262-
263- eventFlow
264- .filterIsInstance<Event .OnDecimalPressed >()
265- .onEach {
266- numberInputHelper.onDot()
267- dispatchEvent(Event .OnEnteredNumberChanged ())
268- }
269- .launchIn(viewModelScope)
270-
271- eventFlow
272- .filterIsInstance<Event .OnBackspace >()
273- .onEach {
274- numberInputHelper.onBackspace()
275- dispatchEvent(Event .OnEnteredNumberChanged (true ))
276- }.launchIn(viewModelScope)
277-
278- eventFlow
279- .filterIsInstance<Event .OnEnteredNumberChanged >()
280- .map { it.backspace }
281- .onEach { backspace ->
282- val current = stateFlow.value.amountAnimatedModel
283- val model = stateFlow.value.amountAnimatedModel
284- val amount = numberInputHelper.getFormattedStringForAnimation(includeCommas = true )
285-
286- val updated = model.copy(
287- amountDataLast = current.amountData,
288- amountData = amount,
289- lastPressedBackspace = backspace
290- )
291-
292- dispatchEvent(Event .OnAmountChanged (updated))
293- }.launchIn(viewModelScope)
294-
295213 stateFlow
296214 .filter { it.limits != null }
297215 .map { it.limits to (it.token?.balance ? : LocalFiat .Zero ) }
@@ -304,15 +222,15 @@ internal class CashScreenViewModel @Inject constructor(
304222
305223 eventFlow
306224 .filterIsInstance<Event .OnGive >()
307- .map { stateFlow.value.amountAnimatedModel }
308225 .filter { ! (checkBalanceLimit() || checkSendLimit()) }
309- .onEach { data ->
226+ .onEach {
310227 dispatchEvent(Event .UpdateLoadingState (loading = true ))
311228 val (token, balance) = stateFlow.value.token!!
312229 val rate = exchange.preferredRate
230+ val amount = amountDelegate.state.value.enteredAmount
313231
314232 val result = verifiedFiatCalculator.compute(
315- amount = Fiat (data.amountData. amount, rate.currency),
233+ amount = Fiat (amount, rate.currency),
316234 token = token,
317235 balance = balance.underlyingTokenAmount,
318236 rate = rate,
@@ -361,6 +279,45 @@ internal class CashScreenViewModel @Inject constructor(
361279 }
362280
363281
282+ val config: kotlinx.coroutines.flow.StateFlow <AmountEntryConfig > = combine(
283+ amountDelegate.state,
284+ stateFlow,
285+ ) { delegateState, vmState ->
286+ val isError = run {
287+ if (delegateState.isEmpty) false
288+ else if (vmState.maxForGive != null ) {
289+ val enteredAmount = Fiat (
290+ fiat = delegateState.enteredAmount,
291+ currencyCode = vmState.maxForGive.second
292+ )
293+ val limit = Fiat (vmState.maxForGive.first, vmState.maxForGive.second)
294+ ! enteredAmount.valueLessThanOrEqualTo(limit)
295+ } else true
296+ }
297+
298+ AmountEntryConfig (
299+ hint = if (isError) {
300+ AmountEntryHint .Error (
301+ resources.getString(R .string.subtitle_giveCashHintLimitExceeded, vmState.maxAvailableForGive)
302+ )
303+ } else {
304+ AmountEntryHint .Info (
305+ resources.getString(R .string.subtitle_giveCashHint, vmState.maxAvailableForGive)
306+ )
307+ },
308+ canConfirm = delegateState.enteredAmount > 0.00 ,
309+ canChangeCurrency = true ,
310+ action = AmountEntryAction (
311+ label = resources.getString(R .string.action_next),
312+ loadingState = vmState.generatingBill,
313+ ),
314+ )
315+ }.stateIn(
316+ viewModelScope,
317+ SharingStarted .WhileSubscribed (5000 ),
318+ AmountEntryConfig (action = AmountEntryAction (label = " " )),
319+ )
320+
364321 internal companion object {
365322 val updateStateForEvent: (Event ) -> ((State ) -> State ) = { event ->
366323 when (event) {
@@ -372,22 +329,12 @@ internal class CashScreenViewModel @Inject constructor(
372329 state.copy(token = event.token)
373330 }
374331
375- is Event .OnAmountChanged -> { state ->
376- state.copy(
377- amountAnimatedModel = event.amountAnimatedModel
378- )
379- }
380-
381332 is Event .InitializeToken -> { state -> state }
382333
383334 is Event .OpenScreen -> { state -> state }
384335
385- Event .OnBackspace ,
386336 Event .OnGive ,
387- is Event .OnEnteredNumberChanged ,
388- is Event .PresentBill ,
389- is Event .OnNumberPressed ,
390- Event .OnDecimalPressed -> { state -> state }
337+ is Event .PresentBill -> { state -> state }
391338
392339 is Event .OnCurrencyChanged -> { state ->
393340 state.copy(currencyModel = CurrencyHolder (event.model))
0 commit comments