Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-15 23:17:35 +04:00
parent 5c57cb85d6
commit b14f487b21
12 changed files with 196 additions and 67 deletions

View file

@ -32,6 +32,6 @@ val TangemBlockCardColors: CardColors
get() = CardColors(
containerColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledContainerColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
disabledContainerColor = TangemTheme.colors.background.primary,
disabledContentColor = TangemTheme.colors.text.primary1,
)

View file

@ -37,11 +37,15 @@ dependencies {
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.walletConnect)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.legacy)
/* SDK */
// TODO: For TangemError model, should be removed after card domain scanning refactoring
implementation(deps.tangem.card.core)
// For image resolving
implementation(deps.tangem.blockchain)
/* AndroidX */
implementation(deps.androidx.fragment.ktx)
@ -55,6 +59,7 @@ dependencies {
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.shimmer)
implementation(deps.compose.coil)
/* DI */
implementation(deps.hilt.android)

View file

@ -19,23 +19,26 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
userWallets = persistentListOf(
UserWalletListUM.UserWalletUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = "My Wallet",
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
imageResId = R.drawable.ill_card_wallet_2_211_343,
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = "Old wallet",
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
imageResId = R.drawable.ill_card_note_eth_211_343,
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = "Multi Card",
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),
imageResId = R.drawable.ill_card_note_bnb_211_343,
imageUrl = "",
isEnabled = false,
onClick = {},
),
),

View file

@ -1,10 +1,11 @@
package com.tangem.features.details.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class UserWalletListUM(
val userWallets: ImmutableList<UserWalletUM>,
val isWalletSavingInProgress: Boolean,
@ -12,12 +13,13 @@ internal data class UserWalletListUM(
val onAddNewWalletClick: () -> Unit,
) {
@Immutable
data class UserWalletUM(
val id: UserWalletId,
val name: String,
val name: TextReference,
val information: TextReference,
@DrawableRes
val imageResId: Int,
val imageUrl: String,
val isEnabled: Boolean,
val onClick: () -> Unit,
)
}

View file

@ -10,15 +10,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.ui.coil.RotationTransformation
@Composable
internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) {
@ -46,6 +52,7 @@ private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modif
BlockCard(
modifier = modifier,
onClick = model.onClick,
enabled = model.isEnabled,
) {
Row(
modifier = Modifier
@ -55,39 +62,81 @@ private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modif
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Image(
modifier = Modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36),
painter = painterResource(id = model.imageResId),
contentScale = ContentScale.FillBounds,
contentDescription = null,
Image(imageUrl = model.imageUrl)
NameAndInfo(
name = model.name,
information = model.information,
)
Column(
modifier = Modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = model.name,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = model.information.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Text(
text = information,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun Image(imageUrl: String, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
}
@Composable
private fun AddWalletButton(
text: TextReference,
@ -110,6 +159,7 @@ private fun AddWalletButton(
AnimatedContent(
modifier = Modifier.size(TangemTheme.dimens.size24),
targetState = isInProgress,
label = "Add wallet progress",
) { isInProgress ->
if (isInProgress) {
CircularProgressIndicator(

View file

@ -0,0 +1,22 @@
package com.tangem.features.details.ui.coil
import android.graphics.Bitmap
import android.graphics.Matrix
import coil.size.Size
import coil.transform.Transformation
internal class RotationTransformation(private val angle: Float) : Transformation {
override val cacheKey: String = "rotate:$angle"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
val matrix = Matrix().apply {
val centerX = input.width / 2f
val centerY = input.height / 2f
postRotate(angle, centerX, centerY)
}
return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true)
}
}

View file

@ -1,9 +1,6 @@
package com.tangem.features.details.utils
import androidx.annotation.DrawableRes
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.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.CardDTO
@ -12,6 +9,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import com.tangem.utils.Strings.STARS
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -19,11 +17,15 @@ internal fun List<UserWallet>.toUiModels(
onClick: (UserWalletId) -> Unit,
appCurrency: AppCurrency? = null,
balances: Map<UserWalletId, TotalFiatBalance> = emptyMap(),
isLoading: Boolean = true,
isBalancesHidden: Boolean = false,
): ImmutableList<UserWalletUM> = this.map { model ->
val balance = balances[model.walletId]
model.mapToUiModel(
balance = balance,
appCurrency = appCurrency,
isLoading = isLoading,
isBalanceHidden = isBalancesHidden,
onClick = { onClick(model.walletId) },
)
}.toImmutableList()
@ -31,22 +33,52 @@ internal fun List<UserWallet>.toUiModels(
private fun UserWallet.mapToUiModel(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
isLoading: Boolean,
isBalanceHidden: Boolean,
onClick: () -> Unit,
): UserWalletUM = UserWalletUM(
id = walletId,
name = name,
information = getInfo(appCurrency, balance),
imageResId = resolveImage(),
name = stringReference(name),
information = getInfo(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
isLoading = isLoading,
),
imageUrl = artworkUrl,
isEnabled = !isLocked,
onClick = onClick,
)
private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBalance?): TextReference {
private fun UserWallet.getInfo(
appCurrency: AppCurrency?,
balance: TotalFiatBalance?,
isBalanceHidden: Boolean,
isLoading: Boolean,
): TextReference {
val dividerRef = stringReference(value = "")
val cardCount = getCardCount()
val cardCountRef = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return when {
isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked))
isLoading -> cardCountRef
isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS))
else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef)
}
}
private fun getBalanceInfo(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
cardCountRef: TextReference,
dividerRef: TextReference,
): TextReference {
val amount = when (balance) {
is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized }
is TotalFiatBalance.Failed,
@ -56,16 +88,15 @@ private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBala
}
return if (amount != null && appCurrency != null) {
val divider = stringReference(value = "")
val formattedAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val amountRef = stringReference(formattedAmount)
TextReference.Combined(wrappedList(cardCountRef, divider, amountRef))
combinedReference(cardCountRef, dividerRef, amountRef)
} else {
cardCountRef
combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN))
}
}
@ -75,10 +106,4 @@ private fun UserWallet.getCardCount() = when (val status = scanResponse.card.bac
is CardDTO.BackupStatus.NoBackup,
null,
-> 1
}
@DrawableRes
private fun UserWallet.resolveImage(): Int {
// TODO: Implement image resolving [REDACTED_JIRA]
return R.drawable.ill_card_wallet_2_211_343
}

View file

@ -10,6 +10,8 @@ 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
@ -24,10 +26,7 @@ import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ComponentScoped
@ -35,6 +34,7 @@ internal class UserWalletsFetcher @Inject constructor(
getWalletsUseCase: GetWalletsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
) {
@ -44,10 +44,16 @@ internal class UserWalletsFetcher @Inject constructor(
emit(wallets.toUiModels(onClick = ::navigateToWalletSettings))
combine(
getSelectedAppCurrencyUseCase(),
getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)),
) { maybeAppCurrency, maybeBalances ->
val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse(
getSelectedAppCurrencyUseCase().distinctUntilChanged(),
getBalanceHidingSettingsUseCase().distinctUntilChanged(),
getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances ->
val models = createUiModels(
wallets = wallets,
maybeAppCurrency = maybeAppCurrency,
maybeBalances = maybeBalances,
balanceHidingSettings = balanceHidingSettings,
).getOrElse(
ifLoading = { return@combine },
ifError = {
val message = resourceReference(R.string.common_unknown_error)
@ -65,10 +71,11 @@ internal class UserWalletsFetcher @Inject constructor(
wallets: List<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
): Lce<Error, ImmutableList<UserWalletUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
block = { maybeBalances.bind() },
block = { maybeBalances.bindOrNull().orEmpty() },
)
val appCurrency = withError(
transform = { Error.UnableToGetAppCurrency },
@ -79,6 +86,8 @@ internal class UserWalletsFetcher @Inject constructor(
appCurrency = appCurrency,
balances = balances,
onClick = ::navigateToWalletSettings,
isBalancesHidden = balanceHidingSettings.isBalanceHidden,
isLoading = maybeBalances.isLoading(),
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="36dp"
android:viewportWidth="22"
android:viewportHeight="36">
<path
android:fillColor="#C9C9CA"
android:pathData="M22,2L22,34A2,2 0,0 1,20 36L2,36A2,2 0,0 1,0 34L0,2A2,2 0,0 1,2 0L20,0A2,2 0,0 1,22 2z" />
<path
android:fillColor="#A1A1A1"
android:pathData="M10.92,22.14H10.96C11.52,22.14 11.85,21.83 11.85,21.35C11.85,20.84 11.48,20.55 10.95,20.55H10.91C10.36,20.55 10.03,20.89 10.03,21.32C10.03,21.79 10.36,22.14 10.92,22.14ZM9.29,19.89V20.58C9,20.63 8.83,20.85 8.83,21.3C8.83,21.82 9.09,22.11 9.62,22.11H10.05C9.77,21.95 9.51,21.59 9.51,21.16C9.51,20.42 10.06,19.86 10.9,19.86H10.94C11.76,19.86 12.38,20.42 12.38,21.17C12.38,21.65 12.16,21.95 11.87,22.11H12.32V22.79H9.61C8.75,22.78 8.32,22.21 8.32,21.3C8.32,20.39 8.73,19.97 9.29,19.89ZM11.17,7.82V9.23H9.41C9.06,9.23 8.88,9.23 8.75,9.16C8.63,9.1 8.54,9 8.48,8.89C8.41,8.75 8.41,8.58 8.41,8.23V7.82H11.17ZM13.69,8.23V6C13.69,5.65 13.69,5.48 13.62,5.34C13.56,5.22 13.47,5.13 13.35,5.07C13.21,5 13.04,5 12.69,5H12.31V9.23H12.69H12.69C13.04,9.23 13.21,9.23 13.35,9.16C13.47,9.1 13.56,9 13.62,8.89C13.69,8.75 13.69,8.58 13.69,8.23ZM11.17,5V6.41H8.41V6C8.41,5.65 8.41,5.48 8.48,5.34C8.54,5.22 8.63,5.13 8.75,5.07C8.88,5 9.06,5 9.41,5L11.17,5ZM10.21,11.96H11.81V11.57H12.32V11.96H12.96L12.96,12.64H12.32V13.28H11.81V12.64H10.27C10.01,12.64 9.89,12.76 9.89,12.98C9.89,13.11 9.91,13.21 9.95,13.31H9.41C9.37,13.2 9.34,13.05 9.34,12.85C9.34,12.27 9.65,11.96 10.21,11.96ZM12.32,17.44V16.76H9.39V17.44H11.11C11.58,17.44 11.81,17.74 11.81,18.12C11.81,18.53 11.61,18.71 11.17,18.71H9.39L9.39,19.38H11.23C12.04,19.38 12.38,18.97 12.38,18.38C12.38,17.9 12.14,17.58 11.85,17.44H12.32ZM11.88,24.74C11.88,25.16 11.66,25.42 11.16,25.45V23.99C11.61,24.06 11.88,24.33 11.88,24.74ZM10.87,23.29H10.82C9.9,23.29 9.33,23.91 9.33,24.77C9.33,25.52 9.67,26.02 10.29,26.11V25.46C10,25.41 9.84,25.19 9.84,24.79C9.84,24.28 10.15,24 10.7,23.98V26.12H10.9C11.95,26.12 12.38,25.47 12.38,24.74C12.38,23.91 11.77,23.29 10.87,23.29ZM12.32,26.64V27.32H11.87C12.14,27.46 12.38,27.78 12.38,28.21C12.38,28.59 12.22,28.89 11.85,29.04C12.22,29.26 12.38,29.66 12.38,30.03C12.38,30.56 12.05,31 11.24,31H9.39V30.32H11.2C11.63,30.32 11.81,30.14 11.81,29.8C11.81,29.47 11.59,29.16 11.14,29.16H9.39V28.48H11.2C11.63,28.48 11.81,28.29 11.81,27.96C11.81,27.63 11.59,27.32 11.14,27.32H9.39V26.64H12.32ZM10.7,15.52H10.41C10.04,15.52 9.82,15.22 9.82,14.8C9.82,14.47 9.98,14.33 10.23,14.33C10.59,14.33 10.7,14.66 10.7,15.18V15.52ZM11.13,15.16C11.13,14.32 10.88,13.66 10.2,13.66C9.59,13.66 9.33,14.1 9.33,14.64C9.33,15.09 9.5,15.34 9.75,15.53H9.39V16.2H11.31C12.11,16.2 12.38,15.68 12.38,15.03C12.38,14.38 12.09,13.83 11.41,13.78V14.43C11.7,14.47 11.87,14.64 11.87,14.99C11.87,15.39 11.67,15.52 11.28,15.52H11.13V15.16Z" />
</vector>