Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-10 11:50:10 +04:00
parent 4f47d876ed
commit e125eff471
40 changed files with 251 additions and 119 deletions

View file

@ -14,6 +14,8 @@ import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSy
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -172,8 +174,16 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase {
return GetCardImageUseCase(verifier = onlineCardVerifier)
fun provideGetCardImageUseCase(
onlineCardVerifier: OnlineCardVerifier,
cardArtworksProvider: CardArtworksProvider,
cardSdkFeatureToggles: CardSdkFeatureToggles,
): GetCardImageUseCase {
return GetCardImageUseCase(
verifier = onlineCardVerifier,
cardArtworksProvider = cardArtworksProvider,
cardSdkFeatureToggles = cardSdkFeatureToggles,
)
}
@Provides

View file

@ -6,6 +6,7 @@ import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
@ -37,7 +38,7 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(
if (artworkId.isNullOrEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}

View file

@ -21,8 +21,6 @@ internal data class UserWalletPublicInformation(
val name: String,
@Json(name = "walletId")
val walletId: UserWalletId,
@Json(name = "artworkUrl")
val artworkUrl: String,
@Json(name = "cardsInWallet")
val cardsInWallet: Set<String>,
@Json(name = "scanResponse")

View file

@ -15,7 +15,6 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
get() = UserWalletPublicInformation(
name = name,
walletId = walletId,
artworkUrl = artworkUrl,
cardsInWallet = cardsInWallet,
isMultiCurrency = isMultiCurrency,
scanResponse = scanResponse.copy(
@ -31,7 +30,6 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
return UserWallet(
name = name,
walletId = walletId,
artworkUrl = artworkUrl,
cardsInWallet = cardsInWallet,
scanResponse = scanResponse,
isMultiCurrency = isMultiCurrency,

View file

@ -31,7 +31,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.extensions.localizedDescriptionRes
@ -250,7 +250,7 @@ private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray,
if (artworkId.isNullOrEmpty()) {
defaultArtwork ?: Uri.EMPTY
} else {
Uri.parse(OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
Uri.parse(CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
}
}
is Result.Failure -> defaultArtwork ?: Uri.EMPTY

View file

@ -32,6 +32,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
@ -76,5 +77,6 @@ data class DaggerGraphState(
val settingsManager: SettingsManager? = null,
val uiMessageSender: UiMessageSender? = null,
val onlineCardVerifier: OnlineCardVerifier? = null,
val cardArworksProvider: CardArtworksProvider? = null,
val userWalletBuilderFactory: UserWalletBuilder.Factory? = null,
) : StateType

View file

@ -39,7 +39,6 @@ internal class DefaultDerivationsRepositoryTest {
private val defaultUserWallet = UserWallet(
name = "",
walletId = defaultUserWalletId,
artworkUrl = "",
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),

View file

@ -194,7 +194,6 @@ internal class BiometricUserWalletsListManagerTest(private val model: Model) {
return UserWallet(
name = "Wallet $id",
walletId = UserWalletId(stringValue = id),
artworkUrl = "",
cardsInWallet = emptySet(),
isMultiCurrency = true,
hasBackupError = false,

View file

@ -23,7 +23,6 @@ object MockUserWalletFactory {
return UserWallet(
walletId = userWalletId,
name = "Wallet 1",
artworkUrl = "",
cardsInWallet = emptySet(),
scanResponse = scanResponse,
isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(),

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.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -61,7 +61,7 @@ fun UserWalletItem(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
CardImage(imageUrl = state.imageUrl)
CardImage(state.imageState)
NameAndInfo(
modifier = Modifier.weight(1f),
name = state.name,
@ -147,39 +147,49 @@ private fun NameAndInfo(
}
@Composable
private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) {
private fun CardImage(imageState: UserWalletItemUM.ImageState, 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 = {
when (imageState) {
is UserWalletItemUM.ImageState.Loading -> {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
}
is UserWalletItemUM.ImageState.Image -> {
SubcomposeAsyncImage(
modifier = imageModifier,
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
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(imageState.artwork.verifiedArtwork?.toByteArray() ?: imageState.artwork.defaultUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
},
contentDescription = null,
)
}
}
}
@Composable
@ -215,7 +225,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("My Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Locked,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -224,7 +233,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("Old wallet"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Hidden,
imageUrl = "",
isEnabled = true,
onClick = {},
endIcon = UserWalletItemUM.EndIcon.Arrow,
@ -234,7 +242,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Failed,
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
@ -244,7 +251,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
@ -257,7 +263,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
value = "1.2345 BTC",
isFlickering = false,
),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
@ -270,7 +275,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
value = "1.2345 BTC",
isFlickering = true,
),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},

View file

@ -2,6 +2,7 @@ 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.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
@ -9,6 +10,7 @@ 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.common.util.getCardsCount
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
@ -31,6 +33,7 @@ class UserWalletItemUMConverter(
private val balance: TotalFiatBalance? = null,
private val isBalanceHidden: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
private val artwork: ArtworkModel? = null,
) : Converter<UserWallet, UserWalletItemUM> {
override fun convert(value: UserWallet): UserWalletItemUM {
@ -40,10 +43,12 @@ class UserWalletItemUMConverter(
name = stringReference(name),
information = getInfo(userWallet = this),
balance = getBalanceInfo(userWallet = this),
imageUrl = artworkUrl,
isEnabled = !isLocked,
endIcon = endIcon,
onClick = { onClick(value.walletId) },
imageState = artwork?.let {
UserWalletItemUM.ImageState.Image(ArtworkUM(it.verifiedArtwork, it.defaultUrl))
} ?: UserWalletItemUM.ImageState.Loading,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import javax.annotation.concurrent.Immutable
@ -10,7 +11,7 @@ data class UserWalletItemUM(
val name: TextReference,
val information: TextReference,
val balance: Balance,
val imageUrl: String,
val imageState: ImageState = ImageState.Loading,
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit,
@ -36,4 +37,14 @@ data class UserWalletItemUM(
val isFlickering: Boolean,
) : Balance()
}
@Immutable
sealed class ImageState {
data object Loading : ImageState()
data class Image(
val artwork: ArtworkUM,
) : ImageState()
}
}

View file

@ -43,6 +43,10 @@
"name": "NOTE_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "NEW_ARTWORK_LOADING",
"version": "5.24.0"
},
{
"name": "NEW_ATTESTATION_ENABLED",
"version": "5.23.0"

View file

@ -0,0 +1,17 @@
package com.tangem.core.ui.components.artwork
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Immutable
data class ArtworkUM(
val verifiedArtwork: ImmutableList<Byte>? = null,
val defaultUrl: String,
) {
constructor(bytes: ByteArray?, defaultUrl: String) : this(
verifiedArtwork = bytes?.toList()?.toImmutableList(),
defaultUrl = defaultUrl,
)
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.models
data class ArtworkModel(
val verifiedArtwork: ByteArray? = null,
val defaultUrl: String,
)

View file

@ -10,7 +10,6 @@ import com.tangem.domain.models.scan.ScanResponse
*
* @property name User wallet name
* @property walletId User wallet [UserWalletId]
* @property artworkUrl User wallet card artwork URL
* @property cardsInWallet List of cards IDs assigned with this user's wallet. The list will be empty if the wallet
* has been backed up on another device.
* @property isMultiCurrency Indicates whether this user wallet can work with more than one currency
@ -22,8 +21,6 @@ data class UserWallet(
val name: String,
@Json(name = "walletId")
val walletId: UserWalletId,
@Json(name = "artworkUrl")
val artworkUrl: String,
@Json(name = "cardsInWallet")
val cardsInWallet: Set<String>,
@Json(name = "isMultiCurrency")

View file

@ -5,7 +5,6 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -13,7 +12,6 @@ import dagger.assisted.AssistedInject
class UserWalletBuilder @AssistedInject constructor(
@Assisted private val scanResponse: ScanResponse,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val getCardImageUseCase: GetCardImageUseCase,
) {
private var backupCardsIds: Set<String> = emptySet()
private var hasBackupError: Boolean = false
@ -38,7 +36,7 @@ class UserWalletBuilder @AssistedInject constructor(
this.hasBackupError = hasBackupError
}
suspend fun build(): UserWallet? {
fun build(): UserWallet? {
return with(scanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse)
.build()
@ -50,7 +48,6 @@ class UserWalletBuilder @AssistedInject constructor(
isBackupNotAllowed = card.isBackupNotAllowed,
isStartToCoin = cardTypesResolver.isStart2Coin(),
),
artworkUrl = getCardImageUseCase.invoke(card.cardId, card.cardPublicKey),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(),

View file

@ -4,8 +4,12 @@ import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
/**
* Use case for getting card image url
@ -14,7 +18,11 @@ import com.tangem.operations.attestation.OnlineCardVerifier
*
[REDACTED_AUTHOR]
*/
class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
class GetCardImageUseCase(
private val verifier: OnlineCardVerifier,
private val cardArtworksProvider: CardArtworksProvider,
private val cardSdkFeatureToggles: CardSdkFeatureToggles,
) {
/**
* Get card image url
@ -22,14 +30,30 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
* @param cardId card id
* @param cardPublicKey card public key
*/
suspend operator fun invoke(cardId: String, cardPublicKey: ByteArray): String {
suspend operator fun invoke(cardId: String, cardPublicKey: ByteArray, size: ArtworkSize): ArtworkModel {
return if (cardSdkFeatureToggles.isNewArtworkLoadingEnabled) {
val result = cardArtworksProvider.getArtwork(
cardId = cardId,
cardPublicKey = cardPublicKey,
size = size,
)
when (result) {
is Result.Failure -> ArtworkModel(null, getFallbackArtworkUrl(cardId))
is Result.Success -> ArtworkModel(result.data, getFallbackArtworkUrl(cardId))
}
} else {
ArtworkModel(null, getLegacyArtwork(cardId, cardPublicKey))
}
}
private suspend fun getLegacyArtwork(cardId: String, cardPublicKey: ByteArray): String {
return when (val result = verifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = result.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
getFallbackArtworkUrl(cardId)
} else {
OnlineCardVerifier.getUrlForArtwork(
CardArtworksProvider.getUrlForArtwork(
cardId = cardId,
cardPublicKey = cardPublicKey.toHexString(),
artworkId = artworkId,
@ -41,8 +65,6 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
}
}
fun getDefaultFallbackUrl(): String = Artwork.DEFAULT_IMG_URL
private fun getFallbackArtworkUrl(cardId: String): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
@ -50,7 +72,7 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL
TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL
else -> getDefaultFallbackUrl()
else -> Artwork.DEFAULT_IMG_URL
}
}
}

View file

@ -29,7 +29,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("My Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -38,7 +37,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("Old wallet"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -47,7 +45,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = false,
onClick = {},
),

View file

@ -18,19 +18,23 @@ 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.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.operations.attestation.ArtworkSize
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class UserWalletsFetcher @Inject constructor(
getWalletsUseCase: GetWalletsUseCase,
@ -39,8 +43,11 @@ internal class UserWalletsFetcher @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
private val getCardImageUseCase: GetCardImageUseCase,
) {
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)
@ -52,12 +59,14 @@ internal class UserWalletsFetcher @Inject constructor(
flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(),
flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances ->
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 = {
@ -72,11 +81,27 @@ internal class UserWalletsFetcher @Inject constructor(
}.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 artwork = getCardImageUseCase(
cardId = wallet.cardId,
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>,
): Lce<Error, ImmutableList<UserWalletItemUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
@ -99,6 +124,7 @@ internal class UserWalletsFetcher @Inject constructor(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
artwork = artworks[userWallet.walletId],
)
.convert(userWallet)
}

View file

@ -27,7 +27,6 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
name = stringReference("Wallet 1"),
information = stringReference("3 cards"),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,
onClick = {},

View file

@ -7,14 +7,15 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.features.onboarding.v2.impl.R
@Composable
internal fun WalletCard(url: String?, modifier: Modifier = Modifier) {
internal fun WalletCard(artwork: ArtworkUM?, modifier: Modifier = Modifier) {
AsyncImage(
modifier = modifier,
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.data(artwork?.verifiedArtwork ?: artwork?.defaultUrl)
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.card_placeholder_black),

View file

@ -6,6 +6,7 @@ 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.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.onboarding.repository.OnboardingRepository
@ -16,6 +17,7 @@ import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -78,18 +80,26 @@ internal class OnboardingMultiWalletModel @Inject constructor(
modelScope.launch {
backups.collectLatest { backup ->
when {
_uiState.value.artwork2Url == null && backup.card2 != null -> {
_uiState.value.artwork2 == null && backup.card2 != null -> {
val artwork =
getCardImageUseCase.invoke(backup.card2.cardId, backup.card2.cardPublicKey)
getCardImageUseCase.invoke(
cardId = backup.card2.cardId,
cardPublicKey = backup.card2.cardPublicKey,
size = ArtworkSize.LARGE,
)
_uiState.update {
it.copy(artwork2Url = artwork)
it.copy(artwork2 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
_uiState.value.artwork3Url == null && backup.card3 != null -> {
_uiState.value.artwork3 == null && backup.card3 != null -> {
val artwork =
getCardImageUseCase.invoke(backup.card3.cardId, backup.card3.cardPublicKey)
getCardImageUseCase.invoke(
cardId = backup.card3.cardId,
cardPublicKey = backup.card3.cardPublicKey,
size = ArtworkSize.LARGE,
)
_uiState.update {
it.copy(artwork3Url = artwork)
it.copy(artwork3 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
}
@ -156,12 +166,14 @@ internal class OnboardingMultiWalletModel @Inject constructor(
private fun loadCardArtwork() {
modelScope.launch {
val artwork =
getCardImageUseCase.invoke(params.scanResponse.card.cardId, params.scanResponse.card.cardPublicKey)
getCardImageUseCase.invoke(
cardId = params.scanResponse.card.cardId,
cardPublicKey = params.scanResponse.card.cardPublicKey,
size = ArtworkSize.LARGE,
)
if (artwork != getCardImageUseCase.getDefaultFallbackUrl()) {
_uiState.update {
it.copy(artwork1Url = artwork)
}
_uiState.update {
it.copy(artwork1 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
}

View file

@ -25,9 +25,9 @@ internal fun OnboardingMultiWallet(
) {
if (isSeedPhraseState.not()) {
WalletArtworks(
url1 = state.artwork1Url,
url2 = state.artwork2Url,
url3 = state.artwork3Url,
artwork1 = state.artwork1,
artwork2 = state.artwork2,
artwork3 = state.artwork3,
modifier = Modifier
.padding(horizontal = 34.dp)
.padding(top = 20.dp)
@ -53,7 +53,7 @@ internal fun OnboardingMultiWallet(
private fun Preview() {
TangemThemePreview {
OnboardingMultiWallet(
state = OnboardingMultiWalletUM(artwork1Url = null),
state = OnboardingMultiWalletUM(artwork1 = null),
artworksState = WalletArtworksState.Folded,
isSeedPhraseState = false,
childContent = { md ->
@ -73,7 +73,7 @@ private fun Preview() {
private fun Preview2() {
TangemThemePreview {
OnboardingMultiWallet(
state = OnboardingMultiWalletUM(artwork1Url = null),
state = OnboardingMultiWalletUM(artwork1 = null),
artworksState = WalletArtworksState.Folded,
isSeedPhraseState = true,
childContent = { md ->

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.common.ui.WalletCard
@ -57,9 +58,9 @@ sealed class WalletArtworksState {
@Composable
fun WalletArtworks(
url1: String?,
url2: String?,
url3: String?,
artwork1: ArtworkUM?,
artwork2: ArtworkUM?,
artwork3: ArtworkUM?,
state: WalletArtworksState,
modifier: Modifier = Modifier,
) {
@ -120,9 +121,9 @@ fun WalletArtworks(
}
AnimatedWalletCards(
url1 = url1,
url2 = url2,
url3 = url3,
artwork1 = artwork1,
artwork2 = artwork2,
artwork3 = artwork3,
modifier = Modifier
.widthIn(max = 450.dp)
.matchParentSize(),
@ -152,9 +153,9 @@ private data class WalletCardTransitionState(
@Suppress("LongParameterList")
@Composable
private fun AnimatedWalletCards(
url1: String?,
url2: String?,
url3: String?,
artwork1: ArtworkUM?,
artwork2: ArtworkUM?,
artwork3: ArtworkUM?,
transition1: Transition<WalletCardTransitionState>,
transition2: Transition<WalletCardTransitionState>,
transition3: Transition<WalletCardTransitionState>,
@ -199,7 +200,7 @@ private fun AnimatedWalletCards(
scaleY = scaleY3
rotationZ = rotation3
},
url = url3,
artwork = artwork3,
)
WalletCard(
@ -217,7 +218,7 @@ private fun AnimatedWalletCards(
scaleX = scaleX2
scaleY = scaleY2
},
url = url2,
artwork = artwork2,
)
WalletCard(
@ -234,7 +235,7 @@ private fun AnimatedWalletCards(
translationY = translationY1
rotationZ = rotation1
},
url = url1,
artwork = artwork1,
)
}
}
@ -447,9 +448,9 @@ private fun Preview() {
var state: WalletArtworksState by remember { mutableStateOf(WalletArtworksState.Folded) }
WalletArtworks(
url1 = null,
url2 = null,
url3 = null,
artwork1 = null,
artwork2 = null,
artwork3 = null,
state = state,
modifier = Modifier.fillMaxWidth(),
)

View file

@ -1,7 +1,9 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
internal data class OnboardingMultiWalletUM(
val artwork1Url: String? = null,
val artwork2Url: String? = null,
val artwork3Url: String? = null,
val artwork1: ArtworkUM? = null,
val artwork2: ArtworkUM? = null,
val artwork3: ArtworkUM? = null,
)

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
@ -93,9 +94,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor(
private fun observeArtwork() {
modelScope.launch {
params.childParams.commonState.collect {
params.childParams.commonState.collect { state ->
_uiState.value = _uiState.value.copy(
artworkUrl = it.cardArtworkUrl,
artwork = state.cardArtwork?.let {
ArtworkUM(it.verifiedArtwork, it.defaultUrl)
},
)
}
}

View file

@ -32,9 +32,9 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod
verticalArrangement = Arrangement.Bottom,
) {
WalletArtworks(
url1 = state.artworkUrl,
url2 = null,
url3 = null,
artwork1 = state.artwork,
artwork2 = null,
artwork3 = null,
modifier = Modifier
.padding(top = 64.dp)
.padding(horizontal = 24.dp)
@ -86,7 +86,7 @@ private fun OnboardingNoteCreatePreview() {
TangemThemePreview {
OnboardingNoteCreateWallet(
state = OnboardingNoteCreateWalletUM(
artworkUrl = null,
artwork = null,
createWalletInProgress = false,
onCreateClick = {},
),

View file

@ -1,7 +1,9 @@
package com.tangem.features.onboarding.v2.note.impl.child.create.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
data class OnboardingNoteCreateWalletUM(
val artworkUrl: String? = null,
val artwork: ArtworkUM? = null,
val createWalletInProgress: Boolean = false,
val onCreateClick: () -> Unit = {},
)

View file

@ -129,7 +129,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
modelScope.launch {
params.childParams.commonState.collect {
_uiState.value = _uiState.value.copy(
cardArtworkUrl = it.cardArtworkUrl,
cardArtwork = it.cardArtwork,
)
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalTangemShimmer
import com.tangem.core.ui.res.TangemTheme
@ -21,11 +22,12 @@ import com.tangem.features.onboarding.v2.common.ui.RefreshButton
import com.tangem.features.onboarding.v2.common.ui.WalletCard
import com.tangem.features.onboarding.v2.impl.R
import com.valentinilk.shimmer.shimmer
import kotlinx.collections.immutable.ImmutableList
@Composable
fun OnboardingNoteTopUpHeader(
balance: String,
cardArtworkUrl: String?,
cardArtwork: ArtworkUM?,
isRefreshing: Boolean,
onRefreshBalanceClick: () -> Unit,
modifier: Modifier = Modifier,
@ -77,7 +79,7 @@ fun OnboardingNoteTopUpHeader(
}
WalletCard(
modifier = Modifier.width(120.dp).align(Alignment.TopCenter),
url = cardArtworkUrl,
artwork = cardArtwork,
)
RefreshButton(
modifier = Modifier.align(Alignment.BottomCenter),
@ -93,7 +95,7 @@ private fun OnboardinNoteTopUpHeaderPreview() {
TangemThemePreview {
OnboardingNoteTopUpHeader(
balance = "0.00000001 BTC",
cardArtworkUrl = "",
cardArtwork = ArtworkUM(null as ImmutableList<Byte>?, ""),
onRefreshBalanceClick = {},
isRefreshing = false,
)

View file

@ -34,7 +34,7 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif
) {
OnboardingNoteTopUpHeader(
balance = state.balance,
cardArtworkUrl = state.cardArtworkUrl,
cardArtwork = state.cardArtwork,
onRefreshBalanceClick = state.onRefreshBalanceClick,
isRefreshing = state.isRefreshing,
modifier = Modifier

View file

@ -1,9 +1,10 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
data class OnboardingNoteTopUpUM(
val cardArtworkUrl: String? = null,
val cardArtwork: ArtworkUM? = null,
val availableForBuy: Boolean = false,
val availableForBuyLoading: Boolean = true,
val balance: String = "",

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.note.impl.model
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
@ -7,6 +8,6 @@ import com.tangem.domain.wallets.models.UserWallet
internal data class OnboardingNoteCommonState(
val scanResponse: ScanResponse,
val userWallet: UserWallet? = null,
val cardArtworkUrl: String? = null,
val cardArtwork: ArtworkUM? = null,
val cryptoCurrencyStatus: CryptoCurrencyStatus? = null,
)

View file

@ -6,6 +6,7 @@ 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.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog
@ -13,6 +14,7 @@ import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState
import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute
import com.tangem.features.onboarding.v2.note.impl.route.stepNum
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -50,7 +52,7 @@ internal class OnboardingNoteModel @Inject constructor(
val commonUiState: MutableStateFlow<OnboardingNoteCommonState> = _uiState
init {
loadArtworkUrl()
loadArtwork()
}
fun updateStepForNewRoute(route: OnboardingNoteRoute) {
@ -85,15 +87,16 @@ internal class OnboardingNoteModel @Inject constructor(
}
}
private fun loadArtworkUrl() {
private fun loadArtwork() {
modelScope.launch {
val cardInfo = params.scanResponse.card
val url = getCardImageUseCase(
val artwork = getCardImageUseCase.invoke(
cardId = cardInfo.cardId,
cardPublicKey = cardInfo.cardPublicKey,
size = ArtworkSize.LARGE,
)
_uiState.update {
it.copy(cardArtworkUrl = url)
it.copy(cardArtwork = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
}

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -212,7 +213,7 @@ private fun AnimatedTwinCards(
scaleX = scaleX2
scaleY = scaleY2
},
url = Artwork.TWIN_CARD_2_URL,
artwork = ArtworkUM(defaultUrl = Artwork.TWIN_CARD_2_URL),
)
WalletCard(
@ -229,7 +230,7 @@ private fun AnimatedTwinCards(
translationY = translationY1
rotationZ = rotation1
},
url = Artwork.TWIN_CARD_1_URL,
artwork = ArtworkUM(defaultUrl = Artwork.TWIN_CARD_1_URL),
)
}
}

View file

@ -1,7 +1,7 @@
[versions]
tangemBlockchainSdk = "develop-1009"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-451"
tangemCardSdk = "develop-453"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem-developments8"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^

View file

@ -1,5 +1,7 @@
package com.tangem.sdk.api.di
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import dagger.Module
import dagger.Provides
@ -14,4 +16,9 @@ internal object CardSdkModule {
@Provides
@Singleton
fun provideOnlineCardVerifier(): OnlineCardVerifier = OnlineCardVerifier()
@Provides
@Singleton
fun provideCardArtworksProvider(sdkRepository: CardSdkConfigRepository): CardArtworksProvider =
CardArtworksProvider(sdkRepository.sdk.config.isTangemAttestationProdEnv)
}

View file

@ -8,4 +8,6 @@ package com.tangem.sdk.api.featuretoggles
interface CardSdkFeatureToggles {
val isNewAttestationEnabled: Boolean
val isNewArtworkLoadingEnabled: Boolean
}

View file

@ -9,4 +9,7 @@ internal class DefaultCardSdkFeatureToggles @Inject constructor(
override val isNewAttestationEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ATTESTATION_ENABLED")
override val isNewArtworkLoadingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ARTWORK_LOADING")
}