Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-03 12:59:04 +03:00
commit 2cf2c9ed1d
11 changed files with 89 additions and 99 deletions

View file

@ -168,15 +168,6 @@ internal object OnrampDomainModule {
)
}
@Provides
@Singleton
fun provideGetOnrampSelectedPaymentMethodUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampSelectedPaymentMethodUseCase {
return GetOnrampSelectedPaymentMethodUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampProviderWithQuoteUseCase(
@ -186,15 +177,6 @@ internal object OnrampDomainModule {
return GetOnrampProviderWithQuoteUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSaveSelectedPaymentMethod(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampSaveSelectedPaymentMethod {
return OnrampSaveSelectedPaymentMethod(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampFetchPairsUseCase(

View file

@ -409,17 +409,6 @@ internal class DefaultOnrampRepository(
return quotes.mapTo(hashSetOf(), OnrampQuote::paymentMethod)
}
override suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod) {
val dto = paymentMethodsConverter.convertBack(paymentMethod)
paymentMethodsStore.store(SELECTED_PAYMENT_METHOD_KEY, listOf(dto))
}
override fun getSelectedPaymentMethod(): Flow<OnrampPaymentMethod> {
return paymentMethodsStore
.get(SELECTED_PAYMENT_METHOD_KEY)
.mapNotNull { paymentMethods -> paymentMethods.firstOrNull()?.let(paymentMethodsConverter::convert) }
}
override suspend fun clearCache() = withContext(NonCancellable) {
paymentMethodsStore.clear()
pairsStore.clear()

View file

@ -1,26 +0,0 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetOnrampSelectedPaymentMethodUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
operator fun invoke(): EitherFlow<OnrampError, OnrampPaymentMethod> {
return repository.getSelectedPaymentMethod()
.map<OnrampPaymentMethod, Either<OnrampError, OnrampPaymentMethod>> { it.right() }
.catch {
emit(errorResolver.resolve(it).left())
}
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class OnrampSaveSelectedPaymentMethod(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either<OnrampError, Unit> {
return Either.catch { repository.saveSelectedPaymentMethod(paymentMethod) }
.mapLeft(errorResolver::resolve)
}
}

View file

@ -41,8 +41,6 @@ interface OnrampRepository {
suspend fun getDefaultCountrySync(): OnrampCountry?
fun getDefaultCountry(): Flow<OnrampCountry?>
suspend fun getAvailablePaymentMethods(): Set<OnrampPaymentMethod>
suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod)
fun getSelectedPaymentMethod(): Flow<OnrampPaymentMethod>
fun getQuotes(): Flow<List<OnrampQuote>>
suspend fun getQuotesSync(): List<OnrampQuote>?
suspend fun clearCache()

View file

@ -36,7 +36,7 @@ class GetCardImageUseCase(
cardPublicKey: ByteArray,
manufacturerName: String,
firmwareVersion: FirmwareVersion,
size: ArtworkSize,
size: ArtworkSize = ArtworkSize.SMALL,
): ArtworkModel {
return if (cardSdkFeatureToggles.isNewArtworkLoadingEnabled) {
val result = cardArtworksProvider.getArtwork(

View file

@ -81,4 +81,7 @@ dependencies {
/* Libs */
implementation(projects.libs.crypto)
implementation(projects.libs.blockchainSdk)
/** Tangem libraries */
implementation(tangemDeps.card.core)
}

View file

@ -7,6 +7,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.ArtworkModel
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.models.isMultiCurrency
@ -45,19 +47,25 @@ internal class AddToPortfolioBSContentUMFactory(
* @param selectedWallet selected wallet
* @param alreadyAddedNetworks already added networks
*/
@Suppress("LongParameterList")
fun create(
currentState: TangemBottomSheetConfig?,
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
selectedWallet: UserWallet?,
alreadyAddedNetworks: Set<String>?,
artworks: HashMap<UserWalletId, ArtworkModel>,
): TangemBottomSheetConfig {
return (currentState ?: TangemBottomSheetConfig.Empty).copy(
isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility,
onDismissRequest = { onAddToPortfolioVisibilityChange(false) },
content = if (selectedWallet != null && alreadyAddedNetworks != null) {
AddToPortfolioBSContentUM(
selectedWallet = selectedWallet.toSelectedUserWalletItemUM(),
selectedWallet = selectedWallet.toSelectedUserWalletItemUM(
portfolioData = portfolioData,
balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(),
artwork = artworks[selectedWallet.walletId],
),
selectNetworkUM = SelectNetworkUMConverter(
networksWithToggle = portfolioUIData.addToPortfolioData.associateWithToggle(
userWalletId = selectedWallet.walletId,
@ -87,6 +95,7 @@ internal class AddToPortfolioBSContentUMFactory(
isShow = portfolioUIData.portfolioBSVisibilityModel.walletSelectorBSVisibility,
portfolioData = portfolioData,
selectedWalletId = selectedWallet.walletId,
artworks = artworks,
),
isWalletBlockVisible = portfolioData.walletsWithCurrencies
.filterKeys(UserWallet::isMultiCurrency).size > 1,
@ -97,10 +106,18 @@ internal class AddToPortfolioBSContentUMFactory(
)
}
private fun UserWallet.toSelectedUserWalletItemUM(): UserWalletItemUM {
private fun UserWallet.toSelectedUserWalletItemUM(
artwork: ArtworkModel? = null,
portfolioData: PortfolioData,
balance: TotalFiatBalance?,
): UserWalletItemUM {
return UserWalletItemUMConverter(
onClick = { onWalletSelectorVisibilityChange(true) },
endIcon = UserWalletItemUM.EndIcon.Arrow,
balance = balance,
artwork = artwork,
appCurrency = portfolioData.appCurrency,
isBalanceHidden = portfolioData.isBalanceHidden,
).convert(value = this)
}
@ -108,6 +125,7 @@ internal class AddToPortfolioBSContentUMFactory(
isShow: Boolean,
portfolioData: PortfolioData,
selectedWalletId: UserWalletId,
artworks: HashMap<UserWalletId, ArtworkModel>,
): TangemBottomSheetConfig {
return TangemBottomSheetConfig(
isShown = isShow,
@ -134,6 +152,7 @@ internal class AddToPortfolioBSContentUMFactory(
} else {
UserWalletItemUM.EndIcon.None
},
artwork = artworks[userWallet.walletId],
).convert(userWallet)
}
.toImmutableList(),

View file

@ -20,12 +20,18 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.markets.SaveMarketTokensUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.lib.crypto.BlockchainUtils
@ -33,6 +39,8 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
@ -50,6 +58,7 @@ internal class MarketsPortfolioModel @Inject constructor(
private val portfolioDataLoader: PortfolioDataLoader,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val getCardImageUseCase: GetCardImageUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
@ -57,6 +66,10 @@ internal class MarketsPortfolioModel @Inject constructor(
val state: StateFlow<MyPortfolioUM> get() = _state
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
private val loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
private val artworksState: MutableStateFlow<HashMap<UserWalletId, ArtworkModel>> = MutableStateFlow(hashMapOf())
private val loadArtworksMutex = Mutex()
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = params.token,
@ -162,14 +175,44 @@ internal class MarketsPortfolioModel @Inject constructor(
private fun subscribeOnStateUpdates() {
combine(
flow = portfolioDataLoader.load(params.token.id, addToPortfolioManager.availableNetworks),
flow = loadPortfolioData(params.token.id, addToPortfolioManager.availableNetworks),
flow2 = getPortfolioUIDataFlow(),
flow3 = artworksState,
transform = factory::create,
)
.onEach { _state.value = it }
.launchIn(modelScope)
}
private fun loadPortfolioData(
currencyRawId: CryptoCurrency.RawID,
availableNetworksFlow: Flow<Set<TokenMarketInfo.Network>?>,
): Flow<PortfolioData> {
portfolioDataLoader.load(currencyRawId, availableNetworksFlow).onEach {
loadArtworks(it.walletsWithCurrencies.keys.toList())
}.also { return it }
}
private fun loadArtworks(wallets: List<UserWallet>) {
modelScope.launch {
loadArtworksMutex.withLock {
wallets.forEach { wallet ->
wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
if (!loadedArtworks.containsKey(wallet.walletId)) {
val artwork = getCardImageUseCase(
cardId = wallet.cardId,
manufacturerName = wallet.scanResponse.card.manufacturer.name,
firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(),
cardPublicKey = wallet.scanResponse.card.cardPublicKey,
)
loadedArtworks[wallet.walletId] = artwork
artworksState.emit(loadedArtworks)
}
}
}
}
}
private fun getPortfolioUIDataFlow(): Flow<PortfolioUIData> {
return combine(
flow = portfolioBSVisibilityModelFlow,

View file

@ -2,8 +2,10 @@ package com.tangem.features.markets.portfolio.impl.model
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
@ -31,7 +33,11 @@ internal class MyPortfolioUMFactory(
private val updateTokens: ((ImmutableList<PortfolioTokenUM>) -> ImmutableList<PortfolioTokenUM>) -> Unit,
) {
fun create(portfolioData: PortfolioData, portfolioUIData: PortfolioUIData): MyPortfolioUM {
fun create(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: HashMap<UserWalletId, ArtworkModel>,
): MyPortfolioUM {
val addToPortfolioData = portfolioUIData.addToPortfolioData
val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true
@ -52,6 +58,7 @@ internal class MyPortfolioUMFactory(
addToPortfolioBSConfig = createAddToPortfolioBSConfig(
portfolioData = portfolioData,
portfolioUIData = portfolioUIData,
artworks = artworks,
),
onAddClick = onAddClick,
)
@ -66,7 +73,11 @@ internal class MyPortfolioUMFactory(
addButtonState = walletsWithCurrencies.getAddButtonState(
availableNetworks = addToPortfolioData.availableNetworks,
),
bsConfig = createAddToPortfolioBSConfig(portfolioData = portfolioData, portfolioUIData = portfolioUIData),
bsConfig = createAddToPortfolioBSConfig(
portfolioData = portfolioData,
portfolioUIData = portfolioUIData,
artworks = artworks,
),
onAddClick = onAddClick,
quickActionsIntents = tokenActionsHandler,
currentState = currentState,
@ -78,6 +89,7 @@ internal class MyPortfolioUMFactory(
private fun createAddToPortfolioBSConfig(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: HashMap<UserWalletId, ArtworkModel>,
): TangemBottomSheetConfig {
val selectedWallet = portfolioData.walletsWithCurrencies.keys
.firstOrNull { it.walletId == portfolioUIData.selectedWalletId }
@ -96,6 +108,7 @@ internal class MyPortfolioUMFactory(
portfolioUIData = portfolioUIData,
selectedWallet = selectedWallet,
alreadyAddedNetworks = alreadyAddedNetworks,
artworks = artworks,
)
}

View file

@ -15,8 +15,6 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.onramp.GetOnrampPaymentMethodsUseCase
import com.tangem.domain.onramp.GetOnrampProviderWithQuoteUseCase
import com.tangem.domain.onramp.GetOnrampSelectedPaymentMethodUseCase
import com.tangem.domain.onramp.OnrampSaveSelectedPaymentMethod
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
@ -36,7 +34,10 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
@ -49,9 +50,7 @@ internal class SelectProviderModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getOnrampPaymentMethodsUseCase: GetOnrampPaymentMethodsUseCase,
private val getOnrampSelectedPaymentMethodUseCase: GetOnrampSelectedPaymentMethodUseCase,
private val getOnrampProviderWithQuoteUseCase: GetOnrampProviderWithQuoteUseCase,
private val saveSelectedPaymentMethod: OnrampSaveSelectedPaymentMethod,
getUserCountryUseCase: GetUserCountryUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -70,7 +69,6 @@ internal class SelectProviderModel @Inject constructor(
analyticsEventHandler.send(OnrampAnalyticsEvent.ProvidersScreenOpened)
getPaymentMethods()
getProviders(params.selectedPaymentMethod)
subscribeToPaymentMethodUpdates()
}
private fun getPaymentMethods() {
@ -104,17 +102,6 @@ internal class SelectProviderModel @Inject constructor(
}
}
private fun subscribeToPaymentMethodUpdates() {
getOnrampSelectedPaymentMethodUseCase.invoke()
.onEach { maybePaymentMethod ->
maybePaymentMethod.fold(
ifLeft = ::sendOnrampErrorEvent,
ifRight = ::getProviders,
)
}
.launchIn(modelScope)
}
private fun getProviders(paymentMethod: OnrampPaymentMethod) {
modelScope.launch {
getOnrampProviderWithQuoteUseCase.invoke(paymentMethod)
@ -176,7 +163,6 @@ internal class SelectProviderModel @Inject constructor(
val firstProvider = methodContainer.providers.firstOrNull()
analyticsEventHandler.send(OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = paymentMethod.name))
modelScope.launch {
saveSelectedPaymentMethod.invoke(paymentMethod)
_state.update { state ->
state.copy(
selectedPaymentMethod = state.selectedPaymentMethod.copy(
@ -184,6 +170,7 @@ internal class SelectProviderModel @Inject constructor(
),
)
}
getProviders(paymentMethod)
if (firstProvider is ProviderListItemUM.Available) {
onProviderSelected(firstProvider.providerResult, firstProvider.isBestRate)
}