Updated on 2026-08-14
This commit is contained in:
commit
e242c36b46
15 changed files with 592 additions and 24 deletions
|
|
@ -366,7 +366,10 @@ class TradeCryptoMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency)
|
||||
val bundle = bundleOf(
|
||||
SendRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
|
||||
}
|
||||
}
|
||||
|
|
@ -416,7 +419,10 @@ class TradeCryptoMiddleware {
|
|||
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
|
||||
}
|
||||
|
||||
val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency)
|
||||
val bundle = bundleOf(
|
||||
SendRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ object BigDecimalFormatter {
|
|||
|
||||
private const val TEMP_CURRENCY_CODE = "USD"
|
||||
|
||||
fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String {
|
||||
fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: String, decimals: Int): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance().apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
|
|
@ -23,11 +25,13 @@ object BigDecimalFormatter {
|
|||
}
|
||||
|
||||
fun formatFiatAmount(
|
||||
fiatAmount: BigDecimal,
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatterCurrency = getCurrency(fiatCurrencyCode)
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ interface SendRouter {
|
|||
|
||||
companion object {
|
||||
const val CRYPTO_CURRENCY_KEY = "send_crypto_currency"
|
||||
const val USER_WALLET_ID_KEY = "send_user_wallet_id"
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.tangem.card.core)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
@ -38,6 +39,7 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.send.ui.SendScreen
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -24,13 +28,16 @@ internal class SendFragment : ComposeBottomSheetFragment() {
|
|||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(color = Color.Transparent)
|
||||
val viewModel = hiltViewModel<SendViewModel>()
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) }
|
||||
BackHandler { dismiss() }
|
||||
|
||||
when (val state = viewModel.uiState) {
|
||||
is SendUiState.Content -> SendScreen(state)
|
||||
SendUiState.Dismiss -> dismiss()
|
||||
}
|
||||
BackHandler {
|
||||
dismiss()
|
||||
}
|
||||
SendScreen()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendAmountStateConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val sendAmountFieldConverter: SendAmountFieldConverter,
|
||||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, SendUiState> {
|
||||
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
val userWallet = userWalletProvider() ?: return currentStateProvider()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
return value.fold(
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
currentStateProvider()
|
||||
},
|
||||
ifRight = {
|
||||
val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals)
|
||||
SendUiState.Content.AmountState(
|
||||
cryptoCurrencyStatus = it,
|
||||
walletName = userWallet.name,
|
||||
walletBalance = "$crypto ($fiat)",
|
||||
tokenIconState = iconStateConverter.convert(it),
|
||||
appCurrency = appCurrency,
|
||||
amountTextField = sendAmountFieldConverter.convert(Unit),
|
||||
isFiatValue = false,
|
||||
clickIntents = clickIntents,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(it.currency.symbol),
|
||||
iconState = iconStateConverter.convert(it),
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconState = iconStateConverter.convert(it),
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) }
|
||||
|
||||
private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) }
|
||||
|
||||
private val amountStateConverter by lazy {
|
||||
SendAmountStateConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
clickIntents = clickIntents,
|
||||
iconStateConverter = iconStateConverter,
|
||||
userWalletProvider = userWalletProvider,
|
||||
sendAmountFieldConverter = amountFieldConverter,
|
||||
)
|
||||
}
|
||||
|
||||
fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
|
||||
fun getAmountState(cryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
return amountStateConverter.convert(cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state as? SendUiState.Content.AmountState ?: return state
|
||||
|
||||
return if (amountState.isFiatValue == isFiat) {
|
||||
state
|
||||
} else {
|
||||
return state.copy(isFiatValue = isFiat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
/**
|
||||
|
|
@ -16,42 +17,54 @@ internal sealed class SendUiState {
|
|||
/** States with content */
|
||||
sealed class Content : SendUiState() {
|
||||
|
||||
abstract val nextButtonEnabled: Boolean
|
||||
/** Is primary button enabled */
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
/** Click intents */
|
||||
abstract val clickIntents: SendClickIntents
|
||||
|
||||
/** Initial state */
|
||||
data class Initial(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
val walletName: String,
|
||||
val walletBalance: String,
|
||||
val tokenIconState: TokenIconState,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonConfig>,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
override val nextButtonEnabled: Boolean = true,
|
||||
override val isPrimaryButtonEnabled: Boolean = true,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
}
|
||||
|
||||
/** Dismiss screen */
|
||||
object Dismiss : SendUiState()
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state.fields
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
||||
internal class SendAmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
) : Converter<String, SendUiState> {
|
||||
override fun convert(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
|
||||
if (
|
||||
state !is SendUiState.Content.AmountState ||
|
||||
value.checkDecimalSeparatorDuplicate()
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
val fiatRate = state.cryptoCurrencyStatus.value.fiatRate
|
||||
|
||||
val trimmedValue = value.trim()
|
||||
|
||||
val cryptoValue = if (state.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
DEFAULT_VALUE
|
||||
}
|
||||
} else {
|
||||
trimmedValue
|
||||
}
|
||||
|
||||
val fiatValue = if (!state.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
DEFAULT_VALUE
|
||||
}
|
||||
} else {
|
||||
trimmedValue
|
||||
}
|
||||
|
||||
val isExceedBalance = value.checkExceedBalance(state)
|
||||
return state.copy(
|
||||
amountTextField = state.amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
),
|
||||
isPrimaryButtonEnabled = !isExceedBalance,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SendUiState.Content.AmountState.emptyState(): SendUiState {
|
||||
return copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
value = if (!isFiatValue) "" else DEFAULT_VALUE,
|
||||
fiatValue = if (isFiatValue) "" else DEFAULT_VALUE,
|
||||
isError = false,
|
||||
),
|
||||
isPrimaryButtonEnabled = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkDecimalSeparatorDuplicate(): Boolean {
|
||||
val regex = "[\\.\\,]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(this).count()
|
||||
|
||||
return decimalSeparatorCount > 1
|
||||
}
|
||||
|
||||
private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean {
|
||||
val currencyStatus = state.cryptoCurrencyStatus.value
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() > currencyStatus.fiatAmount
|
||||
} else {
|
||||
toBigDecimal() > currencyStatus.amount
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.trim(): String {
|
||||
var trimmedValue = this
|
||||
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
|
||||
|
||||
val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state.fields
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.text.NumberFormat
|
||||
|
||||
internal class SendAmountFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<Unit, SendTextField.Amount> {
|
||||
|
||||
override fun convert(value: Unit): SendTextField.Amount {
|
||||
return SendTextField.Amount(
|
||||
value = "",
|
||||
fiatValue = DEFAULT_VALUE,
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(""),
|
||||
placeholder = TextReference.Str(DEFAULT_VALUE),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.send_insufficient_funds),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.send.ui.amount.AmountFieldContainer
|
||||
|
||||
@Composable
|
||||
internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.common_send),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing16)
|
||||
.align(CenterHorizontally),
|
||||
)
|
||||
AmountFieldContainer(amountState = amountState)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
SegmentedButtons(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size40)
|
||||
.weight(1f),
|
||||
config = amountState.segmentedButtonConfig,
|
||||
onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) },
|
||||
) {
|
||||
SendAmountCurrencyButton(it)
|
||||
}
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.send_max_amount),
|
||||
onClick = amountState.clickIntents::onMaxValueClick,
|
||||
size = TangemButtonSize.Text,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.height(TangemTheme.dimens.size40),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing10,
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (button.isFiat) {
|
||||
FiatIcon(
|
||||
url = button.iconUrl,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
} else {
|
||||
button.iconState?.let {
|
||||
TokenIcon(
|
||||
state = it,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = button.title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.button,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier:
|
|||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = uiState.nextButtonEnabled,
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
|
|
@ -84,7 +84,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier:
|
|||
} else {
|
||||
PrimaryButton(
|
||||
text = stringResource(textId),
|
||||
enabled = uiState.nextButtonEnabled,
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
|
||||
@Composable
|
||||
fun SendScreen() {
|
||||
internal fun SendScreen(uiState: SendUiState.Content) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
|
|
@ -37,13 +37,17 @@ fun SendScreen() {
|
|||
.weight(1f)
|
||||
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical),
|
||||
) {
|
||||
SendScreenContent()
|
||||
SendScreenContent(uiState)
|
||||
}
|
||||
SendNavigationButtons(uiState = SendUiState.Content.Initial())
|
||||
SendNavigationButtons(uiState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendScreenContent() {
|
||||
// todo work in progress
|
||||
private fun SendScreenContent(uiState: SendUiState.Content) {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> SendAmountContent(uiState)
|
||||
else -> { /* [REDACTED_TODO_COMMENT]*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.send.impl.presentation.send.viewmodel
|
||||
|
||||
interface SendClickIntents {
|
||||
|
||||
fun onNextClick()
|
||||
|
||||
fun onPrevClick()
|
||||
|
||||
fun onAmountValueChange(value: String)
|
||||
|
||||
fun onCurrencyChangeClick(isFiat: Boolean)
|
||||
|
||||
fun onMaxValueClick()
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.features.send.impl.presentation.send.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendStateFactory
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
||||
private val userWalletId: UserWalletId = savedStateHandle.get<String>(SendRouter.USER_WALLET_ID_KEY)
|
||||
?.let { stringValue -> UserWalletId(stringValue) }
|
||||
?: error("This screen can't open without `UserWalletId`")
|
||||
|
||||
private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
|
||||
?: error("This screen can't open without `CryptoCurrency`")
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
)
|
||||
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
private var userWallet: UserWallet? = null
|
||||
|
||||
private var balanceJobHolder = JobHolder()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
getUserWalletUseCase(userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
getCurrencyStatusUpdates(owner, wallet)
|
||||
},
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
return@launch
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrencyStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) {
|
||||
val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
|
||||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
derivationPath = cryptoCurrency.network.derivationPath,
|
||||
isSingleWalletWithTokens = isSingleWallet,
|
||||
)
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { either ->
|
||||
uiState = stateFactory.getAmountState(
|
||||
cryptoCurrencyStatus = either,
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun onNextClick() {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> onRecipientStateClick()
|
||||
is SendUiState.Content.RecipientState -> onFeeStateClick()
|
||||
else -> {
|
||||
// todo implement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrevClick() {
|
||||
// todo implement
|
||||
}
|
||||
|
||||
private fun onRecipientStateClick() {
|
||||
stateFactory.getOnReceiveState()
|
||||
}
|
||||
|
||||
private fun onFeeStateClick() {
|
||||
// todo implement
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region amount state clicks
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
uiState = stateFactory.getOnCurrencyChangedState(isFiat)
|
||||
}
|
||||
|
||||
override fun onAmountValueChange(value: String) {
|
||||
uiState = stateFactory.getOnAmountValueChange(value)
|
||||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
val amountState = uiState as? SendUiState.Content.AmountState ?: return
|
||||
|
||||
val amount = if (amountState.isFiatValue) {
|
||||
amountState.cryptoCurrencyStatus.value.fiatAmount
|
||||
} else {
|
||||
amountState.cryptoCurrencyStatus.value.amount
|
||||
}
|
||||
onAmountValueChange(amount?.toPlainString() ?: "0.00")
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue