Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-09 14:33:14 +03:00
commit 8eb1a28f29
236 changed files with 3852 additions and 2068 deletions

View file

@ -28,7 +28,7 @@ interface PortfolioFetcher {
val walletBalance: Lce<TokenListError, TotalFiatBalance>,
val accountsBalance: AccountStatusList,
) {
val userWallet: UserWallet get() = accountsBalance.userWallet
val userWalletId: UserWalletId get() = accountsBalance.userWalletId
}
sealed interface Mode {

View file

@ -13,7 +13,10 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
import com.tangem.features.account.details.entity.AccountDetailsUM
@ -30,6 +33,7 @@ internal class AccountDetailsModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
private val params = paramsContainer.require<AccountDetailsComponent.Params>()
@ -42,8 +46,11 @@ internal class AccountDetailsModel @Inject constructor(
}
private fun onManageTokensClick() {
// todo account add account param
router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS))
val route = AppRoute.ManageTokens(
source = AppRoute.ManageTokens.Source.SETTINGS,
portfolioId = PortfolioId(params.account.accountId),
)
router.push(route)
}
private fun onArchiveAccountClick() {
@ -89,6 +96,8 @@ internal class AccountDetailsModel @Inject constructor(
)
}
}
val isMultiCurrency = getUserWalletUseCase(params.account.accountId.userWalletId)
.getOrNull()?.isMultiCurrency ?: false
return AccountDetailsUM(
accountName = params.account.accountName.toUM().value,
accountIcon = params.account.portfolioIcon.toUM(),
@ -96,6 +105,7 @@ internal class AccountDetailsModel @Inject constructor(
onAccountEditClick = ::onEditAccountClick,
onManageTokensClick = ::onManageTokensClick,
archiveMode = archiveMode,
isManageTokensAvailable = isMultiCurrency,
)
}
}

View file

@ -7,6 +7,7 @@ internal data class AccountDetailsUM(
val accountName: TextReference,
val accountIcon: CryptoPortfolioIconUM,
val archiveMode: ArchiveMode,
val isManageTokensAvailable: Boolean,
val onCloseClick: () -> Unit,
val onAccountEditClick: () -> Unit,
val onManageTokensClick: () -> Unit,

View file

@ -22,7 +22,6 @@ import com.tangem.common.ui.R
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.AccountRow
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
@ -49,6 +48,7 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier =
)
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = TangemTheme.dimens.spacing16)
@ -61,21 +61,22 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier =
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,
)
SpacerH16()
AccountRow(state)
SpacerH16()
ManageTokensRow(state)
if (state.isManageTokensAvailable) {
ManageTokensRow(state)
}
when (state.archiveMode) {
is AccountDetailsUM.ArchiveMode.Available -> {
SpacerH16()
ArchiveAccountRow(state.archiveMode)
SpacerH(8.dp)
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = stringResourceSafe(R.string.account_details_archive_description),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Column {
ArchiveAccountRow(state.archiveMode)
SpacerH(8.dp)
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = stringResourceSafe(R.string.account_details_archive_description),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
AccountDetailsUM.ArchiveMode.None -> Unit
}
@ -186,10 +187,12 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountD
),
accountName = stringReference(accountName),
accountIcon = portfolioIcon,
isManageTokensAvailable = true,
)
add(first)
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
add(first.copy(accountIcon = portfolioIcon))
add(first.copy(archiveMode = AccountDetailsUM.ArchiveMode.None))
add(first.copy(isManageTokensAvailable = false))
},
)

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS
@ -31,6 +32,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
) : Model() {
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
@ -60,7 +62,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
it.copy(
isShown = true,
accessCode = "",
useBiometricVisible = attemptRequest.hasBiometry,
useBiometricVisible = attemptRequest.isBiometryButtonVisible(),
onAccessCodeChange = ::onAccessCodeChange,
)
}
@ -83,7 +85,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
it.copy(
accessCodeColor = PinTextColor.WrongCode,
onAccessCodeChange = {},
useBiometricVisible = currentRequest.hasBiometry,
useBiometricVisible = currentRequest.isBiometryButtonVisible(),
)
}
delay(timeMillis = 500) // Delay to show the wrong access code state
@ -212,6 +214,9 @@ internal class HotAccessCodeRequestModel @Inject constructor(
dismiss()
}
private suspend fun HotWalletPasswordRequester.AttemptRequest.isBiometryButtonVisible(): Boolean =
hasBiometry && canUseBiometryUseCase()
private fun dismissState() {
uiState.update {
it.copy(isShown = false)

View file

@ -2,21 +2,13 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface ManageTokensComponent : ComposableContentComponent {
data class Params(
val mode: ManageTokensMode,
val source: ManageTokensSource,
) {
constructor(userWalletId: UserWalletId?, source: ManageTokensSource) : this(
source = source,
mode = userWalletId
?.let { ManageTokensMode.Wallet(userWalletId) }
?: ManageTokensMode.None,
)
}
)
interface Factory : ComponentFactory<Params, ManageTokensComponent>
}

View file

@ -17,6 +17,13 @@ sealed interface ManageTokensMode {
}
sealed interface AddCustomTokenMode {
data class Wallet(val userWalletId: UserWalletId) : AddCustomTokenMode
val userWalletId: UserWalletId
get() = when (this) {
is Account -> accountId.userWalletId
is Wallet -> userWalletId
}
data class Wallet(override val userWalletId: UserWalletId) : AddCustomTokenMode
data class Account(val accountId: AccountId) : AddCustomTokenMode
}

View file

@ -25,6 +25,8 @@ dependencies {
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.account.status)
implementation(projects.domain.account)
implementation(projects.domain.card)
implementation(projects.domain.legacy)
implementation(projects.domain.manageTokens)
@ -35,6 +37,14 @@ dependencies {
implementation(projects.domain.swap.models)
implementation(projects.domain.notifications)
// region Project - Libs
implementation(projects.libs.crypto)
// endregion
// region Tangem SDKs
implementation(tangemDeps.blockchain)
// endregion
/* AndroidX */
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)

View file

@ -54,6 +54,7 @@ internal sealed class ManageTokensUM {
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
scrollToTop: StateEvent<Unit> = this.scrollToTop,
needToInteractWithColdWallet: Boolean = this is ManageContent && this.needToInteractWithColdWallet,
topBar: ManageTokensTopBarUM? = this.topBar,
): ManageTokensUM {
return when (this) {
is ManageContent -> copy(
@ -65,6 +66,7 @@ internal sealed class ManageTokensUM {
isSavingInProgress = isSavingInProgress,
scrollToTop = scrollToTop,
needToInteractWithColdWallet = needToInteractWithColdWallet,
topBar = topBar,
)
is ReadContent -> copy(
search = search,
@ -72,6 +74,7 @@ internal sealed class ManageTokensUM {
isInitialBatchLoading = isInitialBatchLoading,
isNextBatchLoading = isNextBatchLoading,
scrollToTop = scrollToTop,
topBar = topBar,
)
}
}

View file

@ -3,13 +3,22 @@ package com.tangem.features.managetokens.model
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.ui.account.toUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
@ -25,10 +34,12 @@ import com.tangem.features.managetokens.entity.item.SelectableItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel
import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -38,6 +49,8 @@ internal class CustomTokenSelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase,
private val messageSender: UiMessageSender,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val accountsFeatureToggles: AccountsFeatureToggles,
paramsContainer: ParamsContainer,
) : Model() {
@ -146,9 +159,8 @@ internal class CustomTokenSelectorModel @Inject constructor(
return derivationPaths
}
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> {
return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error))
messageSender.send(message)
@ -168,7 +180,60 @@ internal class CustomTokenSelectorModel @Inject constructor(
fun selectCustomDerivationPath(value: SelectedDerivationPath) {
when (params) {
is NetworkSelector -> return
is DerivationPathSelector -> params.onDerivationPathSelected(value)
is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) {
params.checkAccountDerivation(value)
} else {
params.onDerivationPathSelected(value)
}
}
}
private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) =
modelScope.launch {
val accountName = derivationPath.id
?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer)
?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } }
?.let { accountNode ->
fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount &&
this.account.derivationIndex.value.toLong() == accountNode
val accounts = singleAccountStatusListSupplier(mode.userWalletId)
.first().accountStatuses
val account = accounts.find {
when (it) {
is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain()
}
}
val accountName = when (account) {
is AccountStatus.CryptoPortfolio -> account.account.accountName.toUM()
null -> null
}
accountName
}
if (accountName == null) {
onDerivationPathSelected(derivationPath)
} else {
showAccountNameExist(
accountName = accountName.value,
onClick = { onDerivationPathSelected(derivationPath) },
)
}
}
private fun showAccountNameExist(accountName: TextReference, onClick: () -> Unit) {
val firstAction = EventMessageAction(
title = resourceReference(R.string.common_got_it),
onClick = onClick,
)
val dialogMessage = DialogMessage(
title = resourceReference(R.string.custom_token_another_account_dialog_title),
message = resourceReference(
R.string.custom_token_another_account_dialog_description,
wrappedList(accountName),
),
firstActionBuilder = { firstAction },
)
messageSender.send(dialogMessage)
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensComponent
@ -40,13 +41,14 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class ManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val messageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
paramsContainer: ParamsContainer,
@ -86,6 +88,7 @@ internal class ManageTokensModel @Inject constructor(
modelScope.launch {
manageTokensListManager.launchPagination(isCollapsed = true)
}
checkIsSupportAddCustomTokens()
}
fun reloadList() {
@ -105,16 +108,34 @@ internal class ManageTokensModel @Inject constructor(
}
}
private fun getTopBarInitialState(): ManageTokensTopBarUM = when (params.mode) {
is ManageTokensMode.Wallet -> manageContentTopBar()
is ManageTokensMode.Account -> ManageTokensTopBarUM.ReadContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
)
ManageTokensMode.None -> ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
)
}
private fun manageContentTopBar() = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_plus_24,
onClicked = ::navigateToAddCustomToken,
),
)
private fun createReadContentModel(): ManageTokensUM.ReadContent {
return ManageTokensUM.ReadContent(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getLoadingItems(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
),
topBar = getTopBarInitialState(),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
@ -132,14 +153,7 @@ internal class ManageTokensModel @Inject constructor(
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getLoadingItems(),
topBar = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_plus_24,
onClicked = ::navigateToAddCustomToken,
),
),
topBar = getTopBarInitialState(),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
@ -174,6 +188,20 @@ internal class ManageTokensModel @Inject constructor(
.launchIn(modelScope)
}
private fun checkIsSupportAddCustomTokens() {
when (val mode = params.mode) {
is ManageTokensMode.Account -> modelScope.launch {
val mainAccount = singleAccountStatusListSupplier(mode.accountId.userWalletId).first().mainAccount
if (mode.accountId == mainAccount.account.accountId) {
state.update { it.copySealed(topBar = manageContentTopBar()) }
}
}
ManageTokensMode.None,
is ManageTokensMode.Wallet,
-> Unit // use init state
}
}
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
val updatedState = state.updateAndGet { state ->
state.copySealed(

View file

@ -57,6 +57,7 @@ import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
@ -442,20 +443,23 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
showTangemIcon = true,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("0x"),
mode = ManageTokensMode.Wallet(UserWalletId("0x")),
),
),
PreviewManageTokensComponent(
isLoading = false,
showTangemIcon = true,
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
mode = ManageTokensMode.None,
),
),
PreviewManageTokensComponent(
isLoading = false,
showTangemIcon = false,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("0x"),
mode = ManageTokensMode.Wallet(UserWalletId("0x")),
),
),
)

View file

@ -46,6 +46,7 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.domain.transaction.models)
implementation(projects.domain.account.status)
/** DI */
implementation(deps.hilt.android)

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.selecttoken
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
@ -41,11 +42,13 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state = model.state.collectAsStateWithLifecycle()
val state by model.state.collectAsStateWithLifecycle()
val onrampTokenListState by onrampTokenListComponent.uiState.collectAsStateWithLifecycle()
OnrampSelectToken(
state = state.value,
state = state,
onrampTokenListComponent = onrampTokenListComponent,
onrampTokenListState = onrampTokenListState,
hotCryptoComponent = hotCryptoComponent,
modifier = modifier,
)

View file

@ -21,12 +21,14 @@ import com.tangem.features.onramp.hottokens.HotCryptoComponent
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun OnrampSelectToken(
state: OnrampOperationUM,
onrampTokenListComponent: OnrampTokenListComponent,
onrampTokenListState: TokenListUM,
hotCryptoComponent: HotCryptoComponent?,
modifier: Modifier = Modifier,
) {
@ -50,8 +52,9 @@ internal fun OnrampSelectToken(
)
}
item(key = "token_list", contentType = "token_list") {
onrampTokenListComponent.Content(
with(onrampTokenListComponent) {
content(
uiState = onrampTokenListState,
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)

View file

@ -2,6 +2,7 @@ package com.tangem.features.onramp.swap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -54,12 +55,16 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state = model.state.collectAsStateWithLifecycle()
val state by model.state.collectAsStateWithLifecycle()
val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle()
val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle()
SwapSelectTokens(
state = state.value,
state = state,
selectFromTokenListComponent = selectFromTokenListComponent,
selectFromTokenListState = fromTokensState,
selectToTokenListComponent = selectToTokenListComponent,
selectToTokenListState = toTokensState,
modifier = modifier,
)
}

View file

@ -3,14 +3,15 @@ package com.tangem.features.onramp.swap.availablepairs
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableListContentComponent
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import kotlinx.coroutines.flow.StateFlow
/** Token list component that present list of available tokens for swap */
@Stable
internal interface AvailableSwapPairsComponent : ComposableContentComponent {
internal interface AvailableSwapPairsComponent : ComposableListContentComponent<TokenListUM> {
/** Component factory */
interface Factory : ComponentFactory<Params, AvailableSwapPairsComponent>

View file

@ -1,17 +1,17 @@
package com.tangem.features.onramp.swap.availablepairs
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
import com.tangem.features.onramp.tokenlist.ui.TokenList
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.ui.onrampTokenList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.StateFlow
@Stable
internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
@ -21,11 +21,11 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
override val uiState: StateFlow<TokenListUM>
get() = model.state
TokenList(state = state, modifier = modifier)
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
onrampTokenList(state = uiState)
}
@AssistedFactory

View file

@ -0,0 +1,29 @@
package com.tangem.features.onramp.swap.availablepairs.entity.converters
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
internal class LoadingAccountTokenItemConverter(
private val appCurrency: AppCurrency,
) : Converter<AccountStatus.CryptoPortfolio, TokensListItemUM.Portfolio> {
override fun convert(value: AccountStatus.CryptoPortfolio): TokensListItemUM.Portfolio {
val (account, currencies) = value
return TokensListItemUM.Portfolio(
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = account,
onItemClick = null,
).convert(TotalFiatBalance.Failed),
isExpanded = true,
isCollapsable = false,
tokens = currencies.flattenCurrencies().map(LoadingTokenListItemConverter::convert).toPersistentList(),
)
}
}

View file

@ -0,0 +1,61 @@
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal class SetNoAvailablePairsTransformerV2(
private val appCurrency: AppCurrency,
private val accountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>,
private val isBalanceHidden: Boolean,
private val isAccountsMode: Boolean,
private val unavailableErrorText: TextReference,
) : TokenListUMTransformer {
private val unavailableConverter = OnrampTokenItemStateConverterFactory
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = if (isAccountsMode) {
TokenListUMData.AccountList(
tokensList = accountList.map { (account, cryptoCurrencies) ->
TokensListItemUM.Portfolio(
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = account,
onItemClick = null,
).convert(TotalFiatBalance.Failed),
isExpanded = true,
isCollapsable = false,
tokens = unavailableConverter.convertList(cryptoCurrencies)
.map(TokensListItemUM::Token)
.toPersistentList(),
)
}.toPersistentList(),
)
} else {
TokenListUMData.TokenList(
tokensList = accountList.flatMap { (_, cryptoCurrencies) ->
unavailableConverter.convertList(cryptoCurrencies)
.map(TokensListItemUM::Token)
}.toPersistentList(),
)
},
isBalanceHidden = isBalanceHidden,
warning = NotificationUM.Warning.SwapNoAvailablePair,
)
}
}

View file

@ -1,12 +1,15 @@
package com.tangem.features.onramp.swap.availablepairs.model
import arrow.core.getOrElse
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.extensions.capitalize
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -15,6 +18,8 @@ import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
@ -28,11 +33,13 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformerV2
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.*
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
@ -43,7 +50,7 @@ import javax.inject.Inject
private typealias AvailablePairsState = Lce<Throwable, List<SwapPairLeast>>
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class AvailableSwapPairsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@ -53,7 +60,10 @@ internal class AvailableSwapPairsModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getAvailablePairsUseCase: GetAvailablePairsUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
getWalletsUseCase: GetWalletsUseCase,
) : Model() {
val state: StateFlow<TokenListUM> = tokenListUMController.state
@ -62,13 +72,17 @@ internal class AvailableSwapPairsModel @Inject constructor(
private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
private val tokenListFlow = getTokenListUseCaseFlow()
private val accountListFlow = getAccountListUseCaseFlow()
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
init {
initializeSearchBarCallbacks()
if (accountsFeatureToggles.isFeatureEnabled) {
subscribeOnUpdateStateV2()
} else {
subscribeOnUpdateState()
}
subscribeOnUpdateState()
initializeSearchBarCallbacks()
subscribeOnAvailablePairsUpdates()
}
@ -79,12 +93,20 @@ internal class AvailableSwapPairsModel @Inject constructor(
maybeTokenList.getOrElse(
ifLoading = { it ?: TokenList.Empty },
ifError = { TokenList.Empty },
)
.flattenCurrencies()
).flattenCurrencies()
}
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
}
private fun getAccountListUseCaseFlow(): SharedFlow<List<AccountStatus>> {
return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId))
.distinctUntilChanged()
.map { accountStatusList ->
accountStatusList.accountStatuses.toList()
}.flowOn(dispatchers.default)
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
}
private fun initializeSearchBarCallbacks() {
tokenListUMController.update(
transformer = UpdateSearchBarCallbacksTransformer(
@ -130,6 +152,53 @@ internal class AvailableSwapPairsModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeOnUpdateStateV2() {
combine(
flow = getAccountsAndModeFlow(),
flow2 = getAppCurrencyAndBalanceHidingFlow(),
flow3 = params.selectedStatus,
flow4 = searchManager.query,
flow5 = availablePairsByNetworkFlow
.map { it[params.selectedStatus.value?.toLeastTokenInfo()] }
.distinctUntilChanged(),
) { accountListAndMode, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState ->
val (accountList, isAccountsMode) = accountListAndMode
availablePairsState?.fold(
ifLoading = {
SetLoadingAccountTokenListTransformer(
appCurrency = appCurrencyAndBalanceHiding.first,
accountList = accountList,
isAccountsMode = isAccountsMode,
)
},
ifContent = { pairs ->
handleContentStateV2(
appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding,
accountList = accountList,
selectedStatus = selectedStatus,
query = query,
availablePairs = pairs,
isAccountsMode = isAccountsMode,
)
},
ifError = {
handleErrorStateV2(
cause = it,
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
accountList = accountList,
)
},
) ?: SetLoadingAccountTokenListTransformer(
appCurrency = appCurrencyAndBalanceHiding.first,
accountList = accountList,
isAccountsMode = isAccountsMode,
)
}
.onEach(tokenListUMController::update)
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun handleContentState(
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
currencies: List<CryptoCurrencyStatus>,
@ -176,6 +245,53 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
}
private fun handleContentStateV2(
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
accountList: List<AccountStatus>,
selectedStatus: CryptoCurrencyStatus?,
query: String,
availablePairs: List<SwapPairLeast>,
isAccountsMode: Boolean,
): TokenListUMTransformer {
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
val filterByQueryAccountList = accountList.associate { accountStatus ->
when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account to accountStatus.tokenList.flattenCurrencies()
.filter { it.currency != selectedStatus?.currency }
.filterByQuery(query = query)
}
}
if (availablePairs.isEmpty()) {
return SetNoAvailablePairsTransformerV2(
appCurrency = appCurrency,
accountList = filterByQueryAccountList,
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
isBalanceHidden = isBalanceHidden,
isAccountsMode = isAccountsMode,
)
}
return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
SetNothingToFoundStateTransformerV2(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = resourceReference(
id = R.string.action_buttons_swap_empty_search_message,
),
)
} else {
UpdateAccountTokenListTransformer(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs),
isBalanceHidden = isBalanceHidden,
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
isAccountsMode = isAccountsMode,
)
}
}
private fun handleErrorState(
cause: Throwable,
networkInfo: LeastTokenInfo?,
@ -193,6 +309,26 @@ internal class AvailableSwapPairsModel @Inject constructor(
)
}
private fun handleErrorStateV2(
cause: Throwable,
networkInfo: LeastTokenInfo?,
accountList: List<AccountStatus>,
): SetErrorWarningTransformer {
return SetErrorWarningTransformer(
cause = cause,
onRefresh = {
modelScope.launch {
if (networkInfo != null) {
accountList.filterIsInstance<AccountStatus.CryptoPortfolio>()
.forEach { (_, currencies) ->
updateAvailablePairs(networkInfo, currencies.flattenCurrencies())
}
}
}
},
)
}
private fun subscribeOnAvailablePairsUpdates() {
modelScope.launch {
params.selectedStatus
@ -203,9 +339,19 @@ internal class AvailableSwapPairsModel @Inject constructor(
val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true
if (isAlreadyLoaded) return@collectLatest
val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest
updateAvailablePairs(networkInfo = networkInfo, statuses = statuses)
if (accountsFeatureToggles.isFeatureEnabled) {
val accountList = accountListFlow.firstOrNull() ?: return@collectLatest
updateAvailablePairs(
networkInfo = networkInfo,
statuses = accountList.filterIsInstance<AccountStatus.CryptoPortfolio>()
.flatMap { accountStatus ->
accountStatus.flattenCurrencies()
}.toSet().toList(),
)
} else {
val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest
updateAvailablePairs(networkInfo = networkInfo, statuses = statuses)
}
}
}
}
@ -247,6 +393,14 @@ internal class AvailableSwapPairsModel @Inject constructor(
)
}
private fun getAccountsAndModeFlow(): Flow<Pair<List<AccountStatus>, Boolean>> {
return combine(
flow = accountListFlow.distinctUntilChanged(),
flow2 = isAccountsModeEnabledUseCase().distinctUntilChanged(),
transform = ::Pair,
)
}
private fun onSearchQueryChange(newQuery: String) {
if (state.value.searchBarUM.query == newQuery) return
@ -286,6 +440,29 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
}
private fun Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>.filterByAvailability(
availablePairs: List<SwapPairLeast>,
): List<AccountAvailabilityUM> {
return map { (account, currencies) ->
AccountAvailabilityUM(
account = account,
currencyList = currencies.map { status ->
val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo())
val isAvailableToSwap = isAvailable &&
status.value !is CryptoCurrencyStatus.MissedDerivation &&
status.value !is CryptoCurrencyStatus.Unreachable &&
!status.currency.isCustom
AccountCurrencyUM(
cryptoCurrencyStatus = status,
isAvailable = isAvailableToSwap,
)
},
)
}
}
private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo {
return LeastTokenInfo(
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",

View file

@ -0,0 +1,14 @@
package com.tangem.features.onramp.swap.entity
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
internal data class AccountAvailabilityUM(
val account: Account.CryptoPortfolio,
val currencyList: List<AccountCurrencyUM>,
)
internal data class AccountCurrencyUM(
val isAvailable: Boolean,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
)

View file

@ -1,5 +1,7 @@
package com.tangem.features.onramp.swap.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
@ -11,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference
internal sealed interface ExchangeCardUM {
/** Title reference */
val titleReference: TextReference
val titleUM: TitleUM
/** Remove button UI model */
val removeButtonUM: RemoveButtonUM?
@ -19,11 +21,11 @@ internal sealed interface ExchangeCardUM {
/**
* Empty state
*
* @property titleReference title reference
* @property titleUM title reference
* @property subtitleReference empty token subtitle reference
*/
data class Empty(
override val titleReference: TextReference,
override val titleUM: TitleUM,
val subtitleReference: TextReference,
) : ExchangeCardUM {
@ -33,15 +35,29 @@ internal sealed interface ExchangeCardUM {
/**
* Filled
*
* @property titleReference title reference
* @property titleUM title reference
* @property removeButtonUM remove button UI model
* @property tokenItemState token item state
*/
data class Filled(
override val titleReference: TextReference,
override val titleUM: TitleUM,
override val removeButtonUM: RemoveButtonUM?,
val tokenItemState: TokenItemState,
) : ExchangeCardUM
data class RemoveButtonUM(val onClick: () -> Unit)
@Immutable
sealed interface TitleUM {
data class Text(
val title: TextReference,
) : TitleUM
data class Account(
val prefixText: TextReference,
val name: TextReference,
val icon: CryptoPortfolioIconUM,
) : TitleUM
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.swap.entity.transformer
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.models.account.Account
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
@ -17,6 +18,8 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled
internal class SelectFromTokenTransformer(
private val selectedTokenItemState: TokenItemState,
private val onRemoveClick: () -> Unit,
private val account: Account.CryptoPortfolio,
private val isAccountsMode: Boolean,
) : SwapSelectTokensUMTransformer {
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
@ -24,6 +27,9 @@ internal class SelectFromTokenTransformer(
exchangeFrom = prevState.exchangeFrom.toFilled(
selectedTokenItemState = selectedTokenItemState,
removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick),
account = account,
isAccountsMode = isAccountsMode,
isFromCurrency = true,
),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.swap.entity.transformer
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.models.account.Account
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
@ -15,12 +16,19 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled
*/
internal class SelectToTokenTransformer(
private val selectedTokenItemState: TokenItemState,
private val isAccountsMode: Boolean,
private val account: Account.CryptoPortfolio,
) : SwapSelectTokensUMTransformer {
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
return prevState.copy(
exchangeFrom = prevState.exchangeFrom.hideRemoveButton(),
exchangeTo = prevState.exchangeTo.toFilled(selectedTokenItemState = selectedTokenItemState),
exchangeTo = prevState.exchangeTo.toFilled(
selectedTokenItemState = selectedTokenItemState,
isAccountsMode = isAccountsMode,
account = account,
isFromCurrency = false,
),
)
}

View file

@ -1,14 +1,16 @@
package com.tangem.features.onramp.swap.entity.utils
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
/** Create empty exchange "from" card */
internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty {
return ExchangeCardUM.Empty(
titleReference = resourceReference(id = R.string.swapping_from_title),
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
)
}
@ -16,7 +18,7 @@ internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty {
/** Create empty exchange "to" card */
internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty {
return ExchangeCardUM.Empty(
titleReference = resourceReference(id = R.string.swapping_to_title),
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_to_title)),
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_receive),
)
}
@ -29,10 +31,25 @@ internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty {
*/
internal fun ExchangeCardUM.toFilled(
selectedTokenItemState: TokenItemState,
account: Account.CryptoPortfolio,
isAccountsMode: Boolean,
isFromCurrency: Boolean,
removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null,
): ExchangeCardUM.Filled {
return ExchangeCardUM.Filled(
titleReference = titleReference,
titleUM = if (isAccountsMode) {
ExchangeCardUM.TitleUM.Account(
prefixText = if (isFromCurrency) {
resourceReference(R.string.common_from)
} else {
resourceReference(R.string.common_to)
},
name = account.accountName.toUM().value,
icon = account.icon.toUM(),
)
} else {
titleUM
},
tokenItemState = selectedTokenItemState,
removeButtonUM = removeButtonUM,
)

View file

@ -8,7 +8,9 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.onramp.swap.entity.SwapSelectTokensController
@ -32,6 +34,7 @@ internal class SwapSelectTokensModel @Inject constructor(
private val router: Router,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : Model() {
val state: StateFlow<SwapSelectTokensUM> = controller.state
@ -43,9 +46,12 @@ internal class SwapSelectTokensModel @Inject constructor(
private val params = paramsContainer.require<SwapSelectTokensComponent.Params>()
private var isAccountsMode: Boolean = false
init {
controller.update { it.copy(onBackClick = ::onBackClick) }
subscribeOnAccountsMode()
subscribeOnBalanceHidingSettings()
}
@ -66,6 +72,11 @@ internal class SwapSelectTokensModel @Inject constructor(
transformer = SelectFromTokenTransformer(
selectedTokenItemState = selectedTokenItemState,
onRemoveClick = ::onRemoveFromTokenClick,
isAccountsMode = isAccountsMode,
account = Account.CryptoPortfolio.createMainAccount(
userWalletId = params.userWalletId,
cryptoCurrencies = setOf(status.currency),
), // todo account from from cryptocurrency
),
)
}
@ -84,7 +95,16 @@ internal class SwapSelectTokensModel @Inject constructor(
modelScope.launch {
_toCurrencyStatus.value = status
controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState))
controller.update(
transformer = SelectToTokenTransformer(
selectedTokenItemState = selectedTokenItemState,
isAccountsMode = isAccountsMode,
account = Account.CryptoPortfolio.createMainAccount(
userWalletId = params.userWalletId,
cryptoCurrencies = setOf(status.currency),
), // todo account from from cryptocurrency
),
)
// require some delay to show state with selected "from" and "to" tokens
delay(timeMillis = 500)
@ -119,6 +139,16 @@ internal class SwapSelectTokensModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeOnAccountsMode() {
isAccountsModeEnabledUseCase()
.distinctUntilChanged()
.onEach {
isAccountsMode = it
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun onBackClick() {
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.ButtonClose(source = AnalyticsParam.ScreensSources.Swap),

View file

@ -14,11 +14,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountLabel
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.rows.NetworkTitle
@ -46,13 +49,14 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif
modifier = modifier
.fillMaxWidth()
.heightIn(min = 116.dp)
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.SpaceBetween,
) {
Title(titleReference = state.titleReference, removeButtonUM = state.removeButtonUM)
Title(
titleUM = state.titleUM,
removeButtonUM = state.removeButtonUM,
)
AnimatedContent(
targetState = state,
@ -73,16 +77,39 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif
}
@Composable
private fun Title(titleReference: TextReference, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) {
private fun Title(titleUM: ExchangeCardUM.TitleUM, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) {
NetworkTitle(
title = {
Text(
text = titleReference.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.subtitle2,
)
AnimatedContent(
titleUM,
) { currentState ->
when (currentState) {
is ExchangeCardUM.TitleUM.Account -> Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = currentState.prefixText.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
AccountLabel(
name = currentState.name,
icon = currentState.icon,
iconSize = AccountIconSize.ExtraSmall,
nameStyle = TangemTheme.typography.subtitle2,
nameColor = TangemTheme.colors.text.tertiary,
)
}
is ExchangeCardUM.TitleUM.Text -> Text(
text = currentState.title.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.subtitle2,
)
}
}
},
action = { RemoveButton(state = removeButtonUM) },
)
@ -153,7 +180,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider<ExchangeCardUM>
override val values: Sequence<ExchangeCardUM> = sequenceOf(
ExchangeCardUM.Empty(
titleReference = resourceReference(id = R.string.swapping_from_title),
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
),
createFilled(removeButtonUM = null),
@ -162,7 +189,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider<ExchangeCardUM>
private fun createFilled(removeButtonUM: ExchangeCardUM.RemoveButtonUM?): ExchangeCardUM.Filled {
return ExchangeCardUM.Filled(
titleReference = resourceReference(id = R.string.swapping_from_title),
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
removeButtonUM = removeButtonUM,
tokenItemState = TokenItemState.Content(
id = "1",

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -23,6 +24,7 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
/**
* Swap select tokens
@ -39,7 +41,9 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
internal fun SwapSelectTokens(
state: SwapSelectTokensUM,
selectFromTokenListComponent: OnrampTokenListComponent,
selectFromTokenListState: TokenListUM,
selectToTokenListComponent: AvailableSwapPairsComponent,
selectToTokenListState: TokenListUM,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = state.onBackClick)
@ -77,33 +81,33 @@ internal fun SwapSelectTokens(
}
if (state.exchangeFrom is ExchangeCardUM.Empty) {
item(key = "select_from", contentType = "select_from") {
selectFromTokenListComponent.Content(
modifier = Modifier
.padding(horizontal = 16.dp)
.animateItem(),
with(selectFromTokenListComponent) {
content(
uiState = selectFromTokenListState,
modifier = Modifier,
)
}
}
if (state.exchangeFrom is ExchangeCardUM.Filled) {
item(key = "exchange_to", contentType = "exchange_to") {
ExchangeCard(
state = state.exchangeTo,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.animateItem(),
)
if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) {
ExchangeCard(
state = state.exchangeTo,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.animateItem(),
)
}
}
if (state.exchangeTo is ExchangeCardUM.Empty) {
item(key = "select_to", contentType = "select_to") {
selectToTokenListComponent.Content(
modifier = Modifier
.padding(horizontal = 16.dp)
.animateItem(),
with(selectToTokenListComponent) {
content(
uiState = selectToTokenListState,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}

View file

@ -1,17 +1,17 @@
package com.tangem.features.onramp.tokenlist
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel
import com.tangem.features.onramp.tokenlist.ui.TokenList
import com.tangem.features.onramp.tokenlist.ui.onrampTokenList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.StateFlow
@Stable
internal class DefaultOnrampTokenListComponent @AssistedInject constructor(
@ -21,11 +21,11 @@ internal class DefaultOnrampTokenListComponent @AssistedInject constructor(
private val model: OnrampTokenListModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
override val uiState: StateFlow<TokenListUM>
get() = model.state
TokenList(state = state, modifier = modifier)
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
onrampTokenList(state = uiState)
}
@AssistedFactory

View file

@ -3,14 +3,15 @@ package com.tangem.features.onramp.tokenlist
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableListContentComponent
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
/** Token list component that present list of token for multi-currency wallet */
@Stable
internal interface OnrampTokenListComponent : ComposableContentComponent {
internal interface OnrampTokenListComponent : ComposableListContentComponent<TokenListUM> {
/** Component factory */
interface Factory : ComponentFactory<Params, OnrampTokenListComponent>

View file

@ -19,6 +19,19 @@ internal data class TokenListUM(
val searchBarUM: SearchBarUM,
val availableItems: ImmutableList<TokensListItemUM>,
val unavailableItems: ImmutableList<TokensListItemUM>,
val tokensListData: TokenListUMData,
val isBalanceHidden: Boolean,
val warning: NotificationUM? = null,
)
)
internal sealed interface TokenListUMData {
data class AccountList(
val tokensList: ImmutableList<TokensListItemUM.Portfolio>,
) : TokenListUMData
data class TokenList(
val tokensList: ImmutableList<TokensListItemUM>,
) : TokenListUMData
data object EmptyList : TokenListUMData
}

View file

@ -31,6 +31,7 @@ internal class TokenListUMController @Inject constructor() {
),
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.EmptyList,
isBalanceHidden = false,
),
)

View file

@ -0,0 +1,45 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal class SetLoadingAccountTokenListTransformer(
appCurrency: AppCurrency,
private val accountList: List<AccountStatus>,
private val isAccountsMode: Boolean,
) : TokenListUMTransformer {
private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency)
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = if (isAccountsMode) {
TokenListUMData.AccountList(
tokensList = accountListItemConverter.convertList(
accountList.filterIsInstance<AccountStatus.CryptoPortfolio>(),
).toPersistentList(),
)
} else {
TokenListUMData.TokenList(
tokensList = accountList.flatMap { account ->
when (account) {
is AccountStatus.CryptoPortfolio -> LoadingTokenListItemConverter.convertList(
account.tokenList.flattenCurrencies(),
)
}
}.toPersistentList(),
)
},
warning = null,
)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -26,9 +27,9 @@ internal class SetNothingToFoundStateTransformer(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
).let(::add)
}
.toImmutableList(),
}.toImmutableList(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.EmptyList,
isBalanceHidden = isBalanceHidden,
)
}

View file

@ -0,0 +1,29 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class SetNothingToFoundStateTransformerV2(
private val isBalanceHidden: Boolean,
private val emptySearchMessageReference: TextReference,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.TokenList(tokensList = buildList {
TokensListItemUM.Text(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
).let(::add)
}.toImmutableList()),
isBalanceHidden = isBalanceHidden,
)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
internal class UpdateAccountTokenItemConverter(
private val appCurrency: AppCurrency,
private val unavailableErrorText: TextReference,
onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
) : Converter<AccountAvailabilityUM, TokensListItemUM.Portfolio> {
private val availableConverter = OnrampTokenItemStateConverterFactory
.createAvailableItemConverter(appCurrency, onItemClick)
private val unavailableConverter = OnrampTokenItemStateConverterFactory
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
override fun convert(value: AccountAvailabilityUM): TokensListItemUM.Portfolio {
return TokensListItemUM.Portfolio(
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = value.account,
onItemClick = null,
).convert(TotalFiatBalance.Failed),
isExpanded = true,
isCollapsable = false,
tokens = value.currencyList.asSequence().map { (isAvailable, status) ->
if (isAvailable) {
availableConverter.convert(status)
} else {
unavailableConverter.convert(status)
}
}.map(TokensListItemUM::Token).toPersistentList(),
)
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal class UpdateAccountTokenListTransformer(
private val appCurrency: AppCurrency,
private val onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
private val accountList: List<AccountAvailabilityUM>,
private val isBalanceHidden: Boolean,
private val unavailableErrorText: TextReference,
private val warning: NotificationUM? = null,
private val isAccountsMode: Boolean,
) : TokenListUMTransformer {
private val accountListItemConverter = UpdateAccountTokenItemConverter(
appCurrency = appCurrency,
onItemClick = onItemClick,
unavailableErrorText = unavailableErrorText,
)
private val availableConverter = OnrampTokenItemStateConverterFactory
.createAvailableItemConverter(appCurrency, onItemClick)
private val unavailableConverter = OnrampTokenItemStateConverterFactory
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = if (isAccountsMode) {
TokenListUMData.AccountList(
tokensList = accountListItemConverter.convertList(accountList).toPersistentList(),
)
} else {
TokenListUMData.TokenList(
tokensList = accountList.flatMap { (_, currencyList) ->
currencyList.asSequence().map { (isAvailable, status) ->
if (isAvailable) {
availableConverter.convert(status)
} else {
unavailableConverter.convert(status)
}
}.map(TokensListItemUM::Token).toPersistentList()
}.toPersistentList(),
)
},
isBalanceHidden = isBalanceHidden,
warning = warning,
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormatte
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -21,7 +22,13 @@ internal object OnrampTokenItemStateConverterFactory {
): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) },
subtitleStateProvider = {
createSubtitleState(
status = it,
isAvailable = true,
text = stringReference(value = it.currency.symbol),
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
@ -40,7 +47,13 @@ internal object OnrampTokenItemStateConverterFactory {
isAvailable = false,
)
},
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) },
subtitleStateProvider = {
createSubtitleState(
status = it,
text = stringReference(value = it.currency.symbol),
isAvailable = false,
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
@ -48,12 +61,43 @@ internal object OnrampTokenItemStateConverterFactory {
)
}
private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState {
fun createUnavailableItemConverterV2(
appCurrency: AppCurrency,
unavailableErrorText: TextReference,
): TokenItemStateConverter {
return TokenItemStateConverter(
appCurrency = appCurrency,
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
titleStateProvider = {
TokenItemState.TitleState.Content(
text = stringReference(value = it.currency.name),
isAvailable = false,
)
},
subtitleStateProvider = {
createSubtitleState(
status = it,
isAvailable = false,
text = unavailableErrorText,
)
},
subtitle2StateProvider = ::createSubtitle2State,
fiatAmountStateProvider = {
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
},
)
}
private fun createSubtitleState(
status: CryptoCurrencyStatus,
isAvailable: Boolean,
text: TextReference,
): TokenItemState.SubtitleState {
return when (status.value) {
CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
else -> {
TokenItemState.SubtitleState.TextContent(
value = stringReference(value = status.currency.symbol),
value = text,
isAvailable = isAvailable,
)
}

View file

@ -6,6 +6,11 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -13,6 +18,8 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
@ -23,13 +30,11 @@ import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.tokenlist.entity.*
import com.tangem.features.onramp.tokenlist.entity.transformer.*
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
@ -42,7 +47,9 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
typealias AccountCryptoList = Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>
@Suppress("LargeClass", "LongParameterList")
internal class OnrampTokenListModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@ -55,6 +62,9 @@ internal class OnrampTokenListModel @Inject constructor(
private val rampStateManager: RampStateManager,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : Model() {
val state: StateFlow<TokenListUM> = tokenListUMController.state
@ -71,8 +81,11 @@ internal class OnrampTokenListModel @Inject constructor(
onActiveChange = ::onSearchBarActiveChange,
),
)
subscribeOnUpdateState()
if (accountsFeatureToggles.isFeatureEnabled) {
subscribeOnUpdateStateV2()
} else {
subscribeOnUpdateState()
}
}
private fun subscribeOnUpdateState() {
@ -95,12 +108,7 @@ internal class OnrampTokenListModel @Inject constructor(
if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) {
SetNothingToFoundStateTransformer(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message
OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message
OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message
}
.let(::resourceReference),
emptySearchMessageReference = getEmptySearchMessageReference(),
)
} else {
val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) {
@ -134,6 +142,62 @@ internal class OnrampTokenListModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeOnUpdateStateV2() {
combine(
flow = singleAccountStatusListSupplier(
SingleAccountStatusListProducer.Params(params.userWalletId),
).distinctUntilChanged(),
flow2 = getAppCurrencyAndBalanceHidingFlow(),
flow3 = isAccountsModeEnabledUseCase(),
flow4 = searchManager.query,
flow5 = hasRestrictionForSellFlow(),
) { accountList, appCurrencyAndBalanceHiding, isAccountsMode, query, hasRestrictionForSell ->
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
val filterByQueryAccountList = accountList.filterAccountsByQuery(query)
if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
updateTokenListUM(
SetNothingToFoundStateTransformerV2(
isBalanceHidden = isBalanceHidden,
emptySearchMessageReference = getEmptySearchMessageReference(),
),
)
} else {
updateTokenListUM(
SetLoadingAccountTokenListTransformer(
appCurrency = appCurrency,
accountList = accountList.accountStatuses.toList(),
isAccountsMode = isAccountsMode,
),
)
updateTokenListUM(
UpdateAccountTokenListTransformer(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
accountList = filterByQueryAccountList.filterByAvailability(),
isBalanceHidden = isBalanceHidden,
unavailableErrorText = getUnavailableTokensHeaderReference(),
warning = getSellWarning(
hasRestrictionForSell = hasRestrictionForSell,
isInsufficientBalanceForSell = accountList.isInsufficientBalanceForSell(),
),
isAccountsMode = isAccountsMode,
),
)
}
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun getAppCurrencyAndBalanceHidingFlow(): Flow<Pair<AppCurrency, Boolean>> {
return combine(
flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
flow2 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
transform = ::Pair,
)
}
private fun hasRestrictionForSellFlow(): Flow<Boolean> {
return if (params.filterOperation == OnrampOperation.SELL) {
getUserCountryUseCase().map { maybe ->
@ -154,25 +218,52 @@ internal class OnrampTokenListModel @Inject constructor(
}
}
private fun AccountStatusList.isInsufficientBalanceForSell(): Boolean {
return if (params.filterOperation == OnrampOperation.SELL) {
(totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true
} else {
false
}
}
private fun getUnavailableTokensHeaderReference() = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header
OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header
OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header
}.let(::resourceReference)
private fun getEmptySearchMessageReference() = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message
OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message
OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message
}.let(::resourceReference)
private fun updateTokenListUM(transformer: TokenListUMTransformer) {
tokenListUMController.update { prevState ->
transformer.transform(prevState).apply {
if (isFirstInitialization(prevState = prevState, newState = this)) {
params.onTokenListInitialized()
modelScope.launch {
tokenListUMController.update { prevState ->
transformer.transform(prevState).apply {
if (isFirstInitialization(prevState = prevState, newState = this)) {
params.onTokenListInitialized()
}
}
}
}
}
private fun getSellWarning(hasRestrictionForSell: Boolean, isInsufficientBalanceForSell: Boolean) = when {
hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction
isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling
else -> null
}
private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean {
return prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() &&
(newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty())
return if (accountsFeatureToggles.isFeatureEnabled) {
prevState.tokensListData == TokenListUMData.EmptyList &&
newState.tokensListData != TokenListUMData.EmptyList
} else {
prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() &&
(newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty())
}
}
private fun onSearchQueryChange(newQuery: String) {
@ -195,6 +286,16 @@ internal class OnrampTokenListModel @Inject constructor(
)
}
private fun AccountStatusList.filterAccountsByQuery(query: String) = accountStatuses.asSequence()
.associate { accountStatus ->
when (accountStatus) {
is AccountStatus.CryptoPortfolio -> {
val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query)
accountStatus.account to filteredList
}
}
}.filter { (_, value) -> value.isNotEmpty() }
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {
return filter {
it.currency.name.contains(other = query, ignoreCase = true) ||
@ -237,6 +338,49 @@ internal class OnrampTokenListModel @Inject constructor(
}
}
private suspend fun AccountCryptoList.filterByAvailability(): List<AccountAvailabilityUM> {
return coroutineScope {
map { (account, currencies) ->
async {
AccountAvailabilityUM(
account = account,
currencyList = currencies.map { status ->
val isOperationAvailable = checkAvailabilityByOperation(status = status)
val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation
val isNotLoading = status.value !is CryptoCurrencyStatus.Loading
val requirements = getAssetRequirementsUseCase(
userWalletId = userWallet.walletId,
currency = status.currency,
).getOrNull()
val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements)
val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable
val isAvailable = when (params.filterOperation) {
OnrampOperation.BUY -> {
isAvailableForBuy
} // unreachable state is available for Buy operation
OnrampOperation.SELL -> isNotUnreachable
OnrampOperation.SWAP -> {
isNotUnreachable && isAvailableForBuy
}
}
val isTotalAvailable =
isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable
AccountCurrencyUM(
cryptoCurrencyStatus = status,
isAvailable = isTotalAvailable,
)
},
)
}
}.awaitAll()
}
}
private suspend fun checkAvailabilityByOperation(status: CryptoCurrencyStatus): Boolean {
return when (params.filterOperation) {
OnrampOperation.BUY -> {

View file

@ -3,32 +3,34 @@ package com.tangem.features.onramp.tokenlist.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
import kotlinx.collections.immutable.ImmutableList
@ -36,17 +38,21 @@ import kotlinx.collections.immutable.ImmutableList
* Token list
*
* @param state state
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
Column(modifier) {
if (state.warning == null) {
SearchBar(searchBarUM = state.searchBarUM)
} else {
AnimatedContent(targetState = state.warning, label = "") { warning ->
internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
val itemModifier = Modifier.padding(horizontal = 16.dp)
if (state.warning == null) {
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)
} else {
item("NotificationsKey") {
AnimatedContent(
targetState = state.warning,
label = "",
modifier = itemModifier,
) { warning ->
when (warning) {
is NotificationUM.Warning.OnrampErrorNotification -> {
Notification(
@ -60,31 +66,45 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
}
}
}
}
if (state.availableItems.isNotEmpty()) {
SpacerH12()
ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
}
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
if (state.unavailableItems.isNotEmpty()) {
SpacerH12()
ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
when (val list = state.tokensListData) {
is TokenListUMData.AccountList -> list.tokensList.forEach { item ->
portfolioTokensList(
portfolio = item,
isBalanceHidden = state.isBalanceHidden,
)
}
is TokenListUMData.TokenList -> {
tokensList(
items = list.tokensList,
isBalanceHidden = state.isBalanceHidden,
)
}
TokenListUMData.EmptyList -> Unit
}
}
@Composable
private fun SearchBar(searchBarUM: SearchBarUM) {
SearchBar(
state = searchBarUM,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) {
item("SearchKey") {
SearchBar(
state = searchBarUM,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
modifier = modifier,
)
}
}
@Composable
private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
items.fastForEachIndexed { index, item ->
key(item.id) {
private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
itemsIndexed(
items = items,
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TokenListItem(
state = item,
isBalanceHidden = isBalanceHidden,
@ -92,13 +112,70 @@ private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden:
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
addDefaultPadding = false,
backgroundColor = TangemTheme.colors.background.primary,
)
.testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
.semantics { lazyListItemPosition = index },
)
}
},
)
}
internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
portfolioItem(
portfolio = portfolio,
modifier = Modifier.padding(top = 8.dp),
isBalanceHidden = isBalanceHidden,
)
if (!isExpanded) return
itemsIndexed(
items = tokens,
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc()
PortfolioTokensListItem(
state = token,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.animateItem()
.roundedShapeItemDecoration(
currentIndex = indexWithHeader,
lastIndex = tokens.lastIndex.inc(),
backgroundColor = TangemTheme.colors.background.primary,
)
.conditional(tokenIndex == tokens.lastIndex) {
Modifier.padding(bottom = 8.dp)
},
)
},
)
}
private fun LazyListScope.portfolioItem(
portfolio: TokensListItemUM.Portfolio,
modifier: Modifier,
isBalanceHidden: Boolean,
) {
item(
key = "account-${portfolio.id}",
contentType = "account",
) {
PortfolioListItem(
state = portfolio,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.animateItem()
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = portfolio.tokens.lastIndex.inc(),
backgroundColor = TangemTheme.colors.background.primary,
)
.then(modifier),
)
}
}
@ -107,12 +184,12 @@ private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden:
@Composable
private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) {
TangemThemePreview {
TokenList(
state = state,
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.secondary)
.padding(16.dp),
)
LazyColumn(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
) {
onrampTokenList(
state = state,
)
}
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import kotlinx.collections.immutable.persistentListOf
internal class PreviewTokenListUMProvider : PreviewParameterProvider<TokenListUM> {
@ -42,6 +43,7 @@ internal class PreviewTokenListUMProvider : PreviewParameterProvider<TokenListUM
createUnavailableTokenItem(),
createUnavailableTokenItem(),
),
tokensListData = TokenListUMData.EmptyList,
isBalanceHidden = false,
)
}

View file

@ -454,7 +454,7 @@ internal class SendModel @Inject constructor(
}
private fun initialState(): SendUM = SendUM(
amountUM = AmountState.Empty(isRedesignEnabled = true),
amountUM = AmountState.Empty,
destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
).transform(DestinationUM.Empty()),

View file

@ -26,12 +26,10 @@ internal class SendAmountComponent(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle()
val isSendWithSwapAvailable by model.isSendWithSwapAvailable.collectAsStateWithLifecycle()
SendAmountContent(
amountState = state,
isBalanceHidden = isBalanceHidden,
clickIntents = model,
isSendWithSwapAvailable = isSendWithSwapAvailable,
modifier = modifier,

View file

@ -172,7 +172,7 @@ internal class SendAmountModel @Inject constructor(
if (uiState.value is AmountState.Empty && userWallet != null) {
val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1
_uiState.update {
AmountStateConverterV2(
AmountStateConverter(
clickIntents = this,
appCurrency = appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,

View file

@ -30,7 +30,6 @@ import com.tangem.features.send.v2.subcomponents.amount.ui.preview.SendAmountCli
@Composable
fun SendAmountContent(
amountState: AmountState,
isBalanceHidden: Boolean,
clickIntents: SendAmountClickIntents,
isSendWithSwapAvailable: Boolean,
modifier: Modifier = Modifier,
@ -38,7 +37,6 @@ fun SendAmountContent(
Column(modifier = modifier.background(TangemTheme.colors.background.tertiary)) {
AmountScreenContent(
amountState = amountState,
isBalanceHidden = isBalanceHidden,
clickIntents = clickIntents,
extraContent = {
SendConvertTokenButton(
@ -95,7 +93,6 @@ private fun SendAmountContent_Preview(@PreviewParameter(SendAmountContentPreview
TangemThemePreview {
SendAmountContent(
amountState = params,
isBalanceHidden = true,
clickIntents = SendAmountClickIntentsStub,
isSendWithSwapAvailable = true,
)

View file

@ -201,16 +201,11 @@ class SendConfirmationNotificationsTransformerV2Test {
return AmountState.Data(
isPrimaryButtonEnabled = true,
isRedesignEnabled = false,
title = mockk(relaxed = true),
availableBalance = mockk(relaxed = true),
availableBalanceCrypto = mockk(relaxed = true),
availableBalanceFiat = mockk(relaxed = true),
tokenName = mockk(relaxed = true),
tokenIconState = mockk(relaxed = true),
segmentedButtonConfig = persistentListOf(),
selectedButton = 0,
isSegmentedButtonsEnabled = false,
amountTextField = AmountFieldModel(
value = "1.5",
onValueChange = {},

View file

@ -52,7 +52,9 @@ internal class StakingAnalyticSender(
source = when (value.currentStep) {
StakingStep.InitialInfo -> StakeScreenSource.Info
StakingStep.Amount -> StakeScreenSource.Amount
StakingStep.Confirmation -> StakeScreenSource.Confirmation
StakingStep.Success,
StakingStep.Confirmation,
-> StakeScreenSource.Confirmation
StakingStep.Validators,
StakingStep.RestakeValidator,
StakingStep.RewardsValidators,

View file

@ -87,7 +87,7 @@ internal class StakingStateController @Inject constructor(
cryptoCurrencyBlockchainId = "",
currentStep = StakingStep.InitialInfo,
initialInfoState = StakingStates.InitialInfoState.Empty(),
amountState = AmountState.Empty(isRedesignEnabled = false),
amountState = AmountState.Empty,
validatorState = StakingStates.ValidatorState.Empty(),
rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(),
confirmationState = StakingStates.ConfirmationState.Empty(),

View file

@ -42,6 +42,7 @@ internal class StakingStateRouter(
StakingStep.Amount,
-> showConfirmation()
StakingStep.Confirmation -> showInitial()
StakingStep.Success -> appRouter.pop()
}
}
@ -65,6 +66,7 @@ internal class StakingStateRouter(
}
}
StakingStep.Validators -> showConfirmation()
StakingStep.Success -> appRouter.pop()
}
}

View file

@ -138,4 +138,5 @@ enum class StakingStep {
RestakeValidator,
Confirmation,
Validators,
Success,
}

View file

@ -46,10 +46,11 @@ internal class SetAmountDataTransformer(
return prevState.copy(
amountState = AmountStateConverter(
clickIntents = clickIntents,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
iconStateConverter = iconStateConverter,
maxEnterAmount = maxEnterAmount,
appCurrency = appCurrencyProvider(),
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
isBalanceHidden = false,
).convert(
AmountParameters(
title = title,

View file

@ -25,7 +25,6 @@ internal class SetButtonsStateTransformer(
val buttonsState = if (prevState.isButtonsVisible()) {
NavigationButtonsState.Data(
primaryButton = getPrimaryButton(prevState),
prevButton = getPrevButton(prevState),
extraButtons = getExtraButtons(prevState).takeIf { txUrl != null },
txUrl = txUrl,
onTextClick = urlOpener::openUrl,
@ -64,18 +63,6 @@ internal class SetButtonsStateTransformer(
)
}
private fun getPrevButton(prevState: StakingUiState): NavigationButton? {
return NavigationButton(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
isSecondary = true,
isIconVisible = true,
shouldShowProgress = false,
isEnabled = true,
onClick = prevState.clickIntents::onPrevClick,
).takeIf { prevState.currentStep.isPrevButtonVisible() }
}
private fun getExtraButtons(prevState: StakingUiState): Pair<NavigationButton, NavigationButton> {
return NavigationButton(
textReference = resourceReference(R.string.common_explore),
@ -111,7 +98,7 @@ internal class SetButtonsStateTransformer(
resourceReference(R.string.common_stake)
}
}
StakingStep.Success -> resourceReference(R.string.common_close)
StakingStep.Confirmation -> getConfirmationButtonText()
StakingStep.Validators -> resourceReference(R.string.common_continue)
StakingStep.Amount,
@ -125,21 +112,17 @@ internal class SetButtonsStateTransformer(
val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data
val amountState = amountState as? AmountState.Data
return if (confirmationState != null && amountState != null) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
resourceReference(R.string.common_close)
} else {
when (actionType) {
is StakingActionCommonType.Enter -> {
val amount = amountState.amountTextField.cryptoAmount.value.orZero()
if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) {
resourceReference(R.string.give_permission_title)
} else {
resourceReference(R.string.common_stake)
}
when (actionType) {
is StakingActionCommonType.Enter -> {
val amount = amountState.amountTextField.cryptoAmount.value.orZero()
if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) {
resourceReference(R.string.give_permission_title)
} else {
resourceReference(R.string.common_stake)
}
is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake)
is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle()
}
is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake)
is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle()
}
} else {
resourceReference(R.string.common_close)
@ -155,6 +138,7 @@ internal class SetButtonsStateTransformer(
StakingStep.Amount -> clickIntents.onAmountEnterClick()
StakingStep.Confirmation -> onConfirmationClick()
StakingStep.RewardsValidators -> Unit
StakingStep.Success -> clickIntents.onBackClick()
}
}
@ -178,17 +162,6 @@ internal class SetButtonsStateTransformer(
}
}
private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.RestakeValidator,
StakingStep.Confirmation,
StakingStep.Validators,
-> false
StakingStep.Amount,
-> true
}
private fun StakingUiState.isPrimaryButtonDisabled(): Boolean {
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
val hasNotStaking = initialState?.yieldBalance == InnerYieldBalanceState.Empty
@ -205,6 +178,7 @@ internal class SetButtonsStateTransformer(
StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled
StakingStep.RestakeValidator,
StakingStep.Validators,
StakingStep.Success,
-> true
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.TransactionDoneState
import com.tangem.utils.transformer.Transformer
@ -17,6 +18,7 @@ internal class SetConfirmationStateCompletedTransformer(
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmationState = prevState.confirmationState.copyWrapped(),
currentStep = StakingStep.Success,
)
}

View file

@ -229,10 +229,11 @@ internal class SetInitialDataStateTransformer(
)
return AmountStateConverter(
clickIntents = clickIntents,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
appCurrencyProvider = appCurrencyProvider,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrencyProvider(),
iconStateConverter = iconStateConverter,
maxEnterAmount = maxEnterAmount,
isBalanceHidden = false,
).convert(
AmountParameters(
title = stringReference(userWalletProvider().name),

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -28,7 +29,7 @@ internal object SetTitleTransformer : Transformer<StakingUiState> {
R.string.staking_title_stake,
wrappedList(prevState.cryptoCurrencyName),
)
StakingStep.Success -> TextReference.EMPTY
StakingStep.Confirmation -> {
when (actionType) {
is StakingActionCommonType.Enter -> resourceReference(

View file

@ -14,12 +14,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.common.ui.amountScreen.ui.AmountBlockV2
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -30,7 +31,6 @@ import com.tangem.features.staking.impl.presentation.state.stub.StakingClickInte
import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock
import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock
import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
@Suppress("LongParameterList")
@Composable
@ -60,7 +60,7 @@ internal fun StakingConfirmationContent(
subtitle = resourceReference(R.string.staking_transaction_in_progress_text),
)
}
AmountBlock(
AmountBlockV2(
amountState = amountState,
isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress,
isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED,

View file

@ -91,6 +91,7 @@ private fun StakingAppBar(uiState: StakingUiState) {
val (backIcon, click) = when (uiState.currentStep) {
StakingStep.Amount,
StakingStep.Confirmation,
StakingStep.Success,
-> R.drawable.ic_close_24 to uiState.clickIntents::onBackClick
StakingStep.Validators,
StakingStep.RewardsValidators,
@ -108,6 +109,7 @@ private fun StakingAppBar(uiState: StakingUiState) {
)
}
@Suppress("LongMethod")
@Composable
private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) {
val currentScreen = uiState.currentStep
@ -136,10 +138,10 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
contentAlignment = Alignment.TopCenter,
label = "Staking Screen Navigation",
transitionSpec = {
val direction = if (initialState.ordinal < targetState.ordinal) {
AnimatedContentTransitionScope.SlideDirection.Start
} else {
AnimatedContentTransitionScope.SlideDirection.End
val direction = when {
targetState == StakingStep.Success -> AnimatedContentTransitionScope.SlideDirection.Up
initialState.ordinal < targetState.ordinal -> AnimatedContentTransitionScope.SlideDirection.Start
else -> AnimatedContentTransitionScope.SlideDirection.End
}
slideIntoContainer(towards = direction, animationSpec = tween())
@ -163,7 +165,6 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
}
StakingStep.Amount -> AmountScreenContent(
amountState = uiState.amountState,
isBalanceHidden = uiState.isBalanceHidden,
clickIntents = uiState.clickIntents,
modifier = Modifier.background(TangemTheme.colors.background.secondary),
)
@ -173,6 +174,12 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
validatorState = uiState.validatorState,
clickIntents = uiState.clickIntents,
)
StakingStep.Success -> StakingSuccessContent(
amountState = uiState.amountState,
state = uiState.confirmationState,
validatorState = uiState.validatorState,
clickIntents = uiState.clickIntents,
)
StakingStep.RestakeValidator,
StakingStep.Validators,
-> StakingValidatorListContent(

View file

@ -0,0 +1,86 @@
package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock
import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock
import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock
@Suppress("LongParameterList")
@Composable
internal fun StakingSuccessContent(
amountState: AmountState,
state: StakingStates.ConfirmationState,
validatorState: StakingStates.ValidatorState,
clickIntents: StakingClickIntents,
) {
if (state !is StakingStates.ConfirmationState.Data) return
val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED
val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress }
Column(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing16)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
TransactionDoneTitle(
title = resourceReference(R.string.common_in_progress),
subtitle = resourceReference(R.string.staking_transaction_in_progress_text),
)
AmountBlock(
amountState = amountState,
isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress,
isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED,
onClick = clickIntents::onPrevClick,
)
ValidatorBlock(
validatorState = validatorState,
isClickable = !isTransactionInProgress,
onClick = clickIntents::openValidators,
)
StakingFeeBlock(feeState = state.feeState)
NotificationsBlock(notifications = state.notifications)
Spacer(Modifier)
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_StakingConfirmationContent() {
TangemThemePreview {
Column(Modifier.background(TangemTheme.colors.background.primary)) {
StakingConfirmationContent(
amountState = AmountStatePreviewData.amountState,
state = ConfirmationStatePreviewData.assentStakingState,
validatorState = ValidatorStatePreviewData.validatorState,
clickIntents = StakingClickIntentsStub,
)
}
}
}

View file

@ -64,19 +64,13 @@ sealed class SwapAmountFieldUM {
data class Empty(
override val amountType: SwapAmountType,
) : SwapAmountFieldUM() {
override val amountField: AmountState = AmountState.Empty(
isPrimaryButtonEnabled = false,
isRedesignEnabled = true,
)
override val amountField: AmountState = AmountState.Empty
}
data class Loading(
override val amountType: SwapAmountType,
) : SwapAmountFieldUM() {
override val amountField: AmountState = AmountState.Empty(
isPrimaryButtonEnabled = false,
isRedesignEnabled = true,
)
override val amountField: AmountState = AmountState.Empty
}
data class Content(

View file

@ -1,7 +1,7 @@
package com.tangem.features.swap.v2.impl.amount.model.converter
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.AmountStateConverterV2
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.core.ui.components.atoms.text.TextEllipsis
@ -44,7 +44,7 @@ internal class SwapAmountFieldConverter(
subtitleEllipsisRight = TextEllipsis.OffsetEnd(appCurrency.symbol.length),
priceImpact = null,
isClickEnabled = selectedType.isViewingField(),
amountField = AmountStateConverterV2(
amountField = AmountStateConverter(
clickIntents = clickIntents,
appCurrency = appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,

View file

@ -46,9 +46,7 @@ internal class SwapAmountBalanceHiddenTransformer(
val newData = recalculatedPrimary.amountField
newData.copy(
amountTextField = oldData.amountTextField,
selectedButton = oldData.selectedButton,
isPrimaryButtonEnabled = oldData.isPrimaryButtonEnabled,
isSegmentedButtonsEnabled = oldData.isSegmentedButtonsEnabled,
isEditingDisabled = oldData.isEditingDisabled,
reduceAmountBy = oldData.reduceAmountBy,
isIgnoreReduce = oldData.isIgnoreReduce,

View file

@ -127,7 +127,6 @@ private fun ConstraintLayoutScope.SwapAmountBlock(
AmountBlockV2(
amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy(
title = resourceReference(R.string.send_with_swap_recipient_amount_title),
availableBalance = TextReference.EMPTY,
availableBalanceCrypto = TextReference.EMPTY,
) ?: amountUM.secondaryAmount.amountField,
isClickDisabled = true,

View file

@ -97,9 +97,7 @@ internal data object SwapAmountContentPreview {
val defaultState = SwapAmountUM.Content(
primaryAmount = SwapAmountFieldUM.Content(
amountType = SwapAmountType.From,
amountField = AmountStatePreviewData.amountState.copy(
availableBalance = stringReference("Balance: 100 BTC"),
),
amountField = AmountStatePreviewData.amountState,
title = stringReference("Tether"),
subtitleLeft = stringReference("11 101,123123456 BTC"),
subtitleRight = stringReference(" ${StringsSigns.DOT} 1 212,12 $"),
@ -112,7 +110,6 @@ internal data object SwapAmountContentPreview {
amountType = SwapAmountType.To,
amountField = AmountStatePreviewData.amountState.copy(
title = stringReference("Amount to receive"),
availableBalance = TextReference.EMPTY,
),
title = stringReference("Shiba Inu"),
priceImpact = stringReference("(-10%)"),

View file

@ -21,6 +21,7 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
companion object {
val loadingUM = TangemPayTxHistoryUM.Loading(isBalanceHidden = true)
val emptyUM = TangemPayTxHistoryUM.Empty(isBalanceHidden = true)
val errorUM = TangemPayTxHistoryUM.Error(isBalanceHidden = true, onReload = {})
val contentUM = TangemPayTxHistoryUM.Content(
isBalanceHidden = false,
loadMore = { false },

View file

@ -0,0 +1,29 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.tangempay.details.impl.R
internal sealed class TangemPayEmptyTransactionHistoryState {
abstract val iconRes: Int
abstract val text: TextReference
data class FailedToLoad(
private val onReload: () -> Unit,
) : TangemPayEmptyTransactionHistoryState() {
override val iconRes: Int = R.drawable.ic_alert_history_64
override val text: TextReference = resourceReference(R.string.transaction_history_error_failed_to_load)
val actionButtonConfig = ActionButtonConfig(
text = resourceReference(R.string.common_reload),
iconResId = R.drawable.ic_refresh_24,
onClick = onReload,
)
}
data object Empty : TangemPayEmptyTransactionHistoryState() {
override val iconRes: Int = R.drawable.ic_empty_token_64
override val text: TextReference = resourceReference(R.string.transaction_history_empty_transactions)
}
}

View file

@ -54,11 +54,16 @@ internal class TangemPayTxHistoryModel @Inject constructor(
.onEach(::updateState)
.launchIn(modelScope)
listManager.paginationStatus
.onEach { paginationStatus -> handlePaginationStatus(paginationStatus) }
.onEach(::handlePaginationStatus)
.launchIn(modelScope)
listManager.emptyStatus
.onEach(::handleEmptyState)
.launchIn(modelScope)
}
private fun updateState(items: ImmutableList<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>) {
if (items.isEmpty()) return // fast exit. If items is empty, no need to update ui items
uiState.update { state ->
if (state is TangemPayTxHistoryUM.Content) {
state.copy(items = items)
@ -72,6 +77,12 @@ internal class TangemPayTxHistoryModel @Inject constructor(
}
}
private fun handleEmptyState(isEmpty: Boolean) {
if (isEmpty) {
uiState.update { getEmptyState(it.isBalanceHidden) }
}
}
private fun handlePaginationStatus(status: PaginationStatus<*>) {
uiState.update { state ->
when (status) {
@ -108,6 +119,10 @@ internal class TangemPayTxHistoryModel @Inject constructor(
Timber.d("onTransactionClick: $item")
}
private fun getEmptyState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Empty {
return TangemPayTxHistoryUM.Empty(isBalanceHidden = isBalanceHidden)
}
private fun getErrorState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Error {
return TangemPayTxHistoryUM.Error(isBalanceHidden = isBalanceHidden, onReload = ::reload)
}

View file

@ -2,12 +2,7 @@ package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -15,12 +10,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -44,21 +35,14 @@ import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.entity.*
import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
@ -415,8 +399,8 @@ private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modi
)
}
@Preview(device = Devices.PIXEL_7_PRO, group = "day")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO, group = "night")
@Preview(device = Devices.PIXEL_7_PRO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO)
@Composable
private fun TangemPayDetailsScreenPreview(
@PreviewParameter(TangemPayDetailsUMProvider::class) state: TangemPayDetailsUM,
@ -473,4 +457,26 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
isBalanceHidden = false,
),
),
)
@Preview(device = Devices.PIXEL_7_PRO)
@Composable
private fun TangemPayDetailsTxHistoryScreenPreview(
@PreviewParameter(TangemPayDetailsTxHistoryProvider::class) state: TangemPayTxHistoryUM,
) {
TangemThemePreview {
TangemPayDetailsScreen(
state = TangemPayDetailsUMProvider().values.first(),
txHistoryComponent = PreviewTangemPayTxHistoryComponent(txHistoryUM = state),
)
}
}
private class TangemPayDetailsTxHistoryProvider : CollectionPreviewParameterProvider<TangemPayTxHistoryUM>(
collection = listOf(
PreviewTangemPayTxHistoryComponent.loadingUM,
PreviewTangemPayTxHistoryComponent.contentUM,
PreviewTangemPayTxHistoryComponent.emptyUM,
PreviewTangemPayTxHistoryComponent.errorUM,
),
)

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ChainStyle
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
@ -27,6 +28,7 @@ import coil.compose.rememberAsyncImagePainter
import com.tangem.core.ui.R
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
@ -34,7 +36,9 @@ import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.EmptyTransactionBlockTestTags
import com.tangem.core.ui.test.TransactionHistoryBlockTestTags
import com.tangem.features.tangempay.entity.TangemPayEmptyTransactionHistoryState
import com.tangem.features.tangempay.entity.TangemPayTransactionState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
@ -43,12 +47,25 @@ private const val LOAD_ITEMS_BUFFER = 20
internal fun LazyListScope.tangemPayTxHistoryItems(listState: LazyListState, state: TangemPayTxHistoryUM) {
when (state) {
is TangemPayTxHistoryUM.Content -> contentItems(listState = listState, state = state)
is TangemPayTxHistoryUM.Empty -> TODO("[REDACTED_JIRA]")
is TangemPayTxHistoryUM.Error -> TODO("[REDACTED_JIRA]")
is TangemPayTxHistoryUM.Empty -> nonContentItem(state = TangemPayEmptyTransactionHistoryState.Empty)
is TangemPayTxHistoryUM.Error -> nonContentItem(
state = TangemPayEmptyTransactionHistoryState.FailedToLoad(onReload = state.onReload),
)
is TangemPayTxHistoryUM.Loading -> loadingItems(state = state)
}
}
private fun LazyListScope.nonContentItem(state: TangemPayEmptyTransactionHistoryState, modifier: Modifier = Modifier) {
item(key = state::class.java, contentType = state::class.java) {
TangemPayEmptyTransactionBlock(
state = state,
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12)
.fillMaxWidth(),
)
}
}
private fun LazyListScope.contentItems(listState: LazyListState, state: TangemPayTxHistoryUM.Content) {
itemsIndexed(
items = state.items,
@ -370,4 +387,49 @@ private fun Timestamp(state: TangemPayTransactionState, modifier: Modifier = Mod
)
}
}
}
@Composable
private fun TangemPayEmptyTransactionBlock(
state: TangemPayEmptyTransactionHistoryState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary)
.padding(vertical = TangemTheme.dimens.spacing24)
.testTag(EmptyTransactionBlockTestTags.BLOCK),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size64)
.testTag(EmptyTransactionBlockTestTags.ICON),
painter = painterResource(id = state.iconRes),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing32)
.testTag(EmptyTransactionBlockTestTags.TEXT),
textAlign = TextAlign.Center,
text = state.text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
when (state) {
is TangemPayEmptyTransactionHistoryState.Empty -> Unit
is TangemPayEmptyTransactionHistoryState.FailedToLoad -> ActionButton(
modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth(),
config = state.actionButtonConfig,
)
}
}
}

View file

@ -33,6 +33,7 @@ internal class TangemPayTxHistoryListManager(
private val uiManager = TangemPayTxHistoryUiManager(state = state, txHistoryUiActions = txHistoryUiActions)
val uiItems: Flow<ImmutableList<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>> = uiManager.items
val emptyStatus: Flow<Boolean> = state.map { it.isEmpty }.distinctUntilChanged()
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
suspend fun launchPagination() = coroutineScope {
@ -80,6 +81,7 @@ internal class TangemPayTxHistoryListManager(
newCurrencyBatches = batchListState.data,
clearUiBatches = clearUiBatches,
),
isEmpty = batchListState.status is PaginationStatus.EndOfPagination && batchListState.data.isEmpty(),
)
}
}

View file

@ -7,4 +7,5 @@ import com.tangem.pagination.PaginationStatus
internal data class TangemPayTxHistoryState(
val status: PaginationStatus<*> = PaginationStatus.None,
val uiBatches: List<Batch<Int, List<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>>> = listOf(),
val isEmpty: Boolean = false,
)

View file

@ -56,7 +56,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
repository.getCustomerInfo()
.onRight { customerInfo ->
when {
!customerInfo.isKycApproved() -> {
!customerInfo.isKycApproved -> {
when (params) {
is TangemPayOnboardingComponent.Params.Deeplink ->
screenState.value = screenState.value.copy(fullScreenLoading = false)

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity
import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen
import com.tangem.feature.tester.presentation.accounts.viewmodel.AccountsViewModel
import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel
import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel
import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen
@ -155,7 +155,7 @@ internal class TesterActivity : ComposeActivity() {
}
composable(route = TesterScreen.ACCOUNTS.name) {
val viewModel = hiltViewModel<AccountsViewModel>().apply {
val viewModel = hiltViewModel<TesterAccountsViewModel>().apply {
setupNavigation(innerTesterRouter)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()

View file

@ -9,9 +9,8 @@ internal data class AccountsUM(
val onBackClick: () -> Unit,
val walletSelector: WalletSelector,
val accountListBottomSheetConfig: AccountListBottomSheetConfig,
val onAccountsClick: () -> Unit,
val onAccountsClick: () -> Boolean,
val onFetchAccountsClick: () -> Unit,
val onCreateMainAccountClick: () -> Unit,
val onClearETagClick: () -> Unit,
) {

View file

@ -62,8 +62,9 @@ internal fun AccountsScreen(state: AccountsUM, modifier: Modifier = Modifier) {
ManageAccountsButtons(
state = state,
onAccountsClick = { context ->
if (state.accountListBottomSheetConfig.accounts.isNotEmpty()) {
state.onAccountsClick()
val isEmpty = state.onAccountsClick()
if (!isEmpty) {
isAccountListShown = true
} else {
Toast.makeText(context, "No accounts found", Toast.LENGTH_SHORT).show()
@ -242,16 +243,4 @@ private fun LazyListScope.ManageAccountsButtons(state: AccountsUM, onAccountsCli
.fillMaxWidth(),
)
}
if (state.accountListBottomSheetConfig.accounts.none { it.isMainAccount }) {
item {
PrimaryButton(
text = "Create Main account",
onClick = state.onCreateMainAccountClick,
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
)
}
}
}

View file

@ -6,13 +6,9 @@ import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.tester.presentation.accounts.entity.AccountsUM
@ -28,11 +24,10 @@ import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
internal class AccountsViewModel @Inject constructor(
internal class TesterAccountsViewModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val singleAccountListFetcher: SingleAccountListFetcher,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val accountsCRUDRepository: AccountsCRUDRepository,
private val eTagsStore: ETagsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel() {
@ -88,7 +83,6 @@ internal class AccountsViewModel @Inject constructor(
),
onAccountsClick = ::updateAccountsList,
onFetchAccountsClick = ::fetchAccounts,
onCreateMainAccountClick = ::createMainAccount,
onClearETagClick = ::clearETag,
)
}
@ -107,8 +101,8 @@ internal class AccountsViewModel @Inject constructor(
}
}
private fun updateAccountsList() {
val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return
private fun updateAccountsList(): Boolean {
val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return false
val accounts = walletAccounts.value[userWalletId]?.accounts
?.filterIsInstance<Account.CryptoPortfolio>()
@ -122,6 +116,8 @@ internal class AccountsViewModel @Inject constructor(
),
)
}
return accounts.isEmpty()
}
private fun fetchAccounts() {
@ -134,28 +130,6 @@ internal class AccountsViewModel @Inject constructor(
}
}
private fun createMainAccount() {
viewModelScope.launch {
val userWallet = uiState.value.walletSelector.selected ?: return@launch
// It's temporary solution to create main account for testing purposes
val accountList = AccountList(
userWallet = userWallet,
accounts = setOf(
Account.CryptoPortfolio.createMainAccount(userWallet.walletId).copy(
accountName = AccountName.invoke(value = "Main Account").getOrNull()!!,
),
),
totalAccounts = 1,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
.getOrNull()!!
accountsCRUDRepository.saveAccounts(accountList)
}
}
private fun clearETag() {
viewModelScope.launch {
val userWallet = uiState.value.walletSelector.selected ?: return@launch

View file

@ -22,6 +22,7 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.scan.CardDTO
@ -81,6 +82,7 @@ internal class WalletSettingsModel @Inject constructor(
private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase,
private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase,
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -173,6 +175,7 @@ internal class WalletSettingsModel @Inject constructor(
isUpgradeNotificationEnabled: Boolean,
accountList: List<WalletSettingsAccountsUM>,
): PersistentList<WalletSettingsItemUM> {
val accountsFeatureEnabled = accountsFeatureToggles.isFeatureEnabled
val isMultiCurrency = when (userWallet) {
is UserWallet.Cold -> userWallet.isMultiCurrency
is UserWallet.Hot -> true
@ -188,7 +191,7 @@ internal class WalletSettingsModel @Inject constructor(
is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup
is UserWallet.Hot -> false
},
isManageTokensAvailable = isMultiCurrency,
isManageTokensAvailable = !accountsFeatureEnabled && isMultiCurrency,
isNFTFeatureEnabled = isMultiCurrency,
isNFTEnabled = isNFTEnabled,
onCheckedNFTChange = ::onCheckedNFTChange,

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.walletsettings.analytics.Settings
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM
@ -207,7 +208,7 @@ internal class ItemsBuilder @Inject constructor(
iconRes = R.drawable.ic_tether_24,
onClick = {
analyticsEventHandler.send(Settings.ButtonManageTokens)
router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId))
router.push(AppRoute.ManageTokens(Source.SETTINGS, PortfolioId(userWalletId)))
},
).let(::add)
}

View file

@ -359,10 +359,10 @@ internal class WalletModel @Inject constructor(
val info = tangemPayMainScreenCustomerInfoUseCase()
if (info != null) {
stateHolder.update(
transformer = TangemPayStateTransformer(
transformer = TangemPayInitialStateTransformer(
value = info,
onIssueOrderClick = ::issueOrder,
onContinueKycClick = innerWalletRouter::openTangemPayOnboarding,
onClickIssue = ::issueOrder,
onClickKyc = innerWalletRouter::openTangemPayOnboarding,
openDetails = innerWalletRouter::openTangemPayDetails,
),
)
@ -371,9 +371,9 @@ internal class WalletModel @Inject constructor(
private fun issueOrder() {
modelScope.launch {
stateHolder.update(TangemPayStateTransformer(issueProgressState = true))
stateHolder.update(TangemPayIssueProgressStateTransformer())
tangemPayIssueOrderUseCase().onLeft {
stateHolder.update(TangemPayStateTransformer(issueState = true, onIssueOrderClick = ::issueOrder))
stateHolder.update(TangemPayIssueAvailableStateTransformer(onClickIssue = ::issueOrder))
}
}
}

View file

@ -4,17 +4,15 @@ import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -39,15 +37,13 @@ internal interface WalletContentClickIntents {
fun onDetailsClick()
fun onManageTokensClick()
fun onOrganizeTokensClick()
fun onDismissMarketsOnboarding()
fun onTokenItemClick(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus)
fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus)
fun onTokenItemLongClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onAccountExpandClick(account: Account)
@ -81,7 +77,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val reduxStateHolder: ReduxStateHolder,
private val walletEventSender: WalletEventSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
@ -122,11 +117,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
}
override fun onManageTokensClick() {
reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess)
router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
override fun onOrganizeTokensClick() {
router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
@ -138,13 +128,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
}
override fun onTokenItemClick(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) {
router.openTokenDetails(portfolioId, currencyStatus)
override fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
router.openTokenDetails(userWalletId, currencyStatus)
}
override fun onTokenItemLongClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
modelScope.launch(dispatchers.main) {
val userWalletId = portfolioId.userWalletId
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
Timber.e(
"""
@ -160,7 +149,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus)
.take(count = 1)
.collectLatest {
showActionsBottomSheet(it, userWallet, portfolioId)
showActionsBottomSheet(it, userWallet)
}
}
}
@ -175,17 +164,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId)
}
private fun showActionsBottomSheet(
tokenActionsState: TokenActionsState,
userWallet: UserWallet,
portfolioId: PortfolioId,
) {
private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) {
stateHolder.showBottomSheet(
ActionsBottomSheetConfig(
actions = MultiWalletCurrencyActionsConverter(
userWallet = userWallet,
clickIntents = currencyActionsClickIntents,
portfolioId = portfolioId,
).convert(tokenActionsState),
),
userWallet.walletId,

View file

@ -29,7 +29,6 @@ import com.tangem.domain.core.utils.lceError
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
@ -43,12 +42,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.NeedShowYieldSupplyDepositedWarningUseCase
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.SaveViewedYieldSupplyWarningUseCase
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
@ -57,6 +51,7 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
@ -70,7 +65,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.WalletFeatureUseCasesFacade
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -84,7 +78,7 @@ import javax.inject.Inject
interface WalletCurrencyActionsClickIntents {
fun onSendClick(
portfolioId: PortfolioId,
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
)
@ -92,32 +86,32 @@ interface WalletCurrencyActionsClickIntents {
fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason)
fun onBuyClick(
portfolioId: PortfolioId,
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
)
fun onSwapClick(
cryptoCurrencyStatus: CryptoCurrencyStatus,
portfolioId: PortfolioId,
userWalletId: UserWalletId,
unavailabilityReason: ScenarioUnavailabilityReason,
)
fun onReceiveClick(
portfolioId: PortfolioId,
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
event: AnalyticsEvent? = null,
)
fun onStakeClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?)
fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?)
fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference?
fun onCopyAddressClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onHideTokensClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onPerformHideToken(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onExploreClick()
@ -139,7 +133,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val useCasesFacade: WalletFeatureUseCasesFacade,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
@ -158,10 +151,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
override fun onSendClick(
portfolioId: PortfolioId,
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
) {
@ -185,27 +180,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
modelScope.launch {
saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name)
stateHolder.hideBottomSheet()
navigateToSend(cryptoCurrencyStatus, portfolioId)
navigateToSend(cryptoCurrencyStatus, userWalletId)
}
},
)
} else {
navigateToSend(cryptoCurrencyStatus, portfolioId)
navigateToSend(cryptoCurrencyStatus, userWalletId)
}
}
}
override fun onReceiveClick(
portfolioId: PortfolioId,
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
event: AnalyticsEvent?,
) {
val userWalletId = portfolioId.userWalletId
if (portfolioId is PortfolioId.Account) {
// todo account find address
TODO("account")
}
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive(
token = cryptoCurrencyStatus.currency.symbol,
@ -278,12 +267,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
}
override fun onCopyAddressClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
val userWalletId = portfolioId.userWalletId
if (portfolioId is PortfolioId.Account) {
// todo account find address
TODO("account")
}
override fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
analyticsEventHandler.send(
event = TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
token = cryptoCurrencyStatus.currency.symbol,
@ -305,7 +289,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
override fun onHideTokensClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol),
)
@ -313,19 +297,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
modelScope.launch(dispatchers.main) {
walletEventSender.send(
event = WalletEvent.ShowAlert(
state = getHideTokeAlertConfig(portfolioId, cryptoCurrencyStatus),
state = getHideTokeAlertConfig(userWalletId, cryptoCurrencyStatus),
),
)
}
}
private suspend fun getHideTokeAlertConfig(
portfolioId: PortfolioId,
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): WalletAlertState.DefaultAlert {
val currency = cryptoCurrencyStatus.currency
val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin &&
!useCasesFacade.isCryptoCurrencyCoinCouldHide(portfolioId = portfolioId, cryptoCurrencyCoin = currency)
!isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency)
return if (isCryptoCurrencyCoinCouldHide) {
WalletAlertState.DefaultAlert(
title = resourceReference(
@ -351,14 +335,14 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)),
),
message = resourceReference(R.string.token_details_hide_alert_message),
onConfirmClick = { onPerformHideToken(portfolioId, cryptoCurrencyStatus) },
onConfirmClick = { onPerformHideToken(userWalletId, cryptoCurrencyStatus) },
)
}
}
override fun onPerformHideToken(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
modelScope.launch(dispatchers.io) {
useCasesFacade.removeCurrencyUseCase(portfolioId, cryptoCurrencyStatus.currency)
removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency)
.fold(
ifLeft = {
walletEventSender.send(
@ -366,7 +350,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
},
ifRight = {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId))
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
},
)
}
@ -399,7 +383,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
override fun onBuyClick(
portfolioId: PortfolioId,
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
) {
@ -415,7 +399,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
appRouter.push(
AppRoute.Onramp(
portfolioId = portfolioId,
userWalletId = userWalletId,
currency = cryptoCurrencyStatus.currency,
source = OnrampSource.TOKEN_LONG_TAP,
),
@ -424,7 +408,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
override fun onSwapClick(
cryptoCurrencyStatus: CryptoCurrencyStatus,
portfolioId: PortfolioId,
userWalletId: UserWalletId,
unavailabilityReason: ScenarioUnavailabilityReason,
) {
analyticsEventHandler.send(
@ -447,12 +431,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
modelScope.launch {
saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name)
stateHolder.hideBottomSheet()
navigateToSwap(cryptoCurrencyStatus, portfolioId)
navigateToSwap(cryptoCurrencyStatus, userWalletId)
}
},
)
} else {
navigateToSwap(cryptoCurrencyStatus, portfolioId)
navigateToSwap(cryptoCurrencyStatus, userWalletId)
}
}
}
@ -493,15 +477,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
override fun onStakeClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId))
override fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
modelScope.launch {
val cryptoCurrency = cryptoCurrencyStatus.currency
appRouter.push(
AppRoute.Staking(
portfolioId = portfolioId,
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
yieldId = yield?.id ?: return@launch,
),
@ -519,15 +503,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {
val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return
val tokenListState = selectedWallet.tokensListState as? WalletTokensListState.ContentState.Content ?: return
val tokenListState = selectedWallet.tokensListState
if (tokenListState.items.count { it is TokensListItemUM.Token } < 2) {
handleError(
alertState = WalletAlertState.InsufficientTokensCountForSwapping,
eventCreator = MainScreenAnalyticsEvent::ButtonSwap,
when (tokenListState) {
is WalletTokensListState.ContentState.Content -> checkSwapCryptoAvailability(
tokenCount = tokenListState.items.count { it is TokensListItemUM.Token },
)
return
is WalletTokensListState.ContentState.PortfolioContent -> checkSwapCryptoAvailability(
tokenCount = tokenListState.items.sumOf { it.tokens.count { it is TokensListItemUM.Token } },
)
WalletTokensListState.ContentState.Loading,
WalletTokensListState.ContentState.Locked,
WalletTokensListState.Empty,
-> return
}
modelScope.launch {
@ -766,21 +754,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
}
private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, portfolioId: PortfolioId) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId))
private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
val route = AppRoute.Send(
currency = cryptoCurrencyStatus.currency,
portfolioId = portfolioId,
userWalletId = userWalletId,
)
appRouter.push(route)
}
private fun navigateToSwap(cryptoCurrencyStatus: CryptoCurrencyStatus, portfolioId: PortfolioId) {
private fun navigateToSwap(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) {
appRouter.push(
AppRoute.Swap(
currencyFrom = cryptoCurrencyStatus.currency,
portfolioId = portfolioId,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.LongTap.value,
),
)
@ -808,4 +796,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
}
}
private fun checkSwapCryptoAvailability(tokenCount: Int) {
if (tokenCount < 2) {
handleError(
alertState = WalletAlertState.InsufficientTokensCountForSwapping,
eventCreator = MainScreenAnalyticsEvent::ButtonSwap,
)
return
}
}
}

View file

@ -365,7 +365,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
source = OnrampSource.SEPA_BANNER,
launchSepa = true,
shouldLaunchSepa = true,
),
)
}

View file

@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarCon
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal object WalletScreenPreviewData {
private val tokenItemState = TokenItemState.Content(
@ -86,15 +87,17 @@ internal object WalletScreenPreviewData {
private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent(
items = persistentListOf(
TokensListItemUM.Portfolio(
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>(),
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
isExpanded = false,
state = AccountItemPreviewData.accountItem
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem
.copy(iconState = AccountItemPreviewData.accountLetterIcon),
),
TokensListItemUM.Portfolio(
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>(),
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
isExpanded = true,
state = AccountItemPreviewData.accountItem,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem,
),
),
organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig(

View file

@ -3,11 +3,9 @@ package com.tangem.feature.wallet.presentation.router
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -67,12 +65,12 @@ internal class DefaultWalletRouter @Inject constructor(
urlOpener.openUrl(url)
}
override fun openTokenDetails(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) {
override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
val networkAddress = currencyStatus.value.networkAddress
if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) {
router.push(
AppRoute.CurrencyDetails(
portfolioId = portfolioId,
userWalletId = userWalletId,
currency = currencyStatus.currency,
),
)
@ -87,10 +85,6 @@ internal class DefaultWalletRouter @Inject constructor(
return router.stack.lastOrNull() is AppRoute.Wallet
}
override fun openManageTokensScreen(userWalletId: UserWalletId) {
router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId))
}
override fun openScanFailedDialog(onTryAgain: () -> Unit) {
reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain))
}

View file

@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.router
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -42,7 +41,7 @@ internal interface InnerWalletRouter {
fun openUrl(url: String)
/** Open token details screen */
fun openTokenDetails(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus)
fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus)
/** Open stories screen */
fun openStoriesScreen()
@ -50,9 +49,6 @@ internal interface InnerWalletRouter {
/** Is wallet last screen */
fun isWalletLastScreen(): Boolean
/** Open manage tokens screen */
fun openManageTokensScreen(userWalletId: UserWalletId)
/** Open scan failed dialog */
fun openScanFailedDialog(onTryAgain: () -> Unit)

View file

@ -14,7 +14,6 @@ import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
@ -38,33 +37,41 @@ internal class TokenListAnalyticsSender @Inject constructor(
private val mutex = Mutex()
private val loadingTraces = mutableMapOf<UserWalletId, Trace>()
suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) {
suspend fun send(
displayedUiState: WalletState?,
userWallet: UserWallet,
totalFiatBalance: TotalFiatBalance,
flattenCurrencies: List<CryptoCurrencyStatus>,
) {
if (screenLifecycleProvider.isBackgroundState.value) return
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) {
startLoadingTraceIfNeeded(userWallet.walletId, tokenList)
if (totalFiatBalance is TotalFiatBalance.Loading) {
startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies)
return
}
if (isTerminalState(tokenList.totalFiatBalance)) {
stopLoadingTraceIfNeeded(userWallet.walletId, tokenList.totalFiatBalance)
if (isTerminalState(totalFiatBalance)) {
stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance)
}
val currenciesStatuses = tokenList.flattenCurrencies()
val currenciesStatuses = flattenCurrencies
sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses)
sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses)
sendBalanceLoadedEventIfNeeded(totalFiatBalance, currenciesStatuses)
sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, currenciesStatuses)
sendUnreachableNetworksEventIfNeeded(currenciesStatuses)
sendTokenBalancesIfNeeded(currenciesStatuses)
}
private suspend fun startLoadingTraceIfNeeded(userWalletId: UserWalletId, tokenList: TokenList) {
private suspend fun startLoadingTraceIfNeeded(
userWalletId: UserWalletId,
flattenCurrencies: List<CryptoCurrencyStatus>,
) {
mutex.withLock {
if (!loadingTraces.containsKey(userWalletId)) {
val trace = FirebasePerformance.getInstance().newTrace(BALANCE_LOADED_TRACE_NAME)
trace.start()
trace.putAttribute(TOKENS_COUNT, tokenList.flattenCurrencies().size.toString())
trace.putAttribute(TOKENS_COUNT, flattenCurrencies.size.toString())
loadingTraces[userWalletId] = trace
}
}

View file

@ -9,17 +9,16 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.onramp.GetOnrampCountryUseCase
@ -45,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject
@ -70,72 +70,81 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
val tokenListFlow = if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) {
val accountStatusList by lazy {
val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
accountDependencies.singleAccountStatusListSupplier(params)
} else {
tokenListStore.getOrThrow(userWallet.walletId)
.map { it.totalFiatBalance to it.flattenCurrencies() }
.map { Lce.Content(it) }
}
fun tokenListFlow(): LceFlow<TokenListError, Pair<TotalFiatBalance, List<CryptoCurrencyStatus>>> {
return if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) {
accountStatusList
} else {
runCatching { tokenListStore.getOrThrow(userWallet.walletId) }
.map { result -> result.map { lce -> lce.map { it.totalFiatBalance to it.flattenCurrencies() } } }
.getOrNull()
// in case of runtime change ft in tester menu
?: accountStatusList
}
}
// val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
// val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params)
return combine(
tokenListFlow,
// todo account just use it, after delete accountsFeatureToggles
// accountStatusListFlow,
isReadyToShowRateAppUseCase(),
isNeedToBackupUseCase(userWallet.walletId),
seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa),
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key),
) { array ->
val totalFiatBalance: Lce<TokenListError, TotalFiatBalance>
val flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>
if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) {
val accountStatusList = array[0] as AccountStatusList
totalFiatBalance = Lce.Content(accountStatusList.totalFiatBalance)
flattenCurrencies = Lce.Content(accountStatusList.flattenCurrencies())
} else {
val maybeTokenList = array[0] as Lce<TokenListError, TokenList>
totalFiatBalance = maybeTokenList.map { it.totalFiatBalance }
flattenCurrencies = maybeTokenList.map { it.flattenCurrencies() }
) { array -> array }
.combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) }
.map { array ->
val lceTokens = array[0] as Lce<TokenListError, Pair<TotalFiatBalance, List<CryptoCurrencyStatus>>>
val totalFiatBalance = lceTokens.map { it.first }
val flattenCurrencies = lceTokens.map { it.second }
val isReadyToShowRating = array[1] as Boolean
val isNeedToBackup = array[2] as Boolean
val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus
val shouldShowReferralPromo = array[4] as Boolean
val shouldShowSepaBanner = array[5] as Boolean
val shouldShowEnablePushesReminderNotification = array[6] as Boolean
buildList {
addUsedOutdatedDataNotification(totalFiatBalance)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents)
addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo)
addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner)
addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)
addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents)
addPushReminderNotification(
clickIntents = clickIntents,
shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification &&
!notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
)
addYieldSupplyNotifications(flattenCurrencies)
val hasCriticalOrWarning = any { notification ->
notification is WalletNotification.Critical || notification is WalletNotification.Warning
}
if (!hasCriticalOrWarning) {
addRateTheAppNotification(isReadyToShowRating, clickIntents)
}
}.toImmutableList()
}
val isReadyToShowRating = array[1] as Boolean
val isNeedToBackup = array[2] as Boolean
val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus
val shouldShowReferralPromo = array[4] as Boolean
val shouldShowSepaBanner = array[5] as Boolean
val shouldShowEnablePushesReminderNotification = array[6] as Boolean
buildList {
addUsedOutdatedDataNotification(totalFiatBalance)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents)
addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo)
addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner)
addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)
addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents)
addPushReminderNotification(
clickIntents = clickIntents,
shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification &&
!notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
)
addYieldSupplyNotifications(flattenCurrencies)
val hasCriticalOrWarning = any { notification ->
notification is WalletNotification.Critical || notification is WalletNotification.Warning
}
if (!hasCriticalOrWarning) {
addRateTheAppNotification(isReadyToShowRating, clickIntents)
}
}.toImmutableList()
}
}
private fun MutableList<WalletNotification>.addUsedOutdatedDataNotification(

View file

@ -50,7 +50,7 @@ internal class SetCryptoCurrencyActionsTransformer(
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = {
clickIntents.onBuyClick(
portfolioId = portfolioId,
userWalletId = portfolioId.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
unavailabilityReason = action.unavailabilityReason,
)
@ -62,7 +62,10 @@ internal class SetCryptoCurrencyActionsTransformer(
enabled = true,
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = {
clickIntents.onReceiveClick(portfolioId, cryptoCurrencyStatus = cryptoCurrencyStatus)
clickIntents.onReceiveClick(
portfolioId.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
},
onLongClick = {
clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus)
@ -87,7 +90,7 @@ internal class SetCryptoCurrencyActionsTransformer(
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = {
clickIntents.onSendClick(
portfolioId = portfolioId,
userWalletId = portfolioId.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
unavailabilityReason = action.unavailabilityReason,
)

View file

@ -9,7 +9,6 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.card.common.util.getCardsCount
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
@ -153,7 +152,7 @@ internal class SetVisaInfoTransformer(
dimContent = false,
onClick = {
clickIntents.onReceiveClick(
portfolioId = PortfolioId(userWalletId), // todo account Visa use Main account?
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
event = MainScreenAnalyticsEvent.ButtonReceive,
)

View file

@ -1,33 +1,28 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus.CANCELED
import com.tangem.domain.pay.model.OrderStatus.NOT_ISSUED
import com.tangem.domain.pay.model.OrderStatus.UNKNOWN
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createKycInProgressState
import java.util.Currency
internal class TangemPayStateTransformer(
internal class TangemPayInitialStateTransformer(
private val value: MainScreenCustomerInfo? = null,
private val onIssueOrderClick: () -> Unit = {},
private val onContinueKycClick: () -> Unit = {},
private val onClickIssue: () -> Unit = {},
private val onClickKyc: () -> Unit = {},
private val openDetails: (customerWalletAddress: String, cardNumberEnd: String) -> Unit = { _, _ -> },
private val issueProgressState: Boolean = false,
private val issueState: Boolean = false,
) : WalletScreenStateTransformer {
override fun transform(prevState: WalletScreenState): WalletScreenState {
val tangemPayState = when {
issueProgressState -> createIssueProgressState()
issueState -> createIssueState()
else -> createInitialState()
}
val tangemPayState = createInitialState()
return prevState.copy(tangemPayState = tangemPayState)
}
@ -35,35 +30,13 @@ internal class TangemPayStateTransformer(
val cardInfo = value?.info?.cardInfo
return when {
value == null -> TangemPayState.Empty
!value.info.isKycApproved() -> createKycInProgressState(onContinueKycClick)
!value.info.isKycApproved -> createKycInProgressState(onClickKyc)
cardInfo != null -> getCardInfoState(cardInfo)
value.orderStatus == NOT_ISSUED || value.orderStatus == CANCELED -> createIssueState()
value.orderStatus == UNKNOWN || value.orderStatus == CANCELED -> createIssueAvailableState(onClickIssue)
else -> createIssueProgressState()
}
}
private fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
showProgress = true,
)
private fun createIssueState() = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = onIssueOrderClick,
)
private fun createKycInProgressState(onContinueKycClick: () -> Unit): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = onContinueKycClick,
)
private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),

View file

@ -0,0 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState
internal class TangemPayIssueAvailableStateTransformer(
private val onClickIssue: () -> Unit = {},
) : WalletScreenStateTransformer {
override fun transform(prevState: WalletScreenState): WalletScreenState =
prevState.copy(tangemPayState = createIssueAvailableState(onClickIssue))
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState
internal class TangemPayIssueProgressStateTransformer : WalletScreenStateTransformer {
override fun transform(prevState: WalletScreenState): WalletScreenState =
prevState.copy(tangemPayState = createIssueProgressState())
}

View file

@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents
@ -18,10 +18,11 @@ import kotlinx.collections.immutable.toImmutableList
internal class MultiWalletCurrencyActionsConverter(
private val userWallet: UserWallet,
private val portfolioId: PortfolioId,
private val clickIntents: WalletCurrencyActionsClickIntents,
) : Converter<TokenActionsState, ImmutableList<TokenActionButtonConfig>> {
private val userWalletId: UserWalletId = userWallet.walletId
override fun convert(value: TokenActionsState): ImmutableList<TokenActionButtonConfig> {
return value.states
.filterIfSingleWithToken()
@ -56,17 +57,17 @@ internal class MultiWalletCurrencyActionsConverter(
is TokenActionsState.ActionState.Buy -> {
title = resourceReference(R.string.common_buy)
icon = R.drawable.ic_plus_24
action = { clickIntents.onBuyClick(portfolioId, cryptoCurrencyStatus, noneReason) }
action = { clickIntents.onBuyClick(userWalletId, cryptoCurrencyStatus, noneReason) }
}
is TokenActionsState.ActionState.Receive -> {
title = resourceReference(R.string.common_receive)
icon = R.drawable.ic_arrow_down_24
action = { clickIntents.onReceiveClick(portfolioId, cryptoCurrencyStatus) }
action = { clickIntents.onReceiveClick(userWalletId, cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.Stake -> {
title = resourceReference(R.string.common_stake)
icon = R.drawable.ic_staking_24
action = { clickIntents.onStakeClick(portfolioId, cryptoCurrencyStatus, actionsState.yield) }
action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.yield) }
}
is TokenActionsState.ActionState.Sell -> {
title = resourceReference(R.string.common_sell)
@ -76,7 +77,7 @@ internal class MultiWalletCurrencyActionsConverter(
is TokenActionsState.ActionState.Send -> {
title = resourceReference(R.string.common_send)
icon = R.drawable.ic_arrow_up_24
action = { clickIntents.onSendClick(portfolioId, cryptoCurrencyStatus, noneReason) }
action = { clickIntents.onSendClick(userWalletId, cryptoCurrencyStatus, noneReason) }
}
is TokenActionsState.ActionState.Swap -> {
title = resourceReference(R.string.swapping_swap_action)
@ -84,7 +85,7 @@ internal class MultiWalletCurrencyActionsConverter(
action = {
clickIntents.onSwapClick(
cryptoCurrencyStatus = cryptoCurrencyStatus,
portfolioId = portfolioId,
userWalletId = userWalletId,
unavailabilityReason = noneReason,
)
}
@ -92,12 +93,12 @@ internal class MultiWalletCurrencyActionsConverter(
is TokenActionsState.ActionState.CopyAddress -> {
title = resourceReference(R.string.common_copy_address)
icon = R.drawable.ic_copy_24
action = { clickIntents.onCopyAddressClick(portfolioId, cryptoCurrencyStatus) }
action = { clickIntents.onCopyAddressClick(userWalletId, cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.HideToken -> {
title = resourceReference(R.string.token_details_hide_token)
icon = R.drawable.ic_hide_24
action = { clickIntents.onHideTokensClick(portfolioId, cryptoCurrencyStatus) }
action = { clickIntents.onHideTokensClick(userWalletId, cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.Analytics -> {
title = resourceReference(R.string.common_analytics)

View file

@ -41,14 +41,12 @@ internal class TokenListStateConverter(
private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit =
{ accountId, currencyStatus ->
val id = accountId?.let { PortfolioId(accountId) } ?: PortfolioId(selectedWallet.walletId)
clickIntents.onTokenItemClick(id, currencyStatus)
clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus)
}
private val onTokenLongClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit =
{ accountId, currencyStatus ->
val id = accountId?.let { PortfolioId(accountId) } ?: PortfolioId(selectedWallet.walletId)
clickIntents.onTokenItemLongClick(id, currencyStatus)
clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus)
}
private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter(
@ -112,9 +110,10 @@ internal class TokenListStateConverter(
is WalletTokensListState.Empty -> listOf()
}
return TokensListItemUM.Portfolio(
state = accountItem,
tokenItemUM = accountItem,
isExpanded = isExtend,
tokens = items.filterIsInstance<PortfolioTokensListItemUM>(),
isCollapsable = true,
tokens = items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
)
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.wallet.presentation.wallet.state.util
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
internal object TangemPayStateCreator {
fun createKycInProgressState(onClickKyc: () -> Unit): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = onClickKyc,
)
fun createIssueAvailableState(onClickIssue: () -> Unit) = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = onClickIssue,
)
fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
showProgress = true,
)
}

View file

@ -8,7 +8,9 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
@ -63,7 +65,10 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() {
flow = tokenListFlow(coroutineScope)
.onEach { maybeTokenList ->
coroutineScope.launch {
sendTokenListAnalytics(maybeTokenList)
sendTokenListAnalytics(
flattenCurrencies = maybeTokenList.getOrNull()?.flattenCurrencies(),
totalFiatBalance = maybeTokenList.getOrNull()?.totalFiatBalance,
)
}.saveIn(sendAnalyticsJobHolder)
}
.distinctUntilChanged()
@ -140,12 +145,14 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() {
private fun createAccountListFlow(coroutineScope: CoroutineScope): Flow<*> = combine(
flow = accountListFlow(coroutineScope)
// todo account analytics for account total balance
/*.onEach { maybeTokenList ->
.onEach { accountStatusList ->
coroutineScope.launch {
sendTokenListAnalytics(maybeTokenList)
sendTokenListAnalytics(
flattenCurrencies = accountStatusList.flattenCurrencies(),
totalFiatBalance = accountStatusList.totalFiatBalance,
)
}.saveIn(sendAnalyticsJobHolder)
}*/
}
.distinctUntilChanged()
.onEach { accountList ->
// todo account see[onAccountListReceived]
@ -207,13 +214,17 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() {
}
}
private suspend fun sendTokenListAnalytics(maybeTokenList: Lce<TokenListError, TokenList>) {
private suspend fun sendTokenListAnalytics(
flattenCurrencies: List<CryptoCurrencyStatus>?,
totalFiatBalance: TotalFiatBalance?,
) {
val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId)
tokenListAnalyticsSender.send(
displayedUiState = displayedState,
userWallet = userWallet,
tokenList = maybeTokenList.getOrNull() ?: return,
flattenCurrencies = flattenCurrencies ?: return,
totalFiatBalance = totalFiatBalance ?: return,
)
}

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
@ -59,6 +60,7 @@ internal fun LazyListScope.portfolioTokensList(
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc()
val lastIndex = tokens.lastIndex.inc()
val isPreview = LocalInspectionMode.current
val appear = remember {
MutableTransitionState(isPreview).apply { targetState = true }
@ -69,11 +71,12 @@ internal fun LazyListScope.portfolioTokensList(
.animateItem()
.roundedShapeItemDecoration(
currentIndex = indexWithHeader,
lastIndex = tokens.lastIndex.inc(),
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
),
visibleState = appear,
) {
val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier
PortfolioTokensListItem(
state = token,
isBalanceHidden = isBalanceHidden,
@ -114,10 +117,15 @@ private fun LazyListScope.portfolioItem(
modifier = anchorModifier,
visibleState = appear,
) {
val modifier = if (portfolio.tokens.isEmpty()) {
Modifier.padding(vertical = 8.dp)
} else {
Modifier.padding(top = 8.dp)
}
PortfolioListItem(
state = portfolio,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
modifier = modifier,
)
}
} else {

View file

@ -1,24 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
import com.tangem.domain.tokens.RemoveCurrencyUseCase
import javax.inject.Inject
class WalletFeatureUseCasesFacade @Inject constructor(
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
) {
suspend fun isCryptoCurrencyCoinCouldHide(portfolioId: PortfolioId, cryptoCurrencyCoin: CryptoCurrency.Coin) =
when (portfolioId) {
is PortfolioId.Account -> TODO("account")
is PortfolioId.Wallet -> isCryptoCurrencyCoinCouldHide(portfolioId.userWalletId, cryptoCurrencyCoin)
}
suspend fun removeCurrencyUseCase(portfolioId: PortfolioId, currency: CryptoCurrency) = when (portfolioId) {
is PortfolioId.Account -> TODO("account")
is PortfolioId.Wallet -> removeCurrencyUseCase(portfolioId.userWalletId, currency)
}
}

View file

@ -16,13 +16,16 @@ dependencies {
implementation(projects.features.walletconnect.api)
implementation(projects.features.sendV2.api)
/** Core */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Common */
implementation(projects.common.routing)
implementation(projects.common.ui)
/** Core */
implementation(projects.core.analytics)
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.ui)
/** Domain models */
implementation(projects.domain.appCurrency.models)

View file

@ -110,7 +110,9 @@ private fun EmptyConnectionsBlock(onNewConnectionClick: () -> Unit, modifier: Mo
Image(
painter = painterResource(R.drawable.img_wallet_connect_76),
contentDescription = "Wallet Connect",
modifier = Modifier.size(76.dp),
modifier = Modifier
.size(76.dp)
.testTag(WalletConnectScreenTestTags.WALLET_CONNECT_IMAGE),
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing24),

Some files were not shown because too many files have changed in this diff Show more