Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-27 18:10:28 +07:00
parent 62ddda3e69
commit 7868e94865
20 changed files with 254 additions and 169 deletions

View file

@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -93,7 +93,7 @@ fun UserWalletItem(
@Composable
private fun NameAndInfo(
name: TextReference,
information: TextReference,
information: UserWalletItemUM.Information,
balance: UserWalletItemUM.Balance,
modifier: Modifier = Modifier,
) {
@ -113,8 +113,28 @@ private fun NameAndInfo(
Row(
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedContent(
targetState = information,
label = "Information content",
) { information ->
val informationValue = getInformationValue(information)
if (informationValue == null) {
TextShimmer(
style = TangemTheme.typography.caption2,
text = "aaaaa",
)
} else {
Text(
text = informationValue,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
)
}
}
Text(
text = information.resolveReference() + " $DOT ",
text = " $DOT ",
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
@ -203,6 +223,15 @@ fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair<Stri
}
}
@Composable
fun getInformationValue(information: UserWalletItemUM.Information): String? {
return when (information) {
UserWalletItemUM.Information.Failed -> DASH_SIGN
UserWalletItemUM.Information.Loading -> null
is UserWalletItemUM.Information.Loaded -> information.value.resolveReference()
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@ -255,6 +284,15 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Loading,
balance = UserWalletItemUM.Balance.Loading,
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
@ -279,14 +317,37 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Loading,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Failed,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
isEnabled = true,
onClick = {},
),
)
private fun getInformation(cardCount: Int): TextReference {
return TextReference.PluralRes(
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {
val text = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return UserWalletItemUM.Information.Loaded(text)
}
}
// endregion Preview

View file

@ -53,13 +53,14 @@ class UserWalletItemUMConverter(
}
}
private fun getInfo(userWallet: UserWallet): TextReference {
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val cardCount = userWallet.getCardsCount() ?: 1
return TextReference.PluralRes(
val text = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return UserWalletItemUM.Information.Loaded(text)
}
private fun getBalanceInfo(userWallet: UserWallet): UserWalletItemUM.Balance {

View file

@ -9,7 +9,7 @@ import javax.annotation.concurrent.Immutable
data class UserWalletItemUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val information: Information,
val balance: Balance,
val imageState: ImageState = ImageState.Loading,
val isEnabled: Boolean,
@ -38,6 +38,17 @@ data class UserWalletItemUM(
) : Balance()
}
sealed class Information {
data object Failed : Information()
data object Loading : Information()
data class Loaded(
val value: TextReference,
) : Information()
}
@Immutable
sealed class ImageState {

View file

@ -561,6 +561,10 @@
<string name="nft_send">NFT senden</string>
<string name="nft_traits_title">Eigenschaften</string>
<string name="nft_untitled_collection">Unbetitelte Sammlung</string>
<plurals name="nft_wallet_count">
<item quantity="one"></item>
<item quantity="other">%1$d NFTs in der %2$d Sammlung</item>
</plurals>
<string name="nft_wallet_receive_nft">Tippe hier, um das erste NFT zu erhalten</string>
<string name="nft_wallet_title">NFT-Sammlungen</string>
<string name="nft_wallet_unable_to_load">Die Daten konnten nicht geladen werden</string>

View file

@ -443,6 +443,8 @@
<string name="markets_sort_by_top_gainers_title">Top Ganadores</string>
<string name="markets_sort_by_top_losers_title">Top Perdedores</string>
<string name="markets_sort_by_trending_title">Tendencias</string>
<string name="markets_staking_banner_description_placeholder">Staking es la forma más fácil de recibir recompensas por tus criptomonedas. %s</string>
<string name="markets_staking_banner_title">Gana hasta %s APY</string>
<string name="markets_token_details_about_token_title">Acerca de %s</string>
<plurals name="markets_token_details_amount_exchanges">
<item quantity="one">%d intercambio</item>
@ -864,6 +866,11 @@
<string name="staking_notification_restake_text">La opción de volver a staking le permite mover sus fondos de un validador a otro sin necesidad de retirarlos.</string>
<string name="staking_notification_stake_entire_balance_text">Está a punto de hacer staking con todo su saldo. Recomendamos dejar una cantidad para cubrir las tarifas de la red por unstaking o reclamo de recompensas.</string>
<string name="staking_notification_ton_activate_account">Para empezar a hacer staking en TON, realice primero una transacción de salida de cualquier importe - esto activará su billetera.</string>
<string name="staking_notification_ton_extra_reserve_info">Es posible que se requieran hasta 0.2 TON además de la tarifa de red para completar la transacción. Cualquier cantidad no utilizada será reembolsada.</string>
<string name="staking_notification_ton_extra_reserve_is_required">Se requieren 0.2 TON para realizar esta operación, además de la tarifa de red. Por favor, recargue su saldo.</string>
<string name="staking_notification_ton_extra_reserve_title">0.2 Ton Reservado</string>
<string name="staking_notification_ton_have_to_unstake_all_text">Esta acción cerrará otras posiciones o las cambiará al estado de retiro, de acuerdo con las reglas de la red.</string>
<string name="staking_notification_ton_have_to_unstake_all_title">Estado de las posiciones</string>
<string name="staking_notification_unlock_text">Desbloquee su dinero para retirarlo del proceso de staking. El desbloqueo demora %s minuto.</string>
<string name="staking_notification_unstake_cosmos_text">Sus fondos estarán disponibles para su uso después del período de desvinculación de 21 días. La recompensa se retirará junto con los fondos de desvinculación.</string>
<string name="staking_notification_unstake_text">Sus fondos estarán disponibles para su uso después del período de desvinculación de %s.</string>

View file

@ -443,6 +443,8 @@
<string name="markets_sort_by_top_gainers_title">Meilleurs Gagnants</string>
<string name="markets_sort_by_top_losers_title">Top Perdants</string>
<string name="markets_sort_by_trending_title">Tendances</string>
<string name="markets_staking_banner_description_placeholder">Le staking est le moyen le plus simple de recevoir des récompenses sur votre crypto. %s</string>
<string name="markets_staking_banner_title">Gagnez jusqu\'à %s APY</string>
<string name="markets_token_details_about_token_title">À propos de %s</string>
<plurals name="markets_token_details_amount_exchanges">
<item quantity="one">%d échange</item>
@ -516,6 +518,10 @@
<string name="nft_collections_title">Collections NFT</string>
<string name="nft_collections_warning_subtitle">Certaines données peuvent ne pas se charger</string>
<string name="nft_collections_warning_title">Problèmes de chargement temporaires</string>
<plurals name="nft_wallet_count">
<item quantity="one"></item>
<item quantity="other">%1$d NFT dans la collection %2$d</item>
</plurals>
<string name="nft_wallet_receive_nft">Appuyez ici pour recevoir le premier NFT</string>
<string name="nft_wallet_title">Collections NFT</string>
<string name="nft_wallet_unable_to_load">Impossible de charger les données</string>
@ -873,6 +879,11 @@
<string name="staking_notification_restake_text">L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker.</string>
<string name="staking_notification_stake_entire_balance_text">Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses.</string>
<string name="staking_notification_ton_activate_account">Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille.</string>
<string name="staking_notification_ton_extra_reserve_info">Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé.</string>
<string name="staking_notification_ton_extra_reserve_is_required">0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde.</string>
<string name="staking_notification_ton_extra_reserve_title">0,2 Ton réservés</string>
<string name="staking_notification_ton_have_to_unstake_all_text">Cette action fermera d\'autres positions ou les fera passer au statut de retrait, conformément aux règles du réseau.</string>
<string name="staking_notification_ton_have_to_unstake_all_title">Statut des positions</string>
<string name="staking_notification_unlock_text">Débloquez votre argent pour le retirer du processus de staking. Le déverrouillage prend %s.</string>
<string name="staking_notification_unstake_cosmos_text">Vos fonds seront disponibles à l\'utilisation après la période de déblocage de 21 jours. La récompense sera retirée en même temps que vos fonds de déblocage.</string>
<string name="staking_notification_unstake_text">Vos fonds seront disponibles pour utilisation après la période de désengagement %s.</string>

View file

@ -554,6 +554,9 @@
<string name="nft_send">NFTを送信する</string>
<string name="nft_traits_title">特徴</string>
<string name="nft_untitled_collection">無題のコレクション</string>
<plurals name="nft_wallet_count">
<item quantity="other">%1$dコレクションの%2$dNFT</item>
</plurals>
<string name="nft_wallet_receive_nft">ここをタップして最初のNFTを受け取ります</string>
<string name="nft_wallet_title">NFTコレクション</string>
<string name="nft_wallet_unable_to_load">データを読み込めません</string>
@ -687,6 +690,7 @@
<string name="organize_tokens_sort_by_balance">残高順</string>
<string name="organize_tokens_title">トークンを整理する</string>
<string name="organize_tokens_ungroup">グループ解除</string>
<string name="push_notifications_more_info">詳細はこちら</string>
<string name="push_notifications_permission_alert_description">Tangemの通知は設定で有効にできます。</string>
<string name="push_notifications_permission_alert_negative_button">後で有効にする</string>
<string name="push_notifications_permission_alert_positive_button">設定</string>
@ -912,7 +916,7 @@
<string name="staking_notification_restake_text">再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。</string>
<string name="staking_notification_stake_entire_balance_text">残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。</string>
<string name="staking_notification_ton_activate_account">TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。</string>
<string name="staking_notification_ton_extra_reserve_info">ネットワーク手数料に加え、取引の全ステップを完了するには最大0.2 TONが必要です。未使用分は取引完了後に返金されます。</string>
<string name="staking_notification_ton_extra_reserve_info">取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。</string>
<string name="staking_notification_ton_extra_reserve_is_required">この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。</string>
<string name="staking_notification_ton_extra_reserve_title">0.2 TON保留済み</string>
<string name="staking_notification_ton_have_to_unstake_all_text">このアクションは、ネットワークのルールに従って、他のポジションをクローズするか、引き出しステータスに切り替えます。</string>

View file

@ -713,6 +713,8 @@
<string name="organize_tokens_sort_by_balance">По балансу</string>
<string name="organize_tokens_title">Упорядочить токены</string>
<string name="organize_tokens_ungroup">Список</string>
<string name="push_notifications_more_info">Подробнее</string>
<string name="push_notifications_permission_alert_title">Подключить нотификации</string>
<string name="qr_scanner_camera_denied_gallery_button">Выбрать из галереи</string>
<string name="qr_scanner_camera_denied_settings_button">Настройки</string>
<string name="qr_scanner_camera_denied_text">Вы не предоставили доступ к вашей камере</string>
@ -937,9 +939,11 @@
<string name="staking_notification_restake_text">Рестейк позволяет вам переместить средства из одного валидатора в другого без необходимости выхода из стейкинга.</string>
<string name="staking_notification_stake_entire_balance_text">Вы собираетесь застейкать весь баланс, рекомендуем оставить небольшую сумму для оплаты комиссии сети при выходе из стейкинга или получении награды.</string>
<string name="staking_notification_ton_activate_account">Чтобы начать стейкинг в TON, сначала осуществите исходящую транзакцию на любую сумму — это активирует ваш кошелек.</string>
<string name="staking_notification_ton_extra_reserve_info">В дополнение к сетевой комиссии может потребоваться до 0.2 TON для завершения всех этапов транзакции. Неиспользованная часть будет возвращена после выполнения.</string>
<string name="staking_notification_ton_extra_reserve_info">До 0.2 TON может потребоваться сверх сетевой комиссии для завершения транзакции. Неиспользованная часть будет возвращена.</string>
<string name="staking_notification_ton_extra_reserve_is_required">Для выполнения операции требуется дополнительно 0.2 TON, помимо сетевой комиссии. Пожалуйста, пополните баланс.</string>
<string name="staking_notification_ton_extra_reserve_title">Зарезервировано 0.2 TON</string>
<string name="staking_notification_ton_have_to_unstake_all_text">Это действие закроет другие позиции или переведёт их в статус вывода средств в соответствии с правилами сети.</string>
<string name="staking_notification_ton_have_to_unstake_all_title">Статус позиций</string>
<string name="staking_notification_unlock_text">Разблокируйте свои средства, чтобы вывести их из стейкинга. Разблокировка займёт %s.</string>
<string name="staking_notification_unstake_cosmos_text">Ваши средства будут доступны для использования после 21-дневного периода отзыва. Награда будет выведена вместе с вашими выводими средствами.</string>
<string name="staking_notification_unstake_text">Ваши средства будут доступны после %s периода отзыва.</string>
@ -1153,6 +1157,7 @@
<string name="wallet_promo_banner_button_title">Получить с 10% скидкой</string>
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
<string name="wallet_promo_banner_title">Откройте Tangem Wallet</string>
<string name="wallet_settings_push_notifications_title">Уведомления о транзакциях</string>
<string name="wallet_settings_title">Настройки кошелька</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку</string>

View file

@ -65,6 +65,10 @@
<item quantity="one">%d device</item>
<item quantity="other">%d devices</item>
</plurals>
<plurals name="card_label_token_count">
<item quantity="one">%d token</item>
<item quantity="other">%d tokens</item>
</plurals>
<string name="card_settings_access_code_recovery_disabled_description">Disable this option if you don\'t want this card to be used to reset access codes on other cards or rings in this wallet. Please note that this will also prevent you from resetting the access code on this card.</string>
<string name="card_settings_access_code_recovery_enabled_description">Allows you to use this card to reset access code on other cards in this wallet</string>
<string name="card_settings_access_code_recovery_title">Access code recovery</string>
@ -929,7 +933,7 @@
<string name="staking_notification_restake_text">Restake lets you move your funds from one validator to another without the need to unstake</string>
<string name="staking_notification_stake_entire_balance_text">Youre about to stake your entire balance. We recommend leaving a small amount to cover network fees for unstaking or claiming rewards.</string>
<string name="staking_notification_ton_activate_account">To start staking in TON, first make an outgoing transaction of any amount — this will activate your wallet.</string>
<string name="staking_notification_ton_extra_reserve_info">In addition to the network fee, up to 0.2 TON is needed to complete all steps of the transaction. Unused amount will be returned after it\'s finished.</string>
<string name="staking_notification_ton_extra_reserve_info">Up to 0.2 TON may be required in addition to the network fee to complete the transaction. Any unused amount will be refunded.</string>
<string name="staking_notification_ton_extra_reserve_is_required">0.2 TON is required to proceed with this operation, in addition to the network fee. Please top up your balance.</string>
<string name="staking_notification_ton_extra_reserve_title">0.2 Ton Reserved</string>
<string name="staking_notification_ton_have_to_unstake_all_text">This action will close other positions or switch them to withdrawal status, according to network rules.</string>
@ -1215,7 +1219,7 @@
<string name="wallet_promo_banner_description">Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup.</string>
<string name="wallet_promo_banner_title">Discover Tangem Wallet</string>
<string name="wallet_settings_push_notifications_description">Stay notified on wallet incoming transactions and Tangem updates.</string>
<string name="wallet_settings_push_notifications_title">Push Notifications</string>
<string name="wallet_settings_push_notifications_title">Transaction Notifications</string>
<string name="wallet_settings_title">Wallet settings</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Use %s or scan a card/ring to unlock access to your wallet</string>

View file

@ -15,6 +15,7 @@ dependencies {
/* Project - API */
implementation(projects.features.details.api)
implementation(projects.features.wallet.api)
implementation(projects.features.disclaimer.api)
implementation(projects.features.tester.api)

View file

@ -77,11 +77,12 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
}
}
private fun getInformation(cardCount: Int): TextReference {
return TextReference.PluralRes(
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {
val text = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return UserWalletItemUM.Information.Loaded(text)
}
}

View file

@ -1,15 +1,18 @@
package com.tangem.features.details.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.details.utils.UserWalletsFetcher
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -21,13 +24,17 @@ import javax.inject.Inject
@ModelScoped
internal class UserWalletListModel @Inject constructor(
userWalletsFetcher: UserWalletsFetcher,
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
private val userWalletSaver: UserWalletSaver,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
private val userWalletsFetcher = userWalletsFetcherFactory
.create(messageSender) { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }
val state: MutableStateFlow<UserWalletListUM> = MutableStateFlow(
value = UserWalletListUM(

View file

@ -4,6 +4,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.impl.R
@ -25,7 +26,7 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
val userWallet = UserWalletItemUM(
id = UserWalletId("1"),
name = stringReference("Wallet 1"),
information = stringReference("3 cards"),
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
balance = UserWalletItemUM.Balance.Loading,
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,

View file

@ -12,7 +12,14 @@ dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
/** Project - Domain */
implementation(projects.domain.wallets.models)
/** Core */
implementation(projects.core.ui)
implementation(projects.core.decompose)
implementation(projects.common.ui)
/** Other */
implementation(deps.kotlin.immutable.collections)
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.wallet.utils
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
interface UserWalletsFetcher {
val userWallets: Flow<ImmutableList<UserWalletItemUM>>
interface Factory {
fun create(messageSender: UiMessageSender, onWalletClick: (UserWalletId) -> Unit): UserWalletsFetcher
}
}

View file

@ -4,7 +4,9 @@ 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.DefaultUserWalletsFetcher
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.wallet.utils.UserWalletsFetcher
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -19,6 +21,9 @@ internal interface WalletFeatureModule {
@Binds
fun bindComponentFactory(impl: DefaultWalletEntryComponent.Factory): WalletEntryComponent.Factory
@Binds
fun bindUserWalletsFetcher(impl: DefaultUserWalletsFetcher.Factory): UserWalletsFetcher.Factory
@Binds
@IntoMap
@ClassKey(WalletModel::class)

View file

@ -1,11 +1,8 @@
package com.tangem.features.details.utils
package com.tangem.feature.wallet.utils
import arrow.core.Either
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
@ -15,6 +12,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.feature.wallet.impl.R
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.core.utils.toLce
@ -26,31 +24,32 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.impl.R
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.operations.attestation.ArtworkSize
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class UserWalletsFetcher @Inject constructor(
internal class DefaultUserWalletsFetcher @AssistedInject constructor(
getWalletsUseCase: GetWalletsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
@Assisted private val onWalletClick: (UserWalletId) -> Unit,
@Assisted private val messageSender: UiMessageSender,
private val getCardImageUseCase: GetCardImageUseCase,
) {
) : UserWalletsFetcher {
private var loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
@OptIn(ExperimentalCoroutinesApi::class)
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
val uiModels = UserWalletItemUMConverter(onClick = ::navigateToWalletSettings).convertList(wallets)
override val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
val uiModels = UserWalletItemUMConverter(onClick = { onWalletClick(it) }).convertList(wallets)
.toImmutableList()
emit(uiModels)
@ -122,7 +121,7 @@ internal class UserWalletsFetcher @Inject constructor(
balances
.map { (userWallet, balance) ->
UserWalletItemUMConverter(
onClick = ::navigateToWalletSettings,
onClick = { onWalletClick(it) },
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
@ -133,14 +132,18 @@ internal class UserWalletsFetcher @Inject constructor(
.toImmutableList()
}
private fun navigateToWalletSettings(userWalletId: UserWalletId) {
router.push(AppRoute.WalletSettings(userWalletId))
}
sealed class Error {
data object UnableToGetAppCurrency : Error()
data object UnableToGetBalances : Error()
}
@AssistedFactory
interface Factory : UserWalletsFetcher.Factory {
override fun create(
messageSender: UiMessageSender,
onWalletClick: (UserWalletId) -> Unit,
): DefaultUserWalletsFetcher
}
}

View file

@ -27,7 +27,9 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.wallets.models.UserWalletId
@ -138,6 +140,7 @@ private fun WcSelectWalletContent(
}
}
@Suppress("LongMethod")
@Composable
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -165,7 +168,7 @@ private fun WcSelectWalletContent_Preview() {
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("Tangem 2.0"),
information = stringReference("42 tokens"),
information = getInformation(42),
balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false),
isEnabled = true,
onClick = {},
@ -173,7 +176,7 @@ private fun WcSelectWalletContent_Preview() {
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Tangem White"),
information = stringReference("24 tokens"),
information = getInformation(24),
balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false),
isEnabled = true,
onClick = {},
@ -181,7 +184,7 @@ private fun WcSelectWalletContent_Preview() {
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Bitcoin"),
information = stringReference("1 token"),
information = getInformation(1),
balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false),
isEnabled = true,
onClick = {},
@ -189,7 +192,23 @@ private fun WcSelectWalletContent_Preview() {
UserWalletItemUM(
id = UserWalletId("user_wallet_4".encodeToByteArray()),
name = stringReference("Tangem 1.0"),
information = stringReference("21 tokens"),
information = getInformation(21),
balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false),
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_4".encodeToByteArray()),
name = stringReference("Tangem 1.0"),
information = UserWalletItemUM.Information.Loading,
balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false),
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_4".encodeToByteArray()),
name = stringReference("Tangem 1.0"),
information = UserWalletItemUM.Information.Failed,
balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false),
isEnabled = true,
onClick = {},
@ -199,4 +218,13 @@ private fun WcSelectWalletContent_Preview() {
},
)
}
}
private fun getInformation(tokenCount: Int): UserWalletItemUM.Information.Loaded {
val text = TextReference.PluralRes(
id = R.plurals.card_label_token_count,
count = tokenCount,
formatArgs = wrappedList(tokenCount),
)
return UserWalletItemUM.Information.Loaded(text)
}

View file

@ -6,12 +6,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.decompose.ui.UiMessageSender
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.WcSelectWalletParams
import com.tangem.features.walletconnect.connections.entity.WcAppInfoWalletUM
import com.tangem.features.walletconnect.connections.utils.WcUserWalletsFetcher
@ -25,12 +22,9 @@ import javax.inject.Inject
@ModelScoped
internal class WcSelectWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
getWalletsUseCase: GetWalletsUseCase,
getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
messageSender: UiMessageSender,
getCardImageUseCase: GetCardImageUseCase,
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
getTokenListUseCase: GetTokenListUseCase,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@ -46,12 +40,9 @@ internal class WcSelectWalletModel @Inject constructor(
)
private val userWalletsFetcher = WcUserWalletsFetcher(
getWalletsUseCase = getWalletsUseCase,
getWalletTotalBalanceUseCase = getWalletTotalBalanceUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
userWalletsFetcherFactory = userWalletsFetcherFactory,
getTokenListUseCase = getTokenListUseCase,
messageSender = messageSender,
getCardImageUseCase = getCardImageUseCase,
onWalletSelected = ::onWalletSelected,
)

View file

@ -1,140 +1,57 @@
package com.tangem.features.walletconnect.connections.utils
import arrow.core.Either
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.BalanceHidingSettings
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.getOrElse
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.walletconnect.impl.R
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.features.wallet.utils.UserWalletsFetcher
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@Suppress("LongParameterList")
@ModelScoped
internal class WcUserWalletsFetcher(
getWalletsUseCase: GetWalletsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val messageSender: UiMessageSender,
private val getCardImageUseCase: GetCardImageUseCase,
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
messageSender: UiMessageSender,
private val getTokenListUseCase: GetTokenListUseCase,
private val onWalletSelected: (UserWalletId) -> Unit,
) {
private var loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
private val userWalletsFetcher = userWalletsFetcherFactory.create(messageSender) { onWalletSelected(it) }
@OptIn(ExperimentalCoroutinesApi::class)
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
val uiModels = UserWalletItemUMConverter(onClick = onWalletSelected)
.convertList(wallets)
.toImmutableList()
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = userWalletsFetcher.userWallets
.flatMapLatest { listOfWalletItem ->
val flows = listOfWalletItem.map(::getTokenListFlow)
combine(flows) { it.toList().toImmutableList() }
}
emit(uiModels)
combine(
flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(),
flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(),
flow4 = loadArtworks(wallets),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks ->
val models = createUiModels(
wallets = wallets,
maybeAppCurrency = maybeAppCurrency,
maybeBalances = maybeBalances,
balanceHidingSettings = balanceHidingSettings,
artworks = artworks,
).getOrElse(
ifLoading = { return@combine },
ifError = {
val message = resourceReference(R.string.common_unknown_error)
messageSender.send(SnackbarMessage(message))
return@combine
},
private fun getTokenListFlow(walletItem: UserWalletItemUM): Flow<UserWalletItemUM> {
return getTokenListUseCase.launch(walletItem.id).map { lce ->
val information = lce.fold(
ifLoading = { UserWalletItemUM.Information.Loading },
ifError = { UserWalletItemUM.Information.Failed },
ifContent = { tokenList -> tokenCountInfo(tokenList.flattenCurrencies().size) },
)
emit(models)
}.collect()
}
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.forEach { wallet ->
val card = wallet.scanResponse.card
val artwork = getCardImageUseCase(
cardId = wallet.cardId,
cardPublicKey = card.cardPublicKey,
size = ArtworkSize.SMALL,
manufacturerName = card.manufacturer.name,
firmwareVersion = card.firmwareVersion.toSdkFirmwareVersion(),
)
loadedArtworks[wallet.walletId] = artwork
emit(loadedArtworks)
}
walletItem.copy(information = information)
}
}
private fun createUiModels(
wallets: List<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
artworks: HashMap<UserWalletId, ArtworkModel>,
): Lce<Error, ImmutableList<UserWalletItemUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
block = {
maybeBalances.bindOrNull().orEmpty()
.filterKeys { userWalletId -> wallets.any { it.walletId == userWalletId } }
.mapKeys { entry -> wallets.first { it.walletId == entry.key } }
},
private fun tokenCountInfo(count: Int): UserWalletItemUM.Information.Loaded {
val text = TextReference.PluralRes(
id = R.plurals.card_label_token_count,
count = count,
formatArgs = wrappedList(count),
)
val appCurrency = withError(
transform = { Error.UnableToGetAppCurrency },
block = { maybeAppCurrency.toLce().bind() },
)
balances
.map { (userWallet, balance) ->
UserWalletItemUMConverter(
onClick = onWalletSelected,
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
artwork = artworks[userWallet.walletId],
)
.convert(userWallet)
}
.toImmutableList()
}
sealed class Error {
data object UnableToGetAppCurrency : Error()
data object UnableToGetBalances : Error()
return UserWalletItemUM.Information.Loaded(text)
}
}