Updated on 2026-08-14
This commit is contained in:
commit
a1b910cc7e
149 changed files with 4442 additions and 1330 deletions
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
internal sealed class AlertState {
|
||||
|
||||
abstract val message: TextReference
|
||||
|
||||
class DefaultAlert(
|
||||
override val message: TextReference,
|
||||
) : AlertState()
|
||||
|
||||
class TokenUnavailable(
|
||||
val onUpvoteClick: () -> Unit,
|
||||
) : AlertState() {
|
||||
override val message: TextReference = resourceReference(R.string.manage_tokens_unavailable_description)
|
||||
val confirmButtonText: TextReference = resourceReference(R.string.common_close)
|
||||
val dismissButtonText: TextReference = resourceReference(R.string.manage_tokens_unavailable_vote)
|
||||
}
|
||||
|
||||
object NonNative : AlertState() {
|
||||
override val message: TextReference = resourceReference(R.string.manage_tokens_network_selector_non_native_info)
|
||||
}
|
||||
|
||||
object TokensUnsupported : AlertState() {
|
||||
override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_message)
|
||||
}
|
||||
|
||||
object TokensUnsupportedCurve : AlertState() {
|
||||
override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_curve_message)
|
||||
}
|
||||
|
||||
class TokensUnsupportedBlockchainByCard(val token: String) : AlertState() {
|
||||
override val message: TextReference = resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message,
|
||||
formatArgs = wrappedList(token),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal sealed interface Event {
|
||||
data class ShowAlert(val state: AlertState) : Event
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.managetokens.presentation.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
|
||||
@Composable
|
||||
internal fun EventEffect(event: StateEvent<Event>, onAlertStateSet: (AlertState) -> Unit) {
|
||||
com.tangem.core.ui.event.EventEffect(
|
||||
event = event,
|
||||
onTrigger = { value ->
|
||||
when (value) {
|
||||
is Event.ShowAlert -> onAlertStateSet(value.state)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.managetokens.presentation.common.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
|
||||
@Composable
|
||||
internal fun Alert(state: AlertState, onDismiss: () -> Unit) {
|
||||
when (state) {
|
||||
is AlertState.DefaultAlert,
|
||||
is AlertState.NonNative,
|
||||
AlertState.TokensUnsupportedCurve,
|
||||
AlertState.TokensUnsupported,
|
||||
is AlertState.TokensUnsupportedBlockchainByCard,
|
||||
-> DefaultAlert(state, onDismiss)
|
||||
is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultAlert(state: AlertState, onDismiss: () -> Unit) {
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
confirmButton = DialogButton(
|
||||
title = stringResource(id = R.string.common_ok),
|
||||
onClick = onDismiss,
|
||||
),
|
||||
onDismissDialog = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenUnavailableAlert(state: AlertState.TokenUnavailable, onDismiss: () -> Unit) {
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
confirmButton = DialogButton(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = onDismiss,
|
||||
),
|
||||
dismissButton = DialogButton(
|
||||
title = state.dismissButtonText.resolveReference(),
|
||||
onClick = { state.onUpvoteClick() },
|
||||
),
|
||||
onDismissDialog = onDismiss,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
/**
|
||||
* SearchBar state.
|
||||
*/
|
||||
internal data class SearchBarState(
|
||||
val query: String,
|
||||
val onQueryChange: (String) -> Unit,
|
||||
val active: Boolean,
|
||||
val onActiveChange: (Boolean) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.managetokens.state.SearchBarState
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
internal fun TokensSearchBar(state: SearchBarState, modifier: Modifier = Modifier) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
TextField(
|
||||
value = state.query,
|
||||
onValueChange = state.onQueryChange,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
textStyle = TangemTheme.typography.body2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_search_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable { state.onActiveChange(true) },
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (state.query.isNotEmpty() || state.active) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (state.query.isNotEmpty()) {
|
||||
state.onQueryChange("")
|
||||
}
|
||||
focusManager.clearFocus()
|
||||
keyboardController?.hide()
|
||||
state.onActiveChange(false)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.manage_tokens_search_placeholder),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius36),
|
||||
colors = searchbarTextFieldColors(),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
state.onActiveChange(true)
|
||||
} else {
|
||||
state.onActiveChange(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun searchbarTextFieldColors(): TextFieldColors {
|
||||
return TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = TangemTheme.colors.field.primary,
|
||||
textColor = TangemTheme.colors.text.primary1,
|
||||
cursorColor = TangemTheme.colors.icon.primary1,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TokensSearchBar_Light(
|
||||
@PreviewParameter(SearchBarkConfigProvider::class)
|
||||
state: SearchBarState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
TokensSearchBar(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TokensSearchBar_Dark(@PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarState) {
|
||||
TangemTheme(isDark = true) {
|
||||
TokensSearchBar(state)
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchBarkConfigProvider : CollectionPreviewParameterProvider<SearchBarState>(
|
||||
collection = listOf(
|
||||
SearchBarState(
|
||||
query = "BTC",
|
||||
onQueryChange = {},
|
||||
active = true,
|
||||
onActiveChange = {},
|
||||
),
|
||||
SearchBarState(
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
active = false,
|
||||
onActiveChange = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
|
|
@ -25,8 +26,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
@Composable
|
||||
internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) {
|
||||
val iconModifier = modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
val iconModifier = modifier.size(TangemTheme.dimens.size36)
|
||||
if (state.iconReference != null) {
|
||||
DefaultCurrencyIcon(
|
||||
modifier = iconModifier,
|
||||
|
|
@ -73,11 +73,14 @@ private inline fun DefaultCurrencyIcon(
|
|||
crossinline errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
var isBackgroundColorDefined by remember { mutableStateOf(false) }
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier
|
||||
.background(
|
||||
|
|
@ -86,17 +89,21 @@ private inline fun DefaultCurrencyIcon(
|
|||
),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(iconReference.getReference())
|
||||
.size(size = pixelsSize)
|
||||
.memoryCacheKey(key = iconReference.getReference().toString() + pixelsSize)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
onSuccess = { _, result ->
|
||||
if (isDarkTheme) {
|
||||
if (!isBackgroundColorDefined && isDarkTheme) {
|
||||
coroutineScope.launch {
|
||||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = itemBackgroundColor,
|
||||
).getContrastColorIfNeeded(isDarkTheme)
|
||||
size = pixelsSize,
|
||||
).getContrastColor(isDarkTheme = true)
|
||||
iconBackgroundColor = color
|
||||
isBackgroundColorDefined = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ dependencies {
|
|||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.appCompat)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
@ -29,6 +31,12 @@ dependencies {
|
|||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.paging)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common)
|
||||
|
|
@ -40,12 +48,16 @@ dependencies {
|
|||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.transaction)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
|
|
|
|||
|
|
@ -1,43 +1,53 @@
|
|||
package com.tangem.features.send.impl.presentation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import android.os.Bundle
|
||||
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 androidx.fragment.app.viewModels
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.ui.SendScreen
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Send fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
internal class SendFragment : ComposeBottomSheetFragment() {
|
||||
internal class SendFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
override val expandedHeightFraction: Float = 1f
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
viewModel.setRouter(
|
||||
StateRouter(
|
||||
fragmentManager = WeakReference(parentFragmentManager),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
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()
|
||||
val systemBarsColor = TangemTheme.colors.background.tertiary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
SendScreen(viewModel.uiState)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
lifecycle.removeObserver(viewModel)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.send.impl.presentation.domain
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Available wallet to send
|
||||
*
|
||||
* @property name wallet name
|
||||
* @property address blockchain address
|
||||
*/
|
||||
@Immutable
|
||||
data class AvailableWallet(
|
||||
val name: String,
|
||||
val address: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.send.impl.presentation.domain
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendRecipientListContent {
|
||||
data class Item(
|
||||
val id: String,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val info: TextReference? = null,
|
||||
@DrawableRes val subtitleIconRes: Int? = null,
|
||||
) : SendRecipientListContent()
|
||||
|
||||
data class Wallets(
|
||||
val list: PersistentList<Item>,
|
||||
val isWalletsOnly: Boolean,
|
||||
) : SendRecipientListContent()
|
||||
}
|
||||
|
|
@ -1,59 +1,210 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import arrow.core.Either
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeCustomFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.validateMemo
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.verifyAddress
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val walletAddressesProvider: Provider<Set<Address>>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
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,
|
||||
private val customFeeFieldConverter by lazy {
|
||||
SendFeeCustomFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
iconStateConverter = iconStateConverter,
|
||||
userWalletProvider = userWalletProvider,
|
||||
sendAmountFieldConverter = amountFieldConverter,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
|
||||
fun getAmountState(cryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
return amountStateConverter.convert(cryptoCurrencyStatus)
|
||||
private val amountStateConverter by lazy {
|
||||
SendAmountStateConverter(
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
iconStateConverter = iconStateConverter,
|
||||
userWalletProvider = userWalletProvider,
|
||||
sendAmountFieldConverter = amountFieldConverter,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val recipientStateConverter by lazy {
|
||||
SendRecipientStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val feeStateConverter by lazy {
|
||||
SendFeeStateConverter(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
private val recipientListStateConverter by lazy {
|
||||
SendRecipientListConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
// region UI states
|
||||
fun getInitialState(): SendUiState = SendUiState(
|
||||
clickIntents = clickIntents,
|
||||
currentState = MutableStateFlow(SendUiStateType.Amount),
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState = currentStateProvider().copy(
|
||||
amountState = amountStateConverter.convert(Unit),
|
||||
recipientState = recipientStateConverter.convert(Unit),
|
||||
feeState = feeStateConverter.convert(Unit),
|
||||
)
|
||||
//endregion
|
||||
|
||||
//region amount state clicks
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state as? SendUiState.Content.AmountState ?: return state
|
||||
val amountState = state.amountState ?: return state
|
||||
|
||||
return if (amountState.isFiatValue == isFiat) {
|
||||
state
|
||||
} else {
|
||||
return state.copy(isFiatValue = isFiat)
|
||||
return state.copy(amountState = amountState.copy(isFiatValue = isFiat))
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region recipient
|
||||
fun onLoadedRecipientList(
|
||||
wallets: List<AvailableWallet?>,
|
||||
txHistory: PagingData<TxHistoryItem>,
|
||||
txHistoryCount: Int,
|
||||
) {
|
||||
recipientListStateConverter.convert(
|
||||
wallets = wallets,
|
||||
txHistory = txHistory,
|
||||
txHistoryCount = txHistoryCount,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientAddressValueChangeState(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
val isValidMemo = validateMemo(
|
||||
memo = value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
val isAddressInWallet = isNotAddressInWallet(
|
||||
walletAddresses = walletAddressesProvider(),
|
||||
address = recipientState.addressTextField.value.value,
|
||||
)
|
||||
val isValidAddress = verifyAddress(
|
||||
address = recipientState.addressTextField.value.value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
|
||||
recipientState.addressTextField.update {
|
||||
it.copy(
|
||||
value = value,
|
||||
error = when {
|
||||
!isValidAddress -> TextReference.Res(R.string.send_recipient_address_error)
|
||||
!isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
|
||||
else -> null
|
||||
},
|
||||
isError = !isValidAddress || !isAddressInWallet,
|
||||
)
|
||||
}
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientMemoValueChangeState(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
val isValidMemo = validateMemo(
|
||||
memo = value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
val isAddressInWallet = isNotAddressInWallet(
|
||||
walletAddresses = walletAddressesProvider(),
|
||||
address = recipientState.addressTextField.value.value,
|
||||
)
|
||||
val isValidAddress = verifyAddress(
|
||||
address = recipientState.addressTextField.value.value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
|
||||
// todo add memo validation error text
|
||||
recipientState.memoTextField?.update {
|
||||
it.copy(
|
||||
value = value,
|
||||
error = TextReference.Res(R.string.send_memo_destination_tag_error),
|
||||
isError = !isValidMemo,
|
||||
)
|
||||
}
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadingState() {
|
||||
currentStateProvider().feeState?.feeSelectorState?.update {
|
||||
FeeSelectorState.Loading
|
||||
}
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee) {
|
||||
currentStateProvider().feeState?.feeSelectorState?.update {
|
||||
FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region fee
|
||||
fun onFeeSelectedState(feeType: FeeType) {
|
||||
currentStateProvider().feeState?.feeSelectorState?.update {
|
||||
(it as? FeeSelectorState.Content)?.copy(selectedFee = feeType) ?: it
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -1,71 +1,81 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.paging.PagingData
|
||||
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.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* Ui states of the send screen
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed class SendUiState {
|
||||
internal data class SendUiState(
|
||||
val clickIntents: SendClickIntents,
|
||||
val amountState: SendStates.AmountState? = null,
|
||||
val recipientState: SendStates.RecipientState? = null,
|
||||
val feeState: SendStates.FeeState? = null,
|
||||
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val currentState: MutableStateFlow<SendUiStateType>,
|
||||
)
|
||||
|
||||
/** States with content */
|
||||
sealed class Content : SendUiState() {
|
||||
@Stable
|
||||
internal sealed class SendStates {
|
||||
|
||||
/** Is primary button enabled */
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
abstract val type: SendUiStateType
|
||||
|
||||
/** Click intents */
|
||||
abstract val clickIntents: SendClickIntents
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
override val type: SendUiStateType = SendUiStateType.Amount,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val walletName: String,
|
||||
val walletBalance: String,
|
||||
val tokenIconState: TokenIconState,
|
||||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: MutableStateFlow<SendTextField.Amount>,
|
||||
val isPrimaryButtonEnabled: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
/** Initial state */
|
||||
data class Initial(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val type: SendUiStateType = SendUiStateType.Recipient,
|
||||
val addressTextField: MutableStateFlow<SendTextField.RecipientAddress>,
|
||||
val memoTextField: MutableStateFlow<SendTextField.RecipientMemo>?,
|
||||
val recipients: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val network: String,
|
||||
val isPrimaryButtonEnabled: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
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<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
) : Content()
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val type: SendUiStateType = SendUiStateType.Fee,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeSelectorState: MutableStateFlow<FeeSelectorState> = MutableStateFlow(FeeSelectorState.Empty),
|
||||
val isSubtract: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val receivedAmount: MutableStateFlow<String> = MutableStateFlow(""),
|
||||
) : SendStates()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
val isSuccess: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
override val isPrimaryButtonEnabled: Boolean = true,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
}
|
||||
|
||||
/** Dismiss screen */
|
||||
object Dismiss : SendUiState()
|
||||
enum class SendUiStateType {
|
||||
Amount,
|
||||
Recipient,
|
||||
Fee,
|
||||
Send,
|
||||
Done,
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
internal class StateRouter(
|
||||
private val fragmentManager: WeakReference<FragmentManager>,
|
||||
) {
|
||||
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Amount)
|
||||
|
||||
fun onBackClick() {
|
||||
fragmentManager.get()?.popBackStack()
|
||||
}
|
||||
|
||||
fun onNextClick() {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient }
|
||||
SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee }
|
||||
SendUiStateType.Fee -> currentState.update { SendUiStateType.Send }
|
||||
SendUiStateType.Send -> currentState.update { SendUiStateType.Done }
|
||||
SendUiStateType.Done -> onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun onPrevClick() {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Amount -> onBackClick()
|
||||
SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount }
|
||||
SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient }
|
||||
SendUiStateType.Send -> currentState.update { SendUiStateType.Fee }
|
||||
SendUiStateType.Done -> onBackClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +1,55 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
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.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class SendAmountStateConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val sendAmountFieldConverter: SendAmountFieldConverter,
|
||||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, SendUiState> {
|
||||
) : Converter<Unit, SendStates.AmountState> {
|
||||
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
val userWallet = userWalletProvider() ?: return currentStateProvider()
|
||||
override fun convert(value: Unit): SendStates.AmountState {
|
||||
val userWallet = userWalletProvider()
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
val status = cryptoCurrencyStatusProvider()
|
||||
val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
|
||||
|
||||
return SendStates.AmountState(
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = status,
|
||||
walletName = userWallet.name,
|
||||
walletBalance = "$crypto ($fiat)",
|
||||
tokenIconState = iconStateConverter.convert(status),
|
||||
amountTextField = MutableStateFlow(sendAmountFieldConverter.convert(Unit)),
|
||||
isFiatValue = false,
|
||||
isPrimaryButtonEnabled = false,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
iconState = iconStateConverter.convert(status),
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconState = iconStateConverter.convert(status),
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Immutable
|
||||
internal sealed class FeeSelectorState {
|
||||
|
||||
object Loading : FeeSelectorState()
|
||||
|
||||
object Empty : FeeSelectorState()
|
||||
|
||||
data class Content(
|
||||
val fees: TransactionFee,
|
||||
val selectedFee: FeeType = FeeType.MARKET,
|
||||
val customValues: MutableStateFlow<List<SendTextField.CustomFee>> = MutableStateFlow(emptyList()),
|
||||
) : FeeSelectorState()
|
||||
}
|
||||
|
||||
enum class FeeType {
|
||||
SLOW,
|
||||
MARKET,
|
||||
FAST,
|
||||
CUSTOM,
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class SendFeeCustomFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<Fee, MutableStateFlow<List<SendTextField.CustomFee>>> {
|
||||
|
||||
override fun convert(value: Fee): MutableStateFlow<List<SendTextField.CustomFee>> {
|
||||
val ethereumFee = value as? Fee.Ethereum ?: return MutableStateFlow(emptyList())
|
||||
val appCurrency = appCurrencyProvider()
|
||||
|
||||
val maxFeeFiat = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = ethereumFee.amount.value,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
|
||||
return MutableStateFlow(
|
||||
listOf(
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.amount.value.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(0, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(maxFeeFiat),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasPrice.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(1, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasLimit.toString(),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(2, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendFeeStateConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendStates.FeeState> {
|
||||
|
||||
override fun convert(value: Unit): SendStates.FeeState {
|
||||
return SendStates.FeeState(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
||||
|
|
@ -11,21 +14,14 @@ internal class SendAmountFieldChangeConverter(
|
|||
) : Converter<String, SendUiState> {
|
||||
override fun convert(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
|
||||
if (
|
||||
state !is SendUiState.Content.AmountState ||
|
||||
value.checkDecimalSeparatorDuplicate()
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
if (value.checkDecimalSeparatorDuplicate()) return state
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
val fiatRate = state.cryptoCurrencyStatus.value.fiatRate
|
||||
|
||||
val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate
|
||||
val trimmedValue = value.trim()
|
||||
|
||||
val cryptoValue = if (state.isFiatValue) {
|
||||
val cryptoValue = if (amountState.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
|
|
@ -35,7 +31,7 @@ internal class SendAmountFieldChangeConverter(
|
|||
trimmedValue
|
||||
}
|
||||
|
||||
val fiatValue = if (!state.isFiatValue) {
|
||||
val fiatValue = if (!amountState.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
|
|
@ -45,37 +41,48 @@ internal class SendAmountFieldChangeConverter(
|
|||
trimmedValue
|
||||
}
|
||||
|
||||
val isExceedBalance = value.checkExceedBalance(state)
|
||||
return state.copy(
|
||||
amountTextField = state.amountTextField.copy(
|
||||
val isExceedBalance = value.checkExceedBalance(amountState.cryptoCurrencyStatus, amountState)
|
||||
amountState.amountTextField.update {
|
||||
it.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
)
|
||||
}
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = !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,
|
||||
private fun SendUiState.emptyState(): SendUiState {
|
||||
amountState?.amountTextField?.update {
|
||||
it.copy(
|
||||
value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE,
|
||||
fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE,
|
||||
isError = false,
|
||||
)
|
||||
}
|
||||
return copy(
|
||||
amountState = amountState?.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
isPrimaryButtonEnabled = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkDecimalSeparatorDuplicate(): Boolean {
|
||||
val regex = "[\\.\\,]".toRegex()
|
||||
val regex = TRIM_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
|
||||
private fun String.checkExceedBalance(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
state: SendStates.AmountState,
|
||||
): Boolean {
|
||||
val currencyStatus = cryptoCurrencyStatus.value
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() > currencyStatus.fiatAmount
|
||||
} else {
|
||||
|
|
@ -88,10 +95,11 @@ internal class SendAmountFieldChangeConverter(
|
|||
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
|
||||
|
||||
val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar)
|
||||
return trimmedValue.replace(TRIM_REGEX.toRegex(), separatorChar)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
private const val TRIM_REGEX = "[.,]"
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ internal class SendAmountFieldConverter(
|
|||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(""),
|
||||
placeholder = TextReference.Str(DEFAULT_VALUE),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.send_insufficient_funds),
|
||||
|
|
|
|||
|
|
@ -16,20 +16,43 @@ internal sealed class SendTextField {
|
|||
/** Keyboard options */
|
||||
abstract val keyboardOptions: KeyboardOptions
|
||||
|
||||
/** Label */
|
||||
abstract val label: TextReference
|
||||
|
||||
/** Placeholder (hint) */
|
||||
abstract val placeholder: TextReference
|
||||
// /** Placeholder (hint) */
|
||||
// abstract val placeholder: TextReference
|
||||
|
||||
data class Amount(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val placeholder: TextReference,
|
||||
val fiatValue: String,
|
||||
val isError: Boolean,
|
||||
val error: TextReference,
|
||||
) : SendTextField()
|
||||
|
||||
data class RecipientAddress(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
) : SendTextField()
|
||||
|
||||
data class RecipientMemo(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
) : SendTextField()
|
||||
|
||||
data class CustomFee(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val label: TextReference? = null,
|
||||
) : SendTextField()
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
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.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class SendRecipientAddressFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<Unit, MutableStateFlow<SendTextField.RecipientAddress>> {
|
||||
|
||||
override fun convert(value: Unit): MutableStateFlow<SendTextField.RecipientAddress> {
|
||||
return MutableStateFlow(
|
||||
SendTextField.RecipientAddress(
|
||||
value = "",
|
||||
onValueChange = clickIntents::onRecipientAddressValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = TextReference.Res(R.string.send_enter_address_field),
|
||||
label = TextReference.Res(R.string.send_recipient),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import androidx.paging.*
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class SendRecipientListConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
fun convert(wallets: List<AvailableWallet?>, txHistory: PagingData<TxHistoryItem>, txHistoryCount: Int) {
|
||||
val filteredWallets = wallets.filterNotNull()
|
||||
.groupBy { item -> item.name }
|
||||
.values.flatten()
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(
|
||||
name = "${item.name} ${index.inc()}",
|
||||
)
|
||||
}
|
||||
|
||||
val walletsItem = getWalletItems(filteredWallets, txHistoryCount)
|
||||
|
||||
currentStateProvider().recipientList.update {
|
||||
if (txHistoryCount == 0) {
|
||||
PagingData.from(listOf(walletsItem))
|
||||
} else {
|
||||
txHistory.filter { item ->
|
||||
val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
|
||||
val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User
|
||||
val isSingleAddress = if (item.isOutgoing) {
|
||||
item.destinationType is TxHistoryItem.DestinationType.Single
|
||||
} else {
|
||||
item.sourceType is TxHistoryItem.SourceType.Single
|
||||
}
|
||||
isTransfer && isSingleAddress && isNotContract
|
||||
}.map<TxHistoryItem, SendRecipientListContent> { tx ->
|
||||
SendRecipientListContent.Item(
|
||||
id = tx.txHash,
|
||||
title = tx.extractAddress(),
|
||||
subtitle = TextReference.Str(tx.getAmount()),
|
||||
info = tx.extractTimestamp(),
|
||||
subtitleIconRes = tx.extractIconRes(),
|
||||
)
|
||||
}.insertWallets(walletsItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PagingData<SendRecipientListContent>.insertWallets(
|
||||
wallets: SendRecipientListContent.Wallets,
|
||||
): PagingData<SendRecipientListContent> {
|
||||
return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after ->
|
||||
return@insertSeparators when {
|
||||
before == null && after is SendRecipientListContent.Item -> wallets
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWalletItems(wallets: List<AvailableWallet>, txHistoryCount: Int): SendRecipientListContent.Wallets {
|
||||
return SendRecipientListContent.Wallets(
|
||||
wallets.map {
|
||||
SendRecipientListContent.Item(
|
||||
id = it.address,
|
||||
title = TextReference.Str(it.address),
|
||||
subtitle = TextReference.Str(it.name),
|
||||
)
|
||||
}.toPersistentList(),
|
||||
isWalletsOnly = txHistoryCount == 0,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) {
|
||||
when (val destination = destinationType) {
|
||||
is TxHistoryItem.DestinationType.Multiple -> TextReference.Res(
|
||||
R.string.transaction_history_multiple_addresses,
|
||||
)
|
||||
is TxHistoryItem.DestinationType.Single -> TextReference.Str(destination.addressType.address)
|
||||
}
|
||||
} else {
|
||||
when (val source = sourceType) {
|
||||
is TxHistoryItem.SourceType.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses)
|
||||
is TxHistoryItem.SourceType.Single -> TextReference.Str(source.address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIconRes() = if (isOutgoing) {
|
||||
R.drawable.ic_arrow_up_24
|
||||
} else {
|
||||
R.drawable.ic_arrow_down_24
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return amount.toFormattedCurrencyString(
|
||||
currency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
||||
val date = timestampInMillis.toDateFormat(
|
||||
formatter = DateTimeFormatters.dateDDMMYYYY,
|
||||
)
|
||||
val time = timestampInMillis.toTimeFormat()
|
||||
return TextReference.Res(R.string.send_date_format, wrappedList(date, time))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class SendRecipientMemoFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Int, MutableStateFlow<SendTextField.RecipientMemo>> {
|
||||
|
||||
fun convertOrNull(): MutableStateFlow<SendTextField.RecipientMemo>? {
|
||||
val cryptoCurrency = cryptoCurrencyStatus().currency
|
||||
|
||||
return when (cryptoCurrency.network.id.value) {
|
||||
Blockchain.XRP.id -> convert(R.string.send_destination_tag_field)
|
||||
Blockchain.Binance.id,
|
||||
Blockchain.TON.id,
|
||||
Blockchain.Cosmos.id,
|
||||
Blockchain.TerraV1.id,
|
||||
Blockchain.TerraV2.id,
|
||||
Blockchain.Stellar.id,
|
||||
-> convert(R.string.send_extras_hint_memo)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun convert(value: Int): MutableStateFlow<SendTextField.RecipientMemo> {
|
||||
return MutableStateFlow(
|
||||
SendTextField.RecipientMemo(
|
||||
value = "",
|
||||
onValueChange = clickIntents::onRecipientMemoValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = TextReference.Res(R.string.send_optional_field),
|
||||
label = TextReference.Res(value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendRecipientStateConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendStates.RecipientState> {
|
||||
|
||||
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
|
||||
private val memoFieldConverter by lazy {
|
||||
SendRecipientMemoFieldConverter(
|
||||
clickIntents,
|
||||
cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Unit): SendStates.RecipientState {
|
||||
return SendStates.RecipientState(
|
||||
addressTextField = addressFieldConverter.convert(Unit),
|
||||
memoTextField = memoFieldConverter.convertOrNull(),
|
||||
network = cryptoCurrencyStatusProvider().currency.network.name,
|
||||
isPrimaryButtonEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -19,9 +20,10 @@ import com.tangem.core.ui.components.PrimaryButton
|
|||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
|
||||
@Composable
|
||||
internal fun SendNavigationButtons(uiState: SendUiState.Content) {
|
||||
internal fun SendNavigationButtons(uiState: SendUiState) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -38,9 +40,11 @@ internal fun SendNavigationButtons(uiState: SendUiState.Content) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
|
||||
private fun SendSecondaryNavigationButton(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsState()
|
||||
AnimatedVisibility(
|
||||
visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState,
|
||||
visible = currentState.value == SendUiStateType.Recipient ||
|
||||
currentState.value == SendUiStateType.Fee,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
|
|
@ -48,46 +52,52 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
|
|||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable {
|
||||
// todo add prev click
|
||||
uiState.clickIntents.onPrevClick()
|
||||
}
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
painter = painterResource(R.drawable.ic_back_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) {
|
||||
val buttonTextId = when (uiState) {
|
||||
is SendUiState.Content.AmountState,
|
||||
is SendUiState.Content.RecipientState,
|
||||
is SendUiState.Content.FeeState,
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
|
||||
val currentState = uiState.currentState.collectAsState()
|
||||
|
||||
val buttonTextId = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.Fee,
|
||||
-> R.string.common_next
|
||||
is SendUiState.Content.SendState -> R.string.common_send
|
||||
SendUiStateType.Send -> R.string.common_send
|
||||
else -> R.string.common_close
|
||||
}
|
||||
|
||||
val isButtonEnabled = when (currentState.value) {
|
||||
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
|
||||
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
|
||||
else -> true
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = buttonTextId,
|
||||
label = "Update send screen state",
|
||||
modifier = modifier,
|
||||
) { textId ->
|
||||
if (uiState is SendUiState.Content.SendState) {
|
||||
if (currentState.value == SendUiStateType.Send) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
enabled = isButtonEnabled,
|
||||
onClick = uiState.clickIntents::onNextClick,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
text = stringResource(textId),
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
enabled = isButtonEnabled,
|
||||
onClick = uiState.clickIntents::onNextClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,100 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent
|
||||
import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent
|
||||
import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
|
||||
|
||||
@Composable
|
||||
internal fun SendScreen(uiState: SendUiState.Content) {
|
||||
internal fun SendScreen(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
BackHandler { uiState.clickIntents.onPrevClick() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius24,
|
||||
topEnd = TangemTheme.dimens.radius24,
|
||||
),
|
||||
),
|
||||
.background(color = TangemTheme.colors.background.tertiary),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
TangemBottomSheetDraggableHeader(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical),
|
||||
) {
|
||||
SendScreenContent(uiState)
|
||||
val titleRes = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Send,
|
||||
-> R.string.common_send
|
||||
SendUiStateType.Recipient -> R.string.send_recipient
|
||||
SendUiStateType.Fee -> R.string.common_fee_selector_title
|
||||
SendUiStateType.Done -> null
|
||||
}
|
||||
val iconRes = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
-> R.drawable.ic_qrcode_scan_24
|
||||
else -> null
|
||||
}
|
||||
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = titleRes?.let { stringResource(it) },
|
||||
onBackClick = uiState.clickIntents::onBackClick,
|
||||
onIconClick = uiState.clickIntents::onQrCodeScanClick,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
iconRes = iconRes,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
SendScreenContent(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
)
|
||||
SendNavigationButtons(uiState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendScreenContent(uiState: SendUiState.Content) {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> SendAmountContent(uiState)
|
||||
else -> { /* [REDACTED_TODO_COMMENT]*/
|
||||
private fun SendScreenContent(
|
||||
uiState: SendUiState,
|
||||
currentState: State<SendUiStateType>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val recipientList = uiState.recipientList.collectAsLazyPagingItems()
|
||||
AnimatedContent(
|
||||
targetState = currentState.value,
|
||||
label = "Send Scree Navigation",
|
||||
modifier = modifier,
|
||||
) { state ->
|
||||
when (state) {
|
||||
SendUiStateType.Amount -> SendAmountContent(
|
||||
uiState.amountState,
|
||||
uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Recipient -> SendRecipientContent(
|
||||
uiState.recipientState,
|
||||
uiState.clickIntents,
|
||||
recipientList,
|
||||
)
|
||||
SendUiStateType.Fee -> SendSpeedAndFeeContent(
|
||||
uiState.feeState,
|
||||
uiState.clickIntents,
|
||||
)
|
||||
else -> { /* [REDACTED_TODO_COMMENT]*/ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,12 +18,8 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.fields.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -140,29 +136,4 @@ private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: M
|
|||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,14 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
|
||||
@Composable
|
||||
internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) {
|
||||
internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) {
|
||||
val amountTextField = amountState.amountTextField.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -52,10 +54,10 @@ internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState,
|
|||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
AmountField(
|
||||
sendField = amountState.amountTextField,
|
||||
sendField = amountTextField.value,
|
||||
isFiat = amountState.isFiatValue,
|
||||
cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol,
|
||||
fiatSymbol = amountState.appCurrency.symbol,
|
||||
isFiat = amountState.isFiatValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
package com.tangem.features.send.impl.presentation.ui.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -6,10 +6,8 @@ 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
|
||||
|
|
@ -17,30 +15,22 @@ 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.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.ui.amount.AmountFieldContainer
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
@Composable
|
||||
internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
|
||||
internal fun SendAmountContent(amountState: SendStates.AmountState?, clickIntents: SendClickIntents) {
|
||||
if (amountState == null) return
|
||||
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,
|
||||
|
|
@ -49,16 +39,16 @@ internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
|
|||
) {
|
||||
SegmentedButtons(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size40)
|
||||
.weight(1f),
|
||||
.weight(1f)
|
||||
.height(TangemTheme.dimens.size40),
|
||||
config = amountState.segmentedButtonConfig,
|
||||
onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) },
|
||||
onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) },
|
||||
) {
|
||||
SendAmountCurrencyButton(it)
|
||||
}
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.send_max_amount),
|
||||
onClick = amountState.clickIntents::onMaxValueClick,
|
||||
onClick = clickIntents::onMaxValueClick,
|
||||
size = TangemButtonSize.Text,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
|
||||
modifier = Modifier
|
||||
|
|
@ -83,8 +73,8 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
|
|||
if (button.isFiat) {
|
||||
FiatIcon(
|
||||
url = button.iconUrl,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size18),
|
||||
size = TangemTheme.dimens.size18,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
} else {
|
||||
button.iconState?.let {
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.common
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Container for footer info below the text field
|
||||
*
|
||||
* @param modifier of component
|
||||
* @param footer text
|
||||
* @param footerTopPadding padding between footer and field
|
||||
* @param content field content
|
||||
*/
|
||||
@Composable
|
||||
internal fun FooterContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
footerTopPadding: Dp = TangemTheme.dimens.spacing8,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
content()
|
||||
AnimatedVisibility(visible = footer != null) {
|
||||
Text(
|
||||
text = footer.orEmpty(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = footerTopPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.fields.AmountVisualTransformation
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo
|
||||
|
||||
private const val ETHEREUM_UNIT = "GWEI"
|
||||
|
||||
@Composable
|
||||
internal fun SendCustomFeeEthereum(
|
||||
customValues: State<List<SendTextField.CustomFee>>,
|
||||
selectedFee: FeeType,
|
||||
symbol: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val fee = customValues.value[0]
|
||||
val gasPrice = customValues.value[1]
|
||||
val gasLimit = customValues.value[2]
|
||||
|
||||
if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextFieldWithInfo(
|
||||
value = fee.value,
|
||||
label = stringResource(R.string.send_max_fee),
|
||||
footer = stringResource(R.string.send_max_fee_footer),
|
||||
info = fee.label,
|
||||
visualTransformation = AmountVisualTransformation(symbol),
|
||||
keyboardOptions = fee.keyboardOptions,
|
||||
onValueChange = fee.onValueChange,
|
||||
isSingleLine = true,
|
||||
)
|
||||
TextFieldWithInfo(
|
||||
value = gasPrice.value,
|
||||
label = stringResource(R.string.send_gas_price),
|
||||
footer = stringResource(R.string.send_gas_price_footer),
|
||||
onValueChange = gasPrice.onValueChange,
|
||||
visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT),
|
||||
keyboardOptions = fee.keyboardOptions,
|
||||
isSingleLine = true,
|
||||
)
|
||||
TextFieldWithInfo(
|
||||
value = gasLimit.value,
|
||||
label = stringResource(R.string.send_gas_limit),
|
||||
footer = stringResource(R.string.send_gas_limit_footer),
|
||||
onValueChange = gasLimit.onValueChange,
|
||||
keyboardOptions = fee.keyboardOptions,
|
||||
isSingleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
|
||||
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) {
|
||||
if (state == null) return
|
||||
val feeSendState = state.feeSelectorState.collectAsStateWithLifecycle()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
item(
|
||||
key = FEE_SELECTOR_KEY,
|
||||
) {
|
||||
SendSpeedSelector(
|
||||
state = feeSendState,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
if (feeSendState.value is FeeSelectorState.Content) {
|
||||
item(
|
||||
key = FEE_CUSTOM_KEY,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = feeSendState.value is FeeSelectorState.Content,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
val fee = feeSendState.value as FeeSelectorState.Content
|
||||
val customValues = fee.customValues.collectAsStateWithLifecycle()
|
||||
SendCustomFeeEthereum(
|
||||
customValues = customValues,
|
||||
selectedFee = fee.selectedFee,
|
||||
symbol = state.cryptoCurrencyStatus.currency.symbol,
|
||||
modifier = Modifier
|
||||
.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
val topPadding = (feeSendState.value as? FeeSelectorState.Content)?.let { state ->
|
||||
if (state.selectedFee != FeeType.CUSTOM) {
|
||||
TangemTheme.dimens.spacing8
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0
|
||||
}
|
||||
} ?: TangemTheme.dimens.spacing0
|
||||
|
||||
SendSpeedSubtract(
|
||||
receivingAmount = state.receivedAmount,
|
||||
isSubtract = state.isSubtract,
|
||||
onSelectClick = clickIntents::onSubtractSelect,
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SendSpeedSelector(
|
||||
state: State<FeeSelectorState>,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = stringResource(R.string.common_fee_selector_footer),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
when (val selector = state.value) {
|
||||
FeeSelectorState.Loading -> {
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
}
|
||||
is FeeSelectorState.Content -> {
|
||||
when (selector.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_slow,
|
||||
iconRes = R.drawable.ic_tortoise_24,
|
||||
amount = TextReference.Str(selector.fees.minimum.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.minimum.amount.currencySymbol),
|
||||
isSelected = selector.selectedFee == FeeType.SLOW,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) },
|
||||
)
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
amount = TextReference.Str(selector.fees.normal.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol),
|
||||
isSelected = selector.selectedFee == FeeType.MARKET,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
|
||||
)
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_fast,
|
||||
iconRes = R.drawable.ic_hare_24,
|
||||
amount = TextReference.Str(selector.fees.priority.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.priority.amount.currencySymbol),
|
||||
isSelected = selector.selectedFee == FeeType.FAST,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) },
|
||||
showDivider = selector.fees.normal is Fee.Ethereum,
|
||||
)
|
||||
if (selector.fees.normal is Fee.Ethereum) {
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_custom,
|
||||
iconRes = R.drawable.ic_edit_24,
|
||||
isSelected = selector.selectedFee == FeeType.CUSTOM,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) },
|
||||
showDivider = selector.fees.normal !is Fee.Ethereum,
|
||||
)
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> {
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
isSelected = true,
|
||||
amount = TextReference.Str(selector.fees.normal.amount.value.toString()),
|
||||
symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol),
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
|
||||
showDivider = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FeeSelectorState.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSpeedSelectorItemLoading() {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size50,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
SpacerWMax()
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size90,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSpeedSelectorItem(
|
||||
@StringRes titleRes: Int,
|
||||
@DrawableRes iconRes: Int,
|
||||
onSelect: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
amount: TextReference? = null,
|
||||
symbol: TextReference? = null,
|
||||
isSelected: Boolean = false,
|
||||
showDivider: Boolean = true,
|
||||
) {
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
TangemTheme.colors.icon.accent
|
||||
} else {
|
||||
TangemTheme.colors.icon.informative
|
||||
},
|
||||
label = "Selector icon tint change",
|
||||
)
|
||||
|
||||
val textStyle = if (isSelected) {
|
||||
TangemTheme.typography.subtitle2
|
||||
} else {
|
||||
TangemTheme.typography.body2
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelect() },
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
if (amount != null && symbol != null) {
|
||||
SelectorValueContent(
|
||||
amount = amount,
|
||||
symbol = symbol,
|
||||
textStyle = textStyle,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showDivider) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size1)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.background(TangemTheme.colors.stroke.primary)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) {
|
||||
Text(
|
||||
text = amount.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing4,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = symbol.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing1,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FeeSelectorPreview_Light() {
|
||||
TangemTheme {
|
||||
SendSpeedSelectorItemLoading()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FeeSelectorPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
SendSpeedSelectorItemLoading()
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
internal fun SendSpeedSubtract(
|
||||
receivingAmount: StateFlow<String>,
|
||||
isSubtract: StateFlow<Boolean>,
|
||||
onSelectClick: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isSelected = isSubtract.collectAsStateWithLifecycle()
|
||||
val footer = receivingAmount.collectAsStateWithLifecycle()
|
||||
|
||||
val footerText = if (isSelected.value) {
|
||||
stringResource(R.string.send_amount_substract_footer, footer.value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
FooterContainer(
|
||||
footer = footerText,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
horizontal = TangemTheme.dimens.spacing20,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.send_amount_substract),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
TangemSwitch(
|
||||
checked = isSelected.value,
|
||||
onCheckedChange = onSelectClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +13,13 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.constraintlayout.compose.Visibility
|
||||
import com.tangem.core.ui.components.MiddleEllipsisText
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -28,75 +32,98 @@ import com.tangem.features.send.impl.R
|
|||
* @param subtitle subtitle
|
||||
* @param onClick click listener
|
||||
* @param modifier modifier
|
||||
* @param info info
|
||||
* @param subtitleIconRes icon
|
||||
*/
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod")
|
||||
@Composable
|
||||
fun ListItemWithIcon(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
info: String? = null,
|
||||
@DrawableRes subtitleIconRes: Int? = null,
|
||||
) {
|
||||
Row(
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable { onClick() }
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val (iconRef, titleRef, subtitleRef, subtitleIconRef, infoRef) = createRefs()
|
||||
|
||||
val spacing2 = TangemTheme.dimens.spacing2
|
||||
val spacing8 = TangemTheme.dimens.spacing8
|
||||
val spacing10 = TangemTheme.dimens.spacing10
|
||||
val spacing12 = TangemTheme.dimens.spacing12
|
||||
IdentIcon(
|
||||
address = title,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius20)),
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius20))
|
||||
.constrainAs(iconRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(parent.top, margin = spacing8)
|
||||
bottom.linkTo(parent.bottom, margin = spacing8)
|
||||
},
|
||||
)
|
||||
Column(
|
||||
MiddleEllipsisText(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing2,
|
||||
bottom = TangemTheme.dimens.spacing2,
|
||||
),
|
||||
) {
|
||||
MiddleEllipsisText(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
.constrainAs(titleRef) {
|
||||
start.linkTo(iconRef.end, margin = spacing12)
|
||||
end.linkTo(parent.end)
|
||||
top.linkTo(parent.top, margin = spacing10)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = subtitleIconRes ?: R.drawable.ic_arrow_down_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.background(TangemTheme.colors.background.tertiary, CircleShape)
|
||||
.constrainAs(subtitleIconRef) {
|
||||
start.linkTo(iconRef.end, margin = spacing12)
|
||||
top.linkTo(titleRef.bottom)
|
||||
bottom.linkTo(parent.bottom, margin = spacing10)
|
||||
visibility = if (subtitleIconRes == null) Visibility.Gone else Visibility.Visible
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.constrainAs(subtitleRef) {
|
||||
start.linkTo(subtitleIconRef.end, margin = spacing2, goneMargin = spacing12)
|
||||
end.linkTo(infoRef.start)
|
||||
top.linkTo(titleRef.bottom)
|
||||
bottom.linkTo(parent.bottom, margin = spacing10)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
info?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.constrainAs(infoRef) {
|
||||
start.linkTo(subtitleRef.end, goneMargin = spacing12)
|
||||
end.linkTo(parent.end)
|
||||
top.linkTo(titleRef.bottom)
|
||||
bottom.linkTo(parent.bottom, margin = spacing10)
|
||||
},
|
||||
)
|
||||
Row {
|
||||
subtitleIconRes?.let { iconRes ->
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.background(TangemTheme.colors.background.tertiary, CircleShape)
|
||||
.padding(TangemTheme.dimens.spacing3),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (subtitleIconRes != null) {
|
||||
Modifier.padding(start = TangemTheme.dimens.spacing4)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,6 +138,7 @@ private fun ListItemWithIconPreview_Light(
|
|||
ListItemWithIcon(
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
info = config.info,
|
||||
subtitleIconRes = config.iconRes,
|
||||
onClick = {},
|
||||
)
|
||||
|
|
@ -126,6 +154,7 @@ private fun ListItemWithIconPreview_Dark(
|
|||
ListItemWithIcon(
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
info = config.info,
|
||||
subtitleIconRes = config.iconRes,
|
||||
onClick = {},
|
||||
)
|
||||
|
|
@ -135,6 +164,7 @@ private fun ListItemWithIconPreview_Dark(
|
|||
private data class ListItemWithIconPreviewConfig(
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val info: String? = null,
|
||||
val iconRes: Int? = null,
|
||||
)
|
||||
|
||||
|
|
@ -142,7 +172,14 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid
|
|||
collection = listOf(
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "Wallet",
|
||||
subtitle = "0.000000000000000000000000000000 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "1 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.recipient
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
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.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
|
||||
private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY"
|
||||
private const val MY_WALLETS_HEADER_KEY = "MY_WALLETS_HEADER_KEY"
|
||||
|
||||
@Composable
|
||||
internal fun SendRecipientContent(
|
||||
uiState: SendStates.RecipientState?,
|
||||
clickIntents: SendClickIntents,
|
||||
recipientList: LazyPagingItems<SendRecipientListContent>,
|
||||
) {
|
||||
if (uiState == null) return
|
||||
val address = uiState.addressTextField.collectAsState().value
|
||||
val memo = uiState.memoTextField?.collectAsState()?.value
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item(key = ADDRESS_FIELD_KEY) {
|
||||
TextFieldWithPasteAndIcon(
|
||||
value = address.value,
|
||||
label = address.label,
|
||||
placeholder = address.placeholder,
|
||||
footer = stringResource(R.string.send_recipient_address_footer, uiState.network),
|
||||
onValueChange = address.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientAddressValueChange,
|
||||
singleLine = true,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing4),
|
||||
isError = address.isError,
|
||||
error = address.error,
|
||||
)
|
||||
}
|
||||
memo?.let { memoField ->
|
||||
item(key = MEMO_FIELD_KEY) {
|
||||
TextFieldWithPaste(
|
||||
value = memoField.value,
|
||||
label = memoField.label,
|
||||
placeholder = memoField.placeholder,
|
||||
footer = stringResource(R.string.send_recipient_memo_footer),
|
||||
onValueChange = memoField.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientMemoValueChange,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
|
||||
isError = memoField.isError,
|
||||
error = memoField.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
recipientListItem(
|
||||
recipientList = recipientList,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.recipientListItem(
|
||||
recipientList: LazyPagingItems<SendRecipientListContent>,
|
||||
clickIntents: SendClickIntents,
|
||||
) {
|
||||
items(
|
||||
count = recipientList.itemCount,
|
||||
key = recipientList.itemKey {
|
||||
when (it) {
|
||||
is SendRecipientListContent.Wallets -> MY_WALLETS_HEADER_KEY
|
||||
is SendRecipientListContent.Item -> it.id
|
||||
}
|
||||
},
|
||||
contentType = recipientList.itemContentType { it::class.java },
|
||||
) { index ->
|
||||
recipientList[index]?.let { item ->
|
||||
when (item) {
|
||||
is SendRecipientListContent.Wallets -> {
|
||||
RecipientWalletListItem(
|
||||
item = item,
|
||||
clickIntents = clickIntents,
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(top = TangemTheme.dimens.spacing20)
|
||||
.then(
|
||||
if (index == 0) {
|
||||
val bottomRadius = if (item.isWalletsOnly) {
|
||||
TangemTheme.dimens.radius12
|
||||
} else {
|
||||
TangemTheme.dimens.radius0
|
||||
}
|
||||
Modifier.clip(
|
||||
RoundedCornerShape(
|
||||
topEnd = TangemTheme.dimens.radius12,
|
||||
topStart = TangemTheme.dimens.radius12,
|
||||
bottomStart = bottomRadius,
|
||||
bottomEnd = bottomRadius,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
is SendRecipientListContent.Item -> {
|
||||
val title = item.title.resolveReference()
|
||||
ListItemWithIcon(
|
||||
title = item.title.resolveReference(),
|
||||
subtitle = item.subtitle.resolveReference(),
|
||||
info = item.info?.let { ", ${it.resolveReference()}" },
|
||||
subtitleIconRes = item.subtitleIconRes,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (index == recipientList.itemCount - 1) {
|
||||
Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing20)
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
bottomEnd = TangemTheme.dimens.radius12,
|
||||
bottomStart = TangemTheme.dimens.radius12,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
onClick = { clickIntents.onRecipientAddressValueChange(title) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecipientWalletListItem(
|
||||
item: SendRecipientListContent.Wallets,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
if (item.list.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.send_recipient_wallets_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
item.list.forEachIndexed { _, wallet ->
|
||||
val title = wallet.title.resolveReference()
|
||||
ListItemWithIcon(
|
||||
title = wallet.title.resolveReference(),
|
||||
subtitle = wallet.subtitle.resolveReference(),
|
||||
onClick = { clickIntents.onRecipientAddressValueChange(title) },
|
||||
)
|
||||
}
|
||||
if (!item.isWalletsOnly) {
|
||||
val topPadding = if (item.list.isNotEmpty()) {
|
||||
TangemTheme.dimens.spacing8
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.send_recent_transactions),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -23,14 +24,15 @@ import androidx.compose.ui.platform.LocalClipboardManager
|
|||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.ui.common.FooterContainer
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldWithPasteAndIcon(
|
||||
|
|
@ -42,7 +44,14 @@ internal fun TextFieldWithPasteAndIcon(
|
|||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
singleLine: Boolean = false,
|
||||
error: TextReference? = null,
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
val (title, color) = if (isError && error != null) {
|
||||
error to TangemTheme.colors.text.warning
|
||||
} else {
|
||||
label to TangemTheme.colors.text.secondary
|
||||
}
|
||||
FooterContainer(modifier, footer) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -53,9 +62,9 @@ internal fun TextFieldWithPasteAndIcon(
|
|||
),
|
||||
) {
|
||||
Text(
|
||||
text = label.resolveReference(),
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
color = color,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -114,7 +123,14 @@ internal fun TextFieldWithPaste(
|
|||
onPasteClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
error: TextReference? = null,
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
val (title, color) = if (isError && error != null) {
|
||||
error to TangemTheme.colors.text.warning
|
||||
} else {
|
||||
label to TangemTheme.colors.text.secondary
|
||||
}
|
||||
FooterContainer(modifier, footer) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -129,9 +145,9 @@ internal fun TextFieldWithPaste(
|
|||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = label.resolveReference(),
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
color = color,
|
||||
)
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
|
|
@ -160,6 +176,9 @@ internal fun TextFieldWithInfo(
|
|||
modifier: Modifier = Modifier,
|
||||
info: TextReference? = null,
|
||||
footer: String? = null,
|
||||
isSingleLine: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = footer,
|
||||
|
|
@ -189,6 +208,9 @@ internal fun TextFieldWithInfo(
|
|||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
visualTransformation = visualTransformation,
|
||||
singleLine = isSingleLine,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6)
|
||||
.weight(1f),
|
||||
|
|
@ -208,27 +230,6 @@ internal fun TextFieldWithInfo(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FooterContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
footerTopPadding: Dp = TangemTheme.dimens.spacing8,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
content()
|
||||
footer?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = footerTopPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
|
@ -287,14 +288,18 @@ private fun SimpleTextField(
|
|||
modifier: Modifier = Modifier,
|
||||
placeholder: TextReference? = null,
|
||||
singleLine: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
decorationBox = { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
internal fun verifyAddress(address: String, cryptoCurrency: CryptoCurrency?): Boolean {
|
||||
if (address.isEmpty()) return true
|
||||
val blockchain = cryptoCurrency?.let {
|
||||
Blockchain.fromId(cryptoCurrency.id.rawNetworkId)
|
||||
} ?: return false
|
||||
|
||||
return blockchain.validateAddress(address)
|
||||
}
|
||||
|
||||
internal fun isNotAddressInWallet(walletAddresses: Set<Address>, address: String): Boolean {
|
||||
return walletAddresses.all { it.value != address }
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import java.math.BigInteger
|
||||
|
||||
internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean {
|
||||
if (cryptoCurrency == null) return false
|
||||
return when (cryptoCurrency.network.id.value) {
|
||||
Blockchain.XRP.id -> {
|
||||
val tag = memo.toLongOrNull()
|
||||
tag != null && tag <= XRP_TAG_MAX_NUMBER
|
||||
}
|
||||
Blockchain.Stellar.id -> {
|
||||
isAssignableValue(memo)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAssignableValue(value: String): Boolean {
|
||||
val memoType = when {
|
||||
value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID
|
||||
else -> XlmMemoType.TEXT
|
||||
}
|
||||
return when (memoType) {
|
||||
XlmMemoType.TEXT -> {
|
||||
// from org.stellar.sdk.MemoText
|
||||
value.toByteArray().size <= XLM_MEMO_MAX_LENGTH
|
||||
}
|
||||
XlmMemoType.ID -> {
|
||||
try {
|
||||
// from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
|
||||
value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
|
||||
} catch (ex: NumberFormatException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class XlmMemoType { TEXT, ID }
|
||||
|
||||
private const val XRP_TAG_MAX_NUMBER = 4294967295
|
||||
private const val XLM_MEMO_MAX_LENGTH = 28
|
||||
|
|
@ -1,14 +1,36 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
interface SendClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onNextClick()
|
||||
|
||||
fun onPrevClick()
|
||||
|
||||
fun onQrCodeScanClick()
|
||||
|
||||
// region Amount
|
||||
fun onAmountValueChange(value: String)
|
||||
|
||||
fun onCurrencyChangeClick(isFiat: Boolean)
|
||||
|
||||
fun onMaxValueClick()
|
||||
// endregion
|
||||
|
||||
// region Recipient
|
||||
fun onRecipientAddressValueChange(value: String)
|
||||
|
||||
fun onRecipientMemoValueChange(value: String)
|
||||
// endregion
|
||||
|
||||
// region Fee
|
||||
fun onFeeSelectorClick(feeType: FeeType)
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String)
|
||||
|
||||
fun onSubtractSelect(value: Boolean)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -4,32 +4,63 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.SendStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
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.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
||||
|
|
@ -42,26 +73,42 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private var innerRouter: StateRouter by Delegates.notNull()
|
||||
|
||||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
walletAddressesProvider = Provider { walletAddresses },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
)
|
||||
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
private var userWallet: UserWallet? = null
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var walletAddresses = emptySet<Address>()
|
||||
|
||||
private var balanceJobHolder = JobHolder()
|
||||
private var recipientsJobHolder = JobHolder()
|
||||
private var walletAddressesJobHolder = JobHolder()
|
||||
private var feeJobHolder = JobHolder()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
getWalletAddresses()
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
getFee()
|
||||
}
|
||||
|
||||
fun setRouter(router: StateRouter) {
|
||||
innerRouter = router
|
||||
uiState = uiState.copy(currentState = router.currentState)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
getUserWalletUseCase(userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
|
|
@ -86,11 +133,13 @@ internal class SendViewModel @Inject constructor(
|
|||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { either ->
|
||||
uiState = stateFactory.getAmountState(
|
||||
cryptoCurrencyStatus = either,
|
||||
)
|
||||
either.onRight {
|
||||
cryptoCurrencyStatus = it
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
}
|
||||
|
|
@ -107,27 +156,127 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun onNextClick() {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> onRecipientStateClick()
|
||||
is SendUiState.Content.RecipientState -> onFeeStateClick()
|
||||
else -> {
|
||||
// todo implement
|
||||
private fun getWalletsAndRecent() {
|
||||
combine(
|
||||
flow = getUserWallets().conflate(),
|
||||
flow2 = getTxHistory().conflate(),
|
||||
flow3 = getTxHistoryCount().conflate(),
|
||||
) { wallets, txHistory, txHistoryCount ->
|
||||
stateFactory.onLoadedRecipientList(
|
||||
wallets = wallets,
|
||||
txHistory = txHistory,
|
||||
txHistoryCount = txHistoryCount,
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(recipientsJobHolder)
|
||||
}
|
||||
|
||||
private fun getUserWallets(): Flow<List<AvailableWallet?>> {
|
||||
return getWalletsUseCase()
|
||||
.distinctUntilChanged()
|
||||
.map { userWallets ->
|
||||
coroutineScope {
|
||||
userWallets
|
||||
.filterNot { it.walletId == userWalletId || it.isLocked }
|
||||
.map { wallet ->
|
||||
async(dispatchers.io) {
|
||||
getCryptoCurrenciesUseCase(wallet.walletId)
|
||||
.fold(
|
||||
ifRight = { currencyItem ->
|
||||
val walletCurrency = currencyItem.firstOrNull {
|
||||
it.network.id == cryptoCurrency.network.id
|
||||
} ?: return@fold null
|
||||
val addresses = walletManagersFacade.getAddress(
|
||||
userWalletId = wallet.walletId,
|
||||
network = walletCurrency.network,
|
||||
)
|
||||
return@fold AvailableWallet(
|
||||
name = wallet.name,
|
||||
address = addresses.first().value,
|
||||
)
|
||||
},
|
||||
ifLeft = { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTxHistory(): Flow<PagingData<TxHistoryItem>> {
|
||||
return flow {
|
||||
txHistoryItemsUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
).fold(
|
||||
ifRight = { emitAll(it.distinctUntilChanged()) },
|
||||
ifLeft = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrevClick() {
|
||||
// todo implement
|
||||
private fun getTxHistoryCount(): Flow<Int> {
|
||||
return flow {
|
||||
txHistoryItemsCountUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
).fold(
|
||||
ifRight = { emit(it) },
|
||||
ifLeft = { emit(0) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecipientStateClick() {
|
||||
stateFactory.getOnReceiveState()
|
||||
private fun getFee() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState.currentState
|
||||
.filter { it == SendUiStateType.Fee }
|
||||
.onEach {
|
||||
val amountState = uiState.amountState ?: return@onEach
|
||||
val recipientState = uiState.recipientState ?: return@onEach
|
||||
|
||||
stateFactory.onFeeOnLoadingState()
|
||||
getFeeUseCase.invoke(
|
||||
amount = amountState.amountTextField.value.value.toBigDecimal(),
|
||||
destination = recipientState.addressTextField.value.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeFee ->
|
||||
maybeFee.fold(
|
||||
ifRight = {
|
||||
stateFactory.onFeeOnLoadedState(it)
|
||||
},
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}.launchIn(viewModelScope)
|
||||
}.saveIn(feeJobHolder)
|
||||
}
|
||||
|
||||
private fun onFeeStateClick() {
|
||||
// todo implement
|
||||
private fun getWalletAddresses() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
walletAddresses = walletManagersFacade.getAddresses(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
}.saveIn(walletAddressesJobHolder)
|
||||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun onBackClick() = innerRouter.onBackClick()
|
||||
override fun onNextClick() = innerRouter.onNextClick()
|
||||
override fun onPrevClick() = innerRouter.onPrevClick()
|
||||
|
||||
override fun onQrCodeScanClick() {
|
||||
// TODO Add QR code scanning
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -141,14 +290,100 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
val amountState = uiState as? SendUiState.Content.AmountState ?: return
|
||||
|
||||
val amountState = uiState.amountState ?: return
|
||||
val amount = if (amountState.isFiatValue) {
|
||||
amountState.cryptoCurrencyStatus.value.fiatAmount
|
||||
} else {
|
||||
amountState.cryptoCurrencyStatus.value.amount
|
||||
}
|
||||
onAmountValueChange(amount?.toPlainString() ?: "0.00")
|
||||
onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region recipient state clicks
|
||||
override fun onRecipientAddressValueChange(value: String) {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = stateFactory.getOnRecipientAddressValueChangeState(value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientMemoValueChange(value: String) {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = stateFactory.getOnRecipientMemoValueChangeState(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIfXrpAddressValue(value: String): Boolean {
|
||||
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val result = XrpAddressService.decodeXAddress(value)
|
||||
onRecipientAddressValueChange(result?.address.orEmpty())
|
||||
onRecipientMemoValueChange(result?.destinationTag.toString())
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// endregion
|
||||
|
||||
//region fee
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
stateFactory.onFeeSelectedState(feeType)
|
||||
updateReceiveAmount()
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState.feeState?.apply {
|
||||
(feeSelectorState.value as? FeeSelectorState.Content)?.let { feeSelector ->
|
||||
feeSelector.customValues.update {
|
||||
it.toMutableList().apply {
|
||||
set(index, it[index].copy(value = value))
|
||||
}
|
||||
}
|
||||
updateReceiveAmount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubtractSelect(value: Boolean) {
|
||||
uiState.feeState?.isSubtract?.update { value }
|
||||
if (value) {
|
||||
updateReceiveAmount()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateReceiveAmount() {
|
||||
uiState.feeState?.receivedAmount?.update {
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = calculateReceiveAmount(),
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateReceiveAmount(): BigDecimal {
|
||||
val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return BigDecimal.ZERO
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO
|
||||
|
||||
val fee = when (val selectedFee = feeState.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeState.selectedFee) {
|
||||
FeeType.SLOW -> selectedFee.minimum.amount.value
|
||||
FeeType.MARKET -> selectedFee.normal.amount.value
|
||||
FeeType.FAST -> selectedFee.priority.amount.value
|
||||
FeeType.CUSTOM -> feeState.customValues.value.firstOrNull()?.value?.let { BigDecimal(it) }
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> selectedFee.normal.amount.value
|
||||
} ?: BigDecimal.ZERO
|
||||
|
||||
return BigDecimal(amount.value).minus(fee)
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val DEFAULT_VALUE = "0.00"
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -217,6 +218,7 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo
|
|||
@Composable
|
||||
private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @DrawableRes iconPlaceholder: Int?) {
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
var isBackgroundColorDefined by remember { mutableStateOf(false) }
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
|
@ -231,29 +233,35 @@ private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @Drawa
|
|||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
),
|
||||
) {
|
||||
val iconModifier = Modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size40.roundToPx() }
|
||||
val iconModifier = Modifier.size(TangemTheme.dimens.size40)
|
||||
|
||||
val colorFilter = if (!token.available) {
|
||||
val matrix = ColorMatrix().apply { setToSaturation(0f) }
|
||||
ColorFilter.colorMatrix(matrix)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = iconModifier,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(data)
|
||||
.size(size = pixelsSize)
|
||||
.memoryCacheKey(key = data.toString() + pixelsSize)
|
||||
.crossfade(true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
onSuccess = { _, result ->
|
||||
if (isDarkTheme) {
|
||||
if (!isBackgroundColorDefined && isDarkTheme) {
|
||||
coroutineScope.launch {
|
||||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = screenBackgroundColor.toArgb(),
|
||||
).getContrastColorIfNeeded(isDarkTheme)
|
||||
size = pixelsSize,
|
||||
).getContrastColor(isDarkTheme = true)
|
||||
iconBackgroundColor = color
|
||||
isBackgroundColorDefined = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
|
@ -292,8 +293,9 @@ private fun TokenIcon(
|
|||
@DrawableRes iconPlaceholder: Int? = null,
|
||||
@DrawableRes networkIconRes: Int? = null,
|
||||
) {
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
var isBackgroundColorDefined by remember { mutableStateOf(false) }
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
|
@ -310,13 +312,16 @@ private fun TokenIcon(
|
|||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
)
|
||||
|
||||
val data = tokenIconUrl.ifEmpty {
|
||||
iconPlaceholder
|
||||
}
|
||||
val data = tokenIconUrl.ifEmpty { iconPlaceholder }
|
||||
|
||||
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = tokenImageModifier,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(data)
|
||||
.size(size = pixelsSize)
|
||||
.memoryCacheKey(key = data.toString() + pixelsSize)
|
||||
.crossfade(true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
|
|
@ -326,8 +331,10 @@ private fun TokenIcon(
|
|||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = itemBackgroundColor,
|
||||
).getContrastColorIfNeeded(isDarkTheme)
|
||||
size = pixelsSize,
|
||||
).getContrastColor(true)
|
||||
iconBackgroundColor = color
|
||||
isBackgroundColorDefined = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.paging.*
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isToday
|
||||
import com.tangem.utils.extensions.isYesterday
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import java.util.UUID
|
||||
|
||||
internal class TokenDetailsTxHistoryItemFlowConverter(
|
||||
|
|
@ -108,7 +104,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
) {
|
||||
val txContent = txHistoryItemState.state as TransactionState.Content
|
||||
txHistoryItemState.copy(
|
||||
state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()),
|
||||
state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()),
|
||||
)
|
||||
} else {
|
||||
txHistoryItemState
|
||||
|
|
@ -124,26 +120,4 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If [this] timestamp is today or yesterday, returns relative date,
|
||||
* otherwise returns formatting date.
|
||||
*/
|
||||
private fun Long.toDateFormat(): String {
|
||||
val localDate = DateTime(this, DateTimeZone.getDefault())
|
||||
return if (localDate.isToday() || localDate.isYesterday()) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
this,
|
||||
DateTime.now().millis,
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE,
|
||||
).toString()
|
||||
} else {
|
||||
DateTimeFormatters.formatDate(date = localDate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toTimeFormat(): String {
|
||||
return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault()))
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
|
|
@ -136,11 +137,13 @@ private inline fun DefaultCurrencyIcon(
|
|||
crossinline errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
var isBackgroundColorDefined by remember { mutableStateOf(false) }
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size48.roundToPx() }
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier
|
||||
.background(
|
||||
|
|
@ -149,17 +152,21 @@ private inline fun DefaultCurrencyIcon(
|
|||
),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(iconData)
|
||||
.size(pixelsSize)
|
||||
.memoryCacheKey(iconData.toString() + pixelsSize)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
onSuccess = { _, result ->
|
||||
if (isDarkTheme) {
|
||||
if (!isBackgroundColorDefined && isDarkTheme) {
|
||||
coroutineScope.launch {
|
||||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = itemBackgroundColor,
|
||||
).getContrastColorIfNeeded(isDarkTheme)
|
||||
size = pixelsSize,
|
||||
).getContrastColor(isDarkTheme = true)
|
||||
iconBackgroundColor = color
|
||||
isBackgroundColorDefined = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue