Updated on 2026-08-14
This commit is contained in:
commit
512ab6d54d
206 changed files with 8933 additions and 630 deletions
|
|
@ -47,6 +47,9 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.data.tokens)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.common)
|
||||
implementation(projects.domain.card)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed class ChooseWalletState {
|
||||
data class Choose(
|
||||
val wallets: ImmutableList<WalletState>,
|
||||
val selectedWallet: WalletState?,
|
||||
val onChooseWalletClick: () -> Unit,
|
||||
val onCloseChoosingWalletClick: () -> Unit,
|
||||
) : ChooseWalletState()
|
||||
|
||||
object NoSelection : ChooseWalletState()
|
||||
|
||||
class Warning(val type: ChooseWalletWarning) : ChooseWalletState() {
|
||||
val message: TextReference
|
||||
get() = when (type) {
|
||||
ChooseWalletWarning.SINGLE_CURRENCY ->
|
||||
TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title)
|
||||
ChooseWalletWarning.WALLET_INCOMPATIBLE ->
|
||||
TextReference.Res(R.string.manage_tokens_wallet_does_not_supported_blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ChooseWalletWarning {
|
||||
SINGLE_CURRENCY,
|
||||
WALLET_INCOMPATIBLE,
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.getGreyedOutIconRes
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
/**
|
||||
* Network item state
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property id network id
|
||||
* @property blockchain blockchain
|
||||
* @property iconRes network icon id from resources
|
||||
*/
|
||||
internal sealed interface NetworkItemState {
|
||||
|
||||
val name: String
|
||||
val protocolName: String
|
||||
val id: String
|
||||
val blockchain: Blockchain
|
||||
|
||||
val iconRes: Int
|
||||
get() = when (this) {
|
||||
is Selectable -> this.iconResId
|
||||
is Toggleable -> this.iconResId.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Network item state that can be added and deleted
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property id network id
|
||||
* @property blockchain blockchain
|
||||
* @property iconResId network icon id from resources
|
||||
* @property isMainNetwork flag that determines if the network is the main network for the token
|
||||
* @property isAdded flag that determines if the user has saved the network
|
||||
* @property address contract address
|
||||
* @property decimals decimal count
|
||||
* @property onToggleClick lambda be invoked when switch is been toggled
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class Toggleable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val id: String,
|
||||
override val blockchain: Blockchain,
|
||||
val iconResId: MutableState<Int>,
|
||||
val isMainNetwork: Boolean,
|
||||
val isAdded: MutableState<Boolean>,
|
||||
val address: String?,
|
||||
val decimals: Int?,
|
||||
val onToggleClick: (TokenItemState.Loaded, Toggleable) -> Unit,
|
||||
) : NetworkItemState {
|
||||
|
||||
/**
|
||||
* Change toggle state [isAdded].
|
||||
*
|
||||
* It is a hack that helps us to change element of flow
|
||||
*/
|
||||
fun changeToggleState() {
|
||||
val reverseState = !isAdded.value
|
||||
isAdded.value = reverseState
|
||||
iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Network item state that can be selected
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property iconResId network icon id from resources
|
||||
* @property id network id
|
||||
* @property blockchain blockchain
|
||||
* @property onNetworkClick lambda be invoked when network item is been clicked
|
||||
*
|
||||
*/
|
||||
class Selectable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
val iconResId: Int,
|
||||
override val id: String,
|
||||
override val blockchain: Blockchain,
|
||||
val onNetworkClick: (NetworkItemState) -> Unit,
|
||||
) : NetworkItemState
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
internal data class WalletState(
|
||||
val walletId: String,
|
||||
val artworkUrl: String?,
|
||||
val walletName: String,
|
||||
val onSelected: (String) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.managetokens.presentation.common.state.previewdata
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.WalletState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object ChooseWalletStatePreviewData {
|
||||
|
||||
val state: ChooseWalletState.Choose
|
||||
get() = ChooseWalletState.Choose(
|
||||
wallets = persistentListOf(
|
||||
walletState,
|
||||
walletState.copy(walletId = "2"),
|
||||
),
|
||||
selectedWallet = walletState,
|
||||
onChooseWalletClick = {},
|
||||
onCloseChoosingWalletClick = {},
|
||||
)
|
||||
|
||||
private val walletState: WalletState
|
||||
get() = WalletState(
|
||||
walletName = "My wallet",
|
||||
walletId = "1",
|
||||
artworkUrl = "",
|
||||
onSelected = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.managetokens.presentation.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
|
||||
@Composable
|
||||
internal fun ChooseWalletBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<ChooseWalletBottomSheetConfig>(
|
||||
config = config,
|
||||
contentColor = TangemTheme.colors.background.tertiary,
|
||||
) {
|
||||
ChooseWalletScreen(state = it.chooseWalletState)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChooseWalletBottomSheetConfig(
|
||||
val chooseWalletState: ChooseWalletState.Choose,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.tangem.managetokens.presentation.common.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.WalletState
|
||||
import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modifier = Modifier) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size44),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = state.onCloseChoosingWalletClick,
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_wallet_selector_title),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
items(
|
||||
count = state.wallets.count(),
|
||||
key = { index -> state.wallets[index].walletId },
|
||||
) { index ->
|
||||
WalletItem(
|
||||
wallet = state.wallets[index],
|
||||
selectedWallet = state.selectedWallet,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.wallets.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable { wallet.onSelected(wallet.walletId) }
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size72)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.size(height = TangemTheme.dimens.size30, width = TangemTheme.dimens.size50),
|
||||
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(wallet.artworkUrl)
|
||||
.crossfade(enable = true)
|
||||
.build(),
|
||||
loading = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.card_placeholder_primary),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
error = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.card_placeholder_primary),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerW12()
|
||||
Text(
|
||||
text = wallet.walletName,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerWMax()
|
||||
if (selectedWallet == wallet) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_check_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseWalletScreen_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
ChooseWalletScreen(
|
||||
state = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseWalletScreen_Dark() {
|
||||
TangemTheme(isDark = false) {
|
||||
ChooseWalletScreen(
|
||||
state = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package com.tangem.managetokens.presentation.common.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun NetworkItem(state: NetworkItemState, tokenState: TokenItemState.Loaded?, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NetworkIcon(model = state)
|
||||
SpacerW(width = TangemTheme.dimens.spacing12)
|
||||
Text(
|
||||
text = state.name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
SpacerW(width = TangemTheme.dimens.spacing6)
|
||||
Text(
|
||||
text = state.protocolName,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
)
|
||||
if (state is NetworkItemState.Toggleable) {
|
||||
TangemSwitch(
|
||||
onCheckedChange = {
|
||||
state.onToggleClick(tokenState!!, state)
|
||||
},
|
||||
checked = state.isAdded.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.size(size = TangemTheme.dimens.size36)) {
|
||||
val isAdded = when (model) {
|
||||
is NetworkItemState.Selectable -> true
|
||||
is NetworkItemState.Toggleable -> model.isAdded.value
|
||||
}
|
||||
|
||||
if (!isAdded) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.control.unchecked),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(id = model.iconRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size36),
|
||||
tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
if (model is NetworkItemState.Toggleable && model.isMainNetwork) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(TangemTheme.dimens.size10)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.stroke.transparency),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size8)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.icon.accent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) {
|
||||
TangemTheme(isDark = false) {
|
||||
NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) {
|
||||
TangemTheme(isDark = true) {
|
||||
NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded)
|
||||
}
|
||||
}
|
||||
|
||||
private class NetworkItemStateProvider : CollectionPreviewParameterProvider<NetworkItemState>(
|
||||
collection = listOf(
|
||||
NetworkItemState.Toggleable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = mutableStateOf(R.drawable.img_polygon_22),
|
||||
isMainNetwork = true,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
address = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Toggleable(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
address = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.BSC,
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Selectable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = R.drawable.img_polygon_22,
|
||||
id = "",
|
||||
onNetworkClick = { },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.managetokens.presentation.common.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun SimpleSelectionBlock(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
roundedCorners: Boolean = true,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (roundedCorners) {
|
||||
Modifier.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.background(color = TangemTheme.colors.background.action)
|
||||
.clickable { onClick() }
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing20,
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing4)
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_SimpleSelectionBlock_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { })
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_SimpleSelectionBlock_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ChooseNetworkState(
|
||||
val nativeNetworks: ImmutableList<NetworkItemState>,
|
||||
val nonNativeNetworks: ImmutableList<NetworkItemState>,
|
||||
val onNonNativeNetworkHintClick: () -> Unit,
|
||||
val onCloseChooseNetworkScreen: () -> Unit,
|
||||
)
|
||||
|
|
@ -8,7 +8,8 @@ import com.tangem.features.managetokens.impl.R
|
|||
|
||||
data class DerivationNotificationState(
|
||||
val totalNeeded: Int,
|
||||
val missingAddressesCount: Int,
|
||||
val totalWallets: Int,
|
||||
val walletsToDerive: Int,
|
||||
val onGenerateClick: () -> Unit,
|
||||
) {
|
||||
val config = NotificationConfig(
|
||||
|
|
@ -25,8 +26,8 @@ data class DerivationNotificationState(
|
|||
onClick = onGenerateClick,
|
||||
additionalText = pluralReference(
|
||||
id = R.plurals.manage_tokens_number_of_wallets_android,
|
||||
count = totalNeeded,
|
||||
formatArgs = wrappedList(missingAddressesCount, totalNeeded),
|
||||
count = totalWallets,
|
||||
formatArgs = wrappedList(walletsToDerive, totalWallets),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal data class ManageTokensState(
|
||||
val searchBarState: SearchBarState,
|
||||
val tokens: Flow<PagingData<TokenItemState>>,
|
||||
val isLoading: Boolean,
|
||||
val addCustomTokenButton: AddCustomTokenButton,
|
||||
val chooseWalletState: ChooseWalletState,
|
||||
val derivationNotification: DerivationNotificationState? = null,
|
||||
val selectedToken: TokenItemState.Loaded? = null,
|
||||
val showChooseWalletScreen: Boolean = false,
|
||||
val event: StateEvent<Event>,
|
||||
)
|
||||
|
||||
data class AddCustomTokenButton(
|
||||
val isVisible: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
|
||||
internal sealed class TokenItemState {
|
||||
|
||||
abstract val id: String
|
||||
|
|
@ -9,11 +11,13 @@ internal sealed class TokenItemState {
|
|||
data class Loaded(
|
||||
override val id: String,
|
||||
val name: String,
|
||||
val currencyId: String,
|
||||
val currencySymbol: String,
|
||||
val tokenId: String,
|
||||
val tokenIcon: TokenIconState,
|
||||
val quotes: QuotesState,
|
||||
val rate: String?,
|
||||
val availableAction: TokenButtonType,
|
||||
val onButtonClick: (String) -> Unit,
|
||||
val availableAction: MutableState<TokenButtonType>,
|
||||
val chooseNetworkState: ChooseNetworkState,
|
||||
val onButtonClick: (Loaded) -> Unit,
|
||||
) : TokenItemState()
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal object ChooseNetworkStatePreviewData {
|
||||
|
||||
val state = ChooseNetworkState(
|
||||
nativeNetworks = nativeNetworks.toImmutableList(),
|
||||
nonNativeNetworks = nonNativeNetworks.toImmutableList(),
|
||||
onNonNativeNetworkHintClick = {},
|
||||
onCloseChooseNetworkScreen = {},
|
||||
)
|
||||
}
|
||||
|
||||
internal val nativeNetworks = listOf(
|
||||
NetworkItemState.Toggleable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = mutableStateOf(R.drawable.img_polygon_22),
|
||||
isMainNetwork = true,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
address = "",
|
||||
decimals = 0,
|
||||
),
|
||||
)
|
||||
|
||||
internal val nonNativeNetworks = listOf(
|
||||
NetworkItemState.Toggleable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = mutableStateOf(R.drawable.img_kusama_22),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
address = "",
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Toggleable(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.BSC,
|
||||
address = "",
|
||||
decimals = 0,
|
||||
),
|
||||
)
|
||||
|
|
@ -4,8 +4,9 @@ import com.tangem.managetokens.presentation.managetokens.state.DerivationNotific
|
|||
|
||||
object DerivationNotificationStatePreviewData {
|
||||
val state = DerivationNotificationState(
|
||||
totalNeeded = 3,
|
||||
missingAddressesCount = 2,
|
||||
totalNeeded = 5,
|
||||
totalWallets = 3,
|
||||
walletsToDerive = 2,
|
||||
onGenerateClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData
|
||||
import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.SearchBarState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
internal object ManageTokensStatePreviewData {
|
||||
val loadedState: ManageTokensState
|
||||
get() = ManageTokensState(
|
||||
searchBarState = searchState,
|
||||
tokens = flowOf(PagingData.from(tokens)),
|
||||
isLoading = false,
|
||||
addCustomTokenButton = AddCustomTokenButton(true, {}),
|
||||
derivationNotification = DerivationNotificationStatePreviewData.state,
|
||||
event = consumedEvent(),
|
||||
chooseWalletState = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
|
||||
val loadingState: ManageTokensState
|
||||
get() = loadedState.copy(isLoading = true)
|
||||
|
||||
private val tokens: List<TokenItemState>
|
||||
get() = listOf(
|
||||
TokenItemStatePreviewData.loadedPriceDown,
|
||||
TokenItemStatePreviewData.loadedPriceUp,
|
||||
)
|
||||
|
||||
private val searchState: SearchBarState
|
||||
get() = SearchBarState(
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
active = false,
|
||||
onActiveChange = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.managetokens.presentation.managetokens.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -13,7 +14,8 @@ internal object TokenItemStatePreviewData {
|
|||
get() = TokenItemState.Loaded(
|
||||
id = "BTC",
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencyId = "BTC",
|
||||
tokenId = "BTC",
|
||||
currencySymbol = "BTC",
|
||||
tokenIcon = tokenIconState,
|
||||
quotes = QuotesState.Content(
|
||||
priceChange = "0.43%",
|
||||
|
|
@ -21,15 +23,17 @@ internal object TokenItemStatePreviewData {
|
|||
chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f),
|
||||
),
|
||||
rate = "31 285.72$",
|
||||
availableAction = TokenButtonType.ADD,
|
||||
availableAction = mutableStateOf(TokenButtonType.ADD),
|
||||
onButtonClick = {},
|
||||
chooseNetworkState = ChooseNetworkStatePreviewData.state,
|
||||
)
|
||||
|
||||
val loadedPriceUp: TokenItemState
|
||||
get() = TokenItemState.Loaded(
|
||||
id = "BTC",
|
||||
name = "Bitcoin",
|
||||
currencyId = "BTC",
|
||||
tokenId = "BTC",
|
||||
currencySymbol = "BTC",
|
||||
tokenIcon = tokenIconState,
|
||||
quotes = QuotesState.Content(
|
||||
priceChange = "0.43%",
|
||||
|
|
@ -37,8 +41,9 @@ internal object TokenItemStatePreviewData {
|
|||
chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f),
|
||||
),
|
||||
rate = "31 285.72$",
|
||||
availableAction = TokenButtonType.NOT_AVAILABLE,
|
||||
availableAction = mutableStateOf(TokenButtonType.NOT_AVAILABLE),
|
||||
onButtonClick = {},
|
||||
chooseNetworkState = ChooseNetworkStatePreviewData.state,
|
||||
)
|
||||
|
||||
private val tokenIconState: TokenIconState
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
@Composable
|
||||
internal fun ChooseNetworkBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<ChooseNetworkBottomSheetConfig>(
|
||||
config = config,
|
||||
contentColor = TangemTheme.colors.background.tertiary,
|
||||
) {
|
||||
ChooseNetworkScreen(state = it.selectedToken, walletState = it.chooseWalletState)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChooseNetworkBottomSheetConfig(
|
||||
val selectedToken: TokenItemState.Loaded,
|
||||
val chooseWalletState: ChooseWalletState,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.WarningCardTitleOnly
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData
|
||||
import com.tangem.managetokens.presentation.common.ui.components.NetworkItem
|
||||
import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun ChooseNetworkScreen(
|
||||
state: TokenItemState.Loaded,
|
||||
walletState: ChooseWalletState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val networkState = state.chooseNetworkState
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_title),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
when (walletState) {
|
||||
is ChooseWalletState.Choose -> {
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing10)
|
||||
}
|
||||
item {
|
||||
SimpleSelectionBlock(
|
||||
title = stringResource(id = R.string.manage_tokens_network_selector_wallet),
|
||||
subtitle = walletState.selectedWallet?.walletName ?: "",
|
||||
onClick = walletState.onChooseWalletClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
ChooseWalletState.NoSelection -> Unit
|
||||
is ChooseWalletState.Warning -> {
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing10)
|
||||
}
|
||||
item {
|
||||
WarningCardTitleOnly(
|
||||
title = stringResource(id = R.string.manage_tokens_wallet_support_only_one_network_title),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
|
||||
item {
|
||||
if (networkState.nativeNetworks.isNotEmpty()) {
|
||||
NativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
}
|
||||
|
||||
if (networkState.nonNativeNetworks.isNotEmpty()) {
|
||||
item {
|
||||
NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick)
|
||||
}
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing8)
|
||||
}
|
||||
item {
|
||||
this@LazyColumn.NonNativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_native_title),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_native_subtitle),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing8)
|
||||
|
||||
networkState.nativeNetworks.forEachIndexed { index, network ->
|
||||
NetworkItem(
|
||||
state = network,
|
||||
tokenState = tokenState,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = networkState.nativeNetworks.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyListScope.NonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
items(
|
||||
count = networkState.nonNativeNetworks.count(),
|
||||
key = { index -> networkState.nonNativeNetworks[index].id },
|
||||
) { index ->
|
||||
NetworkItem(
|
||||
state = networkState.nonNativeNetworks[index],
|
||||
tokenState = tokenState,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = networkState.nonNativeNetworks.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) {
|
||||
Column {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_non_native_title),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
SpacerW(width = TangemTheme.dimens.spacing2)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
tint = TangemTheme.colors.icon.inactive,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.clickable { onNonNativeNetworkHintClick() },
|
||||
)
|
||||
}
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_non_native_subtitle),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseNetworkScreen_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
ChooseNetworkScreen(
|
||||
state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded,
|
||||
walletState = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseNetworkScreen_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
ChooseNetworkScreen(
|
||||
state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded,
|
||||
walletState = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet
|
||||
import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig
|
||||
import com.tangem.managetokens.presentation.common.ui.EventEffect
|
||||
import com.tangem.managetokens.presentation.common.ui.components.Alert
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.ManageTokensStatePreviewData
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.components.DerivationNotification
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.components.TokensList
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar
|
||||
|
||||
@Composable
|
||||
internal fun ManageTokensScreen(state: ManageTokensState) {
|
||||
var alertState by remember { mutableStateOf<AlertState?>(value = null) }
|
||||
|
||||
EventEffect(
|
||||
event = state.event,
|
||||
onAlertStateSet = { alertState = it },
|
||||
)
|
||||
alertState?.let {
|
||||
Alert(state = it, onDismiss = { alertState = null })
|
||||
}
|
||||
|
||||
Content(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: ManageTokensState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(top = TangemTheme.dimens.spacing32),
|
||||
) {
|
||||
Column {
|
||||
val listState = rememberLazyListState()
|
||||
val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
|
||||
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (raiseSearchBar) {
|
||||
TangemTheme.dimens.elevation8
|
||||
} else {
|
||||
TangemTheme.dimens.elevation0
|
||||
},
|
||||
label = "top_bar_elevation",
|
||||
)
|
||||
Surface(
|
||||
elevation = elevation,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
TokensSearchBar(
|
||||
state = state.searchBarState,
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing20),
|
||||
)
|
||||
}
|
||||
val tokens = state.tokens.collectAsLazyPagingItems()
|
||||
TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton)
|
||||
}
|
||||
state.derivationNotification?.let {
|
||||
DerivationNotification(
|
||||
config = it.config,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
state.selectedToken?.let { selectedToken ->
|
||||
ManageTokensBottomSheet(selectedToken = selectedToken, state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) {
|
||||
if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
content = ChooseWalletBottomSheetConfig(state.chooseWalletState),
|
||||
onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick,
|
||||
)
|
||||
ChooseWalletBottomSheet(config)
|
||||
} else {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
content = ChooseNetworkBottomSheetConfig(
|
||||
selectedToken = selectedToken,
|
||||
chooseWalletState = state.chooseWalletState,
|
||||
),
|
||||
onDismissRequest = selectedToken.chooseNetworkState.onCloseChooseNetworkScreen,
|
||||
)
|
||||
ChooseNetworkBottomSheet(config)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ManageTokensScreen_LightTheme(
|
||||
@PreviewParameter(ManageTokensConfigProvider::class)
|
||||
state: ManageTokensState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
ManageTokensScreen(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ManageTokensScreen_DarkTheme(
|
||||
@PreviewParameter(ManageTokensConfigProvider::class)
|
||||
state: ManageTokensState,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
ManageTokensScreen(state)
|
||||
}
|
||||
}
|
||||
|
||||
private class ManageTokensConfigProvider : CollectionPreviewParameterProvider<ManageTokensState>(
|
||||
collection = listOf(
|
||||
ManageTokensStatePreviewData.loadingState,
|
||||
ManageTokensStatePreviewData.loadedState,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
|
||||
.fillMaxWidth()
|
||||
.clickable { onButtonClick() }
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.background(color = TangemTheme.colors.button.secondary, shape = CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_plus_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
SpacerW(width = TangemTheme.dimens.spacing12)
|
||||
Text(
|
||||
text = stringResource(id = R.string.add_custom_token_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AddCustomTokenButton_Preview_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
AddCustomTokenButton(onButtonClick = { })
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AddCustomTokenButton_Preview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
AddCustomTokenButton(onButtonClick = { })
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M
|
|||
modifier = Modifier
|
||||
.weight(weight = 1f),
|
||||
) {
|
||||
TokenName(name = state.name, currencyId = state.currencyId)
|
||||
TokenName(name = state.name, currencyId = state.currencySymbol)
|
||||
TokenPriceData(price = state.rate, quotesState = state.quotes)
|
||||
}
|
||||
SpacerW24()
|
||||
|
|
@ -67,8 +67,8 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M
|
|||
}
|
||||
|
||||
TokenButton(
|
||||
type = state.availableAction,
|
||||
onClick = { state.onButtonClick(state.currencyId) },
|
||||
type = state.availableAction.value,
|
||||
onClick = { state.onButtonClick(state) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
private const val PLACEHOLDER_ITEMS_COUNT = 50
|
||||
|
||||
@Composable
|
||||
internal fun TokensList(tokens: LazyPagingItems<TokenItemState>, addCustomTokenButton: AddCustomTokenButton) {
|
||||
LazyColumn {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
|
||||
if (tokens.loadState.refresh is LoadState.Loading) {
|
||||
items(PLACEHOLDER_ITEMS_COUNT) {
|
||||
TokenRowItem(state = TokenItemState.Loading(it.toString()))
|
||||
}
|
||||
} else {
|
||||
val tokensList = tokens.itemSnapshotList
|
||||
if (tokensList.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_nothing_found),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(items = tokensList.items, key = TokenItemState::id) { token ->
|
||||
TokenRowItem(state = token)
|
||||
}
|
||||
|
||||
if (addCustomTokenButton.isVisible) {
|
||||
item {
|
||||
AddCustomTokenButton(onButtonClick = { addCustomTokenButton.onClick })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ dependencies {
|
|||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
|
|
@ -58,6 +59,8 @@ dependencies {
|
|||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.di
|
||||
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.DefaultSendRouter
|
||||
import dagger.Module
|
||||
|
|
@ -17,7 +18,7 @@ internal object SendRouterModule {
|
|||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideSendRouter(): SendRouter {
|
||||
return DefaultSendRouter()
|
||||
fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter {
|
||||
return DefaultSendRouter(reduxNavController)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,17 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.features.send.impl.presentation.SendFragment
|
||||
|
||||
internal class DefaultSendRouter : SendRouter {
|
||||
internal class DefaultSendRouter(
|
||||
private val reduxNavController: ReduxNavController,
|
||||
) : InnerSendRouter {
|
||||
|
||||
override fun getEntryFragment(): Fragment = SendFragment.create()
|
||||
|
||||
override fun openUrl(url: String) {
|
||||
reduxNavController.navigate(NavigationAction.OpenUrl(url = url))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
||||
interface InnerSendRouter : SendRouter {
|
||||
|
||||
/** Open website by [url] */
|
||||
fun openUrl(url: String)
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import com.tangem.core.ui.components.SystemBarsEffect
|
|||
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.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.InnerSendRouter
|
||||
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
|
||||
|
|
@ -24,12 +26,20 @@ internal class SendFragment : ComposeFragment() {
|
|||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
@Inject
|
||||
lateinit var router: SendRouter
|
||||
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
private val innerSendRouter: InnerSendRouter
|
||||
get() = requireNotNull(router as? InnerSendRouter) {
|
||||
"innerSendRouter should be instance of InnerSendRouter"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
viewModel.setRouter(
|
||||
innerSendRouter,
|
||||
StateRouter(
|
||||
fragmentManager = WeakReference(parentFragmentManager),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.send.impl.presentation.domain
|
||||
|
||||
sealed class SendNotification {
|
||||
|
||||
sealed class Info(val message: String) : SendNotification()
|
||||
|
||||
sealed class Critical(val message: String) : SendNotification()
|
||||
|
||||
sealed class Error(val message: String) : SendNotification()
|
||||
}
|
||||
|
|
@ -86,6 +86,7 @@ internal class SendStateFactory(
|
|||
amountState = amountStateConverter.convert(Unit),
|
||||
recipientState = recipientStateConverter.convert(Unit),
|
||||
feeState = feeStateConverter.convert(Unit),
|
||||
sendState = SendStates.SendState(),
|
||||
)
|
||||
//endregion
|
||||
|
||||
|
|
@ -122,15 +123,15 @@ internal class SendStateFactory(
|
|||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
val isValidMemo = validateMemo(
|
||||
memo = value,
|
||||
memo = recipientState.addressTextField.value.value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
val isAddressInWallet = isNotAddressInWallet(
|
||||
address = value,
|
||||
walletAddresses = walletAddressesProvider(),
|
||||
address = recipientState.addressTextField.value.value,
|
||||
)
|
||||
val isValidAddress = verifyAddress(
|
||||
address = recipientState.addressTextField.value.value,
|
||||
address = value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
|
||||
|
|
@ -138,8 +139,7 @@ internal class SendStateFactory(
|
|||
it.copy(
|
||||
value = value,
|
||||
error = when {
|
||||
!isValidAddress -> TextReference.Res(R.string.send_recipient_address_error)
|
||||
!isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
|
||||
!isValidAddress || !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
|
||||
else -> null
|
||||
},
|
||||
isError = !isValidAddress || !isAddressInWallet,
|
||||
|
|
@ -169,11 +169,9 @@ internal class SendStateFactory(
|
|||
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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ internal data class SendUiState(
|
|||
val amountState: SendStates.AmountState? = null,
|
||||
val recipientState: SendStates.RecipientState? = null,
|
||||
val feeState: SendStates.FeeState? = null,
|
||||
val sendState: SendStates.SendState? = null,
|
||||
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val currentState: MutableStateFlow<SendUiStateType>,
|
||||
)
|
||||
|
|
@ -65,11 +66,14 @@ internal sealed class SendStates {
|
|||
val receivedAmount: MutableStateFlow<String> = MutableStateFlow(""),
|
||||
) : SendStates()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
val isSuccess: Boolean,
|
||||
)
|
||||
override val type: SendUiStateType = SendUiStateType.Send,
|
||||
val isSending: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val isSuccess: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val transactionDate: MutableStateFlow<Long> = MutableStateFlow(0L),
|
||||
val txUrl: MutableStateFlow<String> = MutableStateFlow(""),
|
||||
) : SendStates()
|
||||
}
|
||||
|
||||
enum class SendUiStateType {
|
||||
|
|
@ -77,5 +81,4 @@ enum class SendUiStateType {
|
|||
Recipient,
|
||||
Fee,
|
||||
Send,
|
||||
Done,
|
||||
}
|
||||
|
|
@ -9,28 +9,61 @@ internal class StateRouter(
|
|||
private val fragmentManager: WeakReference<FragmentManager>,
|
||||
) {
|
||||
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Amount)
|
||||
private set
|
||||
|
||||
private var isFromSend: Boolean = false
|
||||
|
||||
fun popBackStack() {
|
||||
fragmentManager.get()?.popBackStack()
|
||||
}
|
||||
|
||||
fun onBackClick() {
|
||||
fragmentManager.get()?.popBackStack()
|
||||
if (isFromSend) {
|
||||
showSend()
|
||||
} else {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Amount -> popBackStack()
|
||||
SendUiStateType.Recipient -> showAmount()
|
||||
SendUiStateType.Fee -> showRecipient()
|
||||
SendUiStateType.Send -> showFee()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
SendUiStateType.Amount -> showRecipient()
|
||||
SendUiStateType.Recipient -> showFee()
|
||||
SendUiStateType.Fee -> showSend()
|
||||
SendUiStateType.Send -> 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()
|
||||
SendUiStateType.Amount -> popBackStack()
|
||||
SendUiStateType.Recipient -> showAmount()
|
||||
SendUiStateType.Fee -> showRecipient()
|
||||
SendUiStateType.Send -> popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
fun showAmount(isFromSend: Boolean = false) {
|
||||
this.isFromSend = isFromSend
|
||||
currentState.update { SendUiStateType.Amount }
|
||||
}
|
||||
|
||||
fun showRecipient(isFromSend: Boolean = false) {
|
||||
this.isFromSend = isFromSend
|
||||
currentState.update { SendUiStateType.Recipient }
|
||||
}
|
||||
|
||||
fun showFee(isFromSend: Boolean = false) {
|
||||
this.isFromSend = isFromSend
|
||||
currentState.update { SendUiStateType.Fee }
|
||||
}
|
||||
|
||||
fun showSend() {
|
||||
currentState.update { SendUiStateType.Send }
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ internal class SendRecipientMemoFieldConverter(
|
|||
),
|
||||
placeholder = TextReference.Res(R.string.send_optional_field),
|
||||
label = TextReference.Res(value),
|
||||
error = TextReference.Res(R.string.send_memo_destination_tag_error),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.extensions.shareText
|
||||
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
|
||||
|
|
@ -64,16 +71,16 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) {
|
|||
|
||||
@Composable
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
|
||||
val currentState = uiState.currentState.collectAsState()
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()?.value ?: false
|
||||
val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false
|
||||
val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty()
|
||||
|
||||
val buttonTextId = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.Fee,
|
||||
-> R.string.common_next
|
||||
SendUiStateType.Send -> R.string.common_send
|
||||
else -> R.string.common_close
|
||||
}
|
||||
val (buttonTextId, buttonClick) = getButtonData(
|
||||
currentState = currentState,
|
||||
isSuccess = isSuccess,
|
||||
uiState = uiState,
|
||||
)
|
||||
|
||||
val isButtonEnabled = when (currentState.value) {
|
||||
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
|
||||
|
|
@ -86,19 +93,92 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
|
|||
label = "Update send screen state",
|
||||
modifier = modifier,
|
||||
) { textId ->
|
||||
if (currentState.value == SendUiStateType.Send) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = isButtonEnabled,
|
||||
onClick = uiState.clickIntents::onNextClick,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
text = stringResource(textId),
|
||||
enabled = isButtonEnabled,
|
||||
onClick = uiState.clickIntents::onNextClick,
|
||||
)
|
||||
when {
|
||||
currentState.value == SendUiStateType.Send && !isSuccess -> {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = isButtonEnabled,
|
||||
onClick = buttonClick,
|
||||
showProgress = isSending,
|
||||
)
|
||||
}
|
||||
currentState.value == SendUiStateType.Send && isSuccess -> {
|
||||
PrimaryButtonsDone(
|
||||
textRes = textId,
|
||||
txUrl = txUrl,
|
||||
onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) },
|
||||
onDoneClick = buttonClick,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
PrimaryButton(
|
||||
text = stringResource(textId),
|
||||
enabled = isButtonEnabled,
|
||||
onClick = buttonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrimaryButtonsDone(
|
||||
@StringRes textRes: Int,
|
||||
txUrl: String,
|
||||
onExploreClick: () -> Unit,
|
||||
onDoneClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(modifier = modifier) {
|
||||
if (txUrl.isNotBlank()) {
|
||||
Row {
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = R.string.common_explore),
|
||||
iconResId = R.drawable.ic_web_24,
|
||||
onClick = onExploreClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SpacerW12()
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = R.string.common_share),
|
||||
iconResId = R.drawable.ic_share_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
context.shareText(txUrl)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
SpacerH12()
|
||||
}
|
||||
PrimaryButton(
|
||||
text = stringResource(id = textRes),
|
||||
enabled = true,
|
||||
onClick = onDoneClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getButtonData(
|
||||
uiState: SendUiState,
|
||||
currentState: State<SendUiStateType>,
|
||||
isSuccess: Boolean,
|
||||
): Pair<Int, () -> Unit> {
|
||||
return when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.Fee,
|
||||
-> R.string.common_next to uiState.clickIntents::onNextClick
|
||||
SendUiStateType.Send -> if (isSuccess) {
|
||||
R.string.common_close
|
||||
} else {
|
||||
R.string.common_send
|
||||
} to uiState.clickIntents::onSendClick
|
||||
}
|
||||
}
|
||||
|
|
@ -22,11 +22,13 @@ 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
|
||||
import com.tangem.features.send.impl.presentation.ui.send.SendContent
|
||||
|
||||
@Composable
|
||||
internal fun SendScreen(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
BackHandler { uiState.clickIntents.onPrevClick() }
|
||||
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()
|
||||
BackHandler { uiState.clickIntents.onBackClick() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -36,12 +38,10 @@ internal fun SendScreen(uiState: SendUiState) {
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val titleRes = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Send,
|
||||
-> R.string.common_send
|
||||
SendUiStateType.Amount -> R.string.common_send
|
||||
SendUiStateType.Recipient -> R.string.send_recipient
|
||||
SendUiStateType.Fee -> R.string.common_fee_selector_title
|
||||
SendUiStateType.Done -> null
|
||||
SendUiStateType.Send -> if (isSuccess?.value == false) R.string.common_send else null
|
||||
}
|
||||
val iconRes = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
|
|
@ -52,7 +52,7 @@ internal fun SendScreen(uiState: SendUiState) {
|
|||
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = titleRes?.let { stringResource(it) },
|
||||
onBackClick = uiState.clickIntents::onBackClick,
|
||||
onBackClick = uiState.clickIntents::popBackStack,
|
||||
onIconClick = uiState.clickIntents::onQrCodeScanClick,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
iconRes = iconRes,
|
||||
|
|
@ -94,7 +94,7 @@ private fun SendScreenContent(
|
|||
uiState.feeState,
|
||||
uiState.clickIntents,
|
||||
)
|
||||
else -> { /* [REDACTED_TODO_COMMENT]*/ }
|
||||
SendUiStateType.Send -> SendContent(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.fields.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
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.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.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnter
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterInfo
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
|
||||
private const val ETHEREUM_UNIT = "GWEI"
|
||||
|
||||
|
|
@ -22,42 +26,66 @@ internal fun SendCustomFeeEthereum(
|
|||
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()) {
|
||||
val fee = customValues.value[0]
|
||||
val gasPrice = customValues.value[1]
|
||||
val gasLimit = customValues.value[2]
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextFieldWithInfo(
|
||||
value = fee.value,
|
||||
label = stringResource(R.string.send_max_fee),
|
||||
FooterContainer(
|
||||
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),
|
||||
) {
|
||||
InputRowEnterInfo(
|
||||
text = fee.value,
|
||||
title = TextReference.Res(R.string.send_max_fee),
|
||||
info = fee.label,
|
||||
visualTransformation = AmountVisualTransformation(symbol),
|
||||
keyboardOptions = fee.keyboardOptions,
|
||||
onValueChange = fee.onValueChange,
|
||||
isSingleLine = true,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
FooterContainer(
|
||||
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),
|
||||
) {
|
||||
InputRowEnter(
|
||||
text = gasPrice.value,
|
||||
title = TextReference.Res(R.string.send_gas_price),
|
||||
onValueChange = gasPrice.onValueChange,
|
||||
visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT),
|
||||
keyboardOptions = fee.keyboardOptions,
|
||||
isSingleLine = true,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
FooterContainer(
|
||||
footer = stringResource(R.string.send_gas_limit_footer),
|
||||
onValueChange = gasLimit.onValueChange,
|
||||
keyboardOptions = fee.keyboardOptions,
|
||||
isSingleLine = true,
|
||||
)
|
||||
) {
|
||||
InputRowEnter(
|
||||
text = gasLimit.value,
|
||||
title = TextReference.Res(R.string.send_gas_limit),
|
||||
onValueChange = gasLimit.onValueChange,
|
||||
keyboardOptions = fee.keyboardOptions,
|
||||
isSingleLine = true,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,11 +18,13 @@ 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.components.inputrow.InputRowRecipient
|
||||
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.ui.common.FooterContainer
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
|
||||
|
|
@ -45,18 +47,26 @@ internal fun SendRecipientContent(
|
|||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item(key = ADDRESS_FIELD_KEY) {
|
||||
TextFieldWithPasteAndIcon(
|
||||
value = address.value,
|
||||
label = address.label,
|
||||
placeholder = address.placeholder,
|
||||
FooterContainer(
|
||||
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,
|
||||
)
|
||||
) {
|
||||
InputRowRecipient(
|
||||
value = address.value,
|
||||
title = address.label,
|
||||
placeholder = address.placeholder,
|
||||
onValueChange = address.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientAddressValueChange,
|
||||
singleLine = true,
|
||||
isError = address.isError,
|
||||
error = address.error,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing4)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
memo?.let { memoField ->
|
||||
item(key = MEMO_FIELD_KEY) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.recipient
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.fields.SimpleTextField
|
||||
import com.tangem.core.ui.components.inputrow.inner.PasteButton
|
||||
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.presentation.ui.common.FooterContainer
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldWithPaste(
|
||||
value: String,
|
||||
placeholder: TextReference,
|
||||
label: TextReference,
|
||||
onValueChange: (String) -> Unit,
|
||||
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
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = color,
|
||||
)
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
}
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(end = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.recipient
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
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 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(
|
||||
value: String,
|
||||
placeholder: TextReference,
|
||||
label: TextReference,
|
||||
onValueChange: (String) -> Unit,
|
||||
onPasteClick: (String) -> Unit,
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = color,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Row {
|
||||
IdentIcon(
|
||||
address = value,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
bottom = TangemTheme.dimens.spacing10,
|
||||
)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius20))
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
bottom = TangemTheme.dimens.spacing10,
|
||||
)
|
||||
.weight(1f)
|
||||
.align(CenterVertically),
|
||||
)
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing4,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldWithPaste(
|
||||
value: String,
|
||||
placeholder: TextReference,
|
||||
label: TextReference,
|
||||
onValueChange: (String) -> Unit,
|
||||
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
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = color,
|
||||
)
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
}
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(end = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldWithInfo(
|
||||
value: String,
|
||||
label: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
info: TextReference? = null,
|
||||
footer: String? = null,
|
||||
isSingleLine: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = footer,
|
||||
footerTopPadding = TangemTheme.dimens.spacing6,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Row {
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
visualTransformation = visualTransformation,
|
||||
singleLine = isSingleLine,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6)
|
||||
.weight(1f),
|
||||
)
|
||||
info?.let {
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.align(Alignment.Bottom),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
if (isPasteButtonVisible) {
|
||||
Box(modifier = modifier) {
|
||||
Text(
|
||||
text = "Paste",
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.button.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing10,
|
||||
vertical = TangemTheme.dimens.spacing2,
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onClick(
|
||||
clipboardManager
|
||||
.getText()
|
||||
?.toString()
|
||||
.orEmpty(),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = stringResource(R.string.common_close),
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
|
||||
onClick = { onClick("") },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
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.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) {
|
||||
Text(
|
||||
text = placeholder.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TextFieldPreview_Light() {
|
||||
TangemTheme {
|
||||
Column {
|
||||
TextFieldWithPaste(
|
||||
value = "",
|
||||
label = TextReference.Res(R.string.send_recipient),
|
||||
placeholder = TextReference.Res(R.string.send_enter_address_field),
|
||||
onValueChange = {},
|
||||
onPasteClick = {},
|
||||
)
|
||||
SpacerH8()
|
||||
TextFieldWithPasteAndIcon(
|
||||
value = "",
|
||||
label = TextReference.Res(R.string.send_extras_hint_memo),
|
||||
placeholder = TextReference.Res(R.string.send_optional_field),
|
||||
onValueChange = {},
|
||||
onPasteClick = {},
|
||||
)
|
||||
SpacerH8()
|
||||
TextFieldWithInfo(
|
||||
value = "Text",
|
||||
label = stringResource(R.string.send_extras_hint_memo),
|
||||
info = TextReference.Res(R.string.send_optional_field),
|
||||
footer = stringResource(R.string.send_max_fee),
|
||||
onValueChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TextFieldPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
Column {
|
||||
TextFieldWithPaste(
|
||||
value = "",
|
||||
label = TextReference.Res(R.string.send_recipient),
|
||||
placeholder = TextReference.Res(R.string.send_enter_address_field),
|
||||
onValueChange = {},
|
||||
onPasteClick = {},
|
||||
)
|
||||
SpacerH8()
|
||||
TextFieldWithPasteAndIcon(
|
||||
value = "",
|
||||
label = TextReference.Res(R.string.send_extras_hint_memo),
|
||||
placeholder = TextReference.Res(R.string.send_optional_field),
|
||||
onValueChange = {},
|
||||
onPasteClick = {},
|
||||
)
|
||||
SpacerH8()
|
||||
TextFieldWithInfo(
|
||||
value = "Text",
|
||||
label = stringResource(R.string.send_extras_hint_memo),
|
||||
info = TextReference.Res(R.string.send_optional_field),
|
||||
footer = stringResource(R.string.send_max_fee),
|
||||
onValueChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.send
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
|
||||
import com.tangem.core.ui.components.inputrow.InputRowDefault
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImage
|
||||
import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SendContent(uiState: SendUiState) {
|
||||
val amountState = uiState.amountState ?: return
|
||||
val recipientState = uiState.recipientState ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val sendState = uiState.sendState ?: return
|
||||
|
||||
val isSuccess = sendState.isSuccess.collectAsStateWithLifecycle()
|
||||
val timestamp = sendState.transactionDate.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AnimatedVisibility(visible = isSuccess.value) {
|
||||
TransactionDoneTitle(
|
||||
titleRes = R.string.sent_transaction_sent_title,
|
||||
date = timestamp.value,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = !isSuccess.value) {
|
||||
FromWallet(
|
||||
walletName = amountState.walletName,
|
||||
walletBalance = amountState.walletBalance,
|
||||
)
|
||||
}
|
||||
AmountBlock(
|
||||
amountState = amountState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showAmount,
|
||||
)
|
||||
RecipientBlock(
|
||||
recipientState = recipientState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showRecipient,
|
||||
)
|
||||
FeeBlock(
|
||||
feeState = feeState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showFee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FromWallet(walletName: String, walletBalance: String) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.button.disabled)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResource(R.string.send_from_wallet_android))
|
||||
append(" ")
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(walletName)
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = walletBalance,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State<Boolean>, onClick: () -> Unit) {
|
||||
val amount = amountState.amountTextField.collectAsStateWithLifecycle()
|
||||
|
||||
val cryptoAmount = formatCryptoAmount(
|
||||
cryptoCurrency = amountState.cryptoCurrencyStatus.currency,
|
||||
cryptoAmount = amount.value.value.toBigDecimalOrDefault(),
|
||||
)
|
||||
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amount.value.fiatValue.toBigDecimalOrDefault(),
|
||||
fiatCurrencyCode = amountState.appCurrency.code,
|
||||
fiatCurrencySymbol = amountState.appCurrency.symbol,
|
||||
)
|
||||
InputRowImage(
|
||||
title = TextReference.Res(R.string.send_amount_label),
|
||||
subtitle = TextReference.Str(cryptoAmount),
|
||||
caption = TextReference.Str(fiatAmount),
|
||||
tokenIconState = amountState.tokenIconState,
|
||||
showNetworkIcon = true,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess.value) { onClick() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State<Boolean>, onClick: () -> Unit) {
|
||||
val address = recipientState.addressTextField.collectAsStateWithLifecycle()
|
||||
val memo = recipientState.memoTextField?.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess.value) { onClick() },
|
||||
) {
|
||||
val showMemo = memo != null && memo.value.value.isNotBlank()
|
||||
InputRowRecipientDefault(
|
||||
title = TextReference.Res(R.string.send_recipient),
|
||||
value = address.value.value,
|
||||
showDivider = showMemo,
|
||||
)
|
||||
if (showMemo) {
|
||||
InputRowDefault(
|
||||
title = TextReference.Res(R.string.send_extras_hint_memo),
|
||||
text = TextReference.Str(memo?.value?.value.orEmpty()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State<Boolean>, onClick: () -> Unit) {
|
||||
val feeSelector =
|
||||
feeState.feeSelectorState.collectAsStateWithLifecycle().value as? FeeSelectorState.Content ?: return
|
||||
val customValue = feeSelector.customValues.collectAsStateWithLifecycle().value.getOrNull(0)
|
||||
|
||||
val feeValue = formatCryptoAmount(
|
||||
cryptoCurrency = feeState.cryptoCurrencyStatus.currency,
|
||||
cryptoAmount = when (val selectedFee = feeSelector.fees) {
|
||||
is TransactionFee.Single -> selectedFee.normal.amount.value
|
||||
is TransactionFee.Choosable -> when (feeSelector.selectedFee) {
|
||||
FeeType.SLOW -> selectedFee.minimum.amount.value
|
||||
FeeType.MARKET -> selectedFee.normal.amount.value
|
||||
FeeType.FAST -> selectedFee.priority.amount.value
|
||||
FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault()
|
||||
}
|
||||
},
|
||||
)
|
||||
InputRowDefault(
|
||||
title = TextReference.Res(R.string.send_network_fee_title),
|
||||
text = TextReference.Str(feeValue),
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess.value) { onClick() },
|
||||
)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import java.math.BigInteger
|
|||
|
||||
internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean {
|
||||
if (cryptoCurrency == null) return false
|
||||
if (memo.isEmpty()) return true
|
||||
return when (cryptoCurrency.network.id.value) {
|
||||
Blockchain.XRP.id -> {
|
||||
val tag = memo.toLongOrNull()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
|||
|
||||
interface SendClickIntents {
|
||||
|
||||
fun popBackStack()
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onNextClick()
|
||||
|
|
@ -33,4 +35,16 @@ interface SendClickIntents {
|
|||
|
||||
fun onSubtractSelect(value: Boolean)
|
||||
// endregion
|
||||
|
||||
// region Send
|
||||
fun onSendClick()
|
||||
|
||||
fun showAmount()
|
||||
|
||||
fun showRecipient()
|
||||
|
||||
fun showFee()
|
||||
|
||||
fun onExploreClick(txUrl: String)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ 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.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
|
|
@ -18,8 +19,11 @@ 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.tokens.utils.convertToAmount
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
@ -28,6 +32,7 @@ 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.navigation.InnerSendRouter
|
||||
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
|
||||
|
|
@ -48,7 +53,7 @@ import java.math.BigDecimal
|
|||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -60,6 +65,8 @@ internal class SendViewModel @Inject constructor(
|
|||
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
|
@ -73,7 +80,8 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private var innerRouter: StateRouter by Delegates.notNull()
|
||||
private var inneRrouter: InnerSendRouter by Delegates.notNull()
|
||||
private var stateRouter: StateRouter by Delegates.notNull()
|
||||
|
||||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
|
|
@ -102,9 +110,10 @@ internal class SendViewModel @Inject constructor(
|
|||
getFee()
|
||||
}
|
||||
|
||||
fun setRouter(router: StateRouter) {
|
||||
innerRouter = router
|
||||
uiState = uiState.copy(currentState = router.currentState)
|
||||
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
|
||||
inneRrouter = router
|
||||
this.stateRouter = stateRouter
|
||||
uiState = uiState.copy(currentState = stateRouter.currentState)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
|
||||
|
|
@ -271,9 +280,10 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun onBackClick() = innerRouter.onBackClick()
|
||||
override fun onNextClick() = innerRouter.onNextClick()
|
||||
override fun onPrevClick() = innerRouter.onPrevClick()
|
||||
override fun popBackStack() = stateRouter.popBackStack()
|
||||
override fun onBackClick() = stateRouter.onBackClick()
|
||||
override fun onNextClick() = stateRouter.onNextClick()
|
||||
override fun onPrevClick() = stateRouter.onPrevClick()
|
||||
|
||||
override fun onQrCodeScanClick() {
|
||||
// TODO Add QR code scanning
|
||||
|
|
@ -314,7 +324,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun checkIfXrpAddressValue(value: String): Boolean {
|
||||
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) {
|
||||
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val result = XrpAddressService.decodeXAddress(value)
|
||||
onRecipientAddressValueChange(result?.address.orEmpty())
|
||||
|
|
@ -382,8 +392,108 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
//endregion
|
||||
|
||||
// region send state clicks
|
||||
override fun onSendClick() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
|
||||
if (sendState.isSuccess.value) popBackStack()
|
||||
sendState.isSending.update { true }
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
verifyAndSendTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun verifyAndSendTransaction() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return
|
||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val fee = getFee(feeState) ?: return
|
||||
|
||||
val amountToSend = amount.value.toBigDecimal().convertToAmount(cryptoCurrency)
|
||||
|
||||
// todo add notifications [[REDACTED_JIRA]]
|
||||
// val transactionErrors = walletManagersFacade.validateTransaction(
|
||||
// amount = amountToSend,
|
||||
// fee = fee.amount,
|
||||
// userWalletId = userWalletId,
|
||||
// network = cryptoCurrency.network,
|
||||
// )
|
||||
|
||||
val txData = walletManagersFacade.createTransaction(
|
||||
amount = amountToSend,
|
||||
fee = fee,
|
||||
memo = memo?.value,
|
||||
destination = recipient.value,
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
) ?: return
|
||||
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
sendState.isSending.update { false }
|
||||
// todo add notifications [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = {
|
||||
sendState.transactionDate.update {
|
||||
txData.date?.timeInMillis ?: System.currentTimeMillis()
|
||||
}
|
||||
sendState.isSuccess.update { true }
|
||||
sendState.txUrl.update {
|
||||
getTxUrl(txData.hash.orEmpty())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFee(feeState: FeeSelectorState.Content): Fee? {
|
||||
return when (val selectedFee = feeState.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeState.selectedFee) {
|
||||
FeeType.SLOW -> selectedFee.minimum
|
||||
FeeType.MARKET -> selectedFee.normal
|
||||
FeeType.FAST -> selectedFee.priority
|
||||
FeeType.CUSTOM -> {
|
||||
val feeAmount = feeState.customValues.value.firstOrNull()?.value
|
||||
?.let { BigDecimal(it) } ?: return null
|
||||
Fee.Common(feeAmount.convertToAmount(cryptoCurrency))
|
||||
}
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> selectedFee.normal
|
||||
}
|
||||
}
|
||||
|
||||
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
|
||||
|
||||
override fun showRecipient() = stateRouter.showRecipient(isFromSend = true)
|
||||
|
||||
override fun showFee() = stateRouter.showFee(isFromSend = true)
|
||||
|
||||
override fun onExploreClick(txUrl: String) = inneRrouter.openUrl(txUrl)
|
||||
|
||||
private fun getTxUrl(hash: String): String {
|
||||
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
|
||||
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
|
||||
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
|
||||
EMPTY
|
||||
} else {
|
||||
getExplorerTransactionUrlUseCase(
|
||||
txHash = hash,
|
||||
networkId = cryptoCurrency.network.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val DEFAULT_VALUE = "0.00"
|
||||
private const val EMPTY = ""
|
||||
}
|
||||
}
|
||||
|
|
@ -9,4 +9,6 @@ interface WalletFeatureToggles {
|
|||
|
||||
/** Availability of redesigned screen */
|
||||
val isRedesignedScreenEnabled: Boolean
|
||||
|
||||
val isWalletsScrollingPreviewEnabled: Boolean
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.di
|
|||
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import com.tangem.features.wallet.navigation.WalletRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -15,7 +16,10 @@ internal object WalletRouterModule {
|
|||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter {
|
||||
return DefaultWalletRouter(reduxNavController = reduxNavController)
|
||||
fun provideWalletRouter(
|
||||
reduxNavController: ReduxNavController,
|
||||
walletFeatureToggles: WalletFeatureToggles,
|
||||
): WalletRouter {
|
||||
return DefaultWalletRouter(reduxNavController = reduxNavController, walletFeatureToggles = walletFeatureToggles)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,4 +16,7 @@ internal class DefaultWalletFeatureToggles(
|
|||
|
||||
override val isRedesignedScreenEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_WALLET_SCREEN_ENABLED")
|
||||
|
||||
override val isWalletsScrollingPreviewEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "WALLETS_SCROLLING_PREVIEW_ENABLED")
|
||||
}
|
||||
|
|
@ -26,12 +26,18 @@ import com.tangem.feature.wallet.presentation.WalletFragment
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreenV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModelV2
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/** Default implementation of wallet feature router */
|
||||
internal class DefaultWalletRouter(private val reduxNavController: ReduxNavController) : InnerWalletRouter {
|
||||
internal class DefaultWalletRouter(
|
||||
private val reduxNavController: ReduxNavController,
|
||||
private val walletFeatureToggles: WalletFeatureToggles,
|
||||
) : InnerWalletRouter {
|
||||
|
||||
private var navController: NavHostController by Delegates.notNull()
|
||||
private var onFinish: () -> Unit = {}
|
||||
|
|
@ -47,10 +53,20 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr
|
|||
startDestination = WalletRoute.Wallet.route,
|
||||
) {
|
||||
composable(WalletRoute.Wallet.route) {
|
||||
val viewModel = hiltViewModel<WalletViewModel>().apply { router = this@DefaultWalletRouter }
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) {
|
||||
val viewModel = hiltViewModel<WalletViewModelV2>().apply {
|
||||
setWalletRouter(router = this@DefaultWalletRouter)
|
||||
}
|
||||
|
||||
WalletScreen(state = viewModel.uiState)
|
||||
WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value)
|
||||
} else {
|
||||
val viewModel = hiltViewModel<WalletViewModel>().apply {
|
||||
router = this@DefaultWalletRouter
|
||||
}
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
WalletScreen(state = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
|
||||
composable(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
internal class TokenListAnalyticsSender @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
fun send(maybeTokenList: Either<TokenListError, TokenList>) {
|
||||
val tokenList = (maybeTokenList as? Either.Right)?.value ?: return
|
||||
|
||||
createCardBalanceState(tokenList)?.let {
|
||||
analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState? {
|
||||
return when (val fiatBalance = tokenList.totalFiatBalance) {
|
||||
is TokenList.FiatBalance.Failed -> fiatBalance.toCardBalanceState(tokenList)
|
||||
is TokenList.FiatBalance.Loaded -> fiatBalance.toCardBalanceState()
|
||||
TokenList.FiatBalance.Loading -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenList.FiatBalance.Failed.toCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState {
|
||||
val currenciesStatuses = when (tokenList) {
|
||||
is TokenList.Empty -> emptyList()
|
||||
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
|
||||
is TokenList.Ungrouped -> tokenList.currencies
|
||||
}
|
||||
|
||||
return when {
|
||||
currenciesStatuses.isEmpty() -> AnalyticsParam.CardBalanceState.Empty
|
||||
currenciesStatuses.any { it.value is CryptoCurrencyStatus.NoQuote } -> {
|
||||
AnalyticsParam.CardBalanceState.NoRate
|
||||
}
|
||||
else -> AnalyticsParam.CardBalanceState.BlockchainError
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState? {
|
||||
return if (amount > BigDecimal.ZERO) {
|
||||
AnalyticsParam.CardBalanceState.Full
|
||||
} else if (amount.isZero()) {
|
||||
AnalyticsParam.CardBalanceState.Empty
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
) {
|
||||
|
||||
private var readyForRateAppNotification = false
|
||||
|
||||
fun create(clickIntents: WalletClickIntentsV2): Flow<ImmutableList<WalletNotification>> {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Failed to get selected wallet $it")
|
||||
return flowOf(value = persistentListOf())
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
return combine(
|
||||
flow = getTokenListUseCase(userWallet.walletId).conflate(),
|
||||
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
||||
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
||||
// flow4 = getMissedAddressCryptoCurrenciesUseCase(userWallet.walletId).conflate(),
|
||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup ->
|
||||
// maybeTokenList.onRight { Timber.e(it.toString()) }
|
||||
// maybeMissedAddressCurrencies.onRight { Timber.e(it.toString()) }
|
||||
readyForRateAppNotification = true
|
||||
buildList {
|
||||
addCriticalNotifications(cardTypesResolver)
|
||||
|
||||
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
|
||||
|
||||
addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents)
|
||||
|
||||
addRateTheAppNotification(isReadyToShowRating, clickIntents)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
|
||||
addIf(
|
||||
element = WalletNotification.Critical.DevCard,
|
||||
condition = !cardTypesResolver.isReleaseFirmwareType(),
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Critical.FailedCardValidation,
|
||||
condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(),
|
||||
)
|
||||
|
||||
cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures ->
|
||||
addIf(
|
||||
element = WalletNotification.Warning.LowSignatures(count = remainingSignatures),
|
||||
condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addInformationalNotifications(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
maybeTokenList: Either<TokenListError, TokenList>,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.Informational.DemoCard,
|
||||
condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
|
||||
)
|
||||
|
||||
addMissingAddressesNotification(maybeTokenList, clickIntents)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addMissingAddressesNotification(
|
||||
maybeTokenList: Either<TokenListError, TokenList>,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
) {
|
||||
val currencies = maybeTokenList.getMissingAddressCurrencies()
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Informational.MissingAddresses(
|
||||
missingAddressesCount = currencies.count(),
|
||||
onGenerateClick = {
|
||||
clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies)
|
||||
},
|
||||
),
|
||||
condition = currencies.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Either<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
|
||||
return fold(
|
||||
ifLeft = { emptyList() },
|
||||
ifRight = { tokenList ->
|
||||
val currencies = when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
|
||||
is TokenList.Ungrouped -> tokenList.currencies
|
||||
is TokenList.Empty -> emptyList()
|
||||
}
|
||||
|
||||
currencies
|
||||
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
|
||||
.map(CryptoCurrencyStatus::currency)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addWarningNotifications(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
tokenList: Either<TokenListError, TokenList>,
|
||||
isNeedToBackup: Boolean,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.Warning.MissingBackup(
|
||||
onStartBackupClick = clickIntents::onAddBackupCardClick,
|
||||
),
|
||||
condition = isNeedToBackup,
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Warning.TestNetCard,
|
||||
condition = cardTypesResolver.isTestCard(),
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Warning.SomeNetworksUnreachable,
|
||||
condition = tokenList.hasUnreachableNetworks(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Either<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
|
||||
return fold(
|
||||
ifLeft = { false },
|
||||
ifRight = { tokenList ->
|
||||
val currencies = when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
|
||||
is TokenList.Ungrouped -> tokenList.currencies
|
||||
is TokenList.Empty -> emptyList()
|
||||
}
|
||||
|
||||
currencies.any { it.value is CryptoCurrencyStatus.Unreachable }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addRateTheAppNotification(
|
||||
isReadyToShowRating: Boolean,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.RateApp(
|
||||
onLikeClick = clickIntents::onLikeAppClick,
|
||||
onDislikeClick = clickIntents::onDislikeAppClick,
|
||||
onCloseClick = clickIntents::onCloseRateAppWarningClick,
|
||||
),
|
||||
condition = isReadyToShowRating && readyForRateAppNotification,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
|
||||
if (condition) {
|
||||
add(element = element)
|
||||
if (element is WalletNotification.Critical || element is WalletNotification.Warning) {
|
||||
readyForRateAppNotification = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_REMAINING_SIGNATURES_COUNT = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
internal class GetSingleWalletWarningsFactory @Inject constructor(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
|
||||
) {
|
||||
|
||||
private var readyForRateAppNotification = false
|
||||
|
||||
fun create(clickIntents: WalletClickIntentsV2): Flow<ImmutableList<WalletNotification>> {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Failed to get selected wallet $it")
|
||||
return flowOf(value = persistentListOf())
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
return combine(
|
||||
flow = getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId),
|
||||
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
||||
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
||||
) { primaryCurrencyStatus, isReadyToShowRating, isNeedToBackup ->
|
||||
readyForRateAppNotification = true
|
||||
buildList {
|
||||
addCriticalNotifications(cardTypesResolver)
|
||||
|
||||
addInformationalNotifications(cardTypesResolver)
|
||||
|
||||
addWarningNotifications(
|
||||
userWallet,
|
||||
cardTypesResolver,
|
||||
primaryCurrencyStatus,
|
||||
isNeedToBackup,
|
||||
clickIntents,
|
||||
)
|
||||
|
||||
addRateTheAppNotification(isReadyToShowRating, clickIntents)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
|
||||
addIf(
|
||||
element = WalletNotification.Critical.DevCard,
|
||||
condition = !cardTypesResolver.isReleaseFirmwareType(),
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Critical.FailedCardValidation,
|
||||
condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(),
|
||||
)
|
||||
|
||||
cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures ->
|
||||
addIf(
|
||||
element = WalletNotification.Warning.LowSignatures(count = remainingSignatures),
|
||||
condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addInformationalNotifications(cardTypesResolver: CardTypesResolver) {
|
||||
addIf(
|
||||
element = WalletNotification.Informational.DemoCard,
|
||||
condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MutableList<WalletNotification>.addWarningNotifications(
|
||||
userWallet: UserWallet,
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
maybePrimaryCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>,
|
||||
isNeedToBackup: Boolean,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
) {
|
||||
val cryptoCurrencyStatus = maybePrimaryCurrencyStatus.fold(ifLeft = { null }, ifRight = { it })
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Warning.MissingBackup(
|
||||
onStartBackupClick = clickIntents::onAddBackupCardClick,
|
||||
),
|
||||
condition = isNeedToBackup,
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Warning.TestNetCard,
|
||||
condition = cardTypesResolver.isTestCard(),
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Warning.NetworksUnreachable,
|
||||
condition = cryptoCurrencyStatus?.value is CryptoCurrencyStatus.Unreachable,
|
||||
)
|
||||
|
||||
addNoAccountWarning(cryptoCurrencyStatus)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Warning.NumberOfSignedHashesIncorrect(
|
||||
onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick,
|
||||
),
|
||||
condition = hasSignedHashes(userWallet, cryptoCurrencyStatus),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) {
|
||||
val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount
|
||||
if (noAccountStatus != null) {
|
||||
add(
|
||||
element = WalletNotification.Informational.NoAccount(
|
||||
network = cryptoCurrencyStatus.currency.name,
|
||||
amount = noAccountStatus.amountToCreateAccount.toString(),
|
||||
symbol = cryptoCurrencyStatus.currency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hasSignedHashes(
|
||||
selectedWallet: UserWallet,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
): Boolean {
|
||||
return cryptoCurrencyStatus?.currency?.network?.let {
|
||||
hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.firstOrNull()
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addRateTheAppNotification(
|
||||
isReadyToShowRating: Boolean,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.RateApp(
|
||||
onLikeClick = clickIntents::onLikeAppClick,
|
||||
onDislikeClick = clickIntents::onDislikeAppClick,
|
||||
onCloseClick = clickIntents::onCloseRateAppWarningClick,
|
||||
),
|
||||
condition = isReadyToShowRating && readyForRateAppNotification,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
|
||||
if (condition) {
|
||||
add(element = element)
|
||||
if (element is WalletNotification.Critical || element is WalletNotification.Warning) {
|
||||
readyForRateAppNotification = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_REMAINING_SIGNATURES_COUNT = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? {
|
||||
return this().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Impossible to get selected wallet $it")
|
||||
null
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
}
|
||||
|
||||
internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId: UserWalletId): CryptoCurrencyStatus? {
|
||||
return this(userWalletId)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.filter(Either<CurrencyStatusError, CryptoCurrencyStatus>::isRight)
|
||||
.firstOrNull()
|
||||
?.fold(
|
||||
ifLeft = {
|
||||
Timber.e("Impossible to get primary currency status $it")
|
||||
null
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
}
|
||||
|
||||
internal suspend fun GetSelectedAppCurrencyUseCase.unwrap(): AppCurrency {
|
||||
return this()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.firstOrNull()
|
||||
?: AppCurrency.Default
|
||||
}
|
||||
|
||||
internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.collectLatest(
|
||||
userWalletId: UserWalletId,
|
||||
onRight: suspend (CryptoCurrencyStatus) -> Unit,
|
||||
) {
|
||||
this(userWalletId = userWalletId)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { maybeStatus ->
|
||||
maybeStatus.onRight { onRight(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class WalletWithFundsChecker @Inject constructor(
|
||||
private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase,
|
||||
) {
|
||||
|
||||
suspend fun check(maybeTokenList: Either<TokenListError, TokenList>) {
|
||||
val tokenList = (maybeTokenList as? Either.Right)?.value ?: return
|
||||
|
||||
val hasNonZeroWallets = when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> {
|
||||
tokenList.groups
|
||||
.flatMap(NetworkGroup::currencies)
|
||||
.hasNonZeroWallets()
|
||||
}
|
||||
is TokenList.Ungrouped -> tokenList.currencies.hasNonZeroWallets()
|
||||
is TokenList.Empty -> false
|
||||
}
|
||||
|
||||
if (hasNonZeroWallets) setWalletWithFundsFoundUseCase()
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.hasNonZeroWallets(): Boolean {
|
||||
return any {
|
||||
val amount = it.value.amount ?: return@any false
|
||||
!amount.isZero()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.MultiWalletContentLoaderFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletContentLoaderFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletWithTokenContentLoaderFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.WalletContentLoader
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
internal class WalletContentLoaderFactory @Inject constructor(
|
||||
private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory,
|
||||
private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory,
|
||||
private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
userWallet: UserWallet,
|
||||
appCurrency: AppCurrency,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
isRefresh: Boolean = false,
|
||||
): WalletContentLoader? {
|
||||
return when {
|
||||
userWallet.isMultiCurrency -> {
|
||||
multiWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents)
|
||||
}
|
||||
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> {
|
||||
singleWalletWithTokenContentLoaderFactory.create(userWallet, appCurrency, clickIntents)
|
||||
}
|
||||
!userWallet.isMultiCurrency -> {
|
||||
singleWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents, isRefresh)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class WalletLoaderStorage @Inject constructor() {
|
||||
|
||||
private val loaders = ConcurrentHashMap<UserWalletId, List<Job>>()
|
||||
|
||||
fun contains(id: UserWalletId) = loaders.containsKey(id)
|
||||
|
||||
fun set(id: UserWalletId, jobs: List<Job>) {
|
||||
loaders[id] = jobs
|
||||
}
|
||||
|
||||
fun remove(id: UserWalletId) {
|
||||
loaders[id]?.let {
|
||||
it.forEach(Job::cancel)
|
||||
loaders.remove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Base wallet screen content loader. Use it to load content by [UserWallet].
|
||||
*
|
||||
* @property factory factory that creates loader
|
||||
* @property storage storage that save loader's jobs
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@ViewModelScoped
|
||||
internal class WalletScreenContentLoader @Inject constructor(
|
||||
private val factory: WalletContentLoaderFactory,
|
||||
private val storage: WalletLoaderStorage,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Load content by [UserWallet]
|
||||
*
|
||||
* @param userWallet user wallet
|
||||
* @param appCurrency app currency
|
||||
* @param clickIntents click intents
|
||||
* @param isRefresh flag that determinate if content must load again
|
||||
* @param coroutineScope coroutine scope
|
||||
*/
|
||||
fun load(
|
||||
userWallet: UserWallet,
|
||||
appCurrency: AppCurrency,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
isRefresh: Boolean = false,
|
||||
coroutineScope: CoroutineScope,
|
||||
) {
|
||||
if (userWallet.isLocked) return
|
||||
|
||||
val id = userWallet.walletId
|
||||
if (!storage.contains(id)) {
|
||||
loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, isRefresh)
|
||||
} else {
|
||||
if (isRefresh) {
|
||||
storage.remove(id)
|
||||
loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, true)
|
||||
} else {
|
||||
Timber.d("$id content loading has already started")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancel loading by [id] */
|
||||
fun cancel(id: UserWalletId) {
|
||||
Timber.d("$id content loading is canceled")
|
||||
storage.remove(id)
|
||||
}
|
||||
|
||||
private fun loadInternal(
|
||||
userWallet: UserWallet,
|
||||
appCurrency: AppCurrency,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
coroutineScope: CoroutineScope,
|
||||
isRefresh: Boolean,
|
||||
) {
|
||||
val loader = factory.create(
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
clickIntents = clickIntents,
|
||||
isRefresh = isRefresh,
|
||||
)
|
||||
|
||||
if (loader == null) {
|
||||
Timber.e("Impossible to create loader for $userWallet")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started")
|
||||
|
||||
loader.subscribers
|
||||
.map { it.subscribe(coroutineScope, dispatchers) }
|
||||
.let { storage.set(userWallet.walletId, it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.TokenListSubscriber
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class MultiWalletContentLoader(
|
||||
private val userWallet: UserWallet,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
private val stateHolder: WalletStateHolderV2,
|
||||
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
||||
private val walletWithFundsChecker: WalletWithFundsChecker,
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
) : WalletContentLoader(id = userWallet.walletId) {
|
||||
|
||||
override fun create(): List<WalletSubscriber<*>> {
|
||||
return listOf(
|
||||
TokenListSubscriber(
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
tokenListAnalyticsSender = tokenListAnalyticsSender,
|
||||
walletWithFundsChecker = walletWithFundsChecker,
|
||||
getTokenListUseCase = getTokenListUseCase,
|
||||
),
|
||||
MultiWalletWarningsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
internal class MultiWalletContentLoaderFactory @Inject constructor(
|
||||
private val stateHolder: WalletStateHolderV2,
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
||||
private val walletWithFundsChecker: WalletWithFundsChecker,
|
||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
userWallet: UserWallet,
|
||||
appCurrency: AppCurrency,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
): WalletContentLoader {
|
||||
return MultiWalletContentLoader(
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
clickIntents = clickIntents,
|
||||
stateHolder = stateHolder,
|
||||
tokenListAnalyticsSender = tokenListAnalyticsSender,
|
||||
walletWithFundsChecker = walletWithFundsChecker,
|
||||
getTokenListUseCase = getTokenListUseCase,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SingleWalletContentLoader(
|
||||
private val userWallet: UserWallet,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
private val isRefresh: Boolean,
|
||||
private val stateHolder: WalletStateHolderV2,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
|
||||
private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory,
|
||||
private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase,
|
||||
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : WalletContentLoader(id = userWallet.walletId) {
|
||||
|
||||
override fun create(): List<WalletSubscriber<*>> {
|
||||
return listOf(
|
||||
PrimaryCurrencySubscriber(
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
stateHolder = stateHolder,
|
||||
getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase,
|
||||
setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
),
|
||||
SingleWalletButtonsSubscriber(
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase,
|
||||
getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase,
|
||||
),
|
||||
SingleWalletNotificationsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getSingleWalletWarningsFactory = getSingleWalletWarningsFactory,
|
||||
),
|
||||
TxHistorySubscriber(
|
||||
userWallet = userWallet,
|
||||
isRefresh = isRefresh,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase,
|
||||
txHistoryItemsCountUseCase = txHistoryItemsCountUseCase,
|
||||
txHistoryItemsUseCase = txHistoryItemsUseCase,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class SingleWalletContentLoaderFactory @Inject constructor(
|
||||
private val stateHolder: WalletStateHolderV2,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
|
||||
private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory,
|
||||
private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase,
|
||||
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
userWallet: UserWallet,
|
||||
appCurrency: AppCurrency,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
isRefresh: Boolean,
|
||||
): WalletContentLoader {
|
||||
return SingleWalletContentLoader(
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
clickIntents = clickIntents,
|
||||
isRefresh = isRefresh,
|
||||
stateHolder = stateHolder,
|
||||
getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase,
|
||||
getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase,
|
||||
getSingleWalletWarningsFactory = getSingleWalletWarningsFactory,
|
||||
setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase,
|
||||
txHistoryItemsCountUseCase = txHistoryItemsCountUseCase,
|
||||
txHistoryItemsUseCase = txHistoryItemsUseCase,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCardTokensListUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SingleWalletWithTokenContentLoader(
|
||||
private val userWallet: UserWallet,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
private val stateHolder: WalletStateHolderV2,
|
||||
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
||||
private val walletWithFundsChecker: WalletWithFundsChecker,
|
||||
private val getCardTokensListUseCase: GetCardTokensListUseCase,
|
||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
) : WalletContentLoader(id = userWallet.walletId) {
|
||||
|
||||
override fun create(): List<WalletSubscriber<*>> {
|
||||
return listOf(
|
||||
SingleWalletWithTokenListSubscriber(
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
tokenListAnalyticsSender = tokenListAnalyticsSender,
|
||||
walletWithFundsChecker = walletWithFundsChecker,
|
||||
getCardTokensListUseCase = getCardTokensListUseCase,
|
||||
),
|
||||
MultiWalletWarningsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCardTokensListUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
|
||||
private val stateHolder: WalletStateHolderV2,
|
||||
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
||||
private val walletWithFundsChecker: WalletWithFundsChecker,
|
||||
private val getCardTokensListUseCase: GetCardTokensListUseCase,
|
||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
userWallet: UserWallet,
|
||||
appCurrency: AppCurrency,
|
||||
clickIntents: WalletClickIntentsV2,
|
||||
): SingleWalletWithTokenContentLoader {
|
||||
return SingleWalletWithTokenContentLoader(
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
clickIntents = clickIntents,
|
||||
stateHolder = stateHolder,
|
||||
tokenListAnalyticsSender = tokenListAnalyticsSender,
|
||||
walletWithFundsChecker = walletWithFundsChecker,
|
||||
getCardTokensListUseCase = getCardTokensListUseCase,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber
|
||||
|
||||
/**
|
||||
* Wallet content loader
|
||||
*
|
||||
* @property id loader id
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal abstract class WalletContentLoader(val id: UserWalletId) {
|
||||
|
||||
/** Loader's subscribers */
|
||||
val subscribers: List<WalletSubscriber<*>> get() = create()
|
||||
|
||||
protected abstract fun create(): List<WalletSubscriber<*>>
|
||||
}
|
||||
|
|
@ -17,4 +17,16 @@ internal sealed class WalletEvent {
|
|||
data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent()
|
||||
|
||||
data class RateApp(val onDismissClick: () -> Unit) : WalletEvent()
|
||||
|
||||
data class DemonstrateWalletsScrollPreview(val direction: Direction) : WalletEvent() {
|
||||
|
||||
enum class Direction {
|
||||
|
||||
/** 1 -> 2 */
|
||||
LEFT,
|
||||
|
||||
/** 1 <- 2 */
|
||||
RIGHT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.annotation.concurrent.Immutable
|
||||
|
||||
const val NOT_INITIALIZED_WALLET_INDEX = -1
|
||||
|
||||
internal data class WalletScreenState(
|
||||
val onBackClick: () -> Unit,
|
||||
val topBarConfig: WalletTopBarConfig,
|
||||
val selectedWalletIndex: Int,
|
||||
val wallets: ImmutableList<WalletState>,
|
||||
val onWalletChange: (Int) -> Unit,
|
||||
val event: StateEvent<WalletEvent>,
|
||||
val isHidingMode: Boolean,
|
||||
)
|
||||
|
||||
internal sealed class WalletState {
|
||||
|
||||
abstract val pullToRefreshConfig: WalletPullToRefreshConfig
|
||||
abstract val walletCardState: WalletCardState
|
||||
abstract val warnings: ImmutableList<WalletNotification>
|
||||
abstract val bottomSheetConfig: TangemBottomSheetConfig?
|
||||
|
||||
sealed class MultiCurrency : WalletState() {
|
||||
|
||||
abstract val tokensListState: WalletTokensListState
|
||||
abstract val manageTokensButtonConfig: ManageTokensButtonConfig?
|
||||
|
||||
data class Content(
|
||||
override val pullToRefreshConfig: WalletPullToRefreshConfig,
|
||||
override val walletCardState: WalletCardState,
|
||||
override val warnings: ImmutableList<WalletNotification>,
|
||||
override val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
override val tokensListState: WalletTokensListState,
|
||||
override val manageTokensButtonConfig: ManageTokensButtonConfig?,
|
||||
) : MultiCurrency()
|
||||
|
||||
data class Locked(
|
||||
override val walletCardState: WalletCardState,
|
||||
val onUnlockNotificationClick: () -> Unit,
|
||||
val isBottomSheetShow: Boolean = false,
|
||||
val onBottomSheetDismiss: () -> Unit = {},
|
||||
val onUnlockClick: () -> Unit,
|
||||
val onScanClick: () -> Unit,
|
||||
) : MultiCurrency() {
|
||||
|
||||
override val pullToRefreshConfig: WalletPullToRefreshConfig
|
||||
get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {})
|
||||
|
||||
override val warnings: ImmutableList<WalletNotification> = persistentListOf(
|
||||
WalletNotification.UnlockWallets(onUnlockNotificationClick),
|
||||
)
|
||||
|
||||
override val bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = isBottomSheetShow,
|
||||
onDismissRequest = onBottomSheetDismiss,
|
||||
content = WalletBottomSheetConfig.UnlockWallets(
|
||||
onUnlockClick = onUnlockClick,
|
||||
onScanClick = onScanClick,
|
||||
),
|
||||
)
|
||||
|
||||
override val tokensListState = WalletTokensListState.ContentState.Locked
|
||||
override val manageTokensButtonConfig = null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SingleCurrency : WalletState() {
|
||||
|
||||
abstract val buttons: PersistentList<WalletManageButton>
|
||||
abstract val marketPriceBlockState: MarketPriceBlockState?
|
||||
abstract val txHistoryState: TxHistoryState
|
||||
|
||||
data class Content(
|
||||
override val pullToRefreshConfig: WalletPullToRefreshConfig,
|
||||
override val walletCardState: WalletCardState,
|
||||
override val warnings: ImmutableList<WalletNotification>,
|
||||
override val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
override val buttons: PersistentList<WalletManageButton>,
|
||||
override val marketPriceBlockState: MarketPriceBlockState,
|
||||
override val txHistoryState: TxHistoryState,
|
||||
) : SingleCurrency()
|
||||
|
||||
data class Locked(
|
||||
override val walletCardState: WalletCardState,
|
||||
override val buttons: PersistentList<WalletManageButton>,
|
||||
val onUnlockNotificationClick: () -> Unit,
|
||||
val isBottomSheetShow: Boolean = false,
|
||||
val onBottomSheetDismiss: () -> Unit = {},
|
||||
val onUnlockClick: () -> Unit,
|
||||
val onScanClick: () -> Unit,
|
||||
val onExploreClick: () -> Unit,
|
||||
) : SingleCurrency() {
|
||||
|
||||
override val pullToRefreshConfig: WalletPullToRefreshConfig
|
||||
get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {})
|
||||
|
||||
override val warnings: ImmutableList<WalletNotification> = persistentListOf(
|
||||
WalletNotification.UnlockWallets(onUnlockNotificationClick),
|
||||
)
|
||||
|
||||
override val bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = isBottomSheetShow,
|
||||
onDismissRequest = onBottomSheetDismiss,
|
||||
content = WalletBottomSheetConfig.UnlockWallets(
|
||||
onUnlockClick = onUnlockClick,
|
||||
onScanClick = onScanClick,
|
||||
),
|
||||
)
|
||||
|
||||
override val marketPriceBlockState: MarketPriceBlockState? = null
|
||||
|
||||
override val txHistoryState: TxHistoryState = TxHistoryState.Content(
|
||||
contentItems = MutableStateFlow(
|
||||
value = PagingData.from(
|
||||
data = listOf(
|
||||
TxHistoryState.TxHistoryItemState.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryState.TxHistoryItemState.Transaction(
|
||||
state = TransactionState.Locked(txHash = "LOCKED_TX_HASH"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WalletTokensListState {
|
||||
|
||||
object Empty : WalletTokensListState()
|
||||
|
||||
sealed class ContentState : WalletTokensListState() {
|
||||
|
||||
abstract val items: ImmutableList<TokensListItemState>
|
||||
abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig?
|
||||
|
||||
object Loading : ContentState() {
|
||||
override val items = persistentListOf<TokensListItemState>()
|
||||
override val organizeTokensButtonConfig = null
|
||||
}
|
||||
|
||||
data class Content(
|
||||
override val items: ImmutableList<TokensListItemState>,
|
||||
override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?,
|
||||
) : ContentState()
|
||||
|
||||
object Locked : ContentState() {
|
||||
override val items = persistentListOf(
|
||||
TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)),
|
||||
TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")),
|
||||
)
|
||||
override val organizeTokensButtonConfig = null
|
||||
}
|
||||
}
|
||||
|
||||
data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit)
|
||||
|
||||
@Immutable
|
||||
sealed class TokensListItemState {
|
||||
|
||||
abstract val id: Any
|
||||
|
||||
data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState()
|
||||
|
||||
data class Token(val state: TokenItemState) : TokensListItemState() {
|
||||
override val id: String = state.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ManageTokensButtonConfig(val onClick: () -> Unit)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2
|
||||
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.WalletScreenStateTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Wallet state holder
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
internal class WalletStateHolderV2 @Inject constructor() {
|
||||
|
||||
val uiState: StateFlow<WalletScreenState> get() = mutableUiState
|
||||
val value: WalletScreenState get() = uiState.value
|
||||
|
||||
private val mutableUiState: MutableStateFlow<WalletScreenState> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
fun update(function: (WalletScreenState) -> WalletScreenState) {
|
||||
mutableUiState.update(function = function)
|
||||
}
|
||||
|
||||
fun update(transformer: WalletScreenStateTransformer) {
|
||||
mutableUiState.update(function = transformer::transform)
|
||||
}
|
||||
|
||||
fun getSelectedWallet(): WalletState {
|
||||
return with(value) { wallets[selectedWalletIndex] }
|
||||
}
|
||||
|
||||
fun getSelectedWalletId(): UserWalletId {
|
||||
return with(value) { wallets[selectedWalletIndex].walletCardState.id }
|
||||
}
|
||||
|
||||
private fun getInitialState(): WalletScreenState {
|
||||
return WalletScreenState(
|
||||
onBackClick = {},
|
||||
topBarConfig = WalletTopBarConfig(onDetailsClick = {}),
|
||||
selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX,
|
||||
wallets = persistentListOf(),
|
||||
onWalletChange = {},
|
||||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class AddWalletTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
private val walletLoadingStateFactory by lazy {
|
||||
WalletLoadingStateFactory(clickIntents = clickIntents)
|
||||
}
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
|
||||
internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false))
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked -> prevState.copy(isBottomSheetShow = false)
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false))
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked -> prevState.copy(isBottomSheetShow = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DeleteWalletTransformer(
|
||||
private val selectedWalletIndex: Int,
|
||||
private val deletedWalletId: UserWalletId,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
val deletedWalletState = prevState.getDeletedWalletState()
|
||||
|
||||
if (deletedWalletState == null) {
|
||||
Timber.e("Wallets does not contain deleted wallet")
|
||||
return prevState
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
wallets = (prevState.wallets - deletedWalletState).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletScreenState.getDeletedWalletState(): WalletState? {
|
||||
return wallets.firstOrNull { it.walletCardState.id == deletedWalletId }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.utils.createStateByWalletType
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class InitializeWalletsTransformer(
|
||||
private val selectedWalletIndex: Int,
|
||||
private val selectedWallet: UserWallet,
|
||||
private val wallets: List<UserWallet>,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) }
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
topBarConfig = createTopBarConfig(userWallet = selectedWallet),
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
wallets = wallets
|
||||
.map { userWallet ->
|
||||
if (userWallet.isLocked) {
|
||||
createLockedState(userWallet)
|
||||
} else {
|
||||
walletLoadingStateFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
.toImmutableList(),
|
||||
onWalletChange = clickIntents::onWalletChange,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTopBarConfig(userWallet: UserWallet): WalletTopBarConfig {
|
||||
return WalletTopBarConfig(
|
||||
onDetailsClick = if (userWallet.isLocked) {
|
||||
clickIntents::onOpenUnlockWalletsBottomSheetClick
|
||||
} else {
|
||||
clickIntents::onDetailsClick
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createLockedState(userWallet: UserWallet): WalletState {
|
||||
return userWallet.createStateByWalletType(
|
||||
multiCurrencyCreator = {
|
||||
WalletState.MultiCurrency.Locked(
|
||||
walletCardState = userWallet.toLockedWalletCardState(),
|
||||
onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick,
|
||||
onUnlockClick = clickIntents::onUnlockWalletClick,
|
||||
onScanClick = clickIntents::onScanToUnlockWalletClick,
|
||||
)
|
||||
},
|
||||
singleCurrencyCreator = {
|
||||
WalletState.SingleCurrency.Locked(
|
||||
walletCardState = userWallet.toLockedWalletCardState(),
|
||||
buttons = createDisabledButtons(),
|
||||
onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick,
|
||||
onUnlockClick = clickIntents::onUnlockWalletClick,
|
||||
onScanClick = clickIntents::onScanToUnlockWalletClick,
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.toLockedWalletCardState(): WalletCardState {
|
||||
return WalletCardState.LockedContent(
|
||||
id = walletId,
|
||||
title = name,
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = this),
|
||||
imageResId = WalletImageResolver.resolve(userWallet = this),
|
||||
onRenameClick = clickIntents::onRenameClick,
|
||||
onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDisabledButtons(): PersistentList<WalletManageButton> {
|
||||
return persistentListOf(
|
||||
WalletManageButton.Buy(enabled = false, onClick = {}),
|
||||
WalletManageButton.Send(enabled = false, onClick = {}),
|
||||
WalletManageButton.Receive(enabled = false, onClick = {}),
|
||||
WalletManageButton.Sell(enabled = false, onClick = {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
|
||||
internal class OpenBottomSheetTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val content: TangemBottomSheetConfigContent,
|
||||
private val onDismissBottomSheet: () -> Unit,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = onDismissBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked -> {
|
||||
prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet)
|
||||
}
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = onDismissBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked -> {
|
||||
prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ReinitializeWalletTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) }
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
wallets = persistentListOf(
|
||||
walletLoadingStateFactory.create(userWallet),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import timber.log.Timber
|
||||
|
||||
internal class RenameWalletTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val newName: String,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName))
|
||||
}
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName))
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked,
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to rename wallet in locked state")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
|
||||
internal class ScrollToWalletTransformer(
|
||||
private val index: Int,
|
||||
private val currentStateProvider: Provider<WalletScreenState>,
|
||||
private val stateUpdater: (WalletScreenState) -> Unit,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
event = triggeredEvent(
|
||||
data = WalletEvent.ChangeWallet(index),
|
||||
onConsume = {
|
||||
stateUpdater(
|
||||
currentStateProvider().copy(
|
||||
selectedWalletIndex = index,
|
||||
event = consumedEvent(),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
|
||||
internal class SendEventTransformer(
|
||||
private val event: WalletEvent,
|
||||
private val onConsume: () -> Unit,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
event = triggeredEvent(data = event, onConsume = onConsume),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetCryptoCurrencyActionsTransformer(
|
||||
private val tokenActionsState: TokenActionsState,
|
||||
private val userWallet: UserWallet,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletStateTransformer(userWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(buttons = tokenActionsState.toManageButtons())
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load primary currency status for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.MultiCurrency,
|
||||
-> {
|
||||
Timber.e("Impossible to load crypto currency actions for multi-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenActionsState.toManageButtons(): PersistentList<WalletManageButton> {
|
||||
return states
|
||||
.filterIfS2C()
|
||||
.mapNotNull { action ->
|
||||
when (action) {
|
||||
is TokenActionsState.ActionState.Buy -> {
|
||||
WalletManageButton.Buy(
|
||||
enabled = action.enabled,
|
||||
onClick = { clickIntents.onBuyClick(cryptoCurrencyStatus) },
|
||||
)
|
||||
}
|
||||
is TokenActionsState.ActionState.Receive -> {
|
||||
WalletManageButton.Receive(
|
||||
enabled = action.enabled,
|
||||
onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus) },
|
||||
)
|
||||
}
|
||||
is TokenActionsState.ActionState.Sell -> {
|
||||
WalletManageButton.Sell(
|
||||
enabled = action.enabled,
|
||||
onClick = { clickIntents.onSellClick(cryptoCurrencyStatus) },
|
||||
)
|
||||
}
|
||||
is TokenActionsState.ActionState.Send -> {
|
||||
WalletManageButton.Send(
|
||||
enabled = action.enabled,
|
||||
onClick = { clickIntents.onSendClick(cryptoCurrencyStatus) },
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
private fun List<TokenActionsState.ActionState>.filterIfS2C(): List<TokenActionsState.ActionState> {
|
||||
return if (userWallet.scanResponse.cardTypesResolver.isStart2Coin()) {
|
||||
filterNot { it is TokenActionsState.ActionState.Buy || it is TokenActionsState.ActionState.Sell }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletCardStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletMarketPriceConverter
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetPrimaryCurrencyTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
private val status: CryptoCurrencyStatus.Status,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : WalletStateTransformer(userWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(
|
||||
walletCardState = prevState.walletCardState.toLoadedState(),
|
||||
marketPriceBlockState = prevState.marketPriceBlockState.toLoadedState(),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load primary currency status for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.MultiCurrency,
|
||||
-> {
|
||||
Timber.e("Impossible to load primary currency status for multi-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletCardState.toLoadedState(): WalletCardState {
|
||||
return SingleWalletCardStateConverter(status, userWallet, appCurrency).convert(value = this)
|
||||
}
|
||||
|
||||
private fun MarketPriceBlockState.toLoadedState(): MarketPriceBlockState {
|
||||
return SingleWalletMarketPriceConverter(status, appCurrency).convert(value = this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
|
||||
internal class SetRefreshStateTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val isRefreshing: Boolean,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(
|
||||
pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing),
|
||||
tokensListState = prevState.tokensListState.toUpdatedState(),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(
|
||||
pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing),
|
||||
buttons = prevState.buttons.toUpdatedState(),
|
||||
)
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked,
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> prevState
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletPullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): WalletPullToRefreshConfig {
|
||||
return copy(isRefreshing = isRefreshing)
|
||||
}
|
||||
|
||||
private fun WalletTokensListState.toUpdatedState(): WalletTokensListState {
|
||||
return if (this is WalletTokensListState.ContentState.Content && organizeTokensButtonConfig != null) {
|
||||
copy(
|
||||
organizeTokensButtonConfig = organizeTokensButtonConfig.copy(
|
||||
isEnabled = !isRefreshing,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun PersistentList<WalletManageButton>.toUpdatedState(): PersistentList<WalletManageButton> {
|
||||
val isButtonsEnabled = !isRefreshing
|
||||
|
||||
return mutate {
|
||||
it.mapNotNull { button ->
|
||||
when (button) {
|
||||
is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled)
|
||||
is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled)
|
||||
is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled)
|
||||
is WalletManageButton.Receive -> button
|
||||
is WalletManageButton.Swap -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetTokenListErrorTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val error: TokenListError,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (error) {
|
||||
is TokenListError.EmptyTokens -> {
|
||||
when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(tokensListState = WalletTokensListState.Empty)
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load tokens list for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.SingleCurrency,
|
||||
-> {
|
||||
Timber.e("Impossible to load tokens list for single-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
is TokenListError.DataError,
|
||||
is TokenListError.UnableToSortTokenList,
|
||||
-> prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.ManageTokensButtonConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCardStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TokenListStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetTokenListTransformer(
|
||||
private val tokenList: TokenList,
|
||||
private val userWallet: UserWallet,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletStateTransformer(userWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(
|
||||
walletCardState = prevState.walletCardState.toLoadedState(),
|
||||
tokensListState = prevState.tokensListState.toLoadedState(),
|
||||
manageTokensButtonConfig = createManageTokensButtonConfig(),
|
||||
)
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load tokens list for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.SingleCurrency,
|
||||
-> {
|
||||
Timber.e("Impossible to load tokens list for single-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletCardState.toLoadedState(): WalletCardState {
|
||||
return MultiWalletCardStateConverter(
|
||||
fiatBalance = tokenList.totalFiatBalance,
|
||||
selectedWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
).convert(value = this)
|
||||
}
|
||||
|
||||
private fun WalletTokensListState.toLoadedState(): WalletTokensListState {
|
||||
return TokenListStateConverter(
|
||||
tokenList = tokenList,
|
||||
selectedWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
clickIntents = clickIntents,
|
||||
).convert(value = this)
|
||||
}
|
||||
|
||||
private fun createManageTokensButtonConfig(): ManageTokensButtonConfig? {
|
||||
return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
|
||||
null
|
||||
} else {
|
||||
ManageTokensButtonConfig(clickIntents::onManageTokensClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetTxHistoryCountErrorTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
private val error: TxHistoryStateError,
|
||||
private val pendingTransactions: Set<TxHistoryItem>,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletStateTransformer(userWallet.walletId) {
|
||||
|
||||
private val txHistoryItemConverter by lazy {
|
||||
val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
|
||||
TxHistoryItemStateConverter(
|
||||
symbol = blockchain.currency,
|
||||
decimals = blockchain.decimals(),
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.SingleCurrency.Content -> prevState.toErrorState()
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load transactions history for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.MultiCurrency,
|
||||
-> {
|
||||
Timber.e("Impossible to load transactions history for multi-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletState.SingleCurrency.Content.toErrorState(): WalletState {
|
||||
return copy(
|
||||
txHistoryState = when (error) {
|
||||
is TxHistoryStateError.EmptyTxHistories -> {
|
||||
TxHistoryState.Empty(onExploreClick = clickIntents::onExploreClick)
|
||||
}
|
||||
is TxHistoryStateError.DataError -> {
|
||||
TxHistoryState.Error(
|
||||
onReloadClick = clickIntents::onReloadClick,
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
)
|
||||
}
|
||||
is TxHistoryStateError.TxHistoryNotImplemented -> {
|
||||
TxHistoryState.NotSupported(
|
||||
pendingTransactions = txHistoryItemConverter.convertList(pendingTransactions)
|
||||
.toImmutableList(),
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetTxHistoryCountTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val transactionsCount: Int,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.SingleCurrency.Content -> prevState.toLoadingState()
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load transactions history for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.MultiCurrency,
|
||||
-> {
|
||||
Timber.e("Impossible to load transactions history for multi-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletState.SingleCurrency.Content.toLoadingState(): WalletState {
|
||||
return if (txHistoryState is TxHistoryState.Content) {
|
||||
(txHistoryState as? TxHistoryState.Content)?.contentItems?.update {
|
||||
Timber.d("Load transactions history: $transactionsCount")
|
||||
PagingData.from(data = createLoadingItems())
|
||||
}
|
||||
this
|
||||
} else {
|
||||
val txHistoryContent = TxHistoryState.Content(
|
||||
contentItems = MutableStateFlow(
|
||||
value = PagingData.from(data = createLoadingItems()),
|
||||
),
|
||||
)
|
||||
copy(txHistoryState = txHistoryContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLoadingItems(): List<TxHistoryState.TxHistoryItemState> {
|
||||
return buildList {
|
||||
add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick))
|
||||
(1..transactionsCount).forEach {
|
||||
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.domain.txhistory.models.TxHistoryListError
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetTxHistoryItemsErrorTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val error: TxHistoryListError,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(
|
||||
txHistoryState = when (error) {
|
||||
is TxHistoryListError.DataError -> {
|
||||
TxHistoryState.Error(
|
||||
onReloadClick = clickIntents::onReloadClick,
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load transactions history for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.MultiCurrency -> {
|
||||
Timber.e("Impossible to load transactions history for multi-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemFlowConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetTxHistoryItemsTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
private val flow: Flow<PagingData<TxHistoryItem>>,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletStateTransformer(userWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
val converter = TxHistoryItemFlowConverter(
|
||||
userWallet = userWallet,
|
||||
currentState = prevState,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
prevState.copy(
|
||||
txHistoryState = converter.convert(value = flow),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to load transactions history for locked wallet")
|
||||
prevState
|
||||
}
|
||||
is WalletState.MultiCurrency -> {
|
||||
Timber.e("Impossible to load transactions history for multi-currency wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetWarningsTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val warnings: ImmutableList<WalletNotification>,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> prevState.copy(warnings = warnings)
|
||||
is WalletState.SingleCurrency.Content -> prevState.copy(warnings = warnings)
|
||||
is WalletState.MultiCurrency.Locked,
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to update notifications for locked wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import timber.log.Timber
|
||||
|
||||
internal class UnlockWalletTransformer(
|
||||
private val unlockedWallets: List<UserWallet>,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) }
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
topBarConfig = prevState.topBarConfig.toUnlockedState(),
|
||||
wallets = prevState.wallets
|
||||
.map { state ->
|
||||
val unlockedWallet = getUnlockedWallet(state.walletCardState.id)
|
||||
if (unlockedWallet == null) state else createLoadingState(state, unlockedWallet)
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletTopBarConfig.toUnlockedState(): WalletTopBarConfig {
|
||||
return copy(onDetailsClick = clickIntents::onDetailsClick)
|
||||
}
|
||||
|
||||
private fun getUnlockedWallet(walletId: UserWalletId): UserWallet? {
|
||||
return unlockedWallets.firstOrNull { it.walletId == walletId }
|
||||
}
|
||||
|
||||
private fun createLoadingState(prevState: WalletState, unlockedWallet: UserWallet): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Locked,
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> walletLoadingStateFactory.create(userWallet = unlockedWallet)
|
||||
is WalletState.MultiCurrency.Content,
|
||||
is WalletState.SingleCurrency.Content,
|
||||
-> {
|
||||
Timber.e("Impossible to unlock wallet with content state")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
|
||||
internal class UpdateBalanceHidingModeTransformer(
|
||||
private val isHidingMode: Boolean,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(isHidingMode = isHidingMode)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import timber.log.Timber
|
||||
|
||||
internal class UpdateWalletCardsCountTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
) : WalletStateTransformer(userWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState())
|
||||
}
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState())
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked,
|
||||
is WalletState.SingleCurrency.Locked,
|
||||
-> {
|
||||
Timber.e("Impossible to update wallet cards count for locked wallet")
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletCardState.toUpdatedState(): WalletCardState {
|
||||
return when (this) {
|
||||
is WalletCardState.Content -> copy(
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet),
|
||||
imageResId = WalletImageResolver.resolve(userWallet = userWallet),
|
||||
cardCount = userWallet.getCardsCount(),
|
||||
)
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
|
||||
internal interface WalletScreenStateTransformer {
|
||||
|
||||
fun transform(prevState: WalletScreenState): WalletScreenState
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal abstract class WalletStateTransformer(
|
||||
protected val userWalletId: UserWalletId,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
abstract fun transform(prevState: WalletState): WalletState
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
wallets = prevState.wallets
|
||||
.map { state ->
|
||||
if (state.walletCardState.id == userWalletId) transform(state) else state
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class MultiWalletCardStateConverter(
|
||||
private val fiatBalance: TokenList.FiatBalance,
|
||||
private val selectedWallet: UserWallet,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<WalletCardState, WalletCardState> {
|
||||
|
||||
override fun convert(value: WalletCardState): WalletCardState {
|
||||
return when (fiatBalance) {
|
||||
is TokenList.FiatBalance.Loading -> value.toLoadingState()
|
||||
is TokenList.FiatBalance.Failed -> value.toErrorState()
|
||||
is TokenList.FiatBalance.Loaded -> value.toWalletCardState(fiatBalance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletCardState.toLoadingState(): WalletCardState {
|
||||
return WalletCardState.Loading(
|
||||
id = id,
|
||||
title = title,
|
||||
additionalInfo = additionalInfo,
|
||||
imageResId = imageResId,
|
||||
onRenameClick = onRenameClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletCardState.toErrorState(): WalletCardState {
|
||||
return WalletCardState.Error(
|
||||
id = id,
|
||||
title = title,
|
||||
additionalInfo = additionalInfo,
|
||||
imageResId = imageResId,
|
||||
onDeleteClick = onDeleteClick,
|
||||
onRenameClick = onRenameClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletCardState.toWalletCardState(fiatBalance: TokenList.FiatBalance.Loaded): WalletCardState {
|
||||
return WalletCardState.Content(
|
||||
id = id,
|
||||
title = title,
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet),
|
||||
imageResId = imageResId,
|
||||
onRenameClick = onRenameClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
balance = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatBalance.amount,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
cardCount = selectedWallet.getCardsCount(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletCurrencyActionsClickIntentsImplementor
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class MultiWalletCurrencyActionsConverter(
|
||||
private val userWallet: UserWallet,
|
||||
private val clickIntents: WalletCurrencyActionsClickIntentsImplementor,
|
||||
) : Converter<TokenActionsState, ImmutableList<TokenActionButtonConfig>> {
|
||||
|
||||
override fun convert(value: TokenActionsState): ImmutableList<TokenActionButtonConfig> {
|
||||
return value.states
|
||||
.filterIfSingleWithToken()
|
||||
.mapNotNull {
|
||||
mapTokenActionState(actionsState = it, cryptoCurrencyStatus = value.cryptoCurrencyStatus)
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun List<TokenActionsState.ActionState>.filterIfSingleWithToken(): List<TokenActionsState.ActionState> {
|
||||
return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
|
||||
filter { it !is TokenActionsState.ActionState.HideToken }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapTokenActionState(
|
||||
actionsState: TokenActionsState.ActionState,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): TokenActionButtonConfig? {
|
||||
if (actionsState is TokenActionsState.ActionState.Send && cryptoCurrencyStatus.value.amount.isNullOrZero()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val title: TextReference
|
||||
val icon: Int
|
||||
val action: () -> Unit
|
||||
when (actionsState) {
|
||||
is TokenActionsState.ActionState.Buy -> {
|
||||
title = resourceReference(R.string.common_buy)
|
||||
icon = R.drawable.ic_plus_24
|
||||
action = { clickIntents.onBuyClick(cryptoCurrencyStatus) }
|
||||
}
|
||||
is TokenActionsState.ActionState.Receive -> {
|
||||
title = resourceReference(R.string.common_receive)
|
||||
icon = R.drawable.ic_arrow_down_24
|
||||
action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }
|
||||
}
|
||||
is TokenActionsState.ActionState.Sell -> {
|
||||
title = resourceReference(R.string.common_sell)
|
||||
icon = R.drawable.ic_currency_24
|
||||
action = { clickIntents.onSellClick(cryptoCurrencyStatus) }
|
||||
}
|
||||
is TokenActionsState.ActionState.Send -> {
|
||||
title = resourceReference(R.string.common_send)
|
||||
icon = R.drawable.ic_arrow_up_24
|
||||
action = { clickIntents.onSendClick(cryptoCurrencyStatus) }
|
||||
}
|
||||
is TokenActionsState.ActionState.Swap -> {
|
||||
title = resourceReference(R.string.common_swap)
|
||||
icon = R.drawable.ic_exchange_horizontal_24
|
||||
action = { clickIntents.onSwapClick(cryptoCurrencyStatus) }
|
||||
}
|
||||
is TokenActionsState.ActionState.CopyAddress -> {
|
||||
title = resourceReference(R.string.common_copy_address)
|
||||
icon = R.drawable.ic_copy_24
|
||||
action = { clickIntents.onCopyAddressClick(cryptoCurrencyStatus) }
|
||||
}
|
||||
is TokenActionsState.ActionState.HideToken -> {
|
||||
title = resourceReference(R.string.token_details_hide_token)
|
||||
icon = R.drawable.ic_hide_24
|
||||
action = { clickIntents.onHideTokensClick(cryptoCurrencyStatus) }
|
||||
}
|
||||
}
|
||||
|
||||
return TokenActionButtonConfig(
|
||||
text = title,
|
||||
iconResId = icon,
|
||||
onClick = action,
|
||||
isWarning = actionsState is TokenActionsState.ActionState.HideToken,
|
||||
enabled = actionsState.enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SingleWalletCardStateConverter(
|
||||
private val status: CryptoCurrencyStatus.Status,
|
||||
private val selectedWallet: UserWallet,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<WalletCardState, WalletCardState> {
|
||||
|
||||
override fun convert(value: WalletCardState): WalletCardState {
|
||||
return when (status) {
|
||||
is CryptoCurrencyStatus.Loading -> value.toLoadingState()
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
-> value.toErrorState()
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> value.toContentState(status)
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletCardState.toLoadingState(): WalletCardState {
|
||||
return WalletCardState.Loading(
|
||||
id = id,
|
||||
title = title,
|
||||
imageResId = imageResId,
|
||||
onRenameClick = onRenameClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletCardState.toErrorState(): WalletCardState {
|
||||
return WalletCardState.Error(
|
||||
id = id,
|
||||
title = title,
|
||||
imageResId = imageResId,
|
||||
onRenameClick = onRenameClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletCardState.toContentState(status: CryptoCurrencyStatus.Status): WalletCardState {
|
||||
return WalletCardState.Content(
|
||||
id = id,
|
||||
title = title,
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(
|
||||
wallet = selectedWallet,
|
||||
currencyAmount = status.amount,
|
||||
),
|
||||
imageResId = imageResId,
|
||||
onRenameClick = onRenameClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
balance = formatFiatAmount(status = status, appCurrency = appCurrency),
|
||||
cardCount = selectedWallet.getCardsCount(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String {
|
||||
val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatAmount,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SingleWalletMarketPriceConverter(
|
||||
private val status: CryptoCurrencyStatus.Status,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<MarketPriceBlockState, MarketPriceBlockState> {
|
||||
|
||||
override fun convert(value: MarketPriceBlockState): MarketPriceBlockState {
|
||||
return when (status) {
|
||||
CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(value.currencySymbol)
|
||||
is CryptoCurrencyStatus.NoAccount -> value.toNoAccountState()
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> value.toContentState()
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
-> MarketPriceBlockState.Error(value.currencySymbol)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MarketPriceBlockState.toNoAccountState(): MarketPriceBlockState {
|
||||
return if (status.fiatRate == null) MarketPriceBlockState.Error(currencySymbol) else toContentState()
|
||||
}
|
||||
|
||||
private fun MarketPriceBlockState.toContentState(): MarketPriceBlockState {
|
||||
return MarketPriceBlockState.Content(
|
||||
currencySymbol = currencySymbol,
|
||||
price = formatPrice(status = status, appCurrency = appCurrency),
|
||||
priceChangeConfig = PriceChangeState.Content(
|
||||
valueInPercent = formatPriceChange(status = status),
|
||||
type = getPriceChangeType(status = status),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String {
|
||||
val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatRate,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String {
|
||||
val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true)
|
||||
}
|
||||
|
||||
private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType {
|
||||
val priceChange = status.priceChange ?: return PriceChangeType.DOWN
|
||||
|
||||
return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TokenItemStateConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : Converter<CryptoCurrencyStatus, TokenItemState> {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
|
||||
return when (value.value) {
|
||||
is CryptoCurrencyStatus.Loading -> value.mapToLoadingState()
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> value.mapToTokenItemState()
|
||||
is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState()
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> value.mapToUnreachableTokenItemState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading {
|
||||
return TokenItemState.Loading(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateConverter.convert(value = this),
|
||||
titleState = TokenItemState.TitleState.Content(text = currency.name),
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content {
|
||||
return TokenItemState.Content(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateConverter.convert(value = this),
|
||||
titleState = TokenItemState.TitleState.Content(
|
||||
text = currency.name,
|
||||
hasPending = value.hasCurrentNetworkTransactions,
|
||||
),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(
|
||||
text = getFormattedFiatAmount(),
|
||||
),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()),
|
||||
cryptoPriceState = getCryptoPriceState(),
|
||||
onItemClick = { clickIntents.onTokenItemClick(currency) },
|
||||
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
|
||||
val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
|
||||
val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
|
||||
val appCurrency = appCurrencyProvider()
|
||||
|
||||
return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateConverter.convert(value = this),
|
||||
titleState = TokenItemState.TitleState.Content(text = currency.name),
|
||||
onItemClick = { clickIntents.onTokenItemClick(currency) },
|
||||
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
|
||||
)
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState() = TokenItemState.NoAddress(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateConverter.convert(this),
|
||||
titleState = TokenItemState.TitleState.Content(text = currency.name),
|
||||
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
|
||||
)
|
||||
|
||||
private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState {
|
||||
val fiatRate = value.fiatRate
|
||||
val priceChange = value.priceChange
|
||||
|
||||
return if (fiatRate != null && priceChange != null) {
|
||||
TokenItemState.CryptoPriceState.Content(
|
||||
price = fiatRate.getFormattedCryptoPrice(),
|
||||
priceChangePercent = BigDecimalFormatter.formatPercent(
|
||||
percent = priceChange,
|
||||
useAbsoluteValue = true,
|
||||
maxFractionDigits = 1,
|
||||
minFractionDigits = 1,
|
||||
),
|
||||
type = priceChange.getPriceChangeType(),
|
||||
)
|
||||
} else {
|
||||
TokenItemState.CryptoPriceState.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.getFormattedCryptoPrice(): String {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
return BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = this,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal.getPriceChangeType(): PriceChangeType {
|
||||
return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig
|
||||
|
||||
internal class TokenListStateConverter(
|
||||
private val tokenList: TokenList,
|
||||
private val selectedWallet: UserWallet,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : Converter<WalletTokensListState, WalletTokensListState> {
|
||||
|
||||
private val tokenStatusConverter = TokenItemStateConverter(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
override fun convert(value: WalletTokensListState): WalletTokensListState {
|
||||
return when (tokenList) {
|
||||
is TokenList.Empty -> WalletTokensListState.Empty
|
||||
is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content(
|
||||
items = tokenList.toGroupedItems(),
|
||||
organizeTokensButtonConfig = getOrganizeTokensButtonState(
|
||||
currenciesSize = tokenList.groups.flatMap(NetworkGroup::currencies).size,
|
||||
),
|
||||
)
|
||||
is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content(
|
||||
items = tokenList.toUngroupedItems(),
|
||||
organizeTokensButtonConfig = getOrganizeTokensButtonState(currenciesSize = tokenList.currencies.size),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList<TokensListItemState> {
|
||||
return groups.fold(initial = persistentListOf()) { acc, group ->
|
||||
acc.mutate { it.addGroup(group) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList<TokensListItemState> {
|
||||
return currencies.fold(initial = persistentListOf()) { acc, token ->
|
||||
acc.mutate { it.addToken(token) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<TokensListItemState>.addGroup(group: NetworkGroup): List<TokensListItemState> {
|
||||
val groupTitle = TokensListItemState.NetworkGroupTitle(
|
||||
id = group.network.hashCode(),
|
||||
name = stringReference(group.network.name),
|
||||
)
|
||||
|
||||
add(groupTitle)
|
||||
group.currencies.forEach { token -> addToken(token) }
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun MutableList<TokensListItemState>.addToken(token: CryptoCurrencyStatus): List<TokensListItemState> {
|
||||
val tokenItemState = tokenStatusConverter.convert(token)
|
||||
add(TokensListItemState.Token(tokenItemState))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getOrganizeTokensButtonState(currenciesSize: Int): WalletOrganizeTokensButtonConfig? {
|
||||
return if (currenciesSize > 1 && !isSingleCurrencyWalletWithToken()) {
|
||||
WalletOrganizeTokensButtonConfig(
|
||||
isEnabled = tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading,
|
||||
onClick = clickIntents::onOrganizeTokensClick,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSingleCurrencyWalletWithToken(): Boolean {
|
||||
return selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import androidx.paging.*
|
||||
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.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.UUID
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
internal class TxHistoryItemFlowConverter(
|
||||
private val userWallet: UserWallet,
|
||||
private val currentState: WalletState.SingleCurrency.Content,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : Converter<Flow<PagingData<TxHistoryItem>>, TxHistoryState?> {
|
||||
|
||||
private val txHistoryItemConverter by lazy {
|
||||
val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
|
||||
TxHistoryItemStateConverter(
|
||||
symbol = blockchain.currency,
|
||||
decimals = blockchain.decimals(),
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Flow<PagingData<TxHistoryItem>>): TxHistoryState {
|
||||
val txHistoryContent = currentState.txHistoryState as? TxHistoryState.Content
|
||||
?: TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty()))
|
||||
|
||||
// FIXME: TxHistoryRepository should send loading transactions
|
||||
// [REDACTED_JIRA]
|
||||
value
|
||||
.onEach { txHistoryStatePagingData ->
|
||||
txHistoryContent.contentItems.update {
|
||||
txHistoryStatePagingData
|
||||
.map<TxHistoryItem, TxHistoryItemState> { item ->
|
||||
// [createTransactionState] returns timestamp without formatting
|
||||
TxHistoryItemState.Transaction(state = createTransactionState(item))
|
||||
}
|
||||
.insertHeaderItem(
|
||||
terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE,
|
||||
item = TxHistoryItemState.Title(clickIntents::onExploreClick),
|
||||
)
|
||||
.insertGroupTitle() // method uses the raw timestamp
|
||||
.formatTransactionsTimestamp() // method formats the timestamp
|
||||
}
|
||||
}
|
||||
.cachedIn(scope)
|
||||
.launchIn(scope)
|
||||
|
||||
return txHistoryContent
|
||||
}
|
||||
|
||||
private fun createTransactionState(item: TxHistoryItem): TransactionState {
|
||||
return txHistoryItemConverter.convert(value = item)
|
||||
}
|
||||
|
||||
private fun PagingData<TxHistoryItemState>.insertGroupTitle(): PagingData<TxHistoryItemState> {
|
||||
return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after ->
|
||||
// Use raw timestamp to get date
|
||||
|
||||
// If [afterDate] is the first transaction in the flow, add the group title
|
||||
val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
||||
if (before is TxHistoryItemState.Title) {
|
||||
return@insertSeparators TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
||||
}
|
||||
|
||||
/*
|
||||
* If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in
|
||||
* the new group
|
||||
*/
|
||||
val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
||||
return@insertSeparators if (beforeDate != afterDate) {
|
||||
TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the [PagingData] to format the [TxHistoryItemState] timestamp
|
||||
*/
|
||||
private fun PagingData<TxHistoryItemState>.formatTransactionsTimestamp(): PagingData<TxHistoryItemState> {
|
||||
return map { txHistoryItemState ->
|
||||
if (txHistoryItemState is TxHistoryItemState.Transaction &&
|
||||
txHistoryItemState.state is TransactionState.Content
|
||||
) {
|
||||
val txContent = txHistoryItemState.state as TransactionState.Content
|
||||
txHistoryItemState.copy(
|
||||
state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()),
|
||||
)
|
||||
} else {
|
||||
txHistoryItemState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItemState?.getTimestamp(): Long? {
|
||||
return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) {
|
||||
val txContent = this.state as TransactionState.Content
|
||||
requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
|
||||
internal class TxHistoryItemStateConverter(
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
private val clickIntents: WalletClickIntentsV2,
|
||||
) : Converter<TxHistoryItem, TransactionState> {
|
||||
|
||||
override fun convert(value: TxHistoryItem): TransactionState {
|
||||
return createTransactionStateItem(item = value)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun createTransactionStateItem(item: TxHistoryItem): TransactionState {
|
||||
return TransactionState.Content(
|
||||
txHash = item.txHash,
|
||||
amount = item.getAmount(),
|
||||
timestamp = item.getRawTimestamp(),
|
||||
status = item.status.tiUiStatus(),
|
||||
direction = item.extractDirection(),
|
||||
iconRes = item.extractIcon(),
|
||||
title = item.extractTitle(),
|
||||
subtitle = item.extractSubtitle(),
|
||||
onClick = { clickIntents.onTransactionClick(item.txHash) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) {
|
||||
R.drawable.ic_close_24
|
||||
} else {
|
||||
when (type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24
|
||||
is TxHistoryItem.TransactionType.Operation,
|
||||
is TxHistoryItem.TransactionType.Swap,
|
||||
is TxHistoryItem.TransactionType.Transfer,
|
||||
is TxHistoryItem.TransactionType.UnknownOperation,
|
||||
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval)
|
||||
is TxHistoryItem.TransactionType.Operation -> stringReference(type.name)
|
||||
is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap)
|
||||
is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer)
|
||||
is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractSubtitle(): TextReference =
|
||||
when (val interactionAddress = interactionAddressType) {
|
||||
is TxHistoryItem.InteractionAddressType.Contract -> resourceReference(
|
||||
id = R.string.transaction_history_contract_address,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference(
|
||||
id = if (isOutgoing) {
|
||||
R.string.transaction_history_transaction_to_address
|
||||
} else {
|
||||
R.string.transaction_history_transaction_from_address
|
||||
},
|
||||
formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)),
|
||||
)
|
||||
is TxHistoryItem.InteractionAddressType.User -> resourceReference(
|
||||
id = if (isOutgoing) {
|
||||
R.string.transaction_history_transaction_to_address
|
||||
} else {
|
||||
R.string.transaction_history_transaction_from_address
|
||||
},
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractDirection() =
|
||||
if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING
|
||||
|
||||
/**
|
||||
* Get timestamp without formatting.
|
||||
* It's life hack that help us to add transaction's group title to flow.
|
||||
*
|
||||
* @see [convert]
|
||||
*/
|
||||
private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString()
|
||||
|
||||
private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) {
|
||||
TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed
|
||||
TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed
|
||||
TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
val prefix = when (status) {
|
||||
TxHistoryItem.TransactionStatus.Failed -> ""
|
||||
else -> if (isOutgoing) "-" else "+"
|
||||
}
|
||||
return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.utils
|
||||
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
|
||||
|
||||
internal inline fun UserWallet.createStateByWalletType(
|
||||
multiCurrencyCreator: () -> WalletState.MultiCurrency,
|
||||
singleCurrencyCreator: () -> WalletState.SingleCurrency,
|
||||
): WalletState {
|
||||
return if (isWalletWithTokens()) multiCurrencyCreator() else singleCurrencyCreator()
|
||||
}
|
||||
|
||||
private fun UserWallet.isWalletWithTokens(): Boolean {
|
||||
return isMultiCurrency || scanResponse.cardTypesResolver.isSingleWalletWithToken()
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state2.utils
|
||||
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SendEventTransformer
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Component for sending events [WalletEvent] on WalletScreen
|
||||
*
|
||||
* @property stateHolder state holder for changing state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class WalletEventSender @Inject constructor(
|
||||
private val stateHolder: WalletStateHolderV2,
|
||||
) {
|
||||
|
||||
fun send(event: WalletEvent) {
|
||||
stateHolder.update(transformer = SendEventTransformer(event = event, onConsume = ::onConsume))
|
||||
}
|
||||
|
||||
private fun onConsume() {
|
||||
stateHolder.update {
|
||||
it.copy(event = consumedEvent())
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue