Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 13:58:38 +03:00
commit 9ba66ba084
179 changed files with 5757 additions and 1156 deletions

View file

@ -363,6 +363,9 @@ internal class WalletModel @Inject constructor(
is WalletsUpdateActionResolver.Action.RenameWallets -> {
stateHolder.update(transformer = RenameWalletsTransformer(renamedWallets = action.renamedWallets))
}
WalletsUpdateActionResolver.Action.EmptyWallets -> {
Timber.w("Wallets list is empty!")
}
is WalletsUpdateActionResolver.Action.Unknown -> {
Timber.w("Unable to perform action: $action")
}

View file

@ -25,6 +25,10 @@ internal class WalletsUpdateActionResolver @Inject constructor(
) {
fun resolve(wallets: List<UserWallet>, currentState: WalletScreenState): Action {
if (wallets.isEmpty()) {
return Action.EmptyWallets
}
val selectedWallet = getSelectedWalletSyncUseCase().getOrElse {
/* Selected user wallet can be null after reset if remaining user wallets is locked */
return Action.Unknown
@ -319,6 +323,8 @@ internal class WalletsUpdateActionResolver @Inject constructor(
}
}
data object EmptyWallets : Action()
data object Unknown : Action()
}
}

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

@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.common.preview
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
@ -117,7 +120,13 @@ internal object WalletScreenPreviewData {
buttons = persistentListOf(buyButton),
warnings = persistentListOf(
WalletNotification.Warning.SomeNetworksUnreachable,
WalletNotification.FinishWalletActivation { },
WalletNotification.FinishWalletActivation(
iconTint = NotificationConfig.IconTint.Attention,
buttonsState = ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = { },
),
),
),
bottomSheetConfig = null,
tokensListState = textContentTokensState,

View file

@ -1,6 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.domain
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.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
@ -19,7 +22,9 @@ import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.utils.extensions.isPositive
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
@ -54,7 +59,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addFinishWalletActivationNotification(userWallet, clickIntents)
addFinishWalletActivationNotification(userWallet, maybeTokenList, clickIntents)
addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo)
@ -256,26 +261,55 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) add(element = element)
}
private fun MutableList<WalletNotification>.addFinishWalletActivationNotification(
userWallet: UserWallet,
maybeTokenList: Lce<TokenListError, TokenList>,
clickIntents: WalletClickIntents,
) {
if (userWallet !is UserWallet.Hot) return
val shouldShowFinishActivation = !userWallet.backedUp
val iconTint = maybeTokenList.fold(
ifLoading = {
if ((it?.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) {
IconTint.Warning
} else {
IconTint.Attention
}
},
ifContent = {
if ((it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) {
IconTint.Warning
} else {
IconTint.Attention
}
},
ifError = { IconTint.Attention },
)
addIf(
element = WalletNotification.FinishWalletActivation(
onFinishClick = clickIntents::onFinishWalletActivationClick,
iconTint = iconTint,
buttonsState = when (iconTint) {
IconTint.Warning -> ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = clickIntents::onFinishWalletActivationClick,
)
else -> ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = clickIntents::onFinishWalletActivationClick,
)
},
),
condition = shouldShowFinishActivation,
)
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) add(element = element)
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
}

View file

@ -1,7 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
@ -256,16 +259,15 @@ sealed class WalletNotification(val config: NotificationConfig) {
)
data class FinishWalletActivation(
val onFinishClick: () -> Unit,
val iconTint: IconTint,
val buttonsState: ButtonsState,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.hw_activation_need_title),
subtitle = resourceReference(R.string.hw_activation_need_description),
iconResId = R.drawable.img_knight_shield_32,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = onFinishClick,
),
iconTint = iconTint,
buttonsState = buttonsState,
),
)
@ -282,6 +284,7 @@ sealed class WalletNotification(val config: NotificationConfig) {
text = resourceReference(R.string.notification_referral_promo_button),
onClick = onClick,
),
iconSize = 54.dp,
),
)
}

View file

@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.notifications.NoteMigrationNotification
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
@ -35,15 +34,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
)
}
is WalletNotification.FinishWalletActivation -> {
Notification(
config = it.config,
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
)
}
else -> {
Notification(
config = it.config,
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
iconSize = if (it is WalletNotification.ReferralPromo) {
54.dp
} else {
20.dp
},
iconTint = when (it) {
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
is WalletNotification.Informational -> TangemTheme.colors.icon.accent

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