Updated on 2026-08-14
This commit is contained in:
parent
1f233c091b
commit
3e0e8753d4
84 changed files with 6595 additions and 143 deletions
|
|
@ -6,39 +6,43 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object NewsDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetNewsCategoriesUseCase(repository: NewsRepository): GetNewsCategoriesUseCase {
|
||||
return GetNewsCategoriesUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideObserveNewsDetailsUseCase(repository: NewsRepository): ObserveNewsDetailsUseCase {
|
||||
return ObserveNewsDetailsUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideObserveTrendingNewsUseCase(repository: NewsRepository): ManageTrendingNewsUseCase {
|
||||
return ManageTrendingNewsUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase {
|
||||
return GetNewsListBatchFlowUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchTrendingNewsUseCase(repository: NewsRepository): FetchTrendingNewsUseCase {
|
||||
return FetchTrendingNewsUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideMarkArticleAsViewedUseCase(repository: NewsRepository): MarkArticleAsViewedUseCase {
|
||||
return MarkArticleAsViewedUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetNewsUseCase(repository: NewsRepository): GetNewsUseCase {
|
||||
return GetNewsUseCase(repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
|
|||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.trending.DefaultTrendingNewsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
import com.tangem.datasource.local.news.viewed.DefaultNewsViewedStore
|
||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -26,4 +28,10 @@ internal object NewsStoreModule {
|
|||
fun provideTrendingNewsStore(): TrendingNewsStore {
|
||||
return DefaultTrendingNewsStore(store = RuntimeSharedStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNewsViewedStore(): NewsViewedStore {
|
||||
return DefaultNewsViewedStore(store = RuntimeSharedStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.local.news.viewed
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
private typealias NewsViewedCache = Map<Int, Boolean>
|
||||
|
||||
internal class DefaultNewsViewedStore(
|
||||
private val store: RuntimeSharedStore<NewsViewedCache>,
|
||||
) : NewsViewedStore {
|
||||
|
||||
override fun getAll(): Flow<Map<Int, Boolean>> {
|
||||
return store.get().onStart { emit(emptyMap()) }
|
||||
}
|
||||
|
||||
override suspend fun getSync(): Map<Int, Boolean> {
|
||||
return store.getSyncOrNull().orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun updateViewed(articleIds: Collection<Int>, viewed: Boolean) {
|
||||
if (articleIds.isEmpty()) return
|
||||
|
||||
store.update(emptyMap()) { current ->
|
||||
val updated = current.toMutableMap()
|
||||
articleIds.forEach { id ->
|
||||
updated[id] = viewed
|
||||
}
|
||||
updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.datasource.local.news.viewed
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Store for news viewed flags (runtime only).
|
||||
*/
|
||||
interface NewsViewedStore {
|
||||
|
||||
/**
|
||||
* Observes all viewed flags.
|
||||
*/
|
||||
fun getAll(): Flow<Map<Int, Boolean>>
|
||||
|
||||
/**
|
||||
* Gets viewed flags synchronously (returns empty map if no data).
|
||||
*/
|
||||
suspend fun getSync(): Map<Int, Boolean>
|
||||
|
||||
/**
|
||||
* Updates viewed flags for provided article ids.
|
||||
*/
|
||||
suspend fun updateViewed(articleIds: Collection<Int>, viewed: Boolean)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.data.news.repository.DefaultNewsRepository
|
|||
import com.tangem.datasource.api.news.NewsApi
|
||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
||||
import com.tangem.domain.news.repository.NewsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -23,12 +24,14 @@ internal object NewsDataModule {
|
|||
dispatchers: CoroutineDispatcherProvider,
|
||||
newsDetailsStore: NewsDetailsStore,
|
||||
trendingNewsStore: TrendingNewsStore,
|
||||
newsViewedStore: NewsViewedStore,
|
||||
): NewsRepository {
|
||||
return DefaultNewsRepository(
|
||||
newsApi = newsApi,
|
||||
dispatchers = dispatchers,
|
||||
newsDetailsStore = newsDetailsStore,
|
||||
trendingNewsStore = trendingNewsStore,
|
||||
newsViewedStore = newsViewedStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.datasource.api.news.NewsApi
|
|||
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
|
||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
||||
import com.tangem.domain.models.news.*
|
||||
import com.tangem.domain.news.model.NewsListBatchFlow
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
|
|
@ -23,6 +24,7 @@ import kotlinx.coroutines.async
|
|||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
|
@ -36,6 +38,7 @@ internal class DefaultNewsRepository(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val newsDetailsStore: NewsDetailsStore,
|
||||
private val trendingNewsStore: TrendingNewsStore,
|
||||
private val newsViewedStore: NewsViewedStore,
|
||||
) : NewsRepository {
|
||||
|
||||
override fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow {
|
||||
|
|
@ -47,6 +50,27 @@ internal class DefaultNewsRepository(
|
|||
).toBatchFlow()
|
||||
}
|
||||
|
||||
override suspend fun getNews(config: NewsListConfig, limit: Int): List<ShortArticle> {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = newsApi.getNews(
|
||||
page = FIRST_PAGE,
|
||||
limit = limit,
|
||||
language = config.language,
|
||||
snapshot = config.snapshot,
|
||||
tokenIds = config.tokenIds.takeIf { it.isNotEmpty() },
|
||||
categoryIds = config.categoryIds.takeIf { it.isNotEmpty() },
|
||||
).getOrThrow()
|
||||
|
||||
val articles = response.items.map { it.toDomainShortArticle() }
|
||||
val viewedFlags = newsViewedStore.getSync()
|
||||
|
||||
articles.map { article ->
|
||||
val isViewed = viewedFlags[article.id] == true
|
||||
article.copy(viewed = isViewed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetailedArticle(newsId: Int, language: String?): DetailedArticle {
|
||||
val cached = newsDetailsStore.getSyncOrNull(newsId)
|
||||
if (cached != null) return cached
|
||||
|
|
@ -73,29 +97,21 @@ internal class DefaultNewsRepository(
|
|||
}
|
||||
|
||||
override fun observeTrendingNews(): Flow<TrendingNews> {
|
||||
return trendingNewsStore.get(TRENDING_NEWS_KEY)
|
||||
}
|
||||
|
||||
override suspend fun updateTrendingNewsViewed(articleIds: Collection<Int>, viewed: Boolean) {
|
||||
if (articleIds.isEmpty()) return
|
||||
|
||||
val currentResult = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY) ?: return
|
||||
val currentArticles = when (currentResult) {
|
||||
is TrendingNews.Data -> currentResult.articles
|
||||
is TrendingNews.Error -> return
|
||||
}
|
||||
if (currentArticles.isEmpty()) return
|
||||
|
||||
val ids = articleIds.toSet()
|
||||
val updated = currentArticles.map { article ->
|
||||
if (article.id in ids) {
|
||||
article.copy(viewed = viewed)
|
||||
} else {
|
||||
article
|
||||
return combine(
|
||||
trendingNewsStore.get(TRENDING_NEWS_KEY),
|
||||
newsViewedStore.getAll(),
|
||||
) { trendingNews, viewedFlags ->
|
||||
when (trendingNews) {
|
||||
is TrendingNews.Data -> {
|
||||
val articlesWithViewedFlags = trendingNews.articles.map { article ->
|
||||
val isViewed = viewedFlags[article.id] == true
|
||||
article.copy(viewed = isViewed)
|
||||
}
|
||||
TrendingNews.Data(articlesWithViewedFlags)
|
||||
}
|
||||
is TrendingNews.Error -> trendingNews
|
||||
}
|
||||
}
|
||||
|
||||
trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(updated))
|
||||
}
|
||||
|
||||
override suspend fun getCategories(): List<ArticleCategory> {
|
||||
|
|
@ -107,6 +123,10 @@ internal class DefaultNewsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun updateNewsViewed(articleIds: Collection<Int>, viewed: Boolean) {
|
||||
newsViewedStore.updateViewed(articleIds, viewed)
|
||||
}
|
||||
|
||||
private suspend fun fetchDetailedArticlesInternal(newsIds: Collection<Int>, language: String?) =
|
||||
withContext(dispatchers.io) {
|
||||
if (newsIds.isEmpty()) return@withContext
|
||||
|
|
@ -167,41 +187,26 @@ internal class DefaultNewsRepository(
|
|||
}
|
||||
is ApiResponse.Success<NewsTrendingResponse> -> {
|
||||
val freshArticles = result.data.items.map { it.toDomainShortArticle() }
|
||||
val cachedArticles = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY)
|
||||
val currentArticles = when (cachedArticles) {
|
||||
is TrendingNews.Data -> cachedArticles.articles
|
||||
is TrendingNews.Error -> emptyList()
|
||||
null -> emptyList()
|
||||
}
|
||||
val merged = mergeTrendingArticles(current = currentArticles, fresh = freshArticles).take(limit)
|
||||
trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(merged))
|
||||
TrendingNews.Data(merged)
|
||||
val articles = freshArticles.take(limit)
|
||||
trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(articles))
|
||||
TrendingNews.Data(articles)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeTrendingArticles(current: List<ShortArticle>, fresh: List<ShortArticle>): List<ShortArticle> {
|
||||
if (current.isEmpty()) return fresh
|
||||
|
||||
val currentById = current.associateBy(ShortArticle::id)
|
||||
|
||||
return fresh.map { article ->
|
||||
val stored = currentById[article.id] ?: return@map article
|
||||
article.copy(viewed = stored.viewed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBatchFetcher(batchSize: Int): BatchFetcher<NewsListConfig, List<ShortArticle>> {
|
||||
return NewsBatchFetcher(
|
||||
newsApi = newsApi,
|
||||
batchSize = batchSize,
|
||||
newsViewedStore = newsViewedStore,
|
||||
)
|
||||
}
|
||||
|
||||
private class NewsBatchFetcher(
|
||||
private val newsApi: NewsApi,
|
||||
private val batchSize: Int,
|
||||
private val newsViewedStore: NewsViewedStore,
|
||||
) : BatchFetcher<NewsListConfig, List<ShortArticle>> {
|
||||
|
||||
private var state: NewsPaginationState? = null
|
||||
|
|
@ -271,7 +276,13 @@ internal class DefaultNewsRepository(
|
|||
categoryIds = params.categoryIds.takeIf { it.isNotEmpty() },
|
||||
).getOrThrow()
|
||||
|
||||
val items = response.items.map { it.toDomainShortArticle() }
|
||||
val articles = response.items.map { it.toDomainShortArticle() }
|
||||
val viewedFlags = newsViewedStore.getSync()
|
||||
|
||||
val items = articles.map { article ->
|
||||
val isViewed = viewedFlags[article.id] == true
|
||||
article.copy(viewed = isViewed)
|
||||
}
|
||||
|
||||
val batchResult = BatchFetchResult.Success(
|
||||
data = items,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class NewsListConfig(
|
||||
val language: String,
|
||||
val snapshot: String,
|
||||
val snapshot: String?,
|
||||
val tokenIds: List<String> = emptyList(),
|
||||
val categoryIds: List<Int> = emptyList(),
|
||||
)
|
||||
|
|
@ -2,9 +2,11 @@ package com.tangem.domain.news.repository
|
|||
|
||||
import com.tangem.domain.models.news.ArticleCategory
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.models.news.TrendingNews
|
||||
import com.tangem.domain.news.model.NewsListBatchFlow
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
|
|
@ -20,6 +22,13 @@ interface NewsRepository {
|
|||
*/
|
||||
fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow
|
||||
|
||||
/**
|
||||
* Returns list of short article by config.
|
||||
*
|
||||
* @param config config for getting news list
|
||||
*/
|
||||
suspend fun getNews(config: NewsListConfig, limit: Int): List<ShortArticle>
|
||||
|
||||
/**
|
||||
* Returns detailed article by id with locale configuration.
|
||||
* @param newsId news identification
|
||||
|
|
@ -50,13 +59,13 @@ interface NewsRepository {
|
|||
*/
|
||||
fun observeTrendingNews(): Flow<TrendingNews>
|
||||
|
||||
/**
|
||||
* Updates viewed flag for provided trending articles.
|
||||
*/
|
||||
suspend fun updateTrendingNewsViewed(articleIds: Collection<Int>, viewed: Boolean)
|
||||
|
||||
/**
|
||||
* Returns available categories.
|
||||
*/
|
||||
suspend fun getCategories(): List<ArticleCategory>
|
||||
|
||||
/**
|
||||
* Updates viewed flag for provided news articles (applies to both regular and trending news).
|
||||
*/
|
||||
suspend fun updateNewsViewed(articleIds: Collection<Int>, viewed: Boolean)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.news.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.domain.news.repository.NewsRepository
|
||||
|
||||
class GetNewsUseCase(private val repository: NewsRepository) {
|
||||
|
||||
suspend fun getNews(limit: Int, newsListConfig: NewsListConfig): Either<Throwable, List<ShortArticle>> =
|
||||
Either.catch {
|
||||
repository.getNews(
|
||||
config = newsListConfig,
|
||||
limit = limit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,18 +20,4 @@ class ManageTrendingNewsUseCase(private val repository: NewsRepository) {
|
|||
.observeTrendingNews()
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a single article as viewed/unviewed.
|
||||
*/
|
||||
suspend fun markAsViewed(articleId: Int, viewed: Boolean = true) {
|
||||
repository.updateTrendingNewsViewed(listOf(articleId), viewed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks multiple articles at once (useful for bulk updates).
|
||||
*/
|
||||
suspend fun markAsViewed(articleIds: Collection<Int>, viewed: Boolean = true) {
|
||||
repository.updateTrendingNewsViewed(articleIds, viewed)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.news.usecase
|
||||
|
||||
import com.tangem.domain.news.repository.NewsRepository
|
||||
|
||||
class MarkArticleAsViewedUseCase(private val repository: NewsRepository) {
|
||||
|
||||
/**
|
||||
* Marks a single article as viewed/unviewed.
|
||||
*/
|
||||
suspend fun markAsViewed(articleId: Int, viewed: Boolean = true) {
|
||||
repository.updateNewsViewed(listOf(articleId), viewed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks multiple articles at once (useful for bulk updates).
|
||||
*/
|
||||
suspend fun markAsViewed(articleIds: Collection<Int>, viewed: Boolean = true) {
|
||||
repository.updateNewsViewed(articleIds, viewed)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,13 +10,11 @@ import com.arkivanov.decompose.router.stack.StackNavigation
|
|||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
|
|
@ -34,8 +32,6 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted entryRoute: FeedEntryRoute?,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val feedEntryChildFactory: FeedEntryChildFactory,
|
||||
) : FeedEntryComponent, AppComponentContext by context {
|
||||
|
||||
|
|
@ -101,8 +97,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
router = innerRouter,
|
||||
),
|
||||
feedEntryClickIntents = clickIntents,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,18 @@ import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
|||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
||||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class FeedEntryChildFactory @Inject constructor() {
|
||||
internal class FeedEntryChildFactory @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
|
|
@ -45,8 +50,6 @@ internal class FeedEntryChildFactory @Inject constructor() {
|
|||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
feedEntryClickIntents: FeedEntryClickIntents,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
): ComposableModularBottomSheetContentComponent {
|
||||
return when (child) {
|
||||
is Child.TokenDetails -> {
|
||||
|
|
@ -55,6 +58,7 @@ internal class FeedEntryChildFactory @Inject constructor() {
|
|||
params = child.params,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
portfolioComponentFactory = portfolioComponentFactory,
|
||||
)
|
||||
}
|
||||
is Child.TokenList -> {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
|
|
@ -19,10 +20,14 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
|
||||
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
|
||||
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent
|
||||
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal class DefaultMarketsTokenDetailsComponent(
|
||||
|
|
@ -30,7 +35,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
val params: Params,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
// TODO add portfolio in migrate [REDACTED_JIRA]
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
// applying l2 compatibility
|
||||
|
|
@ -42,7 +47,30 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
private val analyticsParams = params.analyticsParams
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
|
||||
|
||||
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) {
|
||||
portfolioComponentFactory.create(
|
||||
context = child("my_portfolio"),
|
||||
params = MarketsPortfolioComponent.Params(
|
||||
updatedParams.token,
|
||||
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
componentScope.launch(dispatchers.default) {
|
||||
model.networksState.collectLatest { state ->
|
||||
when (state) {
|
||||
is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks(state.networks)
|
||||
TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Analytics ===
|
||||
if (analyticsParams != null) {
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -88,8 +116,10 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
backgroundColor = LocalMainBottomSheetColor.current.value,
|
||||
state = state,
|
||||
isAccountEnabled = accountsFeatureToggles.isFeatureEnabled,
|
||||
portfolioBlock = {
|
||||
// TODO add portfolio in migrate [REDACTED_JIRA]
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
|
||||
internal interface AddToPortfolioComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val addToPortfolioManager: AddToPortfolioManager,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
interface Callback {
|
||||
fun onDismiss()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, AddToPortfolioComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.api
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal interface AddToPortfolioManager {
|
||||
|
||||
val token: TokenMarketParams
|
||||
val analyticsParams: MarketsPortfolioComponent.AnalyticsParams?
|
||||
val portfolioFetcher: PortfolioFetcher
|
||||
|
||||
val state: StateFlow<State>
|
||||
|
||||
val allAvailableNetworks: Flow<List<TokenMarketInfo.Network>>
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)
|
||||
|
||||
sealed interface State {
|
||||
data object Init : State
|
||||
data class AvailableToAdd(
|
||||
val availableToAddData: AvailableToAddData,
|
||||
) : State
|
||||
|
||||
data object NothingToAdd : State
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
scope: CoroutineScope,
|
||||
token: TokenMarketParams,
|
||||
analyticsParams: MarketsPortfolioComponent.AnalyticsParams?,
|
||||
): AddToPortfolioManager
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.api
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal data class AvailableToAddData(
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
|
||||
) {
|
||||
val isAvailableToAdd: Boolean
|
||||
get() = availableToAddWallets.isNotEmpty()
|
||||
val isSinglePortfolio: Boolean
|
||||
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
|
||||
}
|
||||
|
||||
internal data class AvailableToAddWallet(
|
||||
val userWallet: UserWallet,
|
||||
val accounts: List<AccountStatus>,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class AvailableToAddAccount(
|
||||
val account: AccountStatus,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val addedNetworks: Set<Network>,
|
||||
) {
|
||||
val isSingleNetwork: Boolean
|
||||
get() = availableNetworks.size == 1
|
||||
|
||||
val availableToAddNetworks: Set<TokenMarketInfo.Network> = availableNetworks
|
||||
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
|
||||
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
|
||||
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class SelectedPortfolio(
|
||||
val userWallet: UserWallet,
|
||||
val account: AvailableToAddAccount,
|
||||
val isAccountMode: Boolean,
|
||||
val isAvailableMorePortfolio: Boolean,
|
||||
)
|
||||
|
||||
internal data class SelectedNetwork(
|
||||
val selectedNetwork: TokenMarketInfo.Network,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val isAvailableMoreNetwork: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.addtoken.AddTokenContent
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedNetwork
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedPortfolio
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddTokenModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class AddTokenComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val model: AddTokenModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
val um = state.value ?: return
|
||||
AddTokenContent(
|
||||
modifier = modifier,
|
||||
state = um,
|
||||
)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
|
||||
val selectedPortfolio: Flow<SelectedPortfolio>,
|
||||
val selectedNetwork: Flow<SelectedNetwork>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onChangeNetworkClick()
|
||||
fun onChangePortfolioClick()
|
||||
fun onTokenAdded(status: CryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, AddTokenComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): AddTokenComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedPortfolio
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.ChooseNetworkModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.ChooseNetworkContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ChooseNetworkComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val model: ChooseNetworkModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ChooseNetworkContent(state)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val selectedPortfolio: SelectedPortfolio,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onNetworkSelected(network: TokenMarketInfo.Network)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, ChooseNetworkComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): ChooseNetworkComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.backStack
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.account.PortfolioSelectorComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes
|
||||
import com.tangem.features.feed.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: AddToPortfolioComponent.Params,
|
||||
portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
|
||||
addTokenComponentFactory: AddTokenComponent.Factory,
|
||||
tokenActionsComponentFactory: TokenActionsComponent.Factory,
|
||||
private val chooseNetworkComponentFactory: ChooseNetworkComponent.Factory,
|
||||
) : AppComponentContext by context, AddToPortfolioComponent {
|
||||
|
||||
private val model: AddToPortfolioModel = getOrCreateModel(params)
|
||||
|
||||
private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create(
|
||||
context = child("portfolioSelectorComponent"),
|
||||
params = PortfolioSelectorComponent.Params(
|
||||
portfolioFetcher = model.portfolioFetcher,
|
||||
controller = model.portfolioSelectorController,
|
||||
),
|
||||
)
|
||||
|
||||
private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create(
|
||||
context = child("addTokenComponent"),
|
||||
params = AddTokenComponent.Params(
|
||||
eventBuilder = model.eventBuilder,
|
||||
callbacks = model,
|
||||
selectedPortfolio = model.selectedPortfolio,
|
||||
selectedNetwork = model.selectedNetwork,
|
||||
),
|
||||
)
|
||||
|
||||
private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create(
|
||||
context = child("tokenActionsComponent"),
|
||||
params = TokenActionsComponent.Params(
|
||||
eventBuilder = model.eventBuilder,
|
||||
callbacks = model,
|
||||
data = model.tokenActionsData,
|
||||
),
|
||||
)
|
||||
|
||||
private val childStack = childStack(
|
||||
key = "addToPortfolioStack",
|
||||
handleBackButton = true,
|
||||
source = model.navigation,
|
||||
serializer = AddToPortfolioRoutes.serializer(),
|
||||
initialStack = { model.currentStack },
|
||||
childFactory = ::contentChild,
|
||||
)
|
||||
|
||||
private fun onBack() {
|
||||
if (childStack.backStack.isNotEmpty()) model.navigation.pop() else dismiss()
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val stack by childStack.subscribeAsState()
|
||||
val contentStack = remember { mutableStateOf(stack) }
|
||||
val currentRoute = stack.active.configuration
|
||||
val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty
|
||||
if (isNotEmpty) {
|
||||
contentStack.value = stack
|
||||
}
|
||||
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
scrollableContent = false,
|
||||
onBack = ::onBack,
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = isNotEmpty,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
title = { state ->
|
||||
AnimatedContent(targetState = contentStack.value) { stack ->
|
||||
BottomSheetTitle(
|
||||
stack = stack,
|
||||
onBackClick = ::onBack,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { state ->
|
||||
AnimatedContent(targetState = contentStack.value) { stack ->
|
||||
val paddingModifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
)
|
||||
val isScrollableContent = when (stack.active.configuration) {
|
||||
AddToPortfolioRoutes.PortfolioSelector -> false
|
||||
AddToPortfolioRoutes.AddToken,
|
||||
AddToPortfolioRoutes.Empty,
|
||||
is AddToPortfolioRoutes.NetworkSelector,
|
||||
AddToPortfolioRoutes.TokenActions,
|
||||
-> true
|
||||
}
|
||||
if (isScrollableContent) {
|
||||
Column(
|
||||
modifier = paddingModifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
stack.active.instance.Content(modifier = Modifier)
|
||||
}
|
||||
} else {
|
||||
stack.active.instance.Content(modifier = paddingModifier)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetTitle(
|
||||
stack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>,
|
||||
onBackClick: (() -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val title: TextReference = when (stack.active.configuration) {
|
||||
AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token)
|
||||
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
|
||||
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
|
||||
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
|
||||
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
|
||||
.title.collectAsStateWithLifecycle().value
|
||||
}
|
||||
val startIconRes: Int?
|
||||
val endIconRes: Int?
|
||||
if (stack.backStack.isNotEmpty()) {
|
||||
startIconRes = R.drawable.ic_back_24
|
||||
endIconRes = null
|
||||
} else {
|
||||
startIconRes = null
|
||||
endIconRes = R.drawable.ic_close_24
|
||||
}
|
||||
TangemModalBottomSheetTitle(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
startIconRes = startIconRes,
|
||||
endIconRes = endIconRes,
|
||||
onStartClick = onBackClick,
|
||||
onEndClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun contentChild(
|
||||
config: AddToPortfolioRoutes,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableContentComponent = when (config) {
|
||||
AddToPortfolioRoutes.AddToken -> addTokenComponent
|
||||
AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent
|
||||
AddToPortfolioRoutes.TokenActions -> tokenActionsComponent
|
||||
AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY
|
||||
is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = ChooseNetworkComponent.Params(
|
||||
selectedPortfolio = config.selectedPortfolio,
|
||||
callbacks = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AddToPortfolioComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: AddToPortfolioComponent.Params,
|
||||
): DefaultAddToPortfolioComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.TokenActionsModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.TokenActionsContent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class TokenActionsComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val model: TokenActionsModel = getOrCreateModel(params)
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
val tokenActionsUM = state.value ?: return
|
||||
TokenActionsContent(
|
||||
modifier = modifier,
|
||||
state = tokenActionsUM,
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
|
||||
data class Params(
|
||||
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
|
||||
val data: Flow<PortfolioData.CryptoCurrencyData>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onLaterClick()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, TokenActionsComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): TokenActionsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.converter
|
||||
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
|
||||
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AvailableToAddAccount
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AvailableToAddData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AvailableToAddWallet
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AvailableToAddDataConverter @Inject constructor(
|
||||
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
|
||||
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) {
|
||||
|
||||
suspend fun convert(
|
||||
balances: Map<UserWalletId, PortfolioFetcher.PortfolioBalance>,
|
||||
availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
marketParams: TokenMarketParams,
|
||||
): AvailableToAddData {
|
||||
suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? {
|
||||
val currencies = availableNetworks
|
||||
.mapNotNull { network ->
|
||||
createCryptoCurrency(
|
||||
userWallet = wallet,
|
||||
network = network,
|
||||
marketParams = marketParams,
|
||||
account = this.account,
|
||||
)
|
||||
}
|
||||
|
||||
if (currencies.isEmpty()) return null
|
||||
|
||||
val addedNetworks = getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, currencies)
|
||||
.fold(
|
||||
ifEmpty = { emptySet() },
|
||||
ifSome = { map ->
|
||||
map.values.flatMapTo(hashSetOf()) { statuses ->
|
||||
statuses.map { it.currency.network }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return AvailableToAddAccount(
|
||||
account = this,
|
||||
availableNetworks = availableNetworks,
|
||||
addedNetworks = addedNetworks,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getAvailableToAddWallet(
|
||||
entry: Map.Entry<UserWalletId, PortfolioFetcher.PortfolioBalance>,
|
||||
): AvailableToAddWallet {
|
||||
val (_, balance) = entry
|
||||
val wallet = balance.userWallet
|
||||
val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks)
|
||||
val accounts = balance.accountsBalance.accountStatuses
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount> = accounts
|
||||
.mapNotNull { accountStatus ->
|
||||
val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null
|
||||
accountStatus.account.accountId to availableToAddAccount
|
||||
}
|
||||
.filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() }
|
||||
.toMap()
|
||||
return AvailableToAddWallet(
|
||||
userWallet = wallet,
|
||||
accounts = accounts,
|
||||
availableNetworks = filteredNetworks,
|
||||
availableToAddAccounts = availableToAddAccounts,
|
||||
)
|
||||
}
|
||||
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet> = balances
|
||||
.map { entry ->
|
||||
val (walletId, _) = entry
|
||||
val availableToAddWallet = getAvailableToAddWallet(entry)
|
||||
walletId to availableToAddWallet
|
||||
}
|
||||
.filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() }
|
||||
.toMap()
|
||||
|
||||
return AvailableToAddData(
|
||||
availableToAddWallets = availableToAddWallets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.filteredAvailableNetworks(networks: Set<TokenMarketInfo.Network>) =
|
||||
filterAvailableNetworksForWalletUseCase(
|
||||
userWalletId = this.walletId,
|
||||
networks = networks,
|
||||
)
|
||||
|
||||
private suspend fun createCryptoCurrency(
|
||||
userWallet: UserWallet,
|
||||
network: TokenMarketInfo.Network,
|
||||
marketParams: TokenMarketParams,
|
||||
account: Account,
|
||||
): CryptoCurrency? {
|
||||
val derivationIndex = when (account) {
|
||||
is Account.CryptoPortfolio -> account.derivationIndex
|
||||
}
|
||||
return getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = marketParams,
|
||||
network = network,
|
||||
accountIndex = derivationIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.di
|
||||
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AddToPortfolioManager
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.DefaultAddToPortfolioManager
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AddToPortfolioComponentModule {
|
||||
|
||||
@Binds
|
||||
fun bindAddToPortfolioComponent(factory: DefaultAddToPortfolioComponent.Factory): AddToPortfolioComponent.Factory
|
||||
|
||||
@Binds
|
||||
fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddTokenModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.ChooseNetworkModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.TokenActionsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface AddToPortfolioModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AddTokenModel::class)
|
||||
fun addTokenModel(model: AddTokenModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AddToPortfolioModel::class)
|
||||
fun addToPortfolioModel(model: AddToPortfolioModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TokenActionsModel::class)
|
||||
fun tokenActionsModel(model: TokenActionsModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ChooseNetworkModel::class)
|
||||
fun chooseNetworkModel(model: ChooseNetworkModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.popToFirst
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceAll
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
|
||||
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.account.PortfolioSelectorController
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.*
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ChooseNetworkComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.TokenActionsComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.PortfolioTokenUMConverter.Companion.toQuickActions
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TOKEN_ACTIONS_DELAY = 500L
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class AddToPortfolioModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val callbackDelegate: AddToPortfolioCallbackDelegate,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2,
|
||||
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
val portfolioSelectorController: PortfolioSelectorController,
|
||||
) : Model(),
|
||||
ChooseNetworkComponent.Callbacks by callbackDelegate,
|
||||
TokenActionsComponent.Callbacks by callbackDelegate,
|
||||
AddTokenComponent.Callbacks by callbackDelegate {
|
||||
|
||||
private val params = paramsContainer.require<AddToPortfolioComponent.Params>()
|
||||
val navigation = StackNavigation<AddToPortfolioRoutes>()
|
||||
var currentStack = listOf<AddToPortfolioRoutes>(AddToPortfolioRoutes.Empty)
|
||||
|
||||
/* Flows that hold state and provide it to child models */
|
||||
val selectedNetwork: MutableSharedFlow<SelectedNetwork> = replayMutableSharedFlow()
|
||||
val selectedPortfolio: MutableSharedFlow<SelectedPortfolio> = replayMutableSharedFlow()
|
||||
val tokenActionsData: MutableSharedFlow<PortfolioData.CryptoCurrencyData> = replayMutableSharedFlow()
|
||||
|
||||
private val addToPortfolioManager = params.addToPortfolioManager
|
||||
val portfolioFetcher = addToPortfolioManager.portfolioFetcher
|
||||
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
|
||||
token = addToPortfolioManager.token,
|
||||
source = addToPortfolioManager.analyticsParams?.source,
|
||||
)
|
||||
|
||||
val featureData: Flow<AddToPortfolioManager.State> = combineFeatureData()
|
||||
|
||||
init {
|
||||
navigation.subscribe { currentStack = it.transformer.invoke(currentStack) }
|
||||
startAddToPortfolioFlow()
|
||||
}
|
||||
|
||||
private fun <T> replayMutableSharedFlow() = MutableSharedFlow<T>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun startAddToPortfolioFlow() {
|
||||
channelFlow<Unit> {
|
||||
fun finishFlow() {
|
||||
params.callback.onDismiss()
|
||||
channel.close()
|
||||
}
|
||||
|
||||
val featureDataFlow: StateFlow<AvailableToAddData> = featureData
|
||||
.filterIsInstance<AddToPortfolioManager.State.AvailableToAdd>()
|
||||
.map { it.availableToAddData }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(this)
|
||||
val isAccountMode = portfolioSelectorController.isAccountModeSync()
|
||||
|
||||
// use snapshot data, looks like we don’t need to remap at runtime
|
||||
val data = featureDataFlow.value
|
||||
|
||||
// you must control it via [AddToPortfolioManager.state]
|
||||
if (!data.isAvailableToAdd) {
|
||||
finishFlow()
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
// init data flows, emits on user/code selection, updates state holder
|
||||
val firstSelectedPortfolio = setupPortfolioFlow(data)
|
||||
.onEach { selectedPortfolio.emit(it) }
|
||||
val firstSelectedNetwork = setupNetworkFlow(firstSelectedPortfolio)
|
||||
.onEach { selectedNetwork.emit(it) }
|
||||
|
||||
val isSinglePortfolio = data.isSinglePortfolio
|
||||
if (isSinglePortfolio) {
|
||||
val accountId = data.availableToAddWallets.values.first()
|
||||
.availableToAddAccounts.values.first()
|
||||
.account.account.accountId
|
||||
// force select a portfolio, triggers [selectedPortfolio]
|
||||
portfolioSelectorController.selectAccount(accountId)
|
||||
} else {
|
||||
logAccountSelector(isAccountMode)
|
||||
navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector)
|
||||
}
|
||||
|
||||
val firstPartOfNavigation: Job = firstSelectedPortfolio
|
||||
.onEach { portfolio ->
|
||||
val isSingleAvailableNetwork = portfolio.account.isSingleNetwork
|
||||
when {
|
||||
// force select a network, triggers [selectedNetwork]
|
||||
isSingleAvailableNetwork -> {
|
||||
val singleNetwork = portfolio.account.availableToAddNetworks.first()
|
||||
callbackDelegate.onNetworkSelected(singleNetwork)
|
||||
}
|
||||
// it's important to control root screen, UI depends on it(close/arrow icon)
|
||||
isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio))
|
||||
else -> navigation.pushNew(routeToNetworkSelector(portfolio))
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
// main flow that combine all require data
|
||||
val allRequireForAdd = combine(
|
||||
flow = firstSelectedNetwork,
|
||||
flow2 = firstSelectedPortfolio,
|
||||
transform = { a, b -> a to b },
|
||||
)
|
||||
|
||||
// suspend until all required data is selected
|
||||
allRequireForAdd.first()
|
||||
// line of navigation to AddToken screen is finished; cancel the job, select a new root screen
|
||||
firstPartOfNavigation.cancel()
|
||||
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
|
||||
|
||||
var middleNavigationJob: Job? = null
|
||||
// handle actions from AddToken screen
|
||||
callbackDelegate.onChangeNetworkClick.receiveAsFlow()
|
||||
.onEach {
|
||||
middleNavigationJob?.cancel()
|
||||
middleNavigationJob = changeNetworkNavigationFlow()
|
||||
.launchIn(this)
|
||||
val route = routeToNetworkSelector(selectedPortfolio.first())
|
||||
navigation.pushNew(route)
|
||||
}
|
||||
.launchIn(this)
|
||||
// handle actions from AddToken screen
|
||||
callbackDelegate.onChangePortfolioClick.receiveAsFlow()
|
||||
.onEach {
|
||||
middleNavigationJob?.cancel()
|
||||
setSelectedAccountToSelectorController()
|
||||
middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this)
|
||||
logAccountSelector(isAccountMode)
|
||||
navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
// suspend until token is added
|
||||
val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first()
|
||||
middleNavigationJob?.cancel()
|
||||
val selectedPortfolio = selectedPortfolio.first()
|
||||
|
||||
messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added)))
|
||||
|
||||
setupTokenActionsFlow(selectedPortfolio, addedToken)
|
||||
.onEach { cryptoCurrencyData ->
|
||||
tokenActionsData.emit(cryptoCurrencyData)
|
||||
navigation.replaceAll(AddToPortfolioRoutes.TokenActions)
|
||||
}
|
||||
.onEmpty { finishFlow() }
|
||||
.launchIn(this)
|
||||
|
||||
callbackDelegate.onLaterClick.receiveAsFlow().first()
|
||||
finishFlow()
|
||||
}
|
||||
.catch { throwable ->
|
||||
Timber.e(throwable)
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun logAccountSelector(isAccountMode: Boolean) {
|
||||
if (isAccountMode) {
|
||||
analyticsEventHandler.send(eventBuilder.popupToChooseAccount())
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeNetworkNavigationFlow(): Flow<SelectedNetwork> {
|
||||
return setupNetworkFlow(selectedPortfolio)
|
||||
.onEach { newNetwork ->
|
||||
selectedNetwork.emit(newNetwork)
|
||||
navigation.popToFirst()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSelectedAccountToSelectorController() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val selectedAccount = selectedPortfolio
|
||||
.first()
|
||||
.account
|
||||
.account
|
||||
.account
|
||||
.accountId
|
||||
portfolioSelectorController.selectAccount(selectedAccount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changePortfolioNavigationFlow(data: AvailableToAddData): Flow<Unit> {
|
||||
val changedPortfolio = setupPortfolioFlow(data)
|
||||
.drop(1)
|
||||
.onEach { portfolio -> navigation.pushNew(routeToNetworkSelector(portfolio)) }
|
||||
val changedNetwork = setupNetworkFlow(changedPortfolio)
|
||||
return combine(
|
||||
flow = changedPortfolio,
|
||||
flow2 = changedNetwork,
|
||||
transform = { newPortfolio, newNetwork ->
|
||||
selectedPortfolio.tryEmit(newPortfolio)
|
||||
selectedNetwork.tryEmit(newNetwork)
|
||||
navigation.popToFirst()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupTokenActionsFlow(
|
||||
selectedPortfolio: SelectedPortfolio,
|
||||
addedToken: CryptoCurrencyStatus,
|
||||
): Flow<PortfolioData.CryptoCurrencyData> {
|
||||
val timeFlow = channelFlow {
|
||||
val timerJob = launch { delay(TOKEN_ACTIONS_DELAY) }
|
||||
getCryptoCurrencyActionsUseCase(
|
||||
currency = addedToken.currency,
|
||||
accountId = selectedPortfolio.account.account.account.accountId,
|
||||
).onEach { state ->
|
||||
val requestedQuickActions = toQuickActions(state.states)
|
||||
when {
|
||||
requestedQuickActions.isNotEmpty() -> {
|
||||
timerJob.cancel()
|
||||
send(state)
|
||||
}
|
||||
// wait any requestedQuickActions while timer active
|
||||
timerJob.isActive -> Unit
|
||||
else -> close()
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
return timeFlow.map { actionsState ->
|
||||
PortfolioData.CryptoCurrencyData(
|
||||
userWallet = selectedPortfolio.userWallet,
|
||||
status = actionsState.cryptoCurrencyStatus,
|
||||
actions = actionsState.states,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPortfolioFlow(data: AvailableToAddData): Flow<SelectedPortfolio> = combine(
|
||||
flow = portfolioSelectorController.isAccountMode,
|
||||
flow2 = portfolioSelectorController.selectedAccount,
|
||||
transform = { isAccountMode, selectedAccountId ->
|
||||
selectedAccountId ?: return@combine null
|
||||
val availableToAddWallets =
|
||||
data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null
|
||||
val availableToAddAccount =
|
||||
availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null
|
||||
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged())
|
||||
SelectedPortfolio(
|
||||
isAccountMode = isAccountMode,
|
||||
userWallet = availableToAddWallets.userWallet,
|
||||
account = availableToAddAccount,
|
||||
isAvailableMorePortfolio = !data.isSinglePortfolio,
|
||||
)
|
||||
},
|
||||
)
|
||||
.filterNotNull()
|
||||
|
||||
private fun setupNetworkFlow(selectedPortfolioFlow: Flow<SelectedPortfolio>): Flow<SelectedNetwork> = combine(
|
||||
flow = selectedPortfolioFlow,
|
||||
flow2 = callbackDelegate.onNetworkSelected.receiveAsFlow(),
|
||||
transform = transform@{ selectedPortfolio, selectedNetwork ->
|
||||
SelectedNetwork(
|
||||
cryptoCurrency = createCryptoCurrency(
|
||||
userWallet = selectedPortfolio.userWallet,
|
||||
network = selectedNetwork,
|
||||
account = selectedPortfolio.account,
|
||||
) ?: return@transform null,
|
||||
selectedNetwork = selectedNetwork,
|
||||
isAvailableMoreNetwork = !selectedPortfolio.account.isSingleNetwork,
|
||||
)
|
||||
},
|
||||
)
|
||||
.filterNotNull()
|
||||
|
||||
private suspend fun createCryptoCurrency(
|
||||
userWallet: UserWallet,
|
||||
network: TokenMarketInfo.Network,
|
||||
account: AvailableToAddAccount,
|
||||
): CryptoCurrency? {
|
||||
val accountIndex = when (account.account) {
|
||||
is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex
|
||||
}
|
||||
return getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = addToPortfolioManager.token,
|
||||
network = network,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
|
||||
private fun routeToNetworkSelector(portfolio: SelectedPortfolio): AddToPortfolioRoutes.NetworkSelector {
|
||||
return AddToPortfolioRoutes.NetworkSelector(selectedPortfolio = portfolio)
|
||||
}
|
||||
|
||||
private fun combineFeatureData() = addToPortfolioManager.state.onEach { state ->
|
||||
when (state) {
|
||||
is AddToPortfolioManager.State.AvailableToAdd ->
|
||||
portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus ->
|
||||
val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId]
|
||||
?: return@isEnabled false
|
||||
val availableAccount =
|
||||
availableWallet.availableToAddAccounts[accountStatus.account.accountId]
|
||||
return@isEnabled availableAccount != null
|
||||
}
|
||||
AddToPortfolioManager.State.Init,
|
||||
AddToPortfolioManager.State.NothingToAdd,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ModelScoped
|
||||
internal class AddToPortfolioCallbackDelegate @Inject constructor() :
|
||||
ChooseNetworkComponent.Callbacks,
|
||||
TokenActionsComponent.Callbacks,
|
||||
AddTokenComponent.Callbacks {
|
||||
|
||||
val onNetworkSelected = Channel<TokenMarketInfo.Network>()
|
||||
val onLaterClick = Channel<Unit>()
|
||||
val onChangeNetworkClick = Channel<Unit>()
|
||||
val onChangePortfolioClick = Channel<Unit>()
|
||||
val onTokenAdded = Channel<CryptoCurrencyStatus>()
|
||||
|
||||
override fun onNetworkSelected(network: TokenMarketInfo.Network) {
|
||||
onNetworkSelected.trySend(network)
|
||||
}
|
||||
|
||||
override fun onLaterClick() {
|
||||
onLaterClick.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onChangeNetworkClick() {
|
||||
onChangeNetworkClick.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onChangePortfolioClick() {
|
||||
onChangePortfolioClick.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onTokenAdded(status: CryptoCurrencyStatus) {
|
||||
onTokenAdded.trySend(status)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedPortfolio
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
internal sealed interface AddToPortfolioRoutes : Route {
|
||||
|
||||
@Serializable
|
||||
data object Empty : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object PortfolioSelector : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data class NetworkSelector(
|
||||
val selectedPortfolio: SelectedPortfolio,
|
||||
) : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object AddToken : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object TokenActions : AddToPortfolioRoutes
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
|
||||
|
||||
import com.tangem.common.ui.addtoken.AddTokenUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedNetwork
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedPortfolio
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class AddTokenModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val uiBuilder: AddTokenUiBuilder,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AddTokenComponent.Params>()
|
||||
private val analyticsEventBuilder = params.eventBuilder
|
||||
private val addTokenJob = JobHolder()
|
||||
|
||||
val uiState: StateFlow<AddTokenUM?>
|
||||
field = MutableStateFlow(value = null)
|
||||
|
||||
init {
|
||||
combine(
|
||||
flow = params.selectedNetwork.distinctUntilChanged(),
|
||||
flow2 = params.selectedPortfolio.distinctUntilChanged(),
|
||||
transform = { selectedNetwork, selectedPortfolio ->
|
||||
addTokenJob.cancel()
|
||||
val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio)
|
||||
uiBuilder.updateContent(
|
||||
selectedPortfolio = selectedPortfolio,
|
||||
selectedNetwork = selectedNetwork,
|
||||
isTangemIconVisible = isTangemIconVisible,
|
||||
onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) },
|
||||
)
|
||||
},
|
||||
)
|
||||
.onEach { newUI -> uiState.value = newUI }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) =
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val um = uiState.value ?: return@launch
|
||||
uiState.value = um.toggleProgress(true)
|
||||
val blockchainNames = listOf(selectedNetwork.selectedNetwork)
|
||||
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
|
||||
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
|
||||
|
||||
val cryptoCurrency = selectedNetwork.cryptoCurrency
|
||||
val account = selectedPortfolio.account.account.account
|
||||
val accountId = account.accountId
|
||||
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
|
||||
.onLeft { throwable ->
|
||||
processError(error = throwable)
|
||||
uiState.value = um.toggleProgress(false)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val status = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = accountId.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrNull()
|
||||
if (status == null) {
|
||||
processError(error = null)
|
||||
} else {
|
||||
when (account) {
|
||||
is Account.CryptoPortfolio -> if (!account.isMainAccount) {
|
||||
analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount())
|
||||
}
|
||||
}
|
||||
params.callbacks.onTokenAdded(status.status)
|
||||
}
|
||||
uiState.value = um.toggleProgress(false)
|
||||
}
|
||||
|
||||
private suspend fun needColdWalletInteraction(
|
||||
selectedNetwork: SelectedNetwork,
|
||||
selectedPortfolio: SelectedPortfolio,
|
||||
): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = selectedPortfolio.userWallet.walletId,
|
||||
networksWithDerivationPath = mapOf(selectedNetwork.selectedNetwork.networkId to null),
|
||||
)
|
||||
|
||||
private fun processError(error: Throwable?) {
|
||||
val message = error?.message?.let { stringReference(it) }
|
||||
?: resourceReference(R.string.common_something_went_wrong)
|
||||
messageSender.send(ToastMessage(message = message))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
|
||||
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
import com.tangem.common.ui.account.PortfolioSelectUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.addtoken.AddTokenUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.iconResId
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedNetwork
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.SelectedPortfolio
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent
|
||||
import com.tangem.features.feed.impl.R
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class AddTokenUiBuilder @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
) {
|
||||
private val params = paramsContainer.require<AddTokenComponent.Params>()
|
||||
|
||||
private fun createNetwork(selectedNetwork: SelectedNetwork): AddTokenUM.Network {
|
||||
return AddTokenUM.Network(
|
||||
icon = selectedNetwork.cryptoCurrency.network.iconResId,
|
||||
name = stringReference(selectedNetwork.cryptoCurrency.network.name),
|
||||
editable = selectedNetwork.isAvailableMoreNetwork,
|
||||
onClick = { params.callbacks.onChangeNetworkClick() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM {
|
||||
val accountIcon: CryptoPortfolioIconUM?
|
||||
val portfolioName: TextReference
|
||||
when (selectedPortfolio.isAccountMode) {
|
||||
false -> {
|
||||
accountIcon = null
|
||||
portfolioName = stringReference(selectedPortfolio.userWallet.name)
|
||||
}
|
||||
true -> {
|
||||
val accountStatus = selectedPortfolio.account.account
|
||||
portfolioName = accountStatus.account.accountName.toUM().value
|
||||
accountIcon = when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.account.icon.toUM()
|
||||
}
|
||||
}
|
||||
}
|
||||
return PortfolioSelectUM(
|
||||
icon = accountIcon,
|
||||
name = portfolioName,
|
||||
isAccountMode = selectedPortfolio.isAccountMode,
|
||||
isMultiChoice = selectedPortfolio.isAvailableMorePortfolio,
|
||||
onClick = { params.callbacks.onChangePortfolioClick() },
|
||||
)
|
||||
}
|
||||
|
||||
fun updateContent(
|
||||
selectedPortfolio: SelectedPortfolio,
|
||||
selectedNetwork: SelectedNetwork,
|
||||
isTangemIconVisible: Boolean,
|
||||
onConfirmClick: () -> Unit,
|
||||
): AddTokenUM {
|
||||
// its may happens when change portfolio after selected both params in line navigation
|
||||
val isAvailableNetwork = selectedPortfolio.account.availableToAddNetworks
|
||||
.any { selectedNetwork.selectedNetwork.networkId == it.networkId }
|
||||
val button = AddTokenUM.Button(
|
||||
isEnabled = isAvailableNetwork,
|
||||
showProgress = false,
|
||||
isTangemIconVisible = isTangemIconVisible,
|
||||
text = resourceReference(R.string.common_add),
|
||||
onConfirmClick = onConfirmClick,
|
||||
)
|
||||
val networkUM = createNetwork(selectedNetwork)
|
||||
val portfolioUM = createPortfolio(selectedPortfolio)
|
||||
val currency = selectedNetwork.cryptoCurrency
|
||||
val tokenToAdd = TokenItemState.Content(
|
||||
id = currency.id.value,
|
||||
iconState = CryptoCurrencyToIconStateConverter().convert(currency),
|
||||
titleState = TokenItemState.TitleState.Content(stringReference(currency.name)),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
return AddTokenUM(
|
||||
tokenToAdd = tokenToAdd,
|
||||
network = networkUM,
|
||||
portfolio = portfolioUM,
|
||||
button = button,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun AddTokenUM.toggleProgress(showProgress: Boolean) = this.copy(
|
||||
button = this.button.copy(showProgress = showProgress),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
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.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ChooseNetworkComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.ChooseNetworkUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.BlockchainRowUMConverter
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class ChooseNetworkModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ChooseNetworkComponent.Params>()
|
||||
|
||||
val uiState: StateFlow<ChooseNetworkUM> = MutableStateFlow(buildUI())
|
||||
|
||||
private fun buildUI(): ChooseNetworkUM {
|
||||
val allAvailable = params.selectedPortfolio.account.availableNetworks
|
||||
val alreadyAdded = allAvailable
|
||||
.subtract(params.selectedPortfolio.account.availableToAddNetworks)
|
||||
val converter = BlockchainRowUMConverter(
|
||||
alreadyAddedNetworks = alreadyAdded.mapTo(mutableSetOf()) { it.networkId },
|
||||
)
|
||||
val allAvailableNetworks = allAvailable.map { it to true }
|
||||
return ChooseNetworkUM(
|
||||
networks = converter.convertList(allAvailableNetworks).toPersistentList(),
|
||||
onNetworkClick = onNetworkClick@{ row ->
|
||||
val network = allAvailable
|
||||
.find { it.networkId == row.id }
|
||||
?: return@onNetworkClick
|
||||
checkNetwork(row, network)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkNetwork(row: BlockchainRowUM, network: TokenMarketInfo.Network) = modelScope.launch {
|
||||
val selectedWalletId = params.selectedPortfolio.userWallet.walletId
|
||||
val unsupportedState = checkCurrencyUnsupportedState(
|
||||
userWalletId = selectedWalletId,
|
||||
rawNetworkId = row.id,
|
||||
isMainNetwork = row.isMainNetwork,
|
||||
)
|
||||
if (unsupportedState != null) {
|
||||
showUnsupportedWarning(unsupportedState)
|
||||
} else {
|
||||
params.callbacks.onNetworkSelected(network)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkCurrencyUnsupportedState(
|
||||
userWalletId: UserWalletId,
|
||||
rawNetworkId: String,
|
||||
isMainNetwork: Boolean,
|
||||
): CurrencyUnsupportedState? {
|
||||
return checkCurrencyUnsupportedUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = rawNetworkId,
|
||||
isMainNetwork = isMainNetwork,
|
||||
).getOrElse { throwable ->
|
||||
Timber.e(
|
||||
throwable,
|
||||
"""
|
||||
Failed to check currency unsupported state
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Network ID: $rawNetworkId
|
||||
|- Is main network: $isMainNetwork
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val message = SnackbarMessage(
|
||||
message = throwable.localizedMessage?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
|
||||
val message = DialogMessage(
|
||||
message = when (unsupportedState) {
|
||||
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.TokenActionsComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.TokenActionsUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.TokenActionsHandler
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokenActionsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
tokenActionsIntentsFactory: TokenActionsHandler.Factory,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val uiBuilder: TokenActionsUiBuilder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TokenActionsComponent.Params>()
|
||||
private val analyticsEventBuilder get() = params.eventBuilder
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase.invokeOrDefault()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val tokenActionsHandler: TokenActionsHandler =
|
||||
tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { },
|
||||
onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) },
|
||||
)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
val uiState: StateFlow<TokenActionsUM?> = params.data
|
||||
.mapLatest { uiBuilder.build(it, tokenActionsHandler) }
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
private fun handledQuickAction(handledAction: TokenActionsHandler.HandledQuickAction) {
|
||||
val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action)
|
||||
analyticsEventHandler.send(event)
|
||||
val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive
|
||||
if (!isReceive) return
|
||||
modelScope.launch {
|
||||
val tokenConfig = receiveAddressesFactory.create(
|
||||
status = handledAction.cryptoCurrencyData.status,
|
||||
userWalletId = handledAction.cryptoCurrencyData.userWallet.walletId,
|
||||
) ?: return@launch
|
||||
bottomSheetNavigation.activate(tokenConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.TokenActionsComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.TokenActionsUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.PortfolioTokenUMConverter
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.TokenActionsHandler
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class TokenActionsUiBuilder @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
private val params = paramsContainer.require<TokenActionsComponent.Params>()
|
||||
|
||||
fun build(data: PortfolioData.CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM {
|
||||
val status = data.status
|
||||
val tokenUM = TokenItemState.Content(
|
||||
id = status.currency.id.value,
|
||||
iconState = CryptoCurrencyToIconStateConverter().convert(status.currency),
|
||||
titleState = TokenItemState.TitleState.Content(stringReference(status.currency.name)),
|
||||
fiatAmountState = null,
|
||||
subtitle2State = null,
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(status.currency.symbol)),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
return TokenActionsUM(
|
||||
token = tokenUM,
|
||||
onLaterClick = {
|
||||
analyticsEventHandler.send(params.eventBuilder.getTokenLater())
|
||||
params.callbacks.onLaterClick()
|
||||
},
|
||||
quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.components.rows.BlockchainRow
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.ChooseNetworkUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.UUID
|
||||
|
||||
private const val DISABLED_ALPHA = 0.4f
|
||||
|
||||
@Composable
|
||||
internal fun ChooseNetworkContent(state: ChooseNetworkUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
state.networks.fastForEachIndexed { index, model ->
|
||||
key(model.id) {
|
||||
BlockchainRow(
|
||||
model = model,
|
||||
itemPadding = PaddingValues(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = model.isEnabled, onClick = { state.onNetworkClick(model) }),
|
||||
) {
|
||||
if (!model.isEnabled) {
|
||||
Label(
|
||||
modifier = Modifier.alpha(DISABLED_ALPHA),
|
||||
state = LabelUM(
|
||||
text = resourceReference(R.string.common_added),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview(@PreviewParameter(ChooseNetworkContentProvider::class) content: ChooseNetworkUM) {
|
||||
TangemThemePreview {
|
||||
ChooseNetworkContent(
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChooseNetworkContentProvider : PreviewParameterProvider<ChooseNetworkUM> {
|
||||
|
||||
private val blockchainRow = BlockchainRowUM(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = "Etherium 3",
|
||||
type = "TEST",
|
||||
iconResId = R.drawable.img_eth_22,
|
||||
isMainNetwork = false,
|
||||
isSelected = true,
|
||||
isEnabled = true,
|
||||
)
|
||||
|
||||
override val values: Sequence<ChooseNetworkUM>
|
||||
get() = sequenceOf(
|
||||
ChooseNetworkUM(
|
||||
onNetworkClick = {},
|
||||
networks = persistentListOf(
|
||||
blockchainRow.copy(
|
||||
type = "MAIN",
|
||||
isMainNetwork = true,
|
||||
),
|
||||
blockchainRow.copy(
|
||||
iconResId = R.drawable.ic_bsc_16,
|
||||
isEnabled = false,
|
||||
),
|
||||
blockchainRow.copy(iconResId = R.drawable.img_polygon_22),
|
||||
blockchainRow.copy(iconResId = R.drawable.img_optimism_22),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AddToPortfolioManager
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.converter.AvailableToAddDataConverter
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class DefaultAddToPortfolioManager @AssistedInject constructor(
|
||||
private val availableToAddDataConverter: AvailableToAddDataConverter,
|
||||
@Assisted override val token: TokenMarketParams,
|
||||
@Assisted override val analyticsParams: MarketsPortfolioComponent.AnalyticsParams?,
|
||||
@Assisted val scope: CoroutineScope,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
portfolioFetcherFactory: PortfolioFetcher.Factory,
|
||||
) : AddToPortfolioManager {
|
||||
|
||||
private val _allAvailableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val allAvailableNetworks: Flow<List<TokenMarketInfo.Network>> = _allAvailableNetworks.asSharedFlow()
|
||||
override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
|
||||
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
override val state: StateFlow<AddToPortfolioManager.State> =
|
||||
combine(
|
||||
flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(),
|
||||
flow2 = allAvailableNetworks.map { it.toSet() }.distinctUntilChanged(),
|
||||
) { balances, availableNetworks ->
|
||||
val data = availableToAddDataConverter.convert(
|
||||
balances = balances,
|
||||
availableNetworks = availableNetworks,
|
||||
marketParams = token,
|
||||
)
|
||||
if (data.isAvailableToAdd) {
|
||||
AddToPortfolioManager.State.AvailableToAdd(data)
|
||||
} else {
|
||||
AddToPortfolioManager.State.NothingToAdd
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AddToPortfolioManager.State.Init,
|
||||
)
|
||||
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
_allAvailableNetworks.tryEmit(networks)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AddToPortfolioManager.Factory {
|
||||
override fun create(
|
||||
scope: CoroutineScope,
|
||||
token: TokenMarketParams,
|
||||
analyticsParams: MarketsPortfolioComponent.AnalyticsParams?,
|
||||
): DefaultAddToPortfolioManager
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.icons.badge.drawBadge
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.TokenActionsUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
internal fun TokenActionsContent(state: TokenActionsUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
TokenItem(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.action),
|
||||
state = state.token,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
SpacerH(TangemTheme.dimens.spacing14)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
state.quickActions.actions.fastForEach { actionUM ->
|
||||
key(actionUM.title) {
|
||||
ActionRow(
|
||||
state = actionUM,
|
||||
onClick = { state.quickActions.onQuickActionClick(actionUM) },
|
||||
onLongClick = { state.quickActions.onQuickActionLongClick(actionUM) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH16()
|
||||
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_later),
|
||||
onClick = state.onLaterClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ActionRow(
|
||||
state: QuickActionUM,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
val onLongClickInternal = {
|
||||
hapticManager.perform(TangemHapticEffect.View.LongPress)
|
||||
onLongClick()
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onLongClick = onLongClickInternal.takeIf { state.isLongClickAvailable },
|
||||
onClick = {
|
||||
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
|
||||
onClick()
|
||||
},
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing15),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val containerColor = TangemTheme.colors.background.action
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(36.dp)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (state is QuickActionUM.Exchange && state.shouldShowBadge) {
|
||||
drawBadge(containerColor = containerColor, offset = 4.dp)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.requiredSize(TangemTheme.dimens.size16),
|
||||
imageVector = ImageVector.vectorResource(id = state.icon),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(TokenActionsContentPreviewProvider::class) state: TokenActionsUM) {
|
||||
TangemThemePreview {
|
||||
TokenActionsContent(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenActionsContentPreviewProvider : PreviewParameterProvider<TokenActionsUM> {
|
||||
private val tokenState
|
||||
get() = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(
|
||||
text = stringReference(value = "Tether"),
|
||||
),
|
||||
fiatAmountState = null,
|
||||
subtitle2State = null,
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
||||
override val values: Sequence<TokenActionsUM>
|
||||
get() = sequenceOf(
|
||||
TokenActionsUM(
|
||||
quickActions = PortfolioTokenUM.QuickActions(
|
||||
actions = persistentListOf(
|
||||
QuickActionUM.Buy,
|
||||
QuickActionUM.Exchange(shouldShowBadge = true),
|
||||
QuickActionUM.Receive,
|
||||
),
|
||||
onQuickActionClick = {},
|
||||
onQuickActionLongClick = {},
|
||||
),
|
||||
token = tokenState,
|
||||
onLaterClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ChooseNetworkUM(
|
||||
val networks: ImmutableList<BlockchainRowUM>,
|
||||
val onNetworkClick: (BlockchainRowUM) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
|
||||
internal data class TokenActionsUM(
|
||||
val token: TokenItemState,
|
||||
val quickActions: PortfolioTokenUM.QuickActions,
|
||||
val onLaterClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.api
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
interface MarketsPortfolioComponent : ComposableContentComponent {
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketParams,
|
||||
val analyticsParams: AnalyticsParams?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsParams(
|
||||
val source: String,
|
||||
)
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)
|
||||
|
||||
fun setNoNetworksAvailable()
|
||||
|
||||
interface Factory : ComponentFactory<Params, MarketsPortfolioComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.MarketsPortfolioModel
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.MarketsPortfolioRoute
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.MyPortfolio
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Stable
|
||||
internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: MarketsPortfolioComponent.Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||
) : AppComponentContext by context, MarketsPortfolioComponent {
|
||||
|
||||
private val model: MarketsPortfolioModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = MarketsPortfolioRoute.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
model.setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
override fun setNoNetworksAvailable() {
|
||||
model.setNoNetworksAvailable()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
MyPortfolio(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
private fun bottomSheetChild(
|
||||
config: MarketsPortfolioRoute,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(
|
||||
addToPortfolioManager = model.newAddToPortfolioManager!!,
|
||||
callback = model.addToPortfolioCallback,
|
||||
),
|
||||
)
|
||||
is MarketsPortfolioRoute.TokenReceive -> tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config.config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsPortfolioComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: MarketsPortfolioComponent.Params,
|
||||
): DefaultMarketsPortfolioComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
|
||||
internal class PortfolioAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
|
||||
|
||||
data class EventBuilder(
|
||||
val token: TokenMarketParams,
|
||||
val source: String?,
|
||||
) {
|
||||
|
||||
fun addToPortfolioClicked() = PortfolioAnalyticsEvent(
|
||||
event = "Button - Add To Portfolio",
|
||||
params = mapOf(
|
||||
"Token" to token.symbol,
|
||||
),
|
||||
)
|
||||
|
||||
fun popupToChooseAccount() = PortfolioAnalyticsEvent(
|
||||
event = "Popup to choose account",
|
||||
)
|
||||
|
||||
fun addToNotMainAccount() = PortfolioAnalyticsEvent(
|
||||
event = "Button - Add (token not to main Account)",
|
||||
)
|
||||
|
||||
fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected")
|
||||
|
||||
fun addToPortfolioContinue(blockchainNames: List<String>) = PortfolioAnalyticsEvent(
|
||||
event = "Token Network Selected",
|
||||
params = mapOf(
|
||||
"Count" to blockchainNames.size.toString(),
|
||||
"Token" to token.symbol,
|
||||
"blockchain" to blockchainNames.joinToString(separator = ", "),
|
||||
),
|
||||
)
|
||||
|
||||
fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) =
|
||||
PortfolioAnalyticsEvent(
|
||||
event = when (actionUM) {
|
||||
TokenActionsBSContentUM.Action.Buy -> "Button - Buy"
|
||||
TokenActionsBSContentUM.Action.Receive -> "Button - Receive"
|
||||
TokenActionsBSContentUM.Action.Exchange -> "Button - Swap"
|
||||
TokenActionsBSContentUM.Action.Stake -> "Button - Stake"
|
||||
TokenActionsBSContentUM.Action.YieldMode -> "Button - Yield Mode"
|
||||
else -> "error"
|
||||
},
|
||||
params = buildMap {
|
||||
put("Token", token.symbol)
|
||||
source?.let { put("Source", it) }
|
||||
put("blockchain", blockchainName)
|
||||
},
|
||||
)
|
||||
|
||||
fun getTokenActionClick(actionUM: TokenActionsBSContentUM.Action) = PortfolioAnalyticsEvent(
|
||||
event = when (actionUM) {
|
||||
TokenActionsBSContentUM.Action.Buy -> "Popup Get token - Button Buy"
|
||||
TokenActionsBSContentUM.Action.Receive -> "Popup Get token - Button Receive"
|
||||
TokenActionsBSContentUM.Action.Exchange -> "Popup Get token - Button Exchange"
|
||||
TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake"
|
||||
else -> "error"
|
||||
},
|
||||
)
|
||||
|
||||
fun getTokenLater() = PortfolioAnalyticsEvent(
|
||||
event = "Popup Get token - Button Later",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.di
|
||||
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.DefaultMarketsPortfolioComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMarketsPortfolioComponent(
|
||||
factory: DefaultMarketsPortfolioComponent.Factory,
|
||||
): MarketsPortfolioComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.model.MarketsPortfolioModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(MarketsPortfolioModel::class)
|
||||
fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.loader
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
|
||||
/**
|
||||
* Portfolio data. Combined data from all flows that required to setup portfolio
|
||||
*
|
||||
* @property walletsWithCurrencies wallets with crypto currency statuses
|
||||
* @property appCurrency app currency
|
||||
* @property isBalanceHidden flag that indicates if balance should be hidden
|
||||
* @property walletsWithBalance wallets with total balance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class PortfolioData(
|
||||
val walletsWithCurrencies: Map<UserWallet, List<CryptoCurrencyData>>,
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
val walletsWithBalance: Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>,
|
||||
) {
|
||||
data class CryptoCurrencyData(
|
||||
val userWallet: UserWallet,
|
||||
val status: CryptoCurrencyStatus,
|
||||
val actions: List<TokenActionsState.ActionState>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.loader
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
|
||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Loader of portfolio data
|
||||
*
|
||||
* @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses
|
||||
* @property getSelectedAppCurrencyUseCase use case for getting selected app currency
|
||||
* @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings
|
||||
* @property getWalletTotalBalanceUseCase use case for getting wallet total balance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class PortfolioDataLoader @Inject constructor(
|
||||
private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
|
||||
private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
|
||||
) {
|
||||
|
||||
/** Load data by [currencyRawId] */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun load(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
|
||||
return combine(
|
||||
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
|
||||
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
) { walletsWithCurrencies, appCurrency, isBalanceHidden ->
|
||||
PortfolioData(
|
||||
walletsWithCurrencies = walletsWithCurrencies,
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
walletsWithBalance = emptyMap(),
|
||||
)
|
||||
}
|
||||
// setup balances for wallets from walletsWithCurrencyStatuses
|
||||
.flatMapLatest { portfolioData ->
|
||||
getWalletsWithTotalBalanceFlow(
|
||||
ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId),
|
||||
)
|
||||
.map { portfolioData.copy(walletsWithBalance = it) }
|
||||
.onEmpty { emit(portfolioData) }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun getAllWalletsCryptoCurrenciesData(
|
||||
currencyRawId: CryptoCurrency.RawID,
|
||||
): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> {
|
||||
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
|
||||
.distinctUntilChanged()
|
||||
.map { walletsWithMaybeStatuses ->
|
||||
walletsWithMaybeStatuses.mapValues { entry ->
|
||||
entry.value.mapNotNull { it.getOrNull() }
|
||||
}
|
||||
}
|
||||
.flatMapLatest { walletsWithStatuses ->
|
||||
val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) ->
|
||||
statuses.map { status ->
|
||||
val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse {
|
||||
YieldSupplyAvailability.Unavailable
|
||||
}
|
||||
getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability)
|
||||
.map { tokenActionsState ->
|
||||
PortfolioData.CryptoCurrencyData(
|
||||
userWallet = wallet,
|
||||
status = status,
|
||||
actions = tokenActionsState.states,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combine(actionsFlows) { actions ->
|
||||
walletsWithStatuses.mapValues { entry ->
|
||||
entry.value.mapNotNull { status ->
|
||||
actions.firstOrNull {
|
||||
it.userWallet == entry.key && it.status == status
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onEmpty {
|
||||
emit(
|
||||
walletsWithStatuses.mapValues { (wallet, statuses) ->
|
||||
statuses.map { status ->
|
||||
PortfolioData.CryptoCurrencyData(
|
||||
userWallet = wallet,
|
||||
status = status,
|
||||
actions = emptyList(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}.onEmpty {
|
||||
emit(emptyMap())
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun getWalletsWithTotalBalanceFlow(
|
||||
ids: List<UserWalletId>,
|
||||
): Flow<Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>> {
|
||||
return combine(
|
||||
flows = ids
|
||||
.map { userWalletId ->
|
||||
getWalletTotalBalanceUseCase(userWalletId)
|
||||
.map { userWalletId to it }
|
||||
.distinctUntilChanged()
|
||||
},
|
||||
transform = { it.toMap() },
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEmpty { ids.associateWith { Lce.Loading<TotalFiatBalance>(partialContent = null) } }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
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.TotalFiatBalance
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig]
|
||||
*
|
||||
* @property token token params
|
||||
* @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed
|
||||
* @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed
|
||||
* @property onNetworkSwitchClick callback is invoked when network switch is clicked
|
||||
* @property onAnotherWalletSelect callback is invoked when wallet is selected
|
||||
* @property onContinueClick callback is invoked when continue button is clicked
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class AddToPortfolioBSContentUMFactory(
|
||||
private val addToPortfolioManager: AddToPortfolioManager,
|
||||
private val token: TokenMarketParams,
|
||||
private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit,
|
||||
private val onWalletSelectorVisibilityChange: (Boolean) -> Unit,
|
||||
private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
|
||||
private val onAnotherWalletSelect: (UserWalletId) -> Unit,
|
||||
private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set<TokenMarketInfo.Network>) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Create [TangemBottomSheetConfig]
|
||||
*
|
||||
|
||||
* @param portfolioData portfolio data
|
||||
* @param portfolioUIData portfolio bottom sheet visibility model
|
||||
* @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: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): TangemBottomSheetConfig {
|
||||
return (currentState ?: TangemBottomSheetConfig.Empty).copy(
|
||||
isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible,
|
||||
onDismissRequest = { onAddToPortfolioVisibilityChange(false) },
|
||||
content = if (selectedWallet != null && alreadyAddedNetworks != null) {
|
||||
AddToPortfolioBSContentUM(
|
||||
selectedWallet = selectedWallet.toSelectedUserWalletItemUM(
|
||||
portfolioData = portfolioData,
|
||||
balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(),
|
||||
artwork = artworks[selectedWallet.walletId],
|
||||
),
|
||||
selectNetworkUM = SelectNetworkUMConverter(
|
||||
networksWithToggle = addToPortfolioManager.associateWithToggle(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworks,
|
||||
addToPortfolioData = portfolioUIData.addToPortfolioData,
|
||||
),
|
||||
alreadyAddedNetworks = alreadyAddedNetworks,
|
||||
onNetworkSwitchClick = onNetworkSwitchClick,
|
||||
).convert(value = token),
|
||||
isScanCardNotificationVisible = portfolioUIData.isNeededColdWalletInteraction,
|
||||
isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
),
|
||||
onContinueButtonClick = {
|
||||
onContinueClick(
|
||||
selectedWallet.walletId,
|
||||
portfolioUIData.addToPortfolioData.getAddedNetworks(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworks,
|
||||
),
|
||||
)
|
||||
},
|
||||
walletSelectorConfig = createWalletSelectorBSConfig(
|
||||
isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible,
|
||||
portfolioData = portfolioData,
|
||||
selectedWalletId = selectedWallet.walletId,
|
||||
artworks = artworks,
|
||||
),
|
||||
isWalletBlockVisible = portfolioData.walletsWithCurrencies
|
||||
.filterKeys(UserWallet::isMultiCurrency).size > 1,
|
||||
)
|
||||
} else {
|
||||
TangemBottomSheetConfigContent.Empty
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.toSelectedUserWalletItemUM(
|
||||
artwork: UserWalletItemUM.ImageState? = 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)
|
||||
}
|
||||
|
||||
private fun createWalletSelectorBSConfig(
|
||||
isShow: Boolean,
|
||||
portfolioData: PortfolioData,
|
||||
selectedWalletId: UserWalletId,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): TangemBottomSheetConfig {
|
||||
return TangemBottomSheetConfig(
|
||||
isShown = isShow,
|
||||
onDismissRequest = { onWalletSelectorVisibilityChange(false) },
|
||||
content = WalletSelectorBSContentUM(
|
||||
userWallets = portfolioData.walletsWithCurrencies
|
||||
.filterKeys(UserWallet::isMultiCurrency)
|
||||
.map { it.key }
|
||||
.map { userWallet ->
|
||||
val balance = portfolioData.walletsWithBalance[userWallet.walletId]
|
||||
|
||||
UserWalletItemUMConverter(
|
||||
onClick = { id ->
|
||||
if (id != selectedWalletId) {
|
||||
onAnotherWalletSelect(id)
|
||||
onWalletSelectorVisibilityChange(false)
|
||||
}
|
||||
},
|
||||
appCurrency = portfolioData.appCurrency,
|
||||
balance = balance?.getOrNull(),
|
||||
isBalanceHidden = portfolioData.isBalanceHidden,
|
||||
endIcon = if (userWallet.walletId == selectedWalletId) {
|
||||
UserWalletItemUM.EndIcon.Checkmark
|
||||
} else {
|
||||
UserWalletItemUM.EndIcon.None
|
||||
},
|
||||
artwork = artworks[userWallet.walletId],
|
||||
).convert(userWallet)
|
||||
}
|
||||
.toImmutableList(),
|
||||
onBack = { onWalletSelectorVisibilityChange(false) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.firstOrNull
|
||||
import kotlin.collections.orEmpty
|
||||
import kotlin.collections.set
|
||||
|
||||
internal typealias WalletsWithNetworks = Map<UserWalletId, Set<TokenMarketInfo.Network>>
|
||||
|
||||
/**
|
||||
* Manager for tracking changing networks in AddToPortfolio
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AddToPortfolioManager @Inject constructor(
|
||||
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
|
||||
) {
|
||||
|
||||
val availableNetworks = MutableStateFlow<Set<TokenMarketInfo.Network>?>(value = null)
|
||||
private val addedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())
|
||||
private val removedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())
|
||||
|
||||
/** Get [AddToPortfolioData] as flow */
|
||||
fun getAddToPortfolioData(): Flow<AddToPortfolioData> {
|
||||
return combine(
|
||||
flow = availableNetworks,
|
||||
flow2 = addedNetworks,
|
||||
flow3 = removedNetworks,
|
||||
transform = ::AddToPortfolioData,
|
||||
)
|
||||
}
|
||||
|
||||
/** Set available networks [networks] */
|
||||
fun setAvailableNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
availableNetworks.value = networks.toSet()
|
||||
}
|
||||
|
||||
/** Add network [networkId] to [userWalletId] */
|
||||
fun addNetwork(userWalletId: UserWalletId, networkId: String) {
|
||||
addedNetworks.add(userWalletId, networkId)
|
||||
|
||||
removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId)
|
||||
}
|
||||
|
||||
/** Remove network [networkId] from [userWalletId] */
|
||||
fun removeNetwork(userWalletId: UserWalletId, networkId: String) {
|
||||
removedNetworks.add(userWalletId, networkId)
|
||||
|
||||
addedNetworks.cancelPrevChangeIfExist(
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
)
|
||||
}
|
||||
|
||||
/** Remove all networks by [userWalletId] */
|
||||
fun removeAllChanges(userWalletId: UserWalletId) {
|
||||
addedNetworks.update {
|
||||
it.toMutableMap().apply { remove(userWalletId) }
|
||||
}
|
||||
|
||||
removedNetworks.update {
|
||||
it.toMutableMap().apply { remove(userWalletId) }
|
||||
}
|
||||
}
|
||||
|
||||
fun associateWithToggle(
|
||||
userWalletId: UserWalletId,
|
||||
alreadyAddedNetworkIds: Set<String>,
|
||||
addToPortfolioData: AddToPortfolioData,
|
||||
): Map<TokenMarketInfo.Network, Boolean> {
|
||||
val filteredNetworks = filterAvailableNetworksForWalletUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networks = addToPortfolioData.availableNetworks.orEmpty(),
|
||||
)
|
||||
// Use user choice or check already added networks
|
||||
return filteredNetworks.associateWith { availableNetwork ->
|
||||
val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork)
|
||||
|
||||
if (isAddedByUser == true) return@associateWith true
|
||||
|
||||
val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork)
|
||||
|
||||
if (isRemovedByUser == true) return@associateWith false
|
||||
|
||||
val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId }
|
||||
|
||||
isAddedBefore
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.cancelPrevChangeIfExist(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
) {
|
||||
if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId)
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.add(userWalletId: UserWalletId, networkId: String) {
|
||||
change(userWalletId = userWalletId, networkId = networkId, isAddAction = true)
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.remove(userWalletId: UserWalletId, networkId: String) {
|
||||
change(userWalletId = userWalletId, networkId = networkId, isAddAction = false)
|
||||
}
|
||||
|
||||
private fun MutableStateFlow<WalletsWithNetworks>.change(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
isAddAction: Boolean,
|
||||
) {
|
||||
val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId }
|
||||
|
||||
if (network == null) {
|
||||
Timber.d(
|
||||
"Network [$networkId] doesn't contain in available networks [%s]",
|
||||
availableNetworks.value?.joinToString { it.networkId },
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
update { walletsWithNetworks ->
|
||||
walletsWithNetworks.toMutableMap().apply {
|
||||
this[userWalletId] = if (isAddAction) {
|
||||
this[userWalletId].orEmpty() + network
|
||||
} else {
|
||||
this[userWalletId].orEmpty() - network
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to portfolio data
|
||||
*
|
||||
* @property availableNetworks available networks that user can add to portfolio
|
||||
* @property addedNetworks networks that user toggled on, but it might have already been added to the wallet
|
||||
* @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet
|
||||
*
|
||||
* Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just
|
||||
* toggle it. But when we will save user changes, we will check what tokens have already been added or
|
||||
* haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks]
|
||||
*/
|
||||
data class AddToPortfolioData(
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>?,
|
||||
val addedNetworks: WalletsWithNetworks,
|
||||
val removedNetworks: WalletsWithNetworks,
|
||||
) {
|
||||
|
||||
fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean {
|
||||
return addedNetworks[userWalletId].orEmpty().isNotEmpty()
|
||||
}
|
||||
|
||||
fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean {
|
||||
return addedNetworks[userWalletId].orEmpty().isNotEmpty() ||
|
||||
removedNetworks[userWalletId].orEmpty().isNotEmpty()
|
||||
}
|
||||
|
||||
/** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */
|
||||
fun getAddedNetworks(
|
||||
userWalletId: UserWalletId,
|
||||
alreadyAddedNetworkIds: Set<String>,
|
||||
): Set<TokenMarketInfo.Network> {
|
||||
val addedNetworksByUser = addedNetworks[userWalletId].orEmpty()
|
||||
|
||||
return addedNetworksByUser.map { it.networkId }
|
||||
.minus(alreadyAddedNetworkIds)
|
||||
.mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
/** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */
|
||||
fun getRemovedNetworks(
|
||||
userWalletId: UserWalletId,
|
||||
alreadyAddedNetworkIds: Set<String>,
|
||||
): Set<TokenMarketInfo.Network> {
|
||||
val removedNetworksByUser = removedNetworks[userWalletId].orEmpty()
|
||||
|
||||
return alreadyAddedNetworkIds
|
||||
.minus(removedNetworksByUser.map { it.networkId }.toSet())
|
||||
.mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } }
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.getGreyedOutIconRes
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketInfo.Network
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [TokenMarketInfo.Network] to [BlockchainRowUM]
|
||||
*
|
||||
* @property alreadyAddedNetworks set of already added networks
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class BlockchainRowUMConverter(
|
||||
private val alreadyAddedNetworks: Set<String>,
|
||||
) : Converter<Pair<Network, Boolean>, BlockchainRowUM> {
|
||||
|
||||
override fun convert(value: Pair<Network, Boolean>): BlockchainRowUM {
|
||||
val (network, isSelected) = value
|
||||
|
||||
val blockchainInfo = BlockchainUtils.getNetworkInfo(networkId = network.networkId)
|
||||
?: error("Can't find blockchain info for ${network.networkId}")
|
||||
|
||||
val isMainNetwork = network.contractAddress == null
|
||||
|
||||
val isEnabled = !alreadyAddedNetworks.contains(network.networkId)
|
||||
|
||||
return BlockchainRowUM(
|
||||
id = network.networkId,
|
||||
name = blockchainInfo.name,
|
||||
type = getNetworkType(network, blockchainInfo),
|
||||
iconResId = if (isEnabled) {
|
||||
if (isSelected) {
|
||||
getActiveIconRes(blockchainInfo.blockchainId)
|
||||
} else {
|
||||
getGreyedOutIconRes(blockchainInfo.blockchainId)
|
||||
}
|
||||
} else {
|
||||
getGreyedOutIconRes(blockchainInfo.blockchainId)
|
||||
},
|
||||
isMainNetwork = isMainNetwork,
|
||||
isSelected = isSelected,
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNetworkType(network: Network, blockchainInfo: BlockchainUtils.BlockchainInfo): String {
|
||||
val isMainNetwork = network.contractAddress == null
|
||||
return when {
|
||||
BlockchainUtils.isL2Network(networkId = network.networkId) -> MAIN_NETWORK_L2_TYPE_NAME
|
||||
isMainNetwork -> MAIN_NETWORK_TYPE_NAME
|
||||
else -> blockchainInfo.protocolName
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAIN_NETWORK_TYPE_NAME = "MAIN"
|
||||
const val MAIN_NETWORK_L2_TYPE_NAME = "MAIN L2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
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.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioDataLoader
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.features.wallet.utils.UserWalletImageFetcher
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.operations.attestation.ArtworkSize
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.api.AddToPortfolioManager as NewAddToPortfolioManager
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class MarketsPortfolioModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
tokenActionsIntentsFactory: TokenActionsHandler.Factory,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val portfolioDataLoader: PortfolioDataLoader,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
|
||||
private val addToPortfolioManager: AddToPortfolioManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val userWalletImageFetcher: UserWalletImageFetcher,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory,
|
||||
newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory,
|
||||
) : Model() {
|
||||
|
||||
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
|
||||
val state: StateFlow<MyPortfolioUM> get() = _state
|
||||
|
||||
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
|
||||
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
|
||||
token = params.token,
|
||||
source = params.analyticsParams?.source,
|
||||
)
|
||||
|
||||
val newAddToPortfolioManager: NewAddToPortfolioManager?
|
||||
val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate?
|
||||
|
||||
/** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */
|
||||
private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null)
|
||||
|
||||
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<MarketsPortfolioRoute> = SlotNavigation()
|
||||
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
}
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val tokenActionsHandler = tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { updateBlock ->
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) {
|
||||
updateTokensState {
|
||||
it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig))
|
||||
}
|
||||
}
|
||||
},
|
||||
onHandleQuickAction = { handledAction ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.quickActionClick(
|
||||
actionUM = handledAction.action,
|
||||
blockchainName = handledAction
|
||||
.cryptoCurrencyData
|
||||
.status
|
||||
.currency
|
||||
.network
|
||||
.name,
|
||||
),
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
configureReceiveAddresses(handledAction)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val factory = MyPortfolioUMFactory(
|
||||
onAddClick = {
|
||||
onAddToPortfolioBSVisibilityChange(isShow = true)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.addToPortfolioClicked(),
|
||||
)
|
||||
},
|
||||
addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory(
|
||||
addToPortfolioManager = addToPortfolioManager,
|
||||
token = params.token,
|
||||
onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange,
|
||||
onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange,
|
||||
onNetworkSwitchClick = ::onNetworkSwitchClick,
|
||||
onAnotherWalletSelect = { walletId ->
|
||||
onWalletSelect(walletId)
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.addToPortfolioWalletChanged(),
|
||||
)
|
||||
},
|
||||
onContinueClick = { selectedWalletId, addedNetworks ->
|
||||
onContinueClick(selectedWalletId, addedNetworks)
|
||||
|
||||
// === Analytics ===
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.addToPortfolioContinue(
|
||||
blockchainNames = addedNetworks.mapNotNull {
|
||||
BlockchainUtils.getNetworkInfo(it.networkId)?.name
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
currentState = Provider { _state.value },
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
updateTokens = { updateBlock ->
|
||||
updateTokensState { state ->
|
||||
state.copy(tokens = updateBlock(state.tokens))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
init {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
newAddToPortfolioManager = newAddToPortfolioManagerFactory
|
||||
.create(
|
||||
modelScope,
|
||||
params.token,
|
||||
params.analyticsParams,
|
||||
)
|
||||
newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create(
|
||||
scope = modelScope,
|
||||
token = params.token,
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
buttonState = newAddToPortfolioManager.state.map { state ->
|
||||
when (state) {
|
||||
is NewAddToPortfolioManager.State.AvailableToAdd -> {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available
|
||||
}
|
||||
NewAddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading
|
||||
NewAddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable
|
||||
}
|
||||
},
|
||||
onAddClick = {
|
||||
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked())
|
||||
bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio)
|
||||
},
|
||||
)
|
||||
newMarketsPortfolioDelegate.combineData()
|
||||
.onEach { _state.value = it }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
} else {
|
||||
newAddToPortfolioManager = null
|
||||
newMarketsPortfolioDelegate = null
|
||||
// Subscribe on selected wallet flow to support actual selected wallet
|
||||
subscribeOnSelectedMultiWalletUpdates()
|
||||
|
||||
subscribeOnStateUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
addToPortfolioManager.setAvailableNetworks(networks)
|
||||
newAddToPortfolioManager?.setTokenNetworks(networks)
|
||||
newMarketsPortfolioDelegate?.setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
fun setNoNetworksAvailable() {
|
||||
addToPortfolioManager.setAvailableNetworks(emptyList())
|
||||
newAddToPortfolioManager?.setTokenNetworks(emptyList())
|
||||
newMarketsPortfolioDelegate?.setTokenNetworks(emptyList())
|
||||
}
|
||||
|
||||
private fun subscribeOnSelectedMultiWalletUpdates() {
|
||||
getSelectedWalletUseCase()
|
||||
.getOrElse { e ->
|
||||
Timber.e("Failed to load selected wallet: $e")
|
||||
error("Failed to load selected wallet")
|
||||
}
|
||||
.onEach { userWallet ->
|
||||
selectedMultiWalletIdFlow.value = userWallet.takeIf { it.isMultiCurrency }?.walletId
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnStateUpdates() {
|
||||
combine(
|
||||
flow = loadPortfolioDataWithArtworks(params.token.id),
|
||||
flow2 = getPortfolioUIDataFlow(),
|
||||
transform = { pair, portfolioUIData ->
|
||||
val (portfolioData, artworks) = pair
|
||||
factory.create(portfolioData, portfolioUIData, artworks)
|
||||
},
|
||||
)
|
||||
.onEach { _state.value = it }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun loadPortfolioDataWithArtworks(
|
||||
currencyRawId: CryptoCurrency.RawID,
|
||||
): Flow<Pair<PortfolioData, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
|
||||
val wallets = Channel<Set<UserWallet>>()
|
||||
val portfolioFlow = portfolioDataLoader
|
||||
.load(currencyRawId)
|
||||
.onEach { wallets.trySend(it.walletsWithCurrencies.keys) }
|
||||
|
||||
val artworksFlow = wallets.receiveAsFlow()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
|
||||
|
||||
return combine(
|
||||
flow = portfolioFlow,
|
||||
flow2 = artworksFlow,
|
||||
) { portfolioData, artworks -> portfolioData to artworks }
|
||||
}
|
||||
|
||||
private fun getPortfolioUIDataFlow(): Flow<PortfolioUIData> {
|
||||
return combine(
|
||||
flow = portfolioBSVisibilityModelFlow,
|
||||
flow2 = selectedMultiWalletIdFlow,
|
||||
flow3 = addToPortfolioManager.getAddToPortfolioData(),
|
||||
transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData ->
|
||||
PortfolioUIData(
|
||||
portfolioBSVisibilityModel = portfolioBSVisibilityModel,
|
||||
selectedWalletId = selectedWalletId,
|
||||
addToPortfolioData = addToPortfolioData,
|
||||
isNeededColdWalletInteraction = isNeededColdWalletInteraction(selectedWalletId, addToPortfolioData),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun isNeededColdWalletInteraction(
|
||||
selectedWalletId: UserWalletId?,
|
||||
addToPortfolioData: AddToPortfolioManager.AddToPortfolioData,
|
||||
): Boolean {
|
||||
return if (selectedWalletId != null) {
|
||||
coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = selectedWalletId,
|
||||
networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty()
|
||||
.associate { it.networkId to null },
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) {
|
||||
val selectedWalletId = selectedMultiWalletIdFlow.value
|
||||
|
||||
if (selectedWalletId == null) {
|
||||
Timber.e("Impossible to switch network when selected wallet is null")
|
||||
return
|
||||
}
|
||||
|
||||
if (isChecked) {
|
||||
modelScope.launch {
|
||||
val unsupportedState = checkCurrencyUnsupportedState(
|
||||
userWalletId = selectedWalletId,
|
||||
rawNetworkId = blockchainRowUM.id,
|
||||
isMainNetwork = blockchainRowUM.isMainNetwork,
|
||||
)
|
||||
if (unsupportedState != null) {
|
||||
showUnsupportedWarning(unsupportedState)
|
||||
} else {
|
||||
addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkCurrencyUnsupportedState(
|
||||
userWalletId: UserWalletId,
|
||||
rawNetworkId: String,
|
||||
isMainNetwork: Boolean,
|
||||
): CurrencyUnsupportedState? {
|
||||
return checkCurrencyUnsupportedUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = rawNetworkId,
|
||||
isMainNetwork = isMainNetwork,
|
||||
).getOrElse { throwable ->
|
||||
Timber.e(
|
||||
throwable,
|
||||
"""
|
||||
Failed to check currency unsupported state
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Network ID: $rawNetworkId
|
||||
|- Is main network: $isMainNetwork
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val message = SnackbarMessage(
|
||||
message = throwable.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
|
||||
val message = DialogMessage(
|
||||
message = when (unsupportedState) {
|
||||
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
private fun onWalletSelect(userWalletId: UserWalletId) {
|
||||
selectedMultiWalletIdFlow.update { prevUserWalletId ->
|
||||
prevUserWalletId?.let(addToPortfolioManager::removeAllChanges)
|
||||
|
||||
userWalletId
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set<TokenMarketInfo.Network>) {
|
||||
modelScope.launch {
|
||||
saveMarketTokensUseCase(
|
||||
userWalletId = userWalletId,
|
||||
tokenMarketParams = params.token,
|
||||
addedNetworks = addedNetworks,
|
||||
removedNetworks = emptySet(),
|
||||
)
|
||||
|
||||
onAddToPortfolioBSVisibilityChange(isShow = false)
|
||||
|
||||
addToPortfolioManager.removeAllChanges(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) {
|
||||
portfolioBSVisibilityModelFlow.update {
|
||||
it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onWalletSelectorVisibilityChange(isShow: Boolean) {
|
||||
portfolioBSVisibilityModelFlow.update {
|
||||
it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) {
|
||||
_state.update { stateToUpdate ->
|
||||
val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate
|
||||
block(tokensState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {
|
||||
val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive &&
|
||||
tokenReceiveFeatureToggle.isNewTokenReceiveEnabled
|
||||
if (isNewReceive) {
|
||||
modelScope.launch {
|
||||
val tokenConfig = receiveAddressesFactory.create(
|
||||
status = quickAction.cryptoCurrencyData.status,
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
) ?: return@launch
|
||||
bottomSheetNavigation.activate(MarketsPortfolioRoute.TokenReceive(tokenConfig))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface MarketsPortfolioRoute : Route {
|
||||
|
||||
@Serializable
|
||||
data object AddToPortfolio : MarketsPortfolioRoute
|
||||
|
||||
@Serializable
|
||||
data class TokenReceive(
|
||||
val config: TokenReceiveConfig,
|
||||
) : MarketsPortfolioRoute
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Factory for creating [MyPortfolioUM]
|
||||
*
|
||||
* @property onAddClick callback when user wants to add new token
|
||||
* @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content
|
||||
* @property tokenActionsHandler token actions handler
|
||||
* @property currentState current state provider
|
||||
* @property updateTokens callback for updating tokens
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MyPortfolioUMFactory(
|
||||
private val onAddClick: () -> Unit,
|
||||
private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory,
|
||||
private val tokenActionsHandler: TokenActionsHandler,
|
||||
private val currentState: Provider<MyPortfolioUM>,
|
||||
private val updateTokens: ((ImmutableList<PortfolioTokenUM>) -> ImmutableList<PortfolioTokenUM>) -> Unit,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
portfolioData: PortfolioData,
|
||||
portfolioUIData: PortfolioUIData,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): MyPortfolioUM {
|
||||
val addToPortfolioData = portfolioUIData.addToPortfolioData
|
||||
|
||||
val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true
|
||||
if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable
|
||||
|
||||
val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) {
|
||||
portfolioData.walletsWithCurrencies
|
||||
} else {
|
||||
portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks)
|
||||
}
|
||||
|
||||
val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty()
|
||||
if (isPortfolioEmpty) {
|
||||
val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty()
|
||||
|
||||
return if (hasMultiWallets) {
|
||||
MyPortfolioUM.AddFirstToken(
|
||||
addToPortfolioBSConfig = createAddToPortfolioBSConfig(
|
||||
portfolioData = portfolioData,
|
||||
portfolioUIData = portfolioUIData,
|
||||
artworks = artworks,
|
||||
),
|
||||
onAddClick = onAddClick,
|
||||
)
|
||||
} else {
|
||||
MyPortfolioUM.UnavailableForWallet
|
||||
}
|
||||
}
|
||||
|
||||
return TokensPortfolioUMConverter(
|
||||
appCurrency = portfolioData.appCurrency,
|
||||
isBalanceHidden = portfolioData.isBalanceHidden,
|
||||
addButtonState = walletsWithCurrencies.getAddButtonState(
|
||||
availableNetworks = addToPortfolioData.availableNetworks,
|
||||
),
|
||||
bsConfig = createAddToPortfolioBSConfig(
|
||||
portfolioData = portfolioData,
|
||||
portfolioUIData = portfolioUIData,
|
||||
artworks = artworks,
|
||||
),
|
||||
onAddClick = onAddClick,
|
||||
quickActionsIntents = tokenActionsHandler,
|
||||
currentState = currentState,
|
||||
updateTokens = updateTokens,
|
||||
)
|
||||
.convert(walletsWithCurrencies)
|
||||
}
|
||||
|
||||
private fun createAddToPortfolioBSConfig(
|
||||
portfolioData: PortfolioData,
|
||||
portfolioUIData: PortfolioUIData,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
): TangemBottomSheetConfig {
|
||||
val selectedWallet = portfolioData.walletsWithCurrencies.keys
|
||||
.firstOrNull { it.walletId == portfolioUIData.selectedWalletId }
|
||||
?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }
|
||||
|
||||
val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty()
|
||||
|
||||
val alreadyAddedNetworks = portfolioData.walletsWithCurrencies
|
||||
.filterAvailableNetworks(availableNetworks)[selectedWallet]
|
||||
?.filter { !it.status.currency.isCustom }
|
||||
?.map { it.status.currency.network.backendId }
|
||||
?.toSet()
|
||||
|
||||
return addToPortfolioBSContentUMFactory.create(
|
||||
currentState = currentState().addToPortfolioBSConfig,
|
||||
portfolioData = portfolioData,
|
||||
portfolioUIData = portfolioUIData,
|
||||
selectedWallet = selectedWallet,
|
||||
alreadyAddedNetworks = alreadyAddedNetworks,
|
||||
artworks = artworks,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.getAddButtonState(
|
||||
availableNetworks: Set<TokenMarketInfo.Network>?,
|
||||
): MyPortfolioUM.Tokens.AddButtonState {
|
||||
if (availableNetworks == null) return MyPortfolioUM.Tokens.AddButtonState.Loading
|
||||
|
||||
val networkIds = availableNetworks.map { it.networkId }
|
||||
|
||||
val isAllAvailableNetworksAdded = this
|
||||
// User can add currencies only in multi-currency wallets
|
||||
.filterKeys(UserWallet::isMultiCurrency)
|
||||
.mapValues { entry -> entry.value.map { it.status.currency.network.backendId } }
|
||||
// Each wallets contains all available networks?
|
||||
.all { it.value.containsAll(networkIds) }
|
||||
|
||||
return if (isAllAvailableNetworksAdded) {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Unavailable
|
||||
} else {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available
|
||||
}
|
||||
}
|
||||
|
||||
/** Filter map values by available networks [networks] */
|
||||
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.filterAvailableNetworks(
|
||||
networks: Set<TokenMarketInfo.Network>,
|
||||
): Map<UserWallet, List<PortfolioData.CryptoCurrencyData>> {
|
||||
return mapValues { entry -> entry.value.filterAvailableNetworks(networks) }
|
||||
}
|
||||
|
||||
/** Filter list of [CryptoCurrencyStatus] by available networks [networks] */
|
||||
private fun List<PortfolioData.CryptoCurrencyData>.filterAvailableNetworks(
|
||||
networks: Set<TokenMarketInfo.Network>,
|
||||
): List<PortfolioData.CryptoCurrencyData> {
|
||||
val networkIds = networks.map(TokenMarketInfo.Network::networkId)
|
||||
|
||||
return mapNotNull { cryptoCurrencyData ->
|
||||
cryptoCurrencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioHeader
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioListItem
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletHeader
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val allAccountSupplier: MultiAccountStatusListSupplier,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
@Assisted private val token: TokenMarketParams,
|
||||
@Assisted private val tokenActionsHandler: TokenActionsHandler,
|
||||
@Assisted private val buttonState: Flow<MyPortfolioUM.Tokens.AddButtonState>,
|
||||
@Assisted private val onAddClick: () -> Unit,
|
||||
) {
|
||||
|
||||
private val currencyRawId: CryptoCurrency.RawID = token.id
|
||||
private var expandedHolder: MutableStateFlow<Set<Pair<UserWalletId, CryptoCurrency.ID>>>? = null
|
||||
|
||||
private val settingsFlow: Flow<SettingsBox> = combine(
|
||||
flow = getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
flow3 = isAccountsModeEnabledUseCase(),
|
||||
transform = ::SettingsBox,
|
||||
).shareIn(
|
||||
replay = 1,
|
||||
started = SharingStarted.Eagerly,
|
||||
scope = scope,
|
||||
).distinctUntilChanged()
|
||||
|
||||
private val availableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
availableNetworks.tryEmit(networks)
|
||||
}
|
||||
|
||||
fun combineData(): Flow<MyPortfolioUM> {
|
||||
return availableNetworks.transformLatest { availableNetworks ->
|
||||
when {
|
||||
availableNetworks.isEmpty() -> emit(MyPortfolioUM.Unavailable)
|
||||
else -> emitAll(onAvailableNetworksFlow().distinctUntilChanged())
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun onAvailableNetworksFlow(): Flow<MyPortfolioUM> =
|
||||
portfolioWithThisCurrencyFLow().transformLatest { portfolioWithCurrency ->
|
||||
when (portfolioWithCurrency.flattenAddedCurrency.isEmpty()) {
|
||||
false -> emitAll(contentFlow(portfolioWithCurrency).distinctUntilChanged())
|
||||
true -> when (portfolioWithCurrency.hasMultiWallets) {
|
||||
true -> emitAll(addFirstTokenFlow())
|
||||
false -> emit(MyPortfolioUM.UnavailableForWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFirstTokenFlow(): Flow<MyPortfolioUM> = buttonState.map { state ->
|
||||
when (state) {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Loading -> MyPortfolioUM.Loading
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken(
|
||||
onAddClick = onAddClick,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
MyPortfolioUM.Tokens.AddButtonState.Unavailable -> MyPortfolioUM.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
private fun contentFlow(portfoliosWithThisCurrency: PortfoliosWithThisCurrency): Flow<MyPortfolioUM.Content> {
|
||||
fun Portfolio.actionsFoAccountCurrencies(): List<Flow<Pair<CryptoCurrency, TokenActionsState>>> =
|
||||
accountsWithAdded.map { account ->
|
||||
fun CryptoCurrencyStatus.actionsFlow() = getCryptoCurrencyActionsUseCase(
|
||||
accountId = account.accountStatus.account.accountId,
|
||||
currency = this.currency,
|
||||
).map { actionsState -> actionsState.cryptoCurrencyStatus.currency to actionsState }
|
||||
account.addedCurrency.map { it.actionsFlow() }
|
||||
}.flatten()
|
||||
|
||||
val allAddedTokenActions =
|
||||
portfoliosWithThisCurrency.portfolios.map { portfolio -> portfolio.actionsFoAccountCurrencies() }.flatten()
|
||||
|
||||
return combine(
|
||||
flow = combine(allAddedTokenActions) { it.toMap() }.distinctUntilChanged(),
|
||||
flow2 = buttonState.distinctUntilChanged(),
|
||||
flow3 = getExpandedHolder(portfoliosWithThisCurrency),
|
||||
flow4 = settingsFlow.distinctUntilChanged(),
|
||||
transform = { actions, addButtonState, expanded, settings ->
|
||||
buildContentState(
|
||||
portfolio = portfoliosWithThisCurrency,
|
||||
allActions = actions,
|
||||
addButtonState = addButtonState,
|
||||
expanded = expanded,
|
||||
settings = settings,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExpandedHolder(
|
||||
portfolio: PortfoliosWithThisCurrency,
|
||||
): StateFlow<Set<Pair<UserWalletId, CryptoCurrency.ID>>> {
|
||||
val expandedHolder = this.expandedHolder
|
||||
if (expandedHolder != null) return expandedHolder
|
||||
val allAddedCurrency = portfolio.flattenAddedCurrency
|
||||
val shouldForceExpand = allAddedCurrency.size == 1 &&
|
||||
allAddedCurrency.first().value.amount?.isZero() == true
|
||||
|
||||
val initValue = when {
|
||||
shouldForceExpand -> {
|
||||
val currency = allAddedCurrency.first()
|
||||
// find userWallet than have this single added token
|
||||
portfolio.portfolios
|
||||
.find { it.accountsWithAdded.any { account -> account.addedCurrency.isNotEmpty() } }
|
||||
?.userWallet
|
||||
?.let { setOf(it.walletId to currency.currency.id) }
|
||||
.orEmpty()
|
||||
}
|
||||
else -> emptySet()
|
||||
}
|
||||
return MutableStateFlow(initValue)
|
||||
.also { this.expandedHolder = it }
|
||||
}
|
||||
|
||||
private fun portfolioWithThisCurrencyFLow(): Flow<PortfoliosWithThisCurrency> =
|
||||
allAccountSupplier().map { list -> list.map { it.addedAccountsFlow() } }.flatMapLatest { flows ->
|
||||
combine(flows) { portfolios ->
|
||||
PortfoliosWithThisCurrency(
|
||||
currencyRawId = currencyRawId,
|
||||
portfolios = portfolios.toList(),
|
||||
)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private fun AccountStatusList.addedAccountsFlow(): Flow<Portfolio> =
|
||||
getUserWalletUseCase.invokeFlow(this.userWalletId).mapNotNull { it.getOrNull() }.map { wallet ->
|
||||
Portfolio(
|
||||
userWallet = wallet,
|
||||
accountStatusList = this,
|
||||
accountsWithAdded = this.filterByRawID(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private fun AccountStatusList.filterByRawID(): List<AccountWithAdded> {
|
||||
fun AccountStatus.filterByRawID(): List<CryptoCurrencyStatus> = when (this) {
|
||||
is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies()
|
||||
.filter { status -> status.currency.id.rawCurrencyId == currencyRawId }
|
||||
}
|
||||
return accountStatuses.map { accountStatus ->
|
||||
AccountWithAdded(
|
||||
accountStatus = accountStatus,
|
||||
addedCurrency = accountStatus.filterByRawID(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildContentState(
|
||||
portfolio: PortfoliosWithThisCurrency,
|
||||
allActions: Map<CryptoCurrency, TokenActionsState>,
|
||||
addButtonState: MyPortfolioUM.Tokens.AddButtonState,
|
||||
expanded: Set<Pair<UserWalletId, CryptoCurrency.ID>>,
|
||||
settings: SettingsBox,
|
||||
): MyPortfolioUM.Content {
|
||||
val appCurrency = settings.appCurrency
|
||||
val isBalanceHidden = settings.isBalanceHidden
|
||||
val isAccountMode = settings.isAccountMode
|
||||
val uiItems: MutableList<PortfolioListItem> = mutableListOf()
|
||||
|
||||
fun toggleQuickActions(key: Pair<UserWalletId, CryptoCurrency.ID>) = expandedHolder?.update { expanded ->
|
||||
val isExpand = expanded.contains(key)
|
||||
if (isExpand) expanded.minus(key) else expanded.plus(key)
|
||||
}
|
||||
|
||||
val tokenUMConverter = PortfolioTokenUMConverter(
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onTokenItemClick = { },
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
)
|
||||
|
||||
portfolio.portfolios.forEach { portfolioItem ->
|
||||
if (portfolioItem.flattenAddedCurrency.isEmpty()) return@forEach
|
||||
val userWallet = portfolioItem.userWallet
|
||||
if (isAccountMode) {
|
||||
uiItems.add(portfolioItem.userWallet.toWalletHeader())
|
||||
} else {
|
||||
uiItems.add(portfolioItem.userWallet.toWalletPortfolioHeader())
|
||||
}
|
||||
|
||||
portfolioItem.accountsWithAdded.forEach { accountWithAdded ->
|
||||
if (accountWithAdded.addedCurrency.isEmpty()) return@forEach
|
||||
if (isAccountMode) {
|
||||
val account = accountWithAdded.accountStatus.account
|
||||
uiItems.add(account.toAccountPortfolioHeader())
|
||||
}
|
||||
|
||||
accountWithAdded.addedCurrency.forEach { currencyStatus ->
|
||||
val actions = allActions[currencyStatus.currency]?.states.orEmpty()
|
||||
val value = PortfolioData.CryptoCurrencyData(
|
||||
userWallet = userWallet,
|
||||
status = currencyStatus,
|
||||
actions = actions,
|
||||
)
|
||||
val expandedKey = portfolioItem.userWallet.walletId to currencyStatus.currency.id
|
||||
val isExpand = expanded.contains(expandedKey)
|
||||
|
||||
val tokenItem = tokenUMConverter.convertV2(
|
||||
onTokenItemClick = { wallet, status ->
|
||||
toggleQuickActions(wallet.walletId to status.currency.id)
|
||||
},
|
||||
value = value,
|
||||
isQuickActionsShown = isExpand,
|
||||
)
|
||||
uiItems.add(tokenItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MyPortfolioUM.Content(
|
||||
items = uiItems.toImmutableList(),
|
||||
buttonState = addButtonState,
|
||||
onAddClick = onAddClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Account.toAccountPortfolioHeader(): PortfolioHeader = PortfolioHeader(
|
||||
id = this.accountId.value,
|
||||
state = AccountTitleUM.Account(
|
||||
prefixText = TextReference.EMPTY,
|
||||
name = this.accountName.toUM().value,
|
||||
icon = when (this) {
|
||||
is Account.CryptoPortfolio -> this.icon.toUM()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
private fun UserWallet.toWalletPortfolioHeader(): PortfolioHeader = PortfolioHeader(
|
||||
id = this.walletId.stringValue,
|
||||
state = AccountTitleUM.Text(
|
||||
title = stringReference(this.name),
|
||||
),
|
||||
)
|
||||
|
||||
private fun UserWallet.toWalletHeader(): WalletHeader = WalletHeader(
|
||||
id = this.walletId.stringValue,
|
||||
name = stringReference(this.name),
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
scope: CoroutineScope,
|
||||
token: TokenMarketParams,
|
||||
tokenActionsHandler: TokenActionsHandler,
|
||||
buttonState: Flow<MyPortfolioUM.Tokens.AddButtonState>,
|
||||
onAddClick: () -> Unit,
|
||||
): NewMarketsPortfolioDelegate
|
||||
}
|
||||
}
|
||||
|
||||
private data class PortfoliosWithThisCurrency(
|
||||
val currencyRawId: CryptoCurrency.RawID,
|
||||
val portfolios: List<Portfolio>,
|
||||
) {
|
||||
|
||||
val hasMultiWallets: Boolean = portfolios.any { it.userWallet.isMultiCurrency }
|
||||
|
||||
val flattenAddedCurrency: List<CryptoCurrencyStatus> =
|
||||
portfolios.map { portfolio -> portfolio.flattenAddedCurrency }.flatten()
|
||||
}
|
||||
|
||||
private data class Portfolio(
|
||||
val userWallet: UserWallet,
|
||||
val accountStatusList: AccountStatusList,
|
||||
val accountsWithAdded: List<AccountWithAdded>,
|
||||
) {
|
||||
val flattenAddedCurrency: List<CryptoCurrencyStatus> =
|
||||
accountsWithAdded.map { it.addedCurrency }.flatten()
|
||||
}
|
||||
|
||||
private data class AccountWithAdded(
|
||||
val addedCurrency: List<CryptoCurrencyStatus>,
|
||||
val accountStatus: AccountStatus,
|
||||
)
|
||||
|
||||
private data class SettingsBox(
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isAccountMode: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
/**
|
||||
* Model for portfolio bottom sheet visibility
|
||||
*
|
||||
* @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet
|
||||
* @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class PortfolioBSVisibilityModel(
|
||||
val isAddToPortfolioBSVisible: Boolean = false,
|
||||
val isWalletSelectorBSVisible: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class PortfolioTokenUMConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit,
|
||||
private val tokenActionsHandler: TokenActionsHandler,
|
||||
) : Converter<PortfolioData.CryptoCurrencyData, PortfolioTokenUM> {
|
||||
|
||||
fun convertV2(
|
||||
value: PortfolioData.CryptoCurrencyData,
|
||||
isQuickActionsShown: Boolean,
|
||||
onTokenItemClick: (UserWallet, CryptoCurrencyStatus) -> Unit,
|
||||
): PortfolioTokenUM {
|
||||
val tokenItemStateConverter = TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
onItemClick = { _, status -> onTokenItemClick(value.userWallet, status) },
|
||||
)
|
||||
return PortfolioTokenUM(
|
||||
tokenItemState = tokenItemStateConverter.convert(value = value.status),
|
||||
walletId = value.userWallet.walletId,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isQuickActionsShown = isQuickActionsShown,
|
||||
quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler),
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM {
|
||||
val tokenItemStateConverter = TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
titleStateProvider = { TokenItemState.TitleState.Content(text = stringReference(value.userWallet.name)) },
|
||||
subtitleStateProvider = {
|
||||
TokenItemState.SubtitleState.TextContent(value = stringReference(value.status.currency.name))
|
||||
},
|
||||
onItemClick = { _, status -> onTokenItemClick(status) },
|
||||
)
|
||||
|
||||
return PortfolioTokenUM(
|
||||
tokenItemState = tokenItemStateConverter.convert(value = value.status),
|
||||
walletId = value.userWallet.walletId,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isQuickActionsShown = false,
|
||||
quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun quickActions(
|
||||
cryptoData: PortfolioData.CryptoCurrencyData,
|
||||
tokenActionsHandler: TokenActionsHandler,
|
||||
): PortfolioTokenUM.QuickActions {
|
||||
return PortfolioTokenUM.QuickActions(
|
||||
actions = toQuickActions(cryptoData.actions),
|
||||
onQuickActionClick = { quickActionUM ->
|
||||
when (quickActionUM) {
|
||||
QuickActionUM.Buy -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Buy,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
is QuickActionUM.Exchange -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Exchange,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
QuickActionUM.Receive -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Receive,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
QuickActionUM.Stake -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Stake,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
is QuickActionUM.YieldMode -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.YieldMode,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
}
|
||||
},
|
||||
onQuickActionLongClick = { actionUM ->
|
||||
if (actionUM == QuickActionUM.Receive) {
|
||||
tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.CopyAddress,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun toQuickActions(actions: List<TokenActionsState.ActionState>) = buildList {
|
||||
actions.forEach { action ->
|
||||
if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) {
|
||||
when (action) {
|
||||
is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy
|
||||
is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(action.showBadge)
|
||||
is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive
|
||||
is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake
|
||||
is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(action.apy)
|
||||
else -> null
|
||||
}?.let(::add)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Portfolio UI data. Combined data from all UI flows that required to setup portfolio
|
||||
*
|
||||
* @property portfolioBSVisibilityModel portfolio bottom sheet visibility model
|
||||
* @property selectedWalletId selected wallet id
|
||||
* @property addToPortfolioData add to portfolio data
|
||||
* @property isNeededColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class PortfolioUIData(
|
||||
val portfolioBSVisibilityModel: PortfolioBSVisibilityModel,
|
||||
val selectedWalletId: UserWalletId?,
|
||||
val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData,
|
||||
val isNeededColdWalletInteraction: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Converter from [TokenMarketParams] to [SelectNetworkUM]
|
||||
*
|
||||
* @property networksWithToggle map of networks with toggles
|
||||
* @property alreadyAddedNetworks already added networks
|
||||
* @property onNetworkSwitchClick callback is called when network switch is clicked
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SelectNetworkUMConverter(
|
||||
private val networksWithToggle: Map<TokenMarketInfo.Network, Boolean>,
|
||||
private val alreadyAddedNetworks: Set<String>,
|
||||
private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
|
||||
) : Converter<TokenMarketParams, SelectNetworkUM> {
|
||||
|
||||
override fun convert(value: TokenMarketParams): SelectNetworkUM {
|
||||
return SelectNetworkUM(
|
||||
tokenId = value.id.value,
|
||||
iconUrl = value.imageUrl,
|
||||
tokenName = value.name,
|
||||
tokenCurrencySymbol = value.symbol,
|
||||
networks = BlockchainRowUMConverter(alreadyAddedNetworks)
|
||||
.convertList(networksWithToggle.toList())
|
||||
.toImmutableList(),
|
||||
onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokenActionsHandler @AssistedInject constructor(
|
||||
private val router: Router,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
||||
@Assisted private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
|
||||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val shareManager: ShareManager,
|
||||
) {
|
||||
|
||||
private val disabledActionsInDemoMode = buildSet {
|
||||
add(TokenActionsBSContentUM.Action.Sell)
|
||||
}
|
||||
|
||||
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
onHandleQuickAction(
|
||||
HandledQuickAction(
|
||||
action = action,
|
||||
cryptoCurrencyData = cryptoCurrencyData,
|
||||
),
|
||||
)
|
||||
val userWallet = cryptoCurrencyData.userWallet
|
||||
if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return
|
||||
|
||||
when (action) {
|
||||
TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData)
|
||||
TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData)
|
||||
TokenActionsBSContentUM.Action.Receive -> onReceiveClick(cryptoCurrencyData)
|
||||
TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData)
|
||||
TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData)
|
||||
TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData)
|
||||
TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData)
|
||||
TokenActionsBSContentUM.Action.YieldMode -> onYieldModeClick(cryptoCurrencyData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean {
|
||||
val isDemoCard = isDemoCardUseCase.invoke(userWallet.cardId)
|
||||
val isNeededShowDemoWarning = isDemoCard && disabledActionsInDemoMode.contains(action)
|
||||
|
||||
if (isNeededShowDemoWarning) {
|
||||
showDemoModeWarning()
|
||||
}
|
||||
|
||||
return isNeededShowDemoWarning
|
||||
}
|
||||
|
||||
private fun showDemoModeWarning() {
|
||||
val message = DialogMessage(
|
||||
message = resourceReference(R.string.alert_demo_feature_disabled),
|
||||
)
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyData.status
|
||||
val currency = cryptoCurrencyStatus.currency
|
||||
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
|
||||
|
||||
updateTokenReceiveBSConfig {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {
|
||||
updateTokenReceiveBSConfig {
|
||||
it.copy(isShown = false)
|
||||
}
|
||||
},
|
||||
content = TokenReceiveBottomSheetConfig(
|
||||
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
),
|
||||
network = currency.network,
|
||||
networkAddress = networkAddress,
|
||||
showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
|
||||
onCopyClick = { clipboardManager.setText(networkAddress.defaultAddress.value, isSensitive = true) },
|
||||
onShareClick = { shareManager.shareText(networkAddress.defaultAddress.value) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyData.status
|
||||
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
|
||||
val addresses = networkAddress.availableAddresses
|
||||
.mapToAddressModels(cryptoCurrencyStatus.currency)
|
||||
.toImmutableList()
|
||||
val defaultAddress = addresses.firstOrNull()?.value ?: return
|
||||
|
||||
clipboardManager.setText(text = defaultAddress, isSensitive = true)
|
||||
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied)))
|
||||
}
|
||||
|
||||
private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
router.push(
|
||||
AppRoute.Onramp(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
currency = cryptoCurrencyData.status.currency,
|
||||
source = OnrampSource.MARKETS,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
router.push(
|
||||
AppRoute.Swap(
|
||||
currencyFrom = cryptoCurrencyData.status.currency,
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
isInitialReverseOrder = true,
|
||||
screenSource = AnalyticsParam.ScreensSources.Markets.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
val route = AppRoute.SendEntryPoint(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
currency = cryptoCurrencyData.status.currency,
|
||||
)
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake }
|
||||
?.let { it as TokenActionsState.ActionState.Stake }
|
||||
?.option ?: return
|
||||
|
||||
router.push(
|
||||
AppRoute.Staking(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyData.status.currency,
|
||||
integrationId = option.integrationId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance<TokenActionsState.ActionState.YieldMode>()
|
||||
.firstOrNull()?.apy ?: return
|
||||
|
||||
val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData ->
|
||||
currencyData.userWallet.walletId to currencyData.status
|
||||
}
|
||||
if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyActive(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = yieldSupplyApy,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router.push(
|
||||
AppRoute.YieldSupplyPromo(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = yieldSupplyApy,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
currentAppCurrency: Provider<AppCurrency>,
|
||||
updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
|
||||
onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
): TokenActionsHandler
|
||||
}
|
||||
|
||||
data class HandledQuickAction(
|
||||
val action: TokenActionsBSContentUM.Action,
|
||||
val cryptoCurrencyData: PortfolioData.CryptoCurrencyData,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlin.collections.map
|
||||
|
||||
/**
|
||||
* Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokensPortfolioUMConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val addButtonState: MyPortfolioUM.Tokens.AddButtonState,
|
||||
private val bsConfig: TangemBottomSheetConfig,
|
||||
private val onAddClick: () -> Unit,
|
||||
private val quickActionsIntents: TokenActionsHandler,
|
||||
private val currentState: Provider<MyPortfolioUM>,
|
||||
private val updateTokens: ((ImmutableList<PortfolioTokenUM>) -> ImmutableList<PortfolioTokenUM>) -> Unit,
|
||||
) : Converter<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>, MyPortfolioUM.Tokens> {
|
||||
|
||||
override fun convert(value: Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>): MyPortfolioUM.Tokens {
|
||||
val currentTokensState = currentState() as? MyPortfolioUM.Tokens
|
||||
|
||||
return MyPortfolioUM.Tokens(
|
||||
tokens = value
|
||||
.flatMap { entry -> entry.value }
|
||||
.map { cryptoData ->
|
||||
PortfolioTokenUMConverter(
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onTokenItemClick = { toggleQuickActions(cryptoData) },
|
||||
tokenActionsHandler = quickActionsIntents,
|
||||
).convert(value = cryptoData) to cryptoData
|
||||
}
|
||||
.setQuickActionsVisibility(currentState = currentTokensState)
|
||||
.toImmutableList(),
|
||||
buttonState = addButtonState,
|
||||
addToPortfolioBSConfig = bsConfig,
|
||||
onAddClick = onAddClick,
|
||||
tokenReceiveBSConfig = (currentState() as? MyPortfolioUM.Tokens)
|
||||
?.tokenReceiveBSConfig
|
||||
?: TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Pair<PortfolioTokenUM, PortfolioData.CryptoCurrencyData>>.setQuickActionsVisibility(
|
||||
currentState: MyPortfolioUM.Tokens?,
|
||||
): List<PortfolioTokenUM> {
|
||||
return when {
|
||||
// if there is only one token and it has empty balance, show quick actions for it
|
||||
currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> {
|
||||
this.map { (token, _) ->
|
||||
token.copy(isQuickActionsShown = true)
|
||||
}
|
||||
}
|
||||
// if there is no previous state, hide quick actions for all tokens
|
||||
currentState == null -> {
|
||||
this.map { (token, _) ->
|
||||
token.copy(isQuickActionsShown = false)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val previousList = currentState.tokens
|
||||
|
||||
// otherwise, keep previous state
|
||||
this.map { (token, _) ->
|
||||
token.copy(
|
||||
isQuickActionsShown = previousList
|
||||
.firstOrNull { it.matchWith(token) }
|
||||
?.isQuickActionsShown == true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean {
|
||||
return cryptoData.status.value.amount?.isZero() == true
|
||||
}
|
||||
|
||||
private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) {
|
||||
updateTokens { tokenList ->
|
||||
tokenList.map { portfolioTokenUM ->
|
||||
portfolioTokenUM.copy(
|
||||
isQuickActionsShown = if (portfolioTokenUM.matchWith(cryptoData)) {
|
||||
!portfolioTokenUM.isQuickActionsShown
|
||||
} else {
|
||||
false
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean {
|
||||
return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id
|
||||
}
|
||||
|
||||
private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean {
|
||||
return this.walletId == cryptoData.userWallet.walletId &&
|
||||
this.tokenItemState.id == cryptoData.status.currency.id.value
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,383 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.rows.ArrowRow
|
||||
import com.tangem.core.ui.components.rows.BlockchainRow
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<AddToPortfolioBSContentUM>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
addBottomInsets = false,
|
||||
titleText = resourceReference(R.string.common_add_to_portfolio),
|
||||
) { contentUM ->
|
||||
Content(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = contentUM,
|
||||
)
|
||||
|
||||
WalletSelectorBottomSheet(contentUM.walletSelectorConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) {
|
||||
var continueButtonAreaHeight by remember { mutableIntStateOf(0) }
|
||||
val density = LocalDensity.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(state = scrollState)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
if (state.isWalletBlockVisible) {
|
||||
UserWalletItem(
|
||||
state = state.selectedWallet,
|
||||
blockColors = TangemBlockCardColors.copy(
|
||||
containerColor = TangemTheme.colors.background.action,
|
||||
disabledContainerColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
)
|
||||
SpacerH12()
|
||||
}
|
||||
|
||||
NetworkSelection(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.selectNetworkUM,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.isScanCardNotificationVisible,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column {
|
||||
ScanWalletWarning(modifier = Modifier.fillMaxWidth())
|
||||
SpacerH12()
|
||||
}
|
||||
|
||||
// Scroll to the bottom when the notification appears and the scroll is at the bottom
|
||||
LaunchedEffect(Unit) {
|
||||
if (scrollState.canScrollForward.not()) {
|
||||
delay(timeMillis = 500)
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(with(density) { continueButtonAreaHeight.toDp() })
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = scrollState.canScrollForward,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
) {
|
||||
BottomFade(Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
|
||||
ContinueButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.onGloballyPositioned {
|
||||
continueButtonAreaHeight = it.size.height
|
||||
},
|
||||
enabled = state.isContinueButtonEnabled,
|
||||
isTangemIconVisible = state.isScanCardNotificationVisible,
|
||||
onClick = state.onContinueButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(
|
||||
enabled: Boolean,
|
||||
isTangemIconVisible: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TangemButton(
|
||||
enabled = enabled,
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.navigationBarsPadding()
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
icon = if (enabled && isTangemIconVisible) {
|
||||
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
},
|
||||
showProgress = false,
|
||||
size = TangemButtonSize.Default,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
onClick = onClick,
|
||||
animateContentChange = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_select_network),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing14),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
url = state.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
SpacerW12()
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
text = state.tokenName,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW6()
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.alignByBaseline(),
|
||||
text = state.tokenCurrencySymbol,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
state.networks.fastForEachIndexed { index, network ->
|
||||
ArrowRow(
|
||||
isLastItem = index == state.networks.lastIndex,
|
||||
content = {
|
||||
BlockchainRow(
|
||||
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
|
||||
model = network,
|
||||
action = {
|
||||
TangemSwitch(
|
||||
checked = network.isSelected,
|
||||
checkedColor = if (network.isEnabled) {
|
||||
TangemTheme.colors.control.checked
|
||||
} else {
|
||||
TangemTheme.colors.icon.inactive
|
||||
},
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
hapticManager.perform(TangemHapticEffect.View.ToggleOn)
|
||||
} else {
|
||||
hapticManager.perform(TangemHapticEffect.View.ToggleOff)
|
||||
}
|
||||
|
||||
state.onNetworkSwitchClick(network, checked)
|
||||
},
|
||||
enabled = network.isEnabled,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanWalletWarning(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.button.disabled,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.requiredSize(TangemTheme.dimens.size20),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_generate_addresses_notification),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
AddToPortfolioBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
content = content,
|
||||
onDismissRequest = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun PreviewContent(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.fillMaxWidth(),
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun PreviewContentRtl(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview(rtl = true) {
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.fillMaxWidth(),
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// For on device testing
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PreviewContentTestOnDevice(
|
||||
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
|
||||
) {
|
||||
TangemThemePreview(
|
||||
alwaysShowBottomSheets = false,
|
||||
) {
|
||||
var isShow by remember { mutableStateOf(false) }
|
||||
|
||||
var contentState by remember {
|
||||
mutableStateOf(content)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
contentState = content.copy(
|
||||
onContinueButtonClick = {
|
||||
contentState = contentState.copy(
|
||||
isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible,
|
||||
)
|
||||
},
|
||||
isContinueButtonEnabled = true,
|
||||
selectedWallet = content.selectedWallet.copy(
|
||||
onClick = {
|
||||
contentState = contentState.copy(
|
||||
isContinueButtonEnabled = !contentState.isContinueButtonEnabled,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AddToPortfolioBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = isShow,
|
||||
content = contentState,
|
||||
onDismissRequest = { isShow = false },
|
||||
),
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { isShow = !isShow },
|
||||
) {
|
||||
Text(text = "Toggle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.common.ui.account.AccountTitle
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.*
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
|
||||
if (state is MyPortfolioUM.Content) {
|
||||
val contentModifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing20,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing32,
|
||||
)
|
||||
PortfolioList(state, contentModifier)
|
||||
return
|
||||
}
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = TangemTheme.dimens.spacing0,
|
||||
title = { Title() },
|
||||
action = {
|
||||
if (state !is MyPortfolioUM.Tokens) return@InformationBlock
|
||||
|
||||
AddButton(state = state.buttonState, onClick = state.onAddClick)
|
||||
},
|
||||
) {
|
||||
val contentModifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
|
||||
when (state) {
|
||||
is MyPortfolioUM.Tokens -> TokenList(state = state)
|
||||
is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier)
|
||||
MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier)
|
||||
MyPortfolioUM.Unavailable -> UnavailableAsset(modifier = contentModifier)
|
||||
MyPortfolioUM.UnavailableForWallet -> UnavailableAssetForWallet(modifier = contentModifier)
|
||||
is MyPortfolioUM.Content -> PortfolioList(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
val bsConfig = state.addToPortfolioBSConfig
|
||||
if (bsConfig != null) {
|
||||
AddToPortfolioBottomSheet(config = bsConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title() {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_my_portfolio),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddButton(state: MyPortfolioUM.Tokens.AddButtonState, onClick: () -> Unit) {
|
||||
when (state) {
|
||||
MyPortfolioUM.Tokens.AddButtonState.Loading -> {
|
||||
Box {
|
||||
SmallButtonShimmer(
|
||||
modifier = Modifier.width(width = TangemTheme.dimens.size63),
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
|
||||
withIcon = true,
|
||||
)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
MyPortfolioUM.Tokens.AddButtonState.Available,
|
||||
MyPortfolioUM.Tokens.AddButtonState.Unavailable,
|
||||
-> {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.markets_add_token),
|
||||
icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24),
|
||||
onClick = onClick,
|
||||
isEnabled = state == MyPortfolioUM.Tokens.AddButtonState.Available,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
state.tokens.fastForEachIndexed { index, token ->
|
||||
key(token.tokenItemState.id) {
|
||||
PortfolioItem(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.action),
|
||||
state = token,
|
||||
lastInList = index == state.tokens.size - 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortfolioList(state: MyPortfolioUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
key("PortfolioListHeader") {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(R.string.markets_common_my_portfolio),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
AddButton(state = state.buttonState, onClick = state.onAddClick)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.fastForEachIndexed { index, item ->
|
||||
val previousItem = state.items.getOrNull(index.dec())
|
||||
val nextItem = state.items.getOrNull(index.inc())
|
||||
val itemModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.getOffsetModifier(item, previousItem)
|
||||
.getBackgroundModifier(item, previousItem, nextItem)
|
||||
|
||||
key(item.id) {
|
||||
PortfolioItem(
|
||||
item = item,
|
||||
modifier = itemModifier,
|
||||
lastInList = index == state.items.size - 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.getBackgroundModifier(
|
||||
item: PortfolioListItem,
|
||||
previousItem: PortfolioListItem?,
|
||||
nextItem: PortfolioListItem?,
|
||||
): Modifier {
|
||||
val color = TangemTheme.colors.background.action
|
||||
val radius = 14.dp
|
||||
val topRound = RoundedCornerShape(topStart = radius, topEnd = radius)
|
||||
val bottomRound = RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
|
||||
val allRound = RoundedCornerShape(size = radius)
|
||||
val backgroundModifier = when (item) {
|
||||
is WalletHeader -> this
|
||||
is PortfolioHeader -> this
|
||||
.clip(topRound)
|
||||
.background(color = color)
|
||||
is PortfolioTokenUM -> when {
|
||||
previousItem is PortfolioHeader && nextItem !is PortfolioTokenUM -> this
|
||||
.clip(bottomRound)
|
||||
.background(color = color)
|
||||
previousItem is WalletHeader && nextItem !is PortfolioTokenUM -> this
|
||||
.clip(allRound)
|
||||
.background(color = color)
|
||||
previousItem is PortfolioTokenUM && nextItem !is PortfolioTokenUM -> this
|
||||
.clip(bottomRound)
|
||||
.background(color = color)
|
||||
else -> this.background(color = color)
|
||||
}
|
||||
}
|
||||
return backgroundModifier
|
||||
}
|
||||
|
||||
private fun Modifier.getOffsetModifier(item: PortfolioListItem, previousItem: PortfolioListItem?): Modifier = when {
|
||||
item is WalletHeader -> this.padding(top = 20.dp, start = 4.dp, end = 4.dp)
|
||||
item is PortfolioHeader && previousItem is PortfolioTokenUM -> this.padding(top = 12.dp)
|
||||
item is PortfolioHeader && previousItem == null -> this.padding(top = 20.dp)
|
||||
previousItem is WalletHeader -> this.padding(top = 12.dp)
|
||||
previousItem is PortfolioTokenUM -> this
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortfolioItem(item: PortfolioListItem, lastInList: Boolean, modifier: Modifier = Modifier) {
|
||||
when (item) {
|
||||
is PortfolioHeader -> AccountTitle(
|
||||
modifier = modifier.padding(
|
||||
start = 12.dp,
|
||||
top = 12.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
accountTitleUM = item.state,
|
||||
textStyle = TangemTheme.typography.caption1,
|
||||
textColor = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
is PortfolioTokenUM -> PortfolioItem(
|
||||
state = item,
|
||||
modifier = modifier,
|
||||
lastInList = lastInList,
|
||||
)
|
||||
is WalletHeader -> Text(
|
||||
modifier = modifier,
|
||||
text = item.name.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnavailableAsset(modifier: Modifier = Modifier) {
|
||||
UnavailableContent(
|
||||
textId = R.string.markets_add_to_my_portfolio_unavailable_description,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnavailableAssetForWallet(modifier: Modifier = Modifier) {
|
||||
UnavailableContent(
|
||||
textId = R.string.markets_add_to_my_portfolio_unavailable_for_wallet_description,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnavailableContent(@StringRes textId: Int, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(textId),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_add_to_my_portfolio_description),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_add_to_portfolio),
|
||||
onClick = state.onAddClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingPlaceholder(modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.7f),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
MyPortfolio(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) {
|
||||
TangemThemePreview(rtl = true) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
MyPortfolio(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.icons.IconTint
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState
|
||||
|
||||
@Composable
|
||||
internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
val tokenItemState = remember(state.tokenItemState) {
|
||||
when (state.tokenItemState) {
|
||||
is TokenItemState.Content -> state.tokenItemState.copy(
|
||||
onItemClick = { tokenItemState ->
|
||||
val onClick = state.tokenItemState.onItemClick
|
||||
if (onClick != null) {
|
||||
hapticManager.perform(TangemHapticEffect.View.ContextClick)
|
||||
onClick.invoke(tokenItemState)
|
||||
}
|
||||
},
|
||||
)
|
||||
else -> state.tokenItemState
|
||||
}
|
||||
}
|
||||
TokenItem(
|
||||
state = tokenItemState,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
itemPaddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing10,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
|
||||
PortfolioQuickActions(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
bottom = if (lastInList) {
|
||||
TangemTheme.dimens.spacing12
|
||||
} else {
|
||||
TangemTheme.dimens.spacing24
|
||||
},
|
||||
),
|
||||
actions = state.quickActions.actions,
|
||||
isVisible = state.isQuickActionsShown,
|
||||
onActionClick = state.quickActions.onQuickActionClick,
|
||||
onActionLongClick = state.quickActions.onQuickActionLongClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) {
|
||||
TangemThemePreview {
|
||||
var isQuickActionsShown by remember { mutableStateOf(value = false) }
|
||||
|
||||
val onItemClick = {
|
||||
isQuickActionsShown = isQuickActionsShown.not()
|
||||
}
|
||||
|
||||
PortfolioItem(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.action),
|
||||
state = tokenUM.copy(
|
||||
tokenItemState = when (tokenUM.tokenItemState) {
|
||||
is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() })
|
||||
is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() })
|
||||
else -> tokenUM.tokenItemState
|
||||
},
|
||||
isQuickActionsShown = isQuickActionsShown,
|
||||
),
|
||||
lastInList = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider<PortfolioTokenUM>(
|
||||
collection = listOf(
|
||||
tokenUM.copy(
|
||||
tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy(
|
||||
fiatAmountState = contentFiatAmount?.copy(
|
||||
icons = persistentListOf(
|
||||
TokenFiatAmountState.Content.IconUM(
|
||||
iconRes = R.drawable.ic_staking_24,
|
||||
tint = IconTint.Accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenUM.copy(
|
||||
tokenItemState = tokenUM.tokenItemState.copy(
|
||||
fiatAmountState = contentFiatAmount?.copy(text = DASH_SIGN),
|
||||
subtitle2State = (tokenUM.tokenItemState.subtitle2State as? TokenItemState.Subtitle2State.TextContent)
|
||||
?.copy(text = DASH_SIGN),
|
||||
),
|
||||
),
|
||||
tokenUM.copy(isBalanceHidden = true),
|
||||
tokenUM.copy(
|
||||
tokenItemState = TokenItemState.Unreachable(
|
||||
id = tokenUM.tokenItemState.id,
|
||||
iconState = tokenUM.tokenItemState.iconState,
|
||||
titleState = tokenUM.tokenItemState.titleState,
|
||||
subtitleState = tokenUM.tokenItemState.subtitleState,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
),
|
||||
tokenUM.copy(
|
||||
tokenItemState = TokenItemState.NoAddress(
|
||||
id = tokenUM.tokenItemState.id,
|
||||
iconState = tokenUM.tokenItemState.iconState,
|
||||
titleState = tokenUM.tokenItemState.titleState,
|
||||
subtitleState = tokenUM.tokenItemState.subtitleState,
|
||||
onItemLongClick = {},
|
||||
),
|
||||
),
|
||||
tokenUM.copy(
|
||||
tokenItemState = TokenItemState.Loading(
|
||||
id = tokenUM.tokenItemState.id,
|
||||
iconState = tokenUM.tokenItemState.iconState,
|
||||
titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content,
|
||||
subtitleState = tokenUM.tokenItemState.subtitleState,
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val tokenUM = PreviewMyPortfolioUMProvider().sampleToken
|
||||
val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as? TokenFiatAmountState.Content
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.icons.badge.drawBadge
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun PortfolioQuickActions(
|
||||
actions: ImmutableList<QuickActionUM>,
|
||||
isVisible: Boolean,
|
||||
onActionClick: (QuickActionUM) -> Unit,
|
||||
onActionLongClick: (QuickActionUM) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (actions.isEmpty()) return
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = isVisible,
|
||||
enter = expandVertically(expandFrom = Alignment.Top),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top),
|
||||
) {
|
||||
Column {
|
||||
actions.fastForEach { action ->
|
||||
LineSeparator()
|
||||
QuickActionItem(
|
||||
state = action,
|
||||
onClick = { onActionClick(action) },
|
||||
onLongClick = { onActionLongClick(action) }.takeIf { action.isLongClickAvailable },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) {
|
||||
val lineColor = TangemTheme.colors.stroke.primary
|
||||
val strokeWidth = TangemTheme.dimens.size1
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val startPadding = TangemTheme.dimens.spacing30
|
||||
|
||||
val height = TangemTheme.dimens.size16
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.animateEnterExit(
|
||||
enter = expandVertically(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
),
|
||||
expandFrom = Alignment.Top,
|
||||
) + fadeIn(),
|
||||
exit = shrinkVertically(
|
||||
spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
),
|
||||
shrinkTowards = Alignment.Top,
|
||||
) + fadeOut(),
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.height(height),
|
||||
) {
|
||||
val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx()
|
||||
|
||||
drawLine(
|
||||
color = lineColor,
|
||||
start = Offset(x, 0f),
|
||||
end = Offset(x, size.height),
|
||||
strokeWidth = strokeWidth.toPx(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun AnimatedVisibilityScope.QuickActionItem(
|
||||
state: QuickActionUM,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
val onLongClickInternal: (() -> Unit)? = if (onLongClick != null) {
|
||||
{
|
||||
hapticManager.perform(TangemHapticEffect.View.LongPress)
|
||||
onLongClick()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onLongClick = onLongClickInternal,
|
||||
onClick = {
|
||||
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
|
||||
onClick()
|
||||
},
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
|
||||
) {
|
||||
QuickActionIcon(state)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.animateEnterExit(
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) {
|
||||
val containerColor = TangemTheme.colors.background.action
|
||||
Box(
|
||||
Modifier
|
||||
.animateEnterExit(
|
||||
enter = scaleIn(),
|
||||
exit = scaleOut(),
|
||||
)
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(TangemTheme.dimens.size32)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (state is QuickActionUM.Exchange && state.shouldShowBadge) {
|
||||
drawBadge(containerColor = containerColor, offset = 4.dp)
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.requiredSize(TangemTheme.dimens.size16),
|
||||
imageVector = ImageVector.vectorResource(id = state.icon),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.button.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(680.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = { isVisible = !isVisible },
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(text = "Toggle")
|
||||
}
|
||||
SpacerH4()
|
||||
Box(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.action),
|
||||
) {
|
||||
PortfolioQuickActions(
|
||||
actions = persistentListOf(
|
||||
QuickActionUM.Buy,
|
||||
QuickActionUM.Exchange(shouldShowBadge = true),
|
||||
QuickActionUM.Receive,
|
||||
),
|
||||
isVisible = isVisible,
|
||||
onActionClick = {},
|
||||
onActionLongClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewRtl() {
|
||||
TangemThemePreview(rtl = true) {
|
||||
Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) {
|
||||
PortfolioQuickActions(
|
||||
actions = persistentListOf(
|
||||
QuickActionUM.Buy,
|
||||
QuickActionUM.Exchange(shouldShowBadge = true),
|
||||
QuickActionUM.Receive,
|
||||
),
|
||||
isVisible = true,
|
||||
onActionClick = {},
|
||||
onActionLongClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SimpleSettingsRow
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<TokenActionsBSContentUM>(
|
||||
config = config,
|
||||
title = { content ->
|
||||
TangemBottomSheetTitle(content.title)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
content = { Content(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(content: TokenActionsBSContentUM) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
content.actions.forEachIndexed { index, action ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = content.actions.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
SimpleSettingsRow(
|
||||
title = action.text.resolveReference(),
|
||||
icon = action.iconRes,
|
||||
redesign = true,
|
||||
onItemsClick = { content.onActionClick(action) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 640)
|
||||
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview(
|
||||
alwaysShowBottomSheets = true,
|
||||
) {
|
||||
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
TokenActionsBottomSheet(
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = TokenActionsBSContentUM(
|
||||
title = "Wallet 1",
|
||||
actions = TokenActionsBSContentUM.Action.entries.toImmutableList(),
|
||||
onActionClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<WalletSelectorBSContentUM>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
addBottomInsets = false,
|
||||
title = { content ->
|
||||
TangemTopAppBar(
|
||||
title = resourceReference(R.string.common_choose_wallet),
|
||||
titleAlignment = Alignment.CenterHorizontally,
|
||||
startButton = TopAppBarButtonUM.Back(content.onBack),
|
||||
height = TangemTopAppBarHeight.BOTTOM_SHEET,
|
||||
)
|
||||
},
|
||||
) { content ->
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
BlockCard(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
colors = TangemBlockCardColors.copy(
|
||||
containerColor = TangemTheme.colors.background.action,
|
||||
disabledContainerColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
) {
|
||||
state.userWallets.forEach { state ->
|
||||
key(state.id) {
|
||||
UserWalletItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
blockColors = TangemBlockCardColors.copy(
|
||||
containerColor = TangemTheme.colors.background.action,
|
||||
disabledContainerColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
WalletSelectorBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = WalletSelectorBSContentUM(
|
||||
userWallets = persistentListOf(
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
),
|
||||
onBack = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewContent() {
|
||||
TangemThemePreview {
|
||||
Content(
|
||||
state = WalletSelectorBSContentUM(
|
||||
userWallets = persistentListOf(
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
),
|
||||
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
|
||||
endIcon = UserWalletItemUM.EndIcon.None,
|
||||
),
|
||||
),
|
||||
onBack = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider<AddToPortfolioBSContentUM> {
|
||||
|
||||
private val blockchainRow = BlockchainRowUM(
|
||||
id = "1",
|
||||
name = "Etherium 3",
|
||||
type = "TEST",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = false,
|
||||
isSelected = false,
|
||||
)
|
||||
|
||||
val userWallet = UserWalletItemUM(
|
||||
id = "1",
|
||||
name = stringReference("Wallet 1"),
|
||||
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
|
||||
balance = UserWalletItemUM.Balance.Loading,
|
||||
isEnabled = true,
|
||||
endIcon = UserWalletItemUM.EndIcon.Arrow,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
override val values: Sequence<AddToPortfolioBSContentUM>
|
||||
get() = sequenceOf(
|
||||
AddToPortfolioBSContentUM(
|
||||
selectedWallet = userWallet,
|
||||
selectNetworkUM = SelectNetworkUM(
|
||||
tokenId = "etherium",
|
||||
tokenName = "Etherium",
|
||||
tokenCurrencySymbol = "ETH",
|
||||
networks = persistentListOf(
|
||||
blockchainRow.copy(
|
||||
type = "MAIN",
|
||||
isMainNetwork = true,
|
||||
isSelected = true,
|
||||
),
|
||||
blockchainRow,
|
||||
blockchainRow,
|
||||
),
|
||||
onNetworkSwitchClick = { _, _ -> },
|
||||
iconUrl = null,
|
||||
),
|
||||
isScanCardNotificationVisible = true,
|
||||
isWalletBlockVisible = true,
|
||||
isContinueButtonEnabled = true,
|
||||
onContinueButtonClick = {},
|
||||
walletSelectorConfig = TangemBottomSheetConfig.Empty,
|
||||
),
|
||||
AddToPortfolioBSContentUM(
|
||||
selectedWallet = userWallet,
|
||||
selectNetworkUM = SelectNetworkUM(
|
||||
tokenId = "etherium",
|
||||
tokenName = "Etherium Etherium Etherium Etherium",
|
||||
tokenCurrencySymbol = "ETH",
|
||||
networks = persistentListOf(
|
||||
blockchainRow.copy(
|
||||
type = "MAIN",
|
||||
isMainNetwork = true,
|
||||
isSelected = true,
|
||||
).copy(name = "Etherium Etherium Etherium Etherium"),
|
||||
*Array(25) { blockchainRow },
|
||||
),
|
||||
|
||||
onNetworkSwitchClick = { _, _ -> },
|
||||
iconUrl = null,
|
||||
),
|
||||
isScanCardNotificationVisible = true,
|
||||
isWalletBlockVisible = false,
|
||||
isContinueButtonEnabled = false,
|
||||
onContinueButtonClick = {},
|
||||
walletSelectorConfig = TangemBottomSheetConfig.Empty,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("PropertyUsedBeforeDeclaration")
|
||||
internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfolioUM> {
|
||||
|
||||
override val values: Sequence<MyPortfolioUM>
|
||||
get() = sequenceOf(
|
||||
MyPortfolioUM.Tokens(
|
||||
tokens = persistentListOf(sampleToken, sampleToken),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Tokens(
|
||||
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Tokens(
|
||||
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.AddFirstToken(
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Content(
|
||||
items = persistentListOf(
|
||||
walletPortfolioHeader,
|
||||
accountToken,
|
||||
accountToken,
|
||||
),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Content(
|
||||
items = persistentListOf(
|
||||
walletHeader,
|
||||
accountHeader,
|
||||
accountToken,
|
||||
accountToken,
|
||||
),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Content(
|
||||
items = persistentListOf(
|
||||
walletHeader,
|
||||
accountHeader,
|
||||
accountToken.copy(isQuickActionsShown = true),
|
||||
accountToken,
|
||||
),
|
||||
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Loading,
|
||||
MyPortfolioUM.Unavailable,
|
||||
MyPortfolioUM.UnavailableForWallet,
|
||||
)
|
||||
|
||||
val walletHeader
|
||||
get() = WalletHeader(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = stringReference("Wallet 1"),
|
||||
)
|
||||
|
||||
val walletPortfolioHeader
|
||||
get() = PortfolioHeader(
|
||||
state = AccountTitleUM.Text(title = stringReference("Wallet 1")),
|
||||
id = UUID.randomUUID().toString(),
|
||||
)
|
||||
|
||||
val accountHeader
|
||||
get() = PortfolioHeader(
|
||||
state = AccountTitleUM.Account(
|
||||
icon = AccountIconPreviewData.randomAccountIcon(),
|
||||
name = stringReference("Main Account"),
|
||||
prefixText = TextReference.EMPTY,
|
||||
),
|
||||
id = UUID.randomUUID().toString(),
|
||||
)
|
||||
val coinIconState
|
||||
get() = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
)
|
||||
val accountToken
|
||||
get() = sampleToken.copy(
|
||||
tokenItemState = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = coinIconState,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
|
||||
fiatAmountState = FiatAmountState.Content(text = "321 $"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(value = "Token")),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
val sampleToken
|
||||
get() = PortfolioTokenUM(
|
||||
tokenItemState = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "My wallet")),
|
||||
fiatAmountState = FiatAmountState.Content(text = "486,65 \$"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference(value = "XRP Ledger token"),
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
isQuickActionsShown = false,
|
||||
quickActions = PortfolioTokenUM.QuickActions(
|
||||
actions = persistentListOf(
|
||||
QuickActionUM.Buy,
|
||||
QuickActionUM.Exchange(shouldShowBadge = true),
|
||||
QuickActionUM.Receive,
|
||||
),
|
||||
onQuickActionClick = {},
|
||||
onQuickActionLongClick = {},
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
walletId = UserWalletId(""),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
internal data class AddToPortfolioBSContentUM(
|
||||
val selectedWallet: UserWalletItemUM,
|
||||
val selectNetworkUM: SelectNetworkUM,
|
||||
val isWalletBlockVisible: Boolean,
|
||||
val isScanCardNotificationVisible: Boolean,
|
||||
val isContinueButtonEnabled: Boolean,
|
||||
val onContinueButtonClick: () -> Unit,
|
||||
val walletSelectorConfig: TangemBottomSheetConfig,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class MyPortfolioUM {
|
||||
|
||||
abstract val addToPortfolioBSConfig: TangemBottomSheetConfig?
|
||||
|
||||
data class Tokens(
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
|
||||
val tokens: ImmutableList<PortfolioTokenUM>,
|
||||
val buttonState: AddButtonState,
|
||||
val tokenReceiveBSConfig: TangemBottomSheetConfig,
|
||||
val onAddClick: () -> Unit,
|
||||
) : MyPortfolioUM() {
|
||||
|
||||
enum class AddButtonState {
|
||||
Loading,
|
||||
Available,
|
||||
Unavailable,
|
||||
}
|
||||
}
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<PortfolioListItem>,
|
||||
val buttonState: Tokens.AddButtonState,
|
||||
val onAddClick: () -> Unit,
|
||||
) : MyPortfolioUM() {
|
||||
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty
|
||||
}
|
||||
|
||||
data class AddFirstToken(
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
|
||||
val onAddClick: () -> Unit,
|
||||
) : MyPortfolioUM()
|
||||
|
||||
data object Loading : MyPortfolioUM() {
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
|
||||
}
|
||||
|
||||
data object Unavailable : MyPortfolioUM() {
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
|
||||
}
|
||||
|
||||
data object UnavailableForWallet : MyPortfolioUM() {
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed interface PortfolioListItem {
|
||||
val id: String
|
||||
}
|
||||
|
||||
internal data class WalletHeader(
|
||||
override val id: String,
|
||||
val name: TextReference,
|
||||
) : PortfolioListItem
|
||||
|
||||
internal data class PortfolioHeader(
|
||||
override val id: String,
|
||||
val state: AccountTitleUM,
|
||||
) : PortfolioListItem
|
||||
|
||||
internal data class PortfolioTokenUM(
|
||||
val tokenItemState: TokenItemState,
|
||||
val walletId: UserWalletId,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isQuickActionsShown: Boolean,
|
||||
val quickActions: QuickActions,
|
||||
) : PortfolioListItem {
|
||||
override val id: String = tokenItemState.id
|
||||
|
||||
data class QuickActions(
|
||||
val actions: ImmutableList<QuickActionUM>,
|
||||
val onQuickActionClick: (QuickActionUM) -> Unit,
|
||||
val onQuickActionLongClick: (QuickActionUM) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed class QuickActionUM(
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
@DrawableRes val icon: Int,
|
||||
val isLongClickAvailable: Boolean = false,
|
||||
) {
|
||||
data object Buy : QuickActionUM(
|
||||
title = resourceReference(R.string.common_buy),
|
||||
description = resourceReference(R.string.buy_token_description),
|
||||
icon = R.drawable.ic_plus_24,
|
||||
)
|
||||
|
||||
data class Exchange(
|
||||
val shouldShowBadge: Boolean,
|
||||
) : QuickActionUM(
|
||||
title = resourceReference(R.string.common_exchange),
|
||||
description = resourceReference(R.string.exсhange_token_description),
|
||||
icon = R.drawable.ic_exchange_vertical_24,
|
||||
)
|
||||
|
||||
data object Receive : QuickActionUM(
|
||||
title = resourceReference(R.string.common_receive),
|
||||
description = resourceReference(R.string.receive_token_description),
|
||||
icon = R.drawable.ic_arrow_down_24,
|
||||
isLongClickAvailable = true,
|
||||
)
|
||||
|
||||
data object Stake : QuickActionUM(
|
||||
title = resourceReference(R.string.common_stake),
|
||||
description = resourceReference(R.string.stake_token_description),
|
||||
icon = R.drawable.ic_staking_24,
|
||||
)
|
||||
|
||||
data class YieldMode(
|
||||
private val apy: String,
|
||||
) : QuickActionUM(
|
||||
title = resourceReference(R.string.yield_module_start_earning),
|
||||
description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)),
|
||||
icon = R.drawable.ic_analytics_up_mini_24,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class SelectNetworkUM(
|
||||
val tokenId: String,
|
||||
val iconUrl: String?,
|
||||
val tokenName: String,
|
||||
val tokenCurrencySymbol: String,
|
||||
val networks: ImmutableList<BlockchainRowUM>,
|
||||
val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.feed.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class TokenActionsBSContentUM(
|
||||
val title: String,
|
||||
val actions: ImmutableList<Action>,
|
||||
val onActionClick: (Action) -> Unit,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
@Immutable
|
||||
enum class Action(
|
||||
val text: TextReference,
|
||||
@DrawableRes val iconRes: Int,
|
||||
) {
|
||||
CopyAddress(
|
||||
text = resourceReference(R.string.common_copy_address),
|
||||
iconRes = R.drawable.ic_copy_24,
|
||||
),
|
||||
Send(
|
||||
text = resourceReference(R.string.common_send),
|
||||
iconRes = R.drawable.ic_arrow_up_24,
|
||||
),
|
||||
Receive(
|
||||
text = resourceReference(R.string.common_receive),
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
Buy(
|
||||
text = resourceReference(R.string.common_buy),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
),
|
||||
Sell(
|
||||
text = resourceReference(R.string.common_sell),
|
||||
iconRes = R.drawable.ic_currency_24,
|
||||
),
|
||||
Exchange(
|
||||
text = resourceReference(R.string.common_exchange),
|
||||
iconRes = R.drawable.ic_exchange_horizontal_24,
|
||||
),
|
||||
Stake(
|
||||
text = resourceReference(R.string.common_stake),
|
||||
iconRes = R.drawable.ic_staking_24,
|
||||
),
|
||||
YieldMode(
|
||||
text = resourceReference(R.string.yield_module_start_earning),
|
||||
iconRes = R.drawable.ic_analytics_up_mini_24,
|
||||
),
|
||||
;
|
||||
|
||||
val order: Int = ordinal
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
|
||||
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class WalletSelectorBSContentUM(
|
||||
val userWallets: ImmutableList<UserWalletItemUM>,
|
||||
val onBack: () -> Unit,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -16,8 +16,8 @@ import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
|
|||
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
|
|||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.all
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
|
|
@ -68,6 +69,11 @@ internal class FeedComponentModel @Inject constructor(
|
|||
TrendingNewsStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onStateUpdate = { newState -> state.update { newState } },
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -88,14 +94,7 @@ internal class FeedComponentModel @Inject constructor(
|
|||
flow4 = manageTrendingNewsUseCase.observeTrendingNews(),
|
||||
) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult ->
|
||||
updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder)
|
||||
trendingNewsStateFactory.updateTrendingNewsState(
|
||||
result = trendingNewsResult,
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
}
|
||||
},
|
||||
)
|
||||
trendingNewsStateFactory.updateTrendingNewsState(result = trendingNewsResult)
|
||||
updateGlobalState()
|
||||
val currentSortType = state.value.marketChartConfig.currentSortByType
|
||||
val items = itemsByOrder[currentSortType]
|
||||
|
|
@ -140,7 +139,11 @@ internal class FeedComponentModel @Inject constructor(
|
|||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
),
|
||||
news = NewsUM.Loading,
|
||||
news = NewsUM(
|
||||
content = persistentListOf(),
|
||||
onRetryClicked = {},
|
||||
newsUMState = NewsUMState.LOADING,
|
||||
),
|
||||
trendingArticle = null,
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = buildMap {
|
||||
|
|
@ -224,10 +227,10 @@ internal class FeedComponentModel @Inject constructor(
|
|||
val newsState = currentState.news
|
||||
val marketCharts = currentState.marketChartConfig.marketCharts
|
||||
|
||||
val isNewsLoading = newsState is NewsUM.Loading
|
||||
val isNewsLoading = newsState.newsUMState == NewsUMState.LOADING
|
||||
val areAllChartsLoading = marketCharts.values.all { it is MarketChartUM.Loading }
|
||||
|
||||
val isNewsError = newsState is NewsUM.Error
|
||||
val isNewsError = newsState.newsUMState == NewsUMState.ERROR
|
||||
val areAllChartsError = marketCharts.values.all { it is MarketChartUM.LoadingError }
|
||||
|
||||
val newGlobalState = when {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
|||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.domain.news.usecase.GetNewsUseCase
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
|
|
@ -38,6 +40,7 @@ import com.tangem.features.feed.impl.R
|
|||
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.details.converter.DescriptionConverter
|
||||
import com.tangem.features.feed.model.market.details.converter.ExchangeItemStateConverter
|
||||
import com.tangem.features.feed.model.market.details.converter.RelatedNewsConverter
|
||||
import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoConverter
|
||||
import com.tangem.features.feed.model.market.details.formatter.*
|
||||
import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater
|
||||
|
|
@ -49,6 +52,7 @@ import com.tangem.utils.Provider
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -76,6 +80,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val getNewsUseCase: GetNewsUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val quotesJob = JobHolder()
|
||||
|
|
@ -138,6 +143,10 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
// ==================
|
||||
)
|
||||
|
||||
private val relatedNewsConverter by lazy {
|
||||
RelatedNewsConverter()
|
||||
}
|
||||
|
||||
private val descriptionConverter = DescriptionConverter(
|
||||
onReadModeClicked = { content ->
|
||||
showBottomSheet(content)
|
||||
|
|
@ -230,6 +239,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
),
|
||||
shouldShowPriceSubtitle = false,
|
||||
onShouldShowPriceSubtitleChange = ::onShouldShowPriceSubtitleChange,
|
||||
relatedNews = MarketsTokenDetailsUM.RelatedNews(
|
||||
articles = persistentListOf(),
|
||||
onArticledClicked = {
|
||||
// TODO in [REDACTED_JIRA]
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -264,6 +279,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
initialLoad()
|
||||
loadRelatedNews()
|
||||
}
|
||||
|
||||
private fun initialLoad() {
|
||||
|
|
@ -286,6 +302,27 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadRelatedNews() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
getNewsUseCase.getNews(
|
||||
limit = RELATED_NEWS_LIMIT,
|
||||
newsListConfig = NewsListConfig(
|
||||
language = Locale.getDefault().language,
|
||||
snapshot = null,
|
||||
tokenIds = listOf(params.token.id.value),
|
||||
),
|
||||
).onRight { articles ->
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
marketsTokenDetailsUM.copy(
|
||||
relatedNews = marketsTokenDetailsUM.relatedNews.copy(
|
||||
articles = relatedNewsConverter.convert(articles),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChart(interval: PriceChangeInterval) {
|
||||
modelScope.launch {
|
||||
state.update { marketsTokenDetailsUM ->
|
||||
|
|
@ -649,5 +686,6 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
|
||||
private companion object {
|
||||
const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L
|
||||
const val RELATED_NEWS_LIMIT = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.feed.model.market.details.converter
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
|
||||
|
||||
override fun convert(value: List<ShortArticle>): ImmutableList<ArticleConfigUM> {
|
||||
return value.map { shortArticle ->
|
||||
ArticleConfigUM(
|
||||
id = shortArticle.id,
|
||||
title = shortArticle.title,
|
||||
score = shortArticle.score,
|
||||
isTrending = false,
|
||||
tags = buildArticleTags(shortArticle),
|
||||
createdAt = mapFormattedDate(shortArticle.createdAt),
|
||||
isViewed = shortArticle.viewed,
|
||||
)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
private fun buildArticleTags(article: ShortArticle): kotlinx.collections.immutable.ImmutableSet<LabelUM> {
|
||||
val categoryLabels = article.categories.map { category ->
|
||||
LabelUM(text = TextReference.Str(category.name))
|
||||
}
|
||||
val tokenLabels = article.relatedTokens.map { token ->
|
||||
LabelUM(
|
||||
text = TextReference.Str(token.symbol),
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = getTokenIconUrlFromDefaultHost(
|
||||
tokenId = CryptoCurrency.RawID(token.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
return (categoryLabels + tokenLabels).toPersistentSet()
|
||||
}
|
||||
|
||||
private fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,7 +238,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.isInSearchStateFlow.collectLatest { isInSearchMode ->
|
||||
activeListManager = if (isInSearchMode) {
|
||||
activeListManager = if (isInSearchMode || marketsListUMStateManager.searchQuery.isNotEmpty()) {
|
||||
searchMarketsListManager
|
||||
} else {
|
||||
searchMarketsListManager.clearStateAndStopAllActions()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
package com.tangem.features.feed.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
@ -39,8 +35,7 @@ internal fun EntryContent(
|
|||
containerColor = background,
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
topBar = {
|
||||
AnimatedContent(
|
||||
targetState = stackState.value.active.instance,
|
||||
Children(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (!isOpenedInBottomSheet) {
|
||||
|
|
@ -56,8 +51,11 @@ internal fun EntryContent(
|
|||
}
|
||||
}
|
||||
},
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentState -> currentState.Title(bottomSheetState) }
|
||||
stack = stackState.value,
|
||||
animation = stackAnimation(fade()),
|
||||
) { child ->
|
||||
child.instance.Title(bottomSheetState)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
Children(
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ import com.tangem.core.ui.extensions.stringResourceSafe
|
|||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
internal fun FeedListHeader(feedListSearchBar: FeedListSearchBar, modifier: Modifier = Modifier) {
|
||||
|
|
@ -269,34 +269,26 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb
|
|||
@Suppress("CanBeNonNullable")
|
||||
@Composable
|
||||
private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
AnimatedContent(news) { newsUM ->
|
||||
when (newsUM) {
|
||||
is NewsUM.Content -> {
|
||||
if (newsUM.content.isNotEmpty()) {
|
||||
AnimatedContent(news.newsUMState) { newsUMState ->
|
||||
when (newsUMState) {
|
||||
NewsUMState.LOADING -> NewsLoadingBlock()
|
||||
NewsUMState.CONTENT -> {
|
||||
if (news.content.isNotEmpty()) {
|
||||
NewsContentBlock(
|
||||
feedListCallbacks = feedListCallbacks,
|
||||
news = newsUM,
|
||||
news = news,
|
||||
trendingArticle = trendingArticle,
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsUM.Loading -> {
|
||||
NewsLoadingBlock()
|
||||
}
|
||||
is NewsUM.Error -> {
|
||||
NewsErrorBlock(onRetryClick = newsUM.onRetryClicked)
|
||||
}
|
||||
NewsUMState.ERROR -> NewsErrorBlock(onRetryClick = news.onRetryClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewsContentBlock(
|
||||
feedListCallbacks: FeedListCallbacks,
|
||||
news: NewsUM.Content,
|
||||
trendingArticle: ArticleConfigUM?,
|
||||
) {
|
||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import kotlinx.collections.immutable.*
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
@ -33,7 +33,11 @@ internal object FeedListPreviewDataProvider {
|
|||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
),
|
||||
news = NewsUM.Content(articles.filter { it.isTrending.not() }.toImmutableList()),
|
||||
news = NewsUM(
|
||||
content = articles.filter { it.isTrending.not() }.toImmutableList(),
|
||||
onRetryClicked = {},
|
||||
newsUMState = NewsUMState.CONTENT,
|
||||
),
|
||||
trendingArticle = articles.first { it.isTrending },
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = createMarketCharts(marketItems, includeErrorState = false),
|
||||
|
|
|
|||
|
|
@ -33,11 +33,16 @@ internal data class FeedListSearchBar(
|
|||
val placeholderText: TextReference,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface NewsUM {
|
||||
data object Loading : NewsUM
|
||||
data class Content(val content: ImmutableList<ArticleConfigUM>) : NewsUM
|
||||
data class Error(val onRetryClicked: () -> Unit) : NewsUM
|
||||
internal data class NewsUM(
|
||||
val content: ImmutableList<ArticleConfigUM>,
|
||||
val onRetryClicked: () -> Unit,
|
||||
val newsUMState: NewsUMState,
|
||||
)
|
||||
|
||||
internal enum class NewsUMState {
|
||||
LOADING,
|
||||
CONTENT,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
internal data class MarketChartConfig(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import com.tangem.domain.models.news.TrendingNews
|
|||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
|
@ -22,34 +24,48 @@ import org.joda.time.DateTime
|
|||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
private val onStateUpdate: (FeedListUM) -> Unit,
|
||||
private val onRetryClicked: () -> Unit,
|
||||
) {
|
||||
|
||||
fun updateTrendingNewsState(result: TrendingNews, onRetryClicked: () -> Unit) {
|
||||
fun updateTrendingNewsState(result: TrendingNews) {
|
||||
val currentState = currentStateProvider()
|
||||
when (result) {
|
||||
is TrendingNews.Data -> handleDataState(currentState, result.articles)
|
||||
is TrendingNews.Error -> handleErrorState(currentState, onRetryClicked)
|
||||
is TrendingNews.Error -> handleErrorState(currentState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDataState(currentState: FeedListUM, articles: List<ShortArticle>) {
|
||||
val (trendingArticle, commonArticles) = separateTrendingAndCommonArticles(articles)
|
||||
val commonArticlesUM = commonArticles.map { mapToArticleConfigUM(it, isTrending = false) }.toPersistentList()
|
||||
val updatedNews = when (currentState.news.newsUMState) {
|
||||
NewsUMState.CONTENT -> currentState.news.copy(content = commonArticlesUM)
|
||||
NewsUMState.LOADING,
|
||||
NewsUMState.ERROR,
|
||||
-> NewsUM(
|
||||
content = commonArticlesUM,
|
||||
onRetryClicked = onRetryClicked,
|
||||
newsUMState = NewsUMState.CONTENT,
|
||||
)
|
||||
}
|
||||
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
trendingArticle = trendingArticle?.let { mapToArticleConfigUM(it, isTrending = true) },
|
||||
news = NewsUM.Content(
|
||||
commonArticles.map { mapToArticleConfigUM(it, isTrending = false) }.toPersistentList(),
|
||||
),
|
||||
news = updatedNews,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleErrorState(currentState: FeedListUM, onRetryClicked: () -> Unit) {
|
||||
private fun handleErrorState(currentState: FeedListUM) {
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
trendingArticle = null,
|
||||
news = NewsUM.Error(onRetryClicked = onRetryClicked),
|
||||
news = NewsUM(
|
||||
content = persistentListOf(),
|
||||
onRetryClicked = onRetryClicked,
|
||||
newsUMState = NewsUMState.ERROR,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -79,7 +95,7 @@ internal class TrendingNewsStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
private fun buildArticleTags(article: ShortArticle): kotlinx.collections.immutable.ImmutableSet<LabelUM> {
|
||||
private fun buildArticleTags(article: ShortArticle): ImmutableSet<LabelUM> {
|
||||
val categoryLabels = article.categories.map { category ->
|
||||
LabelUM(text = TextReference.Str(category.name))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ private fun Content(
|
|||
state = state.body,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
relatedNews = state.relatedNews,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
package com.tangem.features.feed.ui.market.detailed.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.items.DescriptionItem
|
||||
import com.tangem.core.ui.components.items.DescriptionPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews
|
||||
|
||||
@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA]
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
isAccountEnabled: Boolean,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
relatedNews: RelatedNews,
|
||||
) {
|
||||
when (state) {
|
||||
MarketsTokenDetailsUM.Body.Loading -> {
|
||||
|
|
@ -51,6 +58,10 @@ internal fun LazyListScope.tokenMarketDetailsBody(
|
|||
}
|
||||
}
|
||||
|
||||
if (relatedNews.articles.isNotEmpty()) {
|
||||
relatedNews(relatedNews)
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
|
|
@ -190,6 +201,45 @@ private fun LazyListScope.loadingInfoBlocks() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
||||
item("related-news") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 32.dp, top = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
text = "Related news",
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(
|
||||
items = relatedNews.articles,
|
||||
key = ArticleConfigUM::id,
|
||||
) { article ->
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { relatedNews.onArticledClicked(article.id) },
|
||||
modifier = Modifier
|
||||
.height(164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.blockPaddings(): Modifier {
|
||||
return this.padding(
|
||||
|
|
|
|||
|
|
@ -7,13 +7,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
|
|||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InsightsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ListedOnUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MetricsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.PricePerformanceUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object MarketsTokenDetailsPreview {
|
||||
|
|
@ -49,6 +43,10 @@ internal object MarketsTokenDetailsPreview {
|
|||
triggerPriceChange = consumedEvent(),
|
||||
onShouldShowPriceSubtitleChange = {},
|
||||
shouldShowPriceSubtitle = false,
|
||||
relatedNews = MarketsTokenDetailsUM.RelatedNews(
|
||||
articles = persistentListOf(),
|
||||
onArticledClicked = {},
|
||||
),
|
||||
)
|
||||
|
||||
val contentState = MarketsTokenDetailsUM(
|
||||
|
|
@ -135,5 +133,9 @@ internal object MarketsTokenDetailsPreview {
|
|||
triggerPriceChange = consumedEvent(),
|
||||
onShouldShowPriceSubtitleChange = {},
|
||||
shouldShowPriceSubtitle = false,
|
||||
relatedNews = MarketsTokenDetailsUM.RelatedNews(
|
||||
articles = persistentListOf(),
|
||||
onArticledClicked = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -2,11 +2,13 @@ package com.tangem.features.feed.ui.market.detailed.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal data class MarketsTokenDetailsUM(
|
||||
|
|
@ -25,6 +27,7 @@ internal data class MarketsTokenDetailsUM(
|
|||
val body: Body,
|
||||
val shouldShowPriceSubtitle: Boolean,
|
||||
val onShouldShowPriceSubtitleChange: (Boolean) -> Unit,
|
||||
val relatedNews: RelatedNews,
|
||||
) {
|
||||
|
||||
data class ChartState(
|
||||
|
|
@ -69,4 +72,9 @@ internal data class MarketsTokenDetailsUM(
|
|||
val fullDescription: TextReference?,
|
||||
val onReadMoreClick: () -> Unit,
|
||||
)
|
||||
|
||||
data class RelatedNews(
|
||||
val articles: ImmutableList<ArticleConfigUM>,
|
||||
val onArticledClicked: (id: Int) -> Unit,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue