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

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