Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-04 16:46:41 +03:00
commit 512ab6d54d
206 changed files with 8933 additions and 630 deletions

View file

@ -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)

View file

@ -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,
}

View file

@ -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
}

View file

@ -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,
)

View file

@ -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 = {},
)
}

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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,
),
),
)

View file

@ -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 = { })
}
}

View file

@ -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,
)

View file

@ -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),
),
),
)

View file

@ -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,
)

View file

@ -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()
}

View file

@ -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,
),
)

View file

@ -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 = {},
)
}

View file

@ -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 = {},
)
}

View file

@ -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

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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,
),
)

View file

@ -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 = { })
}
}

View file

@ -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) },
)
}
}

View file

@ -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 })
}
}
}
}
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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)
}

View file

@ -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),
),

View file

@ -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()
}

View file

@ -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,
)
}

View file

@ -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,
}

View file

@ -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 }
}
}

View file

@ -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),
),
)
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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,
),
)
}
}
}
}

View file

@ -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) {

View file

@ -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),
)
}
}
}

View file

@ -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

View file

@ -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() },
)
}

View file

@ -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()

View file

@ -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
}

View file

@ -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 = ""
}
}

View file

@ -9,4 +9,6 @@ interface WalletFeatureToggles {
/** Availability of redesigned screen */
val isRedesignedScreenEnabled: Boolean
val isWalletsScrollingPreviewEnabled: Boolean
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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(

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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) }
}
}

View file

@ -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()
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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) }
}
}

View file

@ -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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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<*>>
}

View file

@ -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,
}
}
}

View file

@ -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)

View file

@ -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,
)
}
}

View file

@ -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(),
)
}
}

View file

@ -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)
}
}
}

View file

@ -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 }
}
}

View file

@ -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 = {}),
)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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),
),
)
}
}

View file

@ -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
}
}
}
}

View file

@ -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(),
),
)
},
),
)
}
}

View file

@ -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),
)
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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,
)
}
},
)
}
}

View file

@ -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())))
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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(),
)
}
}

View file

@ -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(),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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