Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-26 19:14:43 +07:00
parent 220a43c1b9
commit 3a96b203e0
18 changed files with 285 additions and 136 deletions

View file

@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
@ -73,6 +74,8 @@ private fun Title(title: TextReference) {
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@ -82,6 +85,8 @@ private fun Subtitle(subtitle: TextReference) {
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
text = subtitle.resolveReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}

View file

@ -2,8 +2,8 @@ package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -12,7 +12,6 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.getCardsCount
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.wallet.UserWallet
@ -37,10 +36,10 @@ class UserWalletItemUMConverter(
private val isBalanceHidden: Boolean = false,
private val authMode: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
private val artwork: ArtworkModel? = null,
artwork: UserWalletItemUM.ImageState? = null,
) : Converter<UserWallet, UserWalletItemUM> {
private val artworkUMConverter = ArtworkUMConverter()
private val artwork = artwork ?: UserWalletItemUM.ImageState.Loading
override fun convert(value: UserWallet): UserWalletItemUM {
return with(value) {
@ -52,7 +51,7 @@ class UserWalletItemUMConverter(
isEnabled = isEnabled(userWallet = this),
endIcon = endIcon,
onClick = { onClick(value.walletId) },
imageState = getImageState(userWallet = value),
imageState = artwork,
label = getLabelOrNull(userWallet = this),
)
}
@ -73,14 +72,6 @@ class UserWalletItemUMConverter(
}
}
private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState {
return when {
userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet
artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork))
else -> UserWalletItemUM.ImageState.Loading
}
}
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val text = when (userWallet) {
is UserWallet.Cold -> {

View file

@ -17,6 +17,7 @@ dependencies {
api(projects.features.onramp.api)
api(projects.features.sendV2.api)
api(projects.features.tokenRecieve.api)
api(projects.features.wallet.api)
/* Data */
implementation(projects.data.common)

View file

@ -7,7 +7,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -56,7 +55,7 @@ internal class AddToPortfolioBSContentUMFactory(
portfolioUIData: PortfolioUIData,
selectedWallet: UserWallet?,
alreadyAddedNetworks: Set<String>?,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
return (currentState ?: TangemBottomSheetConfig.Empty).copy(
isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility,
@ -110,7 +109,7 @@ internal class AddToPortfolioBSContentUMFactory(
}
private fun UserWallet.toSelectedUserWalletItemUM(
artwork: ArtworkModel? = null,
artwork: UserWalletItemUM.ImageState? = null,
portfolioData: PortfolioData,
balance: TotalFiatBalance?,
): UserWalletItemUM {
@ -128,7 +127,7 @@ internal class AddToPortfolioBSContentUMFactory(
isShow: Boolean,
portfolioData: PortfolioData,
selectedWalletId: UserWalletId,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
return TangemBottomSheetConfig(
isShown = isShow,

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -21,7 +22,6 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.markets.SaveMarketTokensUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
@ -31,7 +31,6 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.features.markets.impl.R
@ -42,13 +41,14 @@ import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
@ -66,21 +66,17 @@ internal class MarketsPortfolioModel @Inject constructor(
private val portfolioDataLoader: PortfolioDataLoader,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val getCardImageUseCase: GetCardImageUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getEnsNameUseCase: GetEnsNameUseCase,
private val userWalletImageFetcher: UserWalletImageFetcher,
) : Model() {
val state: StateFlow<MyPortfolioUM> get() = _state
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
private val loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
private val artworksState: MutableStateFlow<HashMap<UserWalletId, ArtworkModel>> = MutableStateFlow(hashMapOf())
private val loadArtworksMutex = Mutex()
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = params.token,
@ -196,38 +192,33 @@ internal class MarketsPortfolioModel @Inject constructor(
private fun subscribeOnStateUpdates() {
combine(
flow = loadPortfolioData(params.token.id),
flow = loadPortfolioDataWithArtworks(params.token.id),
flow2 = getPortfolioUIDataFlow(),
flow3 = artworksState,
transform = factory::create,
transform = { pair, portfolioUIData ->
val (portfolioData, artworks) = pair
factory.create(portfolioData, portfolioUIData, artworks)
},
)
.onEach { _state.value = it }
.launchIn(modelScope)
}
private fun loadPortfolioData(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
portfolioDataLoader.load(currencyRawId).onEach {
loadArtworks(it.walletsWithCurrencies.keys.toList())
}.also { return it }
}
private fun loadPortfolioDataWithArtworks(
currencyRawId: CryptoCurrency.RawID,
): Flow<Pair<PortfolioData, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
val wallets = Channel<Set<UserWallet>>()
val portfolioFlow = portfolioDataLoader
.load(currencyRawId)
.onEach { wallets.trySend(it.walletsWithCurrencies.keys) }
private fun loadArtworks(wallets: List<UserWallet>) {
modelScope.launch {
loadArtworksMutex.withLock {
wallets.filterIsInstance<UserWallet.Cold>().forEach { wallet ->
if (!loadedArtworks.containsKey(wallet.walletId)) {
val artwork = getCardImageUseCase(
cardId = wallet.cardId,
manufacturerName = wallet.scanResponse.card.manufacturer.name,
firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(),
cardPublicKey = wallet.scanResponse.card.cardPublicKey,
)
loadedArtworks[wallet.walletId] = artwork
artworksState.emit(loadedArtworks)
}
}
}
}
val artworksFlow = wallets.receiveAsFlow()
.distinctUntilChanged()
.flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
return combine(
flow = portfolioFlow,
flow2 = artworksFlow,
) { portfolioData, artworks -> portfolioData to artworks }
}
private fun getPortfolioUIDataFlow(): Flow<PortfolioUIData> {

View file

@ -1,8 +1,8 @@
package com.tangem.features.markets.portfolio.impl.model
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -36,7 +36,7 @@ internal class MyPortfolioUMFactory(
fun create(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): MyPortfolioUM {
val addToPortfolioData = portfolioUIData.addToPortfolioData
@ -89,7 +89,7 @@ internal class MyPortfolioUMFactory(
private fun createAddToPortfolioBSConfig(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
val selectedWallet = portfolioData.walletsWithCurrencies.keys
.firstOrNull { it.walletId == portfolioUIData.selectedWalletId }

View file

@ -20,6 +20,7 @@ dependencies {
implementation(projects.features.onboardingV2.api)
implementation(projects.features.pushNotifications.api)
implementation(projects.features.hotWallet.api)
implementation(projects.features.wallet.api)
/* Project - Core */
implementation(projects.core.decompose)
@ -69,4 +70,5 @@ dependencies {
/** Tangem libraries */
implementation(tangemDeps.hot.core)
implementation(tangemDeps.card.core)
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.walletsettings.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
import com.tangem.core.analytics.DummyAnalyticsEventHandler
import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.core.ui.components.block.model.BlockUM
@ -13,6 +14,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM
import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM
import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.impl.R
import com.tangem.feature.walletsettings.ui.WalletSettingsScreen
@ -34,14 +36,11 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
wallets = null,
backedUp = false,
),
userWalletName = "My Wallet",
isReferralAvailable = true,
isLinkMoreCardsAvailable = true,
isRenameWalletAvailable = false,
isNFTFeatureEnabled = true,
isNFTEnabled = true,
onCheckedNFTChange = {},
renameWallet = {},
forgetWallet = {},
onLinkMoreCardsClick = {},
onReferralClick = {},
@ -56,6 +55,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
onUpgradeWalletClick = {},
onDismissUpgradeWalletClick = {},
accountsUM = previewAccounts(),
cardItem = previewCardBlock(),
),
requestPushNotificationsPermission = false,
onPushNotificationPermissionGranted = {},
@ -90,6 +90,15 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
).let(::add)
}
private fun previewCardBlock() = WalletSettingsItemUM.CardBlock(
id = "wallet_name",
title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder),
text = stringReference("Wallet Name"),
isEnabled = true,
onClick = { },
imageState = ImageState.MobileWallet,
)
@Composable
override fun Content(modifier: Modifier) {
WalletSettingsScreen(

View file

@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@ -24,11 +25,12 @@ internal sealed class WalletSettingsItemUM {
val onCheckedChange: (Boolean) -> Unit,
) : WalletSettingsItemUM()
data class WithText(
data class CardBlock(
override val id: String,
val title: TextReference,
val text: TextReference,
val isEnabled: Boolean,
val imageState: ImageState,
val onClick: () -> Unit,
) : WalletSettingsItemUM()

View file

@ -1,6 +1,7 @@
package com.tangem.feature.walletsettings.model
import android.os.Build
import arrow.core.Either
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
@ -44,6 +45,7 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.impl.R
import com.tangem.feature.walletsettings.utils.AccountItemsDelegate
import com.tangem.feature.walletsettings.utils.ItemsBuilder
import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
@ -66,7 +68,7 @@ internal class WalletSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsContextProxy: AnalyticsContextProxy,
private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
walletCardItemDelegateFactory: WalletCardItemDelegate.Factory,
private val isDemoCardUseCase: IsDemoCardUseCase,
getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase,
private val enableWalletNFTUseCase: EnableWalletNFTUseCase,
@ -85,6 +87,7 @@ internal class WalletSettingsModel @Inject constructor(
val params: WalletSettingsComponent.Params = paramsContainer.require()
val dialogNavigation = SlotNavigation<DialogConfig>()
val bottomSheetNavigation: SlotNavigation<NetworksAvailableForNotificationBSConfig> = SlotNavigation()
private val walletCardItemDelegate = walletCardItemDelegateFactory.create(dialogNavigation)
val state: MutableStateFlow<WalletSettingsUM> = MutableStateFlow(
value = WalletSettingsUM(
@ -117,14 +120,12 @@ internal class WalletSettingsModel @Inject constructor(
}
init {
combine(
getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(),
fun combineUI(wallet: UserWallet) = combine(
getWalletNFTEnabledUseCase.invoke(params.userWalletId),
getWalletNotificationsEnabledUseCase(params.userWalletId),
isUpgradeWalletNotificationEnabledUseCase(params.userWalletId),
) { maybeWallet, nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled ->
val wallet = maybeWallet.getOrNull() ?: return@combine
val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase()
walletCardItemDelegate.cardItemFlow(wallet),
) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem ->
val isWalletBackedUp = when (wallet) {
is UserWallet.Hot -> wallet.backedUp
is UserWallet.Cold -> true
@ -135,8 +136,7 @@ internal class WalletSettingsModel @Inject constructor(
value.copy(
items = buildItems(
userWallet = wallet,
dialogNavigation = dialogNavigation,
isRenameWalletAvailable = isRenameWalletAvailable,
cardItem = cardItem,
isNFTEnabled = nftEnabled,
isNotificationsEnabled = notificationsEnabled,
isNotificationsFeatureEnabled = isNeedShowNotifications,
@ -147,6 +147,10 @@ internal class WalletSettingsModel @Inject constructor(
)
}
}
getWalletUseCase.invokeFlow(params.userWalletId)
.distinctUntilChanged()
.filterIsInstance<Either.Right<UserWallet>>()
.flatMapLatest { combineUI(it.value) }
.launchIn(modelScope)
}
@ -162,8 +166,7 @@ internal class WalletSettingsModel @Inject constructor(
private fun buildItems(
userWallet: UserWallet,
dialogNavigation: SlotNavigation<DialogConfig>,
isRenameWalletAvailable: Boolean,
cardItem: WalletSettingsItemUM.CardBlock,
isNFTEnabled: Boolean,
isNotificationsFeatureEnabled: Boolean,
isNotificationsEnabled: Boolean,
@ -176,7 +179,7 @@ internal class WalletSettingsModel @Inject constructor(
}
return itemsBuilder.buildItems(
userWallet = userWallet,
userWalletName = userWallet.name,
cardItem = cardItem,
isReferralAvailable = when (userWallet) {
is UserWallet.Cold -> userWallet.cardTypesResolver.isTangemWallet()
is UserWallet.Hot -> false
@ -186,8 +189,6 @@ internal class WalletSettingsModel @Inject constructor(
is UserWallet.Hot -> false
},
isManageTokensAvailable = isMultiCurrency,
isRenameWalletAvailable = isRenameWalletAvailable,
renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) },
isNFTFeatureEnabled = isMultiCurrency,
isNFTEnabled = isNFTEnabled,
onCheckedNFTChange = ::onCheckedNFTChange,
@ -226,15 +227,6 @@ internal class WalletSettingsModel @Inject constructor(
)
}
private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation<DialogConfig>) {
val config = DialogConfig.RenameWallet(
userWalletId = userWallet.walletId,
currentName = userWallet.name,
)
dialogNavigation.activate(config)
}
private fun forgetWallet() = modelScope.launch {
val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse {
Timber.e("Unable to delete wallet: $it")

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountRow
import com.tangem.common.ui.userwallet.CardImage
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.TangemSwitch
@ -30,10 +31,13 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.BlockItem
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.items.DescriptionItem
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -115,7 +119,7 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) {
modifier = itemModifier,
model = item,
)
is WalletSettingsItemUM.WithText -> TextBlock(
is WalletSettingsItemUM.CardBlock -> CardBlock(
modifier = itemModifier,
model = item,
)
@ -180,30 +184,40 @@ private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier
}
@Composable
private fun TextBlock(model: WalletSettingsItemUM.WithText, modifier: Modifier = Modifier) {
private fun CardBlock(model: WalletSettingsItemUM.CardBlock, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier.fillMaxWidth(),
enabled = model.isEnabled,
onClick = model.onClick,
) {
Column(
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = model.title.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = model.text.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
CardImage(model.imageState)
Column(modifier = Modifier.weight(1f)) {
Text(
text = model.title.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = model.text.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
SecondarySmallButton(
config = SmallButtonConfig(
enabled = model.isEnabled,
text = resourceReference(R.string.common_rename),
onClick = model.onClick,
),
)
}
}

View file

@ -9,7 +9,6 @@ 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.core.ui.extensions.stringReference
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.walletsettings.analytics.Settings
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM
@ -30,12 +29,11 @@ internal class ItemsBuilder @Inject constructor(
@Suppress("LongParameterList")
fun buildItems(
userWallet: UserWallet,
userWalletName: String,
cardItem: WalletSettingsItemUM.CardBlock,
accountsUM: List<WalletSettingsAccountsUM>,
isLinkMoreCardsAvailable: Boolean,
isReferralAvailable: Boolean,
isManageTokensAvailable: Boolean,
isRenameWalletAvailable: Boolean,
isNFTFeatureEnabled: Boolean,
isNFTEnabled: Boolean,
onCheckedNFTChange: (Boolean) -> Unit,
@ -45,7 +43,6 @@ internal class ItemsBuilder @Inject constructor(
onCheckedNotificationsChanged: (Boolean) -> Unit,
onNotificationsDescriptionClick: () -> Unit,
forgetWallet: () -> Unit,
renameWallet: () -> Unit,
onLinkMoreCardsClick: () -> Unit,
onReferralClick: () -> Unit,
onAccessCodeClick: () -> Unit,
@ -53,7 +50,7 @@ internal class ItemsBuilder @Inject constructor(
onUpgradeWalletClick: () -> Unit,
onDismissUpgradeWalletClick: () -> Unit,
): PersistentList<WalletSettingsItemUM> = persistentListOf<WalletSettingsItemUM>()
.add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet))
.add(cardItem)
.addAll(
buildUpgradeWalletItem(
userWallet = userWallet,
@ -62,8 +59,8 @@ internal class ItemsBuilder @Inject constructor(
onDismissUpgradeWalletClick = onDismissUpgradeWalletClick,
),
)
.addAll(accountsUM)
.addAll(buildAccessCodeItem(userWallet, onAccessCodeClick))
.addAll(accountsUM)
.add(
buildCardItem(
userWallet = userWallet,
@ -121,15 +118,6 @@ internal class ItemsBuilder @Inject constructor(
}
}
private fun buildNameItem(walletName: String, isRenameWalletAvailable: Boolean, renameWallet: () -> Unit) =
WalletSettingsItemUM.WithText(
id = "wallet_name",
title = resourceReference(id = R.string.settings_wallet_name_title),
text = stringReference(walletName),
isEnabled = isRenameWalletAvailable,
onClick = renameWallet,
)
private fun buildNFTItem(isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit) =
WalletSettingsItemUM.WithSwitch(
id = "nft",

View file

@ -0,0 +1,55 @@
package com.tangem.feature.walletsettings.utils
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
import com.tangem.feature.walletsettings.entity.DialogConfig
import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM
import com.tangem.feature.walletsettings.impl.R
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.operations.attestation.ArtworkSize
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
internal class WalletCardItemDelegate @AssistedInject constructor(
private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
private val walletImageFetcher: UserWalletImageFetcher,
@Assisted private val dialogNavigation: SlotNavigation<DialogConfig>,
) {
fun cardItemFlow(wallet: UserWallet): Flow<WalletSettingsItemUM.CardBlock> = combine(
flow = walletImageFetcher.walletImage(wallet, ArtworkSize.SMALL),
flow2 = flow { emit(getShouldSaveUserWalletsSyncUseCase()) },
transform = { imageState, isRenameAvailable ->
val walletName = wallet.name
WalletSettingsItemUM.CardBlock(
id = "wallet_name",
title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder),
text = stringReference(walletName),
isEnabled = isRenameAvailable,
onClick = { openRenameWalletDialog(wallet) },
imageState = imageState,
)
},
)
private fun openRenameWalletDialog(userWallet: UserWallet) {
val config = DialogConfig.RenameWallet(
userWalletId = userWallet.walletId,
currentName = userWallet.name,
)
dialogNavigation.activate(config)
}
@AssistedFactory
interface Factory {
fun create(dialogNavigation: SlotNavigation<DialogConfig>): WalletCardItemDelegate
}
}

View file

@ -15,6 +15,9 @@ dependencies {
/** Project - Domain */
implementation(projects.domain.models)
/** Tangem libraries */
implementation(tangemDeps.card.core)
/** Core */
implementation(projects.core.ui)
implementation(projects.core.decompose)

View file

@ -0,0 +1,20 @@
package com.tangem.features.wallet.utils
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.operations.attestation.ArtworkSize
import kotlinx.coroutines.flow.Flow
interface UserWalletImageFetcher {
fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow<UserWalletItemUM.ImageState>
fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow<UserWalletItemUM.ImageState>
fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow<UserWalletItemUM.ImageState>
fun walletsImage(
wallets: Collection<UserWallet>,
size: ArtworkSize,
): Flow<Map<UserWalletId, UserWalletItemUM.ImageState>>
}

View file

@ -4,8 +4,10 @@ import com.tangem.core.decompose.model.Model
import com.tangem.feature.wallet.DefaultWalletEntryComponent
import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel
import com.tangem.feature.wallet.child.wallet.model.WalletModel
import com.tangem.feature.wallet.utils.DefaultUserWalletImageFetcher
import com.tangem.feature.wallet.utils.DefaultUserWalletsFetcher
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.features.wallet.utils.UserWalletsFetcher
import dagger.Binds
import dagger.Module
@ -13,6 +15,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@ -24,6 +27,10 @@ internal interface WalletFeatureModule {
@Binds
fun bindUserWalletsFetcher(impl: DefaultUserWalletsFetcher.Factory): UserWalletsFetcher.Factory
@Binds
@Singleton
fun bindUserWalletImageFetcher(impl: DefaultUserWalletImageFetcher): UserWalletImageFetcher
@Binds
@IntoMap
@ClassKey(WalletModel::class)

View file

@ -0,0 +1,89 @@
package com.tangem.feature.wallet.utils
import arrow.core.Either
import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.operations.attestation.ArtworkSize
import kotlinx.coroutines.flow.*
import javax.inject.Inject
class DefaultUserWalletImageFetcher @Inject constructor(
private val getCardImageUseCase: GetCardImageUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val artworkUMConverter: ArtworkUMConverter,
) : UserWalletImageFetcher {
private val smallCache = MutableStateFlow(mapOf<String, ArtworkUM>())
private val largeCache = MutableStateFlow(mapOf<String, ArtworkUM>())
override fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> = when (wallet) {
is UserWallet.Cold -> walletImage(wallet.scanResponse.card, size)
is UserWallet.Hot -> flowOf(UserWalletItemUM.ImageState.MobileWallet)
}
override fun walletsImage(
wallets: Collection<UserWallet>,
size: ArtworkSize,
): Flow<Map<UserWalletId, UserWalletItemUM.ImageState>> = wallets
.map { userWallet -> walletImage(userWallet, size).map { imageState -> userWallet.walletId to imageState } }
.merge()
.runningFold(mapOf<UserWalletId, UserWalletItemUM.ImageState>()) { map, newState -> map.plus(newState) }
.filter { it.size >= wallets.size } // prevent spam, waiting full map
.distinctUntilChanged()
override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> = flow {
val imagesFlow = getUserWalletUseCase.invokeFlow(walletId)
// emit Loading and wait wallet
.onEach { if (it.isLeft()) emit(UserWalletItemUM.ImageState.Loading) }
.filterIsInstance<Either.Right<UserWallet>>()
.map { it.value }
.distinctUntilChanged()
.flatMapLatest { wallet -> walletImage(wallet, size) }
emitAll(imagesFlow)
}.distinctUntilChanged()
override fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> =
internalGetCardImage(
cardInfo = cardDTO,
size = size,
).distinctUntilChanged()
private fun internalGetCardImage(cardInfo: CardDTO, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> = flow {
emit(cacheOrLoading(cardInfo.cardId, size))
val artwork = getCardImageUseCase.invoke(
cardId = cardInfo.cardId,
cardPublicKey = cardInfo.cardPublicKey,
size = size,
manufacturerName = cardInfo.manufacturer.name,
firmwareVersion = cardInfo.firmwareVersion.toSdkFirmwareVersion(),
)
.let { artworkUMConverter.convert(it) }
.also { save(cardInfo.cardId, size, it) }
emit(UserWalletItemUM.ImageState.Image(artwork))
}
private fun cacheOrLoading(cardId: String, size: ArtworkSize): UserWalletItemUM.ImageState {
val artwork = when (size) {
ArtworkSize.LARGE -> largeCache.value[cardId]
ArtworkSize.SMALL -> smallCache.value[cardId]
}
return artwork
?.let { UserWalletItemUM.ImageState.Image(artwork) }
?: UserWalletItemUM.ImageState.Loading
}
private fun save(cardId: String, size: ArtworkSize, artwork: ArtworkUM) {
when (size) {
ArtworkSize.LARGE -> largeCache.update { it.plus(cardId to artwork) }
ArtworkSize.SMALL -> smallCache.update { it.plus(cardId to artwork) }
}
}
}

View file

@ -14,16 +14,15 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.wallet.impl.R
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -45,11 +44,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
@Assisted private val messageSender: UiMessageSender,
@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean,
@Assisted("authMode") private val authMode: Boolean,
private val getCardImageUseCase: GetCardImageUseCase,
private val userWalletImageFetcher: UserWalletImageFetcher,
dispatchers: CoroutineDispatcherProvider,
) : UserWalletsFetcher {
private var loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
private val walletsFlow =
if (onlyMultiCurrency) getWalletsUseCase().map { it.filter { it.isMultiCurrency } } else getWalletsUseCase()
@ -67,7 +65,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(),
flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(),
flow4 = loadArtworks(wallets),
flow4 = userWalletImageFetcher.walletsImage(wallets, ArtworkSize.SMALL),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks ->
createUiModels(
wallets = wallets,
@ -90,29 +88,12 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
}
.flowOn(dispatchers.default)
private fun loadArtworks(wallets: List<UserWallet>): Flow<HashMap<UserWalletId, ArtworkModel>> {
return flow {
emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish
wallets.filterIsInstance<UserWallet.Cold>().forEach { wallet ->
val artwork = getCardImageUseCase(
cardId = wallet.cardId,
manufacturerName = wallet.scanResponse.card.manufacturer.name,
firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(),
cardPublicKey = wallet.scanResponse.card.cardPublicKey,
size = ArtworkSize.SMALL,
)
loadedArtworks[wallet.walletId] = artwork
emit(loadedArtworks)
}
}
}
private fun createUiModels(
wallets: List<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): Lce<Error, ImmutableList<UserWalletItemUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },