diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt deleted file mode 100644 index 9bf97f2250..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.tangem.tap.common.analytics.events - -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.tap.common.extensions.filterNotNull - -/** -[REDACTED_AUTHOR] - */ -sealed class ManageTokens( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Manage Tokens", event, params) { - - class ScreenOpened : ManageTokens("Manage Tokens Screen Opened") - class TokenSearched : ManageTokens("Token Searched") - - class TokenSwitcherChanged( - type: AnalyticsParam.CurrencyType, - state: AnalyticsParam.OnOffState, - ) : ManageTokens( - "Token Switcher Changed", - params = mapOf( - "Token" to type.value, - "State" to state.value, - ), - ) - - class ButtonSaveChanges : ManageTokens("Button - Save Changes") - class ButtonCustomToken : ManageTokens("Button - Custom Token") - - sealed class CustomToken( - event: String, - params: Map = mapOf(), - ) : ManageTokens(event, params) { - - object ScreenOpened : ManageTokens(event = "Custom Token Screen Opened") - - // TODO("Get rid of strong binding (CustomCurrency) - open class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens( - event = "Custom Token Was Added", - params = convertToParam(customCurrency), - ) { - - data class Token( - val symbol: String, - val derivationPath: String?, - val blockchain: com.tangem.blockchain.common.Blockchain, - val contractAddress: String, - ) : ManageTokens( - event = "Custom Token Was Added", - params = mapOf( - "Token" to symbol, - "Derivation Path" to derivationPath, - "Network Id" to blockchain.currency, - "Contract Address" to contractAddress, - ).filterNotNull(), - ) - - data class Blockchain( - val derivationPath: String?, - val blockchain: com.tangem.blockchain.common.Blockchain, - ) : ManageTokens( - event = "Custom Token Was Added", - params = mapOf( - "Token" to blockchain.currency, - "Derivation Path" to derivationPath, - ).filterNotNull(), - ) - - companion object { - private fun convertToParam(customCurrency: CustomCurrency): Map = with(customCurrency) { - return when (this) { - is CustomCurrency.CustomBlockchain -> mapOf( - "Token" to network.currency, - "Derivation Path" to derivationPath?.rawPath, - ).filterNotNull() - - is CustomCurrency.CustomToken -> mapOf( - "Token" to token.symbol, - "Derivation Path" to derivationPath?.rawPath, - "Network Id" to network.toNetworkId(), - "Contract Address" to token.contractAddress, - ).filterNotNull() - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 1509ffe81b..38732f69ce 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -9,7 +9,6 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer -import com.tangem.tap.features.tokens.legacy.redux.TokensReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphReducer @@ -27,7 +26,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state), twinCardsState = TwinCardsReducer.reduce(action, state), detailsState = DetailsReducer.reduce(action, state), - tokensState = TokensReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), welcomeState = WelcomeReducer.reduce(action, state), saveWalletState = SaveWalletReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 8c30e94166..4fed318b36 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -23,7 +23,6 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware import com.tangem.tap.features.saveWallet.redux.SaveWalletState -import com.tangem.tap.features.tokens.legacy.redux.TokensState import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState @@ -41,7 +40,6 @@ data class AppState( val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(), val twinCardsState: TwinCardsState = TwinCardsState(), val detailsState: DetailsState = DetailsState(), - val tokensState: TokensState = TokensState(), val walletConnectState: WalletConnectState = WalletConnectState(), val welcomeState: WelcomeState = WelcomeState(), val saveWalletState: SaveWalletState = SaveWalletState(), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt deleted file mode 100644 index ca1cf6190f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.tap.features.tokens.impl.data - -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.testnet.TestnetTokensStorage -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.features.tokens.impl.domain.TokensListRepository -import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow - -/** - * Default repository implementation of tokens list feature - * - * @property tangemTechApi Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property getSelectedWalletSyncUseCase use case that returns selected wallet - * @property testnetTokensStorage storage for getting testnet tokens data - * -[REDACTED_AUTHOR] - */ -internal class DefaultTokensListRepository( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val testnetTokensStorage: TestnetTokensStorage, -) : TokensListRepository { - - override fun getAvailableTokens(searchText: String?, needFilterExcluded: Boolean): Flow> { - return Pager( - config = PagingConfig( - pageSize = 100, - prefetchDistance = 70, - enablePlaceholders = false, - ), - pagingSourceFactory = { - val defaultSource = TangemApiTokensPagingSource( - api = tangemTechApi, - dispatchers = dispatchers, - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - searchText = searchText, - needFilterExcluded = needFilterExcluded, - ) - - getSelectedWalletSyncUseCase().fold( - ifLeft = { defaultSource }, - ifRight = { - if (it.scanResponse.card.isTestCard) { - TestnetTokensPagingSource(testnetTokensStorage, searchText) - } else { - defaultSource - } - }, - ) - }, - ).flow - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt deleted file mode 100644 index dcacf9fef2..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.tangem.tap.features.tokens.impl.data - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.isSupportedInApp -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter -import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching - -/** - * Paging source that get tokens by Tangem Tech API - * - * @property api Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property getSelectedWalletSyncUseCase use case that returns selected wallet - * @property searchText search text - * @property needFilterExcluded filter networks that are not supported in the app - */ -internal class TangemApiTokensPagingSource( - private val api: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val searchText: String?, - private val needFilterExcluded: Boolean = false, -) : PagingSource() { - - private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded) - - private val allAvailableBlockchains by lazy { - Blockchain.entries - .filter { - it.isTestnet().not() && (needFilterExcluded.not() || it.isSupportedInApp()) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition?.let { anchorPosition -> - state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) - ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) - } - } - - override suspend fun load(params: LoadParams): LoadResult { - val page = params.key ?: 0 - - return runCatching(dispatchers.io) { - val supportedBlockchains = getSelectedWalletSyncUseCase().fold( - ifLeft = { allAvailableBlockchains }, - ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) }, - ) - - api.getCoins( - networkIds = supportedBlockchains.joinToString(separator = ",", transform = Blockchain::toNetworkId), - active = true, - searchText = searchText, - offset = page * params.loadSize, - limit = params.loadSize, - ).getOrThrow() - }.fold( - onSuccess = { response -> - LoadResult.Page( - data = coinsResponseConverter.convert(response), - prevKey = if (page == 0) null else page.minus(other = 1), - nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1), - ) - }, - onFailure = { LoadResult.Error(it) }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TestnetTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TestnetTokensPagingSource.kt deleted file mode 100644 index 9525c329ba..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TestnetTokensPagingSource.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.tap.features.tokens.impl.data - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.tangem.datasource.local.testnet.TestnetTokensStorage -import com.tangem.datasource.local.testnet.models.TestnetTokensConfig -import com.tangem.tap.features.tokens.impl.data.converters.TestnetTokensConfigConverter -import com.tangem.tap.features.tokens.impl.domain.models.Token - -/** - * Paging source that get tokens by testnet tokens config - * - * @property testnetTokensStorage testnet tokens config storage - * @property searchText search text - */ -internal class TestnetTokensPagingSource( - private val testnetTokensStorage: TestnetTokensStorage, - private val searchText: String?, -) : PagingSource() { - - override fun getRefreshKey(state: PagingState): Int? = null - - override suspend fun load(params: LoadParams): LoadResult { - return LoadResult.Page( - data = TestnetTokensConfigConverter.convert( - value = testnetTokensStorage.getConfig().searchByText(searchText), - ), - prevKey = null, - nextKey = null, - ) - } - - private fun TestnetTokensConfig.searchByText(searchText: String?): TestnetTokensConfig { - if (searchText.isNullOrBlank()) return this - - return copy( - tokens = tokens.filter { token -> - token.symbol.contains(other = searchText, ignoreCase = true) || - token.name.contains(other = searchText, ignoreCase = true) - }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt deleted file mode 100644 index 2d620d45e8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.tap.features.tokens.impl.data.converters - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.blockchainsdk.utils.isSupportedInApp -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.utils.converter.Converter - -/** - * Converter from data model [CoinsResponse] to list of domain models [Token] - * -[REDACTED_AUTHOR] - */ -internal class CoinsResponseConverter(val needFilterExcluded: Boolean) : Converter> { - - override fun convert(value: CoinsResponse): List { - return value.coins.map { token -> - Token( - id = token.id, - name = token.name, - symbol = token.symbol, - iconUrl = getIconUrl(token.id, value.imageHost), - networks = token.networks.mapNotNull { network -> - val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null - - if (needFilterExcluded && !blockchain.isSupportedInApp()) { - return@mapNotNull null - } - - // filter tokens, if contractAddress != null, assume that it is a token - if (network.contractAddress != null && - !blockchain.canHandleTokens() - ) { - return@mapNotNull null - } - - Token.Network( - id = network.networkId, - blockchain = blockchain, - address = network.contractAddress, - iconUrl = getIconUrl(network.networkId, value.imageHost), - decimalCount = network.decimalCount?.toInt(), - ) - }, - ) - }.filter { it.networks.isNotEmpty() } - } - - fun getIconUrl(id: String, imageHost: String? = null): String { - return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png" - } - - private companion object { - const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt deleted file mode 100644 index 561c8c123a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.tap.features.tokens.impl.data.converters - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.datasource.local.testnet.models.TestnetTokensConfig -import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.utils.converter.Converter - -/** - * Converter from data model [TestnetTokensConfig] to list of domain models [Token] - * -[REDACTED_AUTHOR] - */ -internal object TestnetTokensConfigConverter : Converter> { - - private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded = false) - override fun convert(value: TestnetTokensConfig): List { - return value.tokens.map { token -> - Token( - id = token.id, - name = token.name, - symbol = token.symbol, - iconUrl = coinsResponseConverter.getIconUrl(token.id), - networks = token.networks?.mapNotNull { network -> - val blockchain = Blockchain.fromNetworkId(network.id) ?: return@mapNotNull null - - Token.Network( - id = network.id, - blockchain = blockchain, - address = network.address, - iconUrl = coinsResponseConverter.getIconUrl(network.id), - decimalCount = network.decimalCount, - ) - }.orEmpty(), - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt deleted file mode 100644 index 464420346b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.tap.features.tokens.impl.di - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.testnet.TestnetTokensStorage -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository -import com.tangem.tap.features.tokens.impl.domain.DefaultTokensListInteractor -import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -/** -[REDACTED_AUTHOR] - */ -@Module -@InstallIn(ViewModelComponent::class) -internal object TokensListInteractorModule { - - @Provides - @ViewModelScoped - fun provideTokensListInteractor( - tangemTechApi: TangemTechApi, - dispatchers: CoroutineDispatcherProvider, - getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - testnetTokensStorage: TestnetTokensStorage, - ): TokensListInteractor { - return DefaultTokensListInteractor( - repository = DefaultTokensListRepository( - tangemTechApi = tangemTechApi, - dispatchers = dispatchers, - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - testnetTokensStorage = testnetTokensStorage, - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt deleted file mode 100644 index 186545138b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.tap.features.tokens.impl.di - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.testnet.TestnetTokensStorage -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository -import com.tangem.tap.features.tokens.impl.domain.TokensListRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -/** -[REDACTED_AUTHOR] - */ -@Module -@InstallIn(SingletonComponent::class) -internal object TokensListRepositoryModule { - - @Provides - @Singleton - fun providesTokensListRepository( - tangemTechApi: TangemTechApi, - dispatchers: CoroutineDispatcherProvider, - getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - testnetTokensStorage: TestnetTokensStorage, - ): TokensListRepository { - return DefaultTokensListRepository( - tangemTechApi = tangemTechApi, - dispatchers = dispatchers, - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - testnetTokensStorage = testnetTokensStorage, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt deleted file mode 100644 index 2a7da5cca4..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.tokens.impl.di - -import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter -import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -/** -[REDACTED_AUTHOR] - */ -@Module -@InstallIn(ViewModelComponent::class) -internal object TokensListRouterModule { - - @Provides - @ViewModelScoped - fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt deleted file mode 100644 index 0cb9da8c42..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.tokens.impl.domain - -import androidx.paging.PagingData -import com.tangem.tap.features.tokens.impl.domain.models.Token -import kotlinx.coroutines.flow.Flow - -/** - * Default implementation of tokens list interactor - * - * @property repository repository of tokens list feature - */ -internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor { - - override fun getTokensList(searchText: String, needFilterExcluded: Boolean): Flow> { - return repository.getAvailableTokens( - searchText = searchText.ifBlank(defaultValue = { null }), - needFilterExcluded = needFilterExcluded, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt deleted file mode 100644 index 4b8edb914d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.features.tokens.impl.domain - -import androidx.paging.PagingData -import com.tangem.tap.features.tokens.impl.domain.models.Token -import kotlinx.coroutines.flow.Flow - -/** - * Interactor of tokens list feature - * -[REDACTED_AUTHOR] - */ -internal interface TokensListInteractor { - - /** Get tokens list using filter by text [searchText] */ - fun getTokensList(searchText: String, needFilterExcluded: Boolean): Flow> -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListRepository.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListRepository.kt deleted file mode 100644 index 67a9626b6b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.features.tokens.impl.domain - -import androidx.paging.PagingData -import com.tangem.tap.features.tokens.impl.domain.models.Token -import kotlinx.coroutines.flow.Flow - -/** - * Repository of tokens list feature - * -[REDACTED_AUTHOR] - */ -internal interface TokensListRepository { - - /** - * Get available tokens list - * - * @param searchText search text - * @param needFilterExcluded flag that determines if excluded tokens should be filtered - * - * @throws com.tangem.datasource.api.common.response.ApiResponseError - */ - fun getAvailableTokens(searchText: String?, needFilterExcluded: Boolean): Flow> -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/models/Token.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/models/Token.kt deleted file mode 100644 index 72c6ecfdae..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/models/Token.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.tap.features.tokens.impl.domain.models - -import com.tangem.blockchain.common.Blockchain - -/** - * Domain model of token for tokens list screen - * - * @property id token id - * @property name token name - * @property symbol token brief name. Example, "BTC" - * @property iconUrl token icon url - * @property networks token networks - * -[REDACTED_AUTHOR] - */ -internal data class Token( - val id: String, - val name: String, - val symbol: String, - val iconUrl: String, - val networks: List, -) { - - /** - * Domain model of network for tokens list screen - * - * @property id network id - * @property blockchain blockchain - * @property address address. If address equals null, it means it is the main network of token - * @property iconUrl network icon url - * @property decimalCount decimal count - * -[REDACTED_AUTHOR] - */ - data class Network( - val id: String, - val blockchain: Blockchain, - val address: String?, - val iconUrl: String, - val decimalCount: Int?, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt deleted file mode 100644 index ed260691fe..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.hilt.navigation.compose.hiltViewModel -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen -import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -/** - * Fragment with list of tokens - * -[REDACTED_AUTHOR] - */ -@AndroidEntryPoint -internal class TokensListFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - @Composable - override fun ScreenContent(modifier: Modifier) { - val viewModel = hiltViewModel().apply { - LocalLifecycleOwner.current.lifecycle.addObserver(this) - } - TokensListScreen( - stateHolder = viewModel.uiState, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/SupportTokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/SupportTokensState.kt deleted file mode 100644 index 0917dcb2f9..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/SupportTokensState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.models - -/** - * State that shows is token support by given card - */ -sealed class SupportTokensState { - - object NetworkTokensUnsupported : SupportTokensState() - object UnsupportedCurve : SupportTokensState() - object SupportedToken : SupportTokensState() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt deleted file mode 100644 index d9142d235c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.router - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter - -import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchNotification -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.store -import com.tangem.wallet.R - -/** - * Default implementation of tokens list router - * FIXME("Necessary to avoid using redux actions") - * -[REDACTED_AUTHOR] - */ -internal class DefaultTokensListRouter : TokensListRouter { - - override fun popBackStack() { - store.dispatchNavigationAction(AppRouter::pop) - } - - override fun openAddCustomTokenScreen() { - store.dispatchNavigationAction { push(AppRoute.AddCustomToken) } - } - - override fun showAddressCopiedNotification() { - store.dispatchNotification(R.string.contract_address_copied_message) - } - - override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String) { - store.dispatchDialogShow( - dialog = AppDialog.TokensAreLinkedDialog( - currencyTitle = tokenName, - currencySymbol = tokenSymbol, - networkName = networkName, - ), - ) - } - - override fun openRemoveWalletAlert(tokenName: String, onOkClick: () -> Unit) { - store.dispatchDialogShow( - dialog = AppDialog.RemoveWalletDialog(currencyTitle = tokenName, onOk = onOkClick), - ) - } - - override fun openUnsupportedNetworkAlert(blockchain: Blockchain) { - val alert = AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.alert_manage_tokens_unsupported_curve_message, - args = listOf(blockchain.getNetworkName()), - ) - store.dispatchDialogShow(alert) - } - - override fun showGenericErrorAlertAndPopBack() { - val alert = AppDialog.SimpleOkDialogRes( - headerId = R.string.common_error, - messageId = R.string.common_unknown_error, - onOk = { store.dispatchNavigationAction(AppRouter::pop) }, - ) - store.dispatchDialogShow(alert) - } - - override fun openNetworkTokensNotSupportAlert(networkName: String) { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.alert_manage_tokens_unsupported_message, - args = listOf(networkName), - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt deleted file mode 100644 index a11e584564..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.router - -import com.tangem.blockchain.common.Blockchain - -/** Tokens list feature router */ -internal interface TokensListRouter { - - /** Return to last screen */ - fun popBackStack() - - /** Open adding custom token screen */ - fun openAddCustomTokenScreen() - - /** Show notification to inform about copied address */ - fun showAddressCopiedNotification() - - /** - * Open alert if unable to hide the main token - * - * @param tokenName token name - * @param tokenSymbol token brief name - * @param networkName blockchain network full name - */ - fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String) - - /** - * Open alert to remove wallet - * - * @param tokenName token name - * @param onOkClick lambda be invoked if OkButton is been clicked - */ - fun openRemoveWalletAlert(tokenName: String, onOkClick: () -> Unit) - - /** Open alert if solana network is unsupported - * - * @param blockchain blockchain to show alert - */ - fun openUnsupportedNetworkAlert(blockchain: Blockchain) - - fun showGenericErrorAlertAndPopBack() - - /** - * Open alert with unsupported networks tokens error - */ - fun openNetworkTokensNotSupportAlert(networkName: String) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/NetworkItemState.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/NetworkItemState.kt deleted file mode 100644 index a78f074e16..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/NetworkItemState.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.states - -import androidx.compose.runtime.MutableState -import com.tangem.blockchain.common.Blockchain -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.getGreyedOutIconRes - -/** - * Network item state - * -[REDACTED_AUTHOR] - */ -sealed interface NetworkItemState { - - /** Network name */ - val name: String - - /** Network protocol name */ - val protocolName: String - - /** Network icon id from resources */ - val iconResId: MutableState - - /** Flag that determines if the network is the main network for the token */ - val isMainNetwork: Boolean - - /** - * Network item state that is available for read - * - * @property name network name - * @property protocolName network protocol name - * @property iconResId network icon id from resources - * @property isMainNetwork flag that determines if the network is the main network for the token - */ - data class ReadContent( - override val name: String, - override val protocolName: String, - override val iconResId: MutableState, - override val isMainNetwork: Boolean, - ) : NetworkItemState - - /** - * Network item state that is available for read and edit - * - * @property name network name - * @property protocolName network protocol name - * @property iconResId network icon id from resources - * @property isMainNetwork flag that determines if the network is the main network for the token - * @property isAdded flag that determines if the user has saved the token - * @property id network id - * @property address contract address - * @property decimalCount decimal count - * @property blockchain blockchain - * @property onToggleClick lambda be invoked when switch is been toggled - * @property onNetworkClick lambda be invoked when network item is been clicked - */ - data class ManageContent( - override val name: String, - override val protocolName: String, - override val iconResId: MutableState, - override val isMainNetwork: Boolean, - val isAdded: MutableState, - val id: String, - val address: String?, - val decimalCount: Int?, - val blockchain: Blockchain, - val onToggleClick: (TokenItemState.ManageContent, ManageContent) -> Unit, - val onNetworkClick: () -> Unit, - ) : NetworkItemState { - - /** - * Change toggle state [isAdded]. - * - * It is a hack that helps us to change element of flow - */ - fun changeToggleState() { - val reverseState = !isAdded.value - isAdded.value = reverseState - iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokenItemState.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokenItemState.kt deleted file mode 100644 index a5f9c615fd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokenItemState.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.states - -import androidx.compose.runtime.Immutable -import kotlinx.collections.immutable.ImmutableList - -/** - * Token item state. - * All subclasses is stable, but @Immutable annotation is required to use this sealed class like as - * field of TokensListStateHolder. - * -[REDACTED_AUTHOR] - */ -@Immutable -sealed interface TokenItemState { - - /** Token id */ - val id: String - - /** Token full name (name with symbol) */ - val fullName: String - - /** Token icon url */ - val iconUrl: String - - /** List of networks */ - val networks: ImmutableList - - /** Token composed id that unique for tokens in different networks */ - val composedId: String - - /** - * Token item state that is available for read - * - * @property id token id - * @property fullName token name - * @property iconUrl token icon url - * @property networks list of networks that is available for read - * @property composedId token composed id to use in lists - */ - data class ReadContent( - override val id: String, - override val fullName: String, - override val iconUrl: String, - override val networks: ImmutableList, - override val composedId: String, - ) : TokenItemState - - /** - * Token item state that is available for read and manage - * - * @property id token id - * @property fullName token name - * @property iconUrl token icon url - * @property networks list of networks is available for read and edit - * @property composedId token composed id to use in lists - * @property name token name - * @property symbol token brief name - */ - data class ManageContent( - override val id: String, - override val fullName: String, - override val iconUrl: String, - override val networks: ImmutableList, - override val composedId: String, - val name: String, - val symbol: String, - ) : TokenItemState -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListStateHolder.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListStateHolder.kt deleted file mode 100644 index bf48a93269..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListStateHolder.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.states - -import androidx.compose.runtime.Immutable -import androidx.paging.LoadState -import androidx.paging.PagingData -import kotlinx.coroutines.flow.Flow - -/** - * State holder for screen with list of tokens - * -[REDACTED_AUTHOR] - */ -@Immutable -internal sealed interface TokensListStateHolder { - - /** Toolbar state */ - val toolbarState: TokensListToolbarState - - /** Loading state */ - val isLoading: Boolean - - /** Flag that determines if warning block is visible */ - val isDifferentAddressesBlockVisible: Boolean - - /** Tokens list */ - val tokens: Flow> - - /** Callback to be invoked when [tokens] loading state is been changed */ - val onTokensLoadStateChanged: (LoadState) -> Unit - - /** - * Util function that allow to make a copy - * - * @param toolbarState toolbar state - * @param isLoading loading state - * @param isDifferentAddressesBlockVisible flag that determines if warning block is visible - * @param tokens tokens list - * @param onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed - */ - fun copySealed( - toolbarState: TokensListToolbarState = this.toolbarState, - isLoading: Boolean = this.isLoading, - isDifferentAddressesBlockVisible: Boolean = this.isDifferentAddressesBlockVisible, - tokens: Flow> = this.tokens, - onTokensLoadStateChanged: (LoadState) -> Unit = this.onTokensLoadStateChanged, - ): TokensListStateHolder { - return when (this) { - is ManageContent -> copy( - toolbarState, - isLoading, - isDifferentAddressesBlockVisible, - tokens, - onTokensLoadStateChanged, - ) - is ReadContent -> copy( - toolbarState, - isLoading, - isDifferentAddressesBlockVisible, - tokens, - onTokensLoadStateChanged, - ) - } - } - - /** - * State screen that is available only for read - * - * @property toolbarState toolbar state - * @property isLoading loading state - * @property isDifferentAddressesBlockVisible flag that determines if warning block is visible - * @property tokens tokens list - * @property onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed - */ - data class ReadContent( - override val toolbarState: TokensListToolbarState, - override val isLoading: Boolean, - override val isDifferentAddressesBlockVisible: Boolean, - override val tokens: Flow>, - override val onTokensLoadStateChanged: (LoadState) -> Unit, - ) : TokensListStateHolder - - /** - * State screen that is available for read and manage - * - * @property toolbarState toolbar state - * @property isLoading loading state - * @property isDifferentAddressesBlockVisible flag that determines if warning block is visible - * @property tokens tokens list - * @property onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed - * @property onSaveButtonClick callback to be invoked when SaveButton is being clicked - */ - data class ManageContent( - override val toolbarState: TokensListToolbarState, - override val isLoading: Boolean, - override val isDifferentAddressesBlockVisible: Boolean, - override val tokens: Flow>, - override val onTokensLoadStateChanged: (LoadState) -> Unit, - val onSaveButtonClick: () -> Unit, - val isSavingInProgress: Boolean, - ) : TokensListStateHolder -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListToolbarState.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListToolbarState.kt deleted file mode 100644 index a269659991..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListToolbarState.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.states - -import androidx.compose.runtime.Immutable - -/** - * Toolbar state. - * All subclasses is stable, but @Immutable annotation is required to use this sealed class like as - * field of TokensListStateHolder. - */ -@Immutable -sealed interface TokensListToolbarState { - - /** Callback to be invoked when BackButton is being clicked */ - val onBackButtonClick: () -> Unit - - /** Toolbar state as title */ - sealed interface Title : TokensListToolbarState { - - /** Toolbar title id from resources */ - val titleResId: Int - - /** Callback to be invoked when SearchButton is being clicked */ - val onSearchButtonClick: () -> Unit - - /** - * Title state that is available only for read - * - * @property onBackButtonClick callback to be invoked when BackButton is being clicked - * @property titleResId toolbar title id from resources - * @property onSearchButtonClick callback to be invoked when SearchButton is being clicked - */ - data class Read( - override val onBackButtonClick: () -> Unit, - override val titleResId: Int, - override val onSearchButtonClick: () -> Unit, - ) : Title - - /** - * Title state that is available for read and manage - * - * @property onBackButtonClick callback to be invoked when BackButton is being clicked - * @property titleResId toolbar title id from resources - * @property onSearchButtonClick callback to be invoked when SearchButton is being clicked - * @property onAddCustomTokenClick callback to be invoked when AddCustomTokenButton is being clicked - */ - data class Manage( - override val onBackButtonClick: () -> Unit, - override val titleResId: Int, - override val onSearchButtonClick: () -> Unit, - val onAddCustomTokenClick: () -> Unit, - ) : Title - } - - /** - * Toolbar state as input field - * - * @property onBackButtonClick callback to be invoked when BackButton is being clicked - * @property value input value - * @property onValueChange lambda to be invoked when search value is being changed - * @property onCleanButtonClick callback to be invoked when CleanButton is being clicked - */ - data class InputField( - override val onBackButtonClick: () -> Unit, - val value: String, - val onValueChange: (String) -> Unit, - val onCleanButtonClick: () -> Unit, - ) : TokensListToolbarState -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt deleted file mode 100644 index 4091e53c06..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.ui - -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.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.* -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.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState -import kotlinx.collections.immutable.ImmutableCollection - -/** This const configures how many items show to user and hide more than*/ -private const val MAX_VISIBLE_BRIEF_ICONS = 9 - -/** -[REDACTED_AUTHOR] - */ -@Composable -internal fun BriefNetworksList( - isCollapsed: Boolean, - networks: ImmutableCollection, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - visible = isCollapsed, - modifier = modifier, - enter = fadeIn(), - exit = fadeOut(), - ) { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { - for ((index, network) in networks.withIndex()) { - if (index < MAX_VISIBLE_BRIEF_ICONS) { - key(network.name + network.protocolName) { - BriefNetworkItem(model = network) - } - } else { - if (networks.size < MAX_VISIBLE_BRIEF_ICONS + 1) { - key(network.name + network.protocolName) { - BriefNetworkItem(model = network) - } - } else { - HasMoreItem(moreCount = networks.size - index) - break - } - } - } - } - } -} - -/** -[REDACTED_AUTHOR] - */ -@Composable -internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modifier) { - val isAdded = model is NetworkItemState.ManageContent && model.isAdded.value - Box(modifier = modifier.size(size = TangemTheme.dimens.size20)) { - if (!isAdded) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size20) - .clip(CircleShape) - .background(TangemTheme.colors.control.unchecked), - ) - } - Icon( - painter = painterResource(id = model.iconResId.value), - contentDescription = null, - modifier = Modifier - .size(size = TangemTheme.dimens.size20) - .clip(CircleShape), - tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, - ) - - if (model.isMainNetwork) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size7) - .clip(CircleShape) - .background(TangemTheme.colors.background.primary), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size5) - .clip(CircleShape) - .background(TangemTheme.colors.icon.accent), - ) - } - } - } -} - -@Suppress("MagicNumber") -@Composable -internal fun HasMoreItem(moreCount: Int) { - val count = if (moreCount > 99) 99 else moreCount - val themeTextStyle = TangemTheme.typography.overline.copy( - letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp), - ) - var textStyle by remember(themeTextStyle) { mutableStateOf(themeTextStyle) } - var readyToDraw by remember(themeTextStyle) { mutableStateOf(false) } - - Box( - modifier = Modifier - .size(size = TangemTheme.dimens.size20) - .clip(CircleShape) - .background(TangemTheme.colors.control.unchecked), - ) { - Text( - modifier = Modifier - .align(Alignment.Center) - .drawWithContent { if (readyToDraw) drawContent() }, - text = "+$count", - style = textStyle, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Clip, - onTextLayout = { textLayoutResult -> - if (textLayoutResult.hasVisualOverflow) { - textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9) - } else { - readyToDraw = true - } - }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt deleted file mode 100644 index 12620898b6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.ui - -import android.content.res.Configuration -import androidx.compose.animation.* -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material.Switch -import androidx.compose.material.SwitchDefaults -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState -import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState -import kotlinx.collections.immutable.ImmutableCollection - -/** -[REDACTED_AUTHOR] - */ -@Composable -internal fun DetailedNetworksList( - isExpanded: Boolean, - token: TokenItemState, - networks: ImmutableCollection, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - visible = isExpanded, - modifier = modifier, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut(), - ) { - Column { - networks.forEachIndexed { index, network -> - key(network.name + network.protocolName) { - DetailedNetworkItem(token = token, network = network, isLastItem = networks.size - 1 == index) - } - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun DetailedNetworkItem(token: TokenItemState, network: NetworkItemState, isLastItem: Boolean) { - val clipboardManager = LocalClipboardManager.current - val itemHeight = TangemTheme.dimens.size50 - - Row( - modifier = Modifier - .combinedClickable( - enabled = network is NetworkItemState.ManageContent, - onLongClick = { - if (network is NetworkItemState.ManageContent && network.address != null) { - clipboardManager.setText(AnnotatedString(text = network.address)) - network.onNetworkClick() - } - }, - onClick = {}, - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ) - .fillMaxWidth() - .heightIn(min = itemHeight) - .padding(start = TangemTheme.dimens.spacing38), - verticalAlignment = Alignment.CenterVertically, - ) { - NetworkItemArrow(itemHeight = itemHeight, isLastItem = isLastItem) - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing16)) - BriefNetworkItem(model = network) - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6)) - NetworkTitle(model = network) - - if (network is NetworkItemState.ManageContent) { - Switch( - checked = network.isAdded.value, - onCheckedChange = { - network.onToggleClick( - requireNotNull(token as? TokenItemState.ManageContent), - network, - ) - }, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing8), - colors = SwitchDefaults.colors( - checkedThumbColor = TangemTheme.colors.control.key, - checkedTrackColor = TangemTheme.colors.icon.accent, - uncheckedThumbColor = TangemTheme.colors.control.key, - uncheckedTrackColor = TangemTheme.colors.icon.informative, - ), - ) - } - } -} - -@Composable -private fun RowScope.NetworkTitle(model: NetworkItemState) { - Text( - modifier = Modifier.weight(1f), - text = buildAnnotatedString { - append(text = model.name) - append(text = " ") - withStyle( - style = SpanStyle( - fontWeight = FontWeight.Normal, - color = if (model.isMainNetwork) { - TangemTheme.colors.icon.accent - } else { - TangemTheme.colors.icon.secondary - }, - ), - ) { - append(text = model.protocolName) - } - }, - fontWeight = FontWeight.SemiBold, - fontSize = 13.sp, - color = if (model is NetworkItemState.ManageContent && model.isAdded.value) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.secondary - }, - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_DetailedNetworksList_ManageAccess() { - TangemThemePreview { - DetailedNetworksList( - isExpanded = true, - token = TokenListPreviewData.createManageToken(), - networks = TokenListPreviewData.createManageNetworksList(), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_DetailedNetworksList_ReadAccess() { - TangemThemePreview { - DetailedNetworksList( - isExpanded = true, - token = TokenListPreviewData.createReadToken(), - networks = TokenListPreviewData.createReadNetworksList(), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt deleted file mode 100644 index b51b65f1c0..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.wallet.R - -private const val SPECIAL_HEIGHT_3_5 = 3.5 - -/** -[REDACTED_AUTHOR] - */ -@Composable -internal fun NetworkItemArrow(itemHeight: Dp, isLastItem: Boolean) { - Box(modifier = Modifier.height(height = itemHeight)) { - if (!isLastItem) { - Box( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing0_5) - .background(color = TangemTheme.colors.stroke.primary) - .size(width = TangemTheme.dimens.size1, height = itemHeight), - ) - } - - Icon( - painter = painterResource(id = R.drawable.ic_link), - contentDescription = null, - modifier = Modifier.height(height = itemHeight / 2 + SPECIAL_HEIGHT_3_5.dp), - tint = TangemTheme.colors.stroke.primary, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_NetworkItemArrow_Column() { - TangemThemePreview { - Column( - modifier = Modifier - .fillMaxWidth() - .background(color = TangemTheme.colors.background.primary) - .padding(start = TangemTheme.dimens.size36), - ) { - NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = false) - NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = true) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt deleted file mode 100644 index ce1967b48b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.ui - -import android.content.res.Configuration -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.CurrencyPlaceholderIcon -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState -import com.tangem.wallet.R -import kotlinx.coroutines.launch - -/** -[REDACTED_AUTHOR] - */ -@Suppress("LongMethod") -@Composable -internal fun TokenItem(model: TokenItemState) { - var isExpanded by rememberSaveable { mutableStateOf(value = false) } - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .background(color = TangemTheme.colors.background.primary) - .padding(top = TangemTheme.dimens.spacing16), - ) { - val (icon, title, availableNetworksText) = createRefs() - val (briefNetworksList, detailedNetworksList, changeNetworksViewButton) = createRefs() - - val spacing16 = TangemTheme.dimens.spacing16 - val spacing6 = TangemTheme.dimens.spacing6 - - Box( - modifier = Modifier - .background( - color = iconBackgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ) - .size(TangemTheme.dimens.size46) - .constrainAs(icon) { - top.linkTo(parent.top) - start.linkTo(anchor = parent.start, margin = spacing16) - }, - ) { - Icon( - name = model.fullName, - iconUrl = model.iconUrl, - onContrastCalculate = { iconBackgroundColor = it }, - ) - } - - Title( - title = model.fullName, - modifier = Modifier.constrainAs(title) { - top.linkTo(parent.top) - start.linkTo(icon.end, margin = spacing16) - end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16) - width = Dimension.fillToConstraints - }, - ) - - Subtitle( - isExpanded = isExpanded, - modifier = Modifier.constrainAs(availableNetworksText) { - top.linkTo(title.bottom, margin = spacing6) - start.linkTo(icon.end, margin = spacing16) - end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16) - width = Dimension.fillToConstraints - }, - ) - - BriefNetworksList( - isCollapsed = !isExpanded, - networks = model.networks, - modifier = Modifier.constrainAs(briefNetworksList) { - top.linkTo(title.bottom, margin = spacing6) - start.linkTo(icon.end, margin = spacing16) - end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16) - width = Dimension.fillToConstraints - }, - ) - - DetailedNetworksList( - isExpanded = isExpanded, - token = model, - networks = model.networks, - modifier = Modifier.constrainAs(detailedNetworksList) { - top.linkTo(anchor = availableNetworksText.bottom, margin = spacing16) - centerHorizontallyTo(parent) - width = Dimension.fillToConstraints - }, - ) - - ChangeNetworksViewButton( - isExpanded = isExpanded, - onClick = { isExpanded = !isExpanded }, - modifier = Modifier.constrainAs(changeNetworksViewButton) { - top.linkTo(parent.top) - end.linkTo(anchor = parent.end, margin = spacing6) - if (!isExpanded) bottom.linkTo(parent.bottom) - }, - ) - } -} - -@Composable -private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> Unit, modifier: Modifier = Modifier) { - val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size46.roundToPx() } - val iconModifier = modifier - .size(size = TangemTheme.dimens.size46) - .clip(TangemTheme.shapes.roundedCorners8) - - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - var isBackgroundColorDefined by remember { mutableStateOf(false) } - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() - val isDarkTheme = isSystemInDarkTheme() - val coroutineScope = rememberCoroutineScope() - - SubcomposeAsyncImage( - modifier = iconModifier, - model = ImageRequest.Builder(context = LocalContext.current) - .data(data = iconUrl) - .size(size = pixelsSize) - .memoryCacheKey(key = iconUrl + pixelsSize) - .crossfade(enable = true) - .allowHardware(false) - .listener( - onSuccess = { _, result -> - if (!isBackgroundColorDefined && isDarkTheme) { - coroutineScope.launch { - val color = ImageBackgroundContrastChecker( - drawable = result.drawable, - backgroundColor = itemBackgroundColor, - size = pixelsSize, - ).getContrastColor(isDarkTheme = true) - onContrastCalculate(color) - iconBackgroundColor = color - isBackgroundColorDefined = true - } - } - }, - ) - .build(), - contentDescription = null, - loading = { CurrencyPlaceholderIcon(id = name, modifier = iconModifier) }, - error = { CurrencyPlaceholderIcon(id = name, modifier = iconModifier) }, - ) -} - -@Composable -private fun Title(title: String, modifier: Modifier = Modifier) { - Text( - text = title, - modifier = modifier, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Start, - maxLines = 1, - style = TangemTheme.typography.body1, - ) -} - -@Composable -private fun Subtitle(isExpanded: Boolean, modifier: Modifier = Modifier) { - AnimatedVisibility( - visible = isExpanded, - modifier = modifier, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut(), - ) { - Text( - text = stringResource(id = R.string.currency_subtitle_expanded), - color = TangemTheme.colors.text.secondary, - maxLines = 1, - style = TangemTheme.typography.body2, - ) - } -} - -@Composable -private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { - IconButton(onClick = onClick, modifier = modifier.size(size = TangemTheme.dimens.size46)) { - AnimatedContent( - targetState = isExpanded, - transitionSpec = { (fadeIn() + scaleIn()).togetherWith(scaleOut() + fadeOut()) }, - ) { isExpanded -> - Icon( - painter = painterResource( - id = if (isExpanded) R.drawable.ic_arrow_extended else R.drawable.ic_arrow_collapsed, - ), - contentDescription = null, - tint = TangemTheme.colors.icon.secondary, - ) - } - } -} - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_TokenItem_ManageAccess() { - TangemThemePreview { - TokenItem(model = TokenListPreviewData.createManageToken()) - } -} - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_TokenItem_ReadAccess() { - TangemThemePreview { - TokenItem(model = TokenListPreviewData.createReadToken()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt deleted file mode 100644 index fafeb5d244..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.ui - -import androidx.compose.runtime.mutableStateOf -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState -import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState -import com.tangem.wallet.R -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -/** -[REDACTED_AUTHOR] - */ -object TokenListPreviewData { - - fun createManageToken(): TokenItemState.ManageContent { - return TokenItemState.ManageContent( - fullName = "Tether (USDT)", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", - networks = createManageNetworksList(), - composedId = "11231", - id = "", - name = "Tether", - symbol = "", - ) - } - - fun createReadToken(): TokenItemState.ReadContent { - return TokenItemState.ReadContent( - id = "1", - fullName = "Tether (USDT)", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", - networks = createReadNetworksList(), - composedId = "11231", - ) - } - - fun createManageNetworksList(): ImmutableList { - return persistentListOf( - NetworkItemState.ManageContent( - name = "Ethereum", - protocolName = "MAIN", - iconResId = mutableStateOf(R.drawable.ic_eth_16), - isMainNetwork = true, - isAdded = mutableStateOf(true), - id = "", - address = null, - onToggleClick = { _, _ -> }, - onNetworkClick = {}, - decimalCount = null, - blockchain = Blockchain.Ethereum, - ), - NetworkItemState.ManageContent( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_16), - isMainNetwork = false, - isAdded = mutableStateOf(false), - id = "", - address = null, - onToggleClick = { _, _ -> }, - onNetworkClick = {}, - decimalCount = null, - blockchain = Blockchain.BSC, - ), - ) - } - - fun createReadNetworksList(): ImmutableList { - return persistentListOf( - NetworkItemState.ReadContent( - name = "Ethereum", - protocolName = "MAIN", - iconResId = mutableStateOf(R.drawable.ic_eth_16), - isMainNetwork = true, - ), - NetworkItemState.ReadContent( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_16), - isMainNetwork = false, - ), - // check icon clipping - NetworkItemState.ReadContent( - name = "SHIBARIUM", - protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_shibarium_22), - isMainNetwork = false, - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt deleted file mode 100644 index d045437bb8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ /dev/null @@ -1,271 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.ui - -import android.content.res.Configuration -import androidx.activity.compose.BackHandler -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.FabPosition -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType -import androidx.compose.ui.unit.dp -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemContentType -import androidx.paging.compose.itemKey -import com.tangem.core.ui.components.BottomFade -import com.tangem.core.ui.components.NavigationBar3ButtonsScrim -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState -import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder -import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.wallet.R -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf - -/** - * Tokens list screen - * - * @param stateHolder state holder - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modifier = Modifier) { - BackHandler(onBack = stateHolder.toolbarState.onBackButtonClick) - - var floatingButtonHeight by remember { mutableStateOf(value = 0.dp) } - - Scaffold( - modifier = modifier, - topBar = { - TokensListToolbar(state = stateHolder.toolbarState) - }, - floatingActionButton = { - if (stateHolder is TokensListStateHolder.ManageContent) { - val density = LocalDensity.current - val verticalPadding = TangemTheme.dimens.spacing32 - - SaveChangesButton( - modifier = Modifier - .navigationBarsPadding() - .onSizeChanged { - with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding } - }, - showProgress = stateHolder.isSavingInProgress, - onClick = stateHolder.onSaveButtonClick, - ) - } - }, - floatingActionButtonPosition = FabPosition.Center, - backgroundColor = TangemTheme.colors.background.secondary, - ) { _ -> - val tokens = stateHolder.tokens.collectAsLazyPagingItems() - - if (stateHolder !is TokensListStateHolder.ManageContent) { - NavigationBar3ButtonsScrim() - } - - Box { - TokensListContent( - isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible, - tokens = tokens, - bottomMarginDp = floatingButtonHeight, - ) - - if (stateHolder is TokensListStateHolder.ManageContent) { - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - } - - Crossfade(targetState = stateHolder.isLoading, label = "Update progress bar visibility") { - if (it) { - LoadingContent() - } - } - - LaunchedEffect(key1 = tokens.loadState.refresh) { - stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh) - } - } -} - -@Composable -private fun LoadingContent() { - Box( - modifier = Modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.primary), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(color = TangemTheme.colors.icon.accent) - } -} - -@Composable -private fun TokensListContent( - isDifferentAddressesBlockVisible: Boolean, - tokens: LazyPagingItems, - bottomMarginDp: Dp, -) { - val state = rememberLazyListState() - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - LazyColumn( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .imePadding() - .fillMaxSize(), - state = state, - contentPadding = PaddingValues( - bottom = bottomMarginDp + bottomBarHeight, - ), - ) { - item( - key = "DifferentAddressesWarning$isDifferentAddressesBlockVisible", - contentType = "DifferentAddressesWarning$isDifferentAddressesBlockVisible", - ) { - if (isDifferentAddressesBlockVisible) DifferentAddressesWarning() - } - - tokens.itemKey(TokenItemState::composedId) - tokens.itemContentType(TokenItemState::composedId) - - items(items = tokens.itemSnapshotList.items, key = TokenItemState::composedId) { - TokenItem(model = it) - } - } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(key1 = state) { - snapshotFlow(state::isScrollInProgress) - .distinctUntilChanged() - .collectLatest { if (it) keyboardController?.hide() } - } -} - -@Composable -private fun DifferentAddressesWarning() { - Box( - modifier = Modifier - .padding(TangemTheme.dimens.spacing16) - .background( - color = TangemTheme.colors.button.disabled, - shape = RoundedCornerShape(TangemTheme.dimens.radius10), - ), - contentAlignment = Alignment.Center, - ) { - val text = stringResource(id = R.string.warning_manage_tokens_legacy_derivation_message) - Text( - text = text, - modifier = Modifier.padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing8, - ), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Start, - style = TangemTheme.typography.body2.copy( - letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), - lineHeight = TextUnit(value = 25f, type = TextUnitType.Sp), - ), - ) - } -} - -@Composable -private fun SaveChangesButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { - PrimaryButton( - modifier = modifier - .imePadding() - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource(id = R.string.common_save_changes), - showProgress = showProgress, - onClick = onClick, - ) -} - -@Preview(showSystemUi = true) -@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_TokensListScreen( - @PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder, -) { - TangemThemePreview { - TokensListScreen(stateHolder) - } -} - -private class TokensListScreenProvider : CollectionPreviewParameterProvider( - collection = listOf( - TokensListStateHolder.ReadContent( - toolbarState = TokensListToolbarState.Title.Manage( - titleResId = R.string.main_manage_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - onAddCustomTokenClick = {}, - ), - isLoading = true, - isDifferentAddressesBlockVisible = false, - tokens = emptyFlow(), - onTokensLoadStateChanged = {}, - ), - TokensListStateHolder.ManageContent( - toolbarState = TokensListToolbarState.Title.Manage( - titleResId = R.string.main_manage_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - onAddCustomTokenClick = {}, - ), - isLoading = false, - isDifferentAddressesBlockVisible = true, - tokens = flowOf( - PagingData.from( - listOf(TokenListPreviewData.createManageToken()), - ), - ), - onSaveButtonClick = {}, - onTokensLoadStateChanged = {}, - isSavingInProgress = false, - ), - TokensListStateHolder.ReadContent( - toolbarState = TokensListToolbarState.Title.Read( - titleResId = R.string.common_search_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - ), - isLoading = false, - isDifferentAddressesBlockVisible = false, - tokens = flowOf( - PagingData.from( - listOf(TokenListPreviewData.createManageToken()), - ), - ), - onTokensLoadStateChanged = {}, - ), - ), -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt deleted file mode 100644 index 0a4dd9c309..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt +++ /dev/null @@ -1,221 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.ui - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.InputField -import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.Title -import com.tangem.wallet.R -import kotlinx.coroutines.delay - -/** -[REDACTED_AUTHOR] - */ -@Composable -internal fun TokensListToolbar(state: TokensListToolbarState) { - val toolbarElevation = if (isSystemInDarkTheme()) { - TangemTheme.dimens.elevation0 - } else { - AppBarDefaults.TopAppBarElevation - } - val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } - - TopAppBar( - backgroundColor = TangemTheme.colors.background.secondary, - contentPadding = PaddingValues( - start = TangemTheme.dimens.spacing4, - end = TangemTheme.dimens.spacing4, - top = statusBarHeight, - ), - elevation = toolbarElevation, - ) { - IconButton(onClick = state.onBackButtonClick) { - Icon( - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = "Go back", - tint = TangemTheme.colors.icon.primary1, - ) - } - - when (state) { - is Title -> TitleContent(state = state, modifier = Modifier.weight(1f)) - is InputField -> InputContent(state = state, modifier = Modifier.weight(1f)) - } - } -} - -@Composable -private fun TitleContent(state: Title, modifier: Modifier = Modifier) { - Text( - text = stringResource(id = state.titleResId), - modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing26), - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.h3, - ) - - IconButton(onClick = state.onSearchButtonClick) { - Icon( - painter = painterResource(id = R.drawable.ic_search_24), - contentDescription = "Search", - tint = TangemTheme.colors.icon.primary1, - ) - } - - if (state is Title.Manage) { - IconButton(onClick = state.onAddCustomTokenClick) { - Icon( - painter = painterResource(id = R.drawable.ic_plus_24), - contentDescription = "Add custom token", - tint = TangemTheme.colors.icon.primary1, - ) - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun InputContent(state: InputField, modifier: Modifier = Modifier) { - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - - BasicTextField( - value = state.value, - onValueChange = state.onValueChange, - modifier = modifier.focusRequester(focusRequester), - textStyle = TangemTheme.typography.subtitle1.copy( - fontWeight = FontWeight.Normal, - color = TangemTheme.colors.text.primary1, - ), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), - keyboardActions = KeyboardActions(onSearch = { keyboardController?.hide() }), - singleLine = true, - maxLines = 1, - cursorBrush = SolidColor(value = TangemTheme.colors.stroke.secondary), - ) { innerTextField -> - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .weight(1f) - .padding( - start = TangemTheme.dimens.spacing28, - top = TangemTheme.dimens.spacing2, - end = TangemTheme.dimens.spacing16, - ), - ) { - Hint(value = state.value) - innerTextField() - } - - IconButton(onClick = state.onCleanButtonClick) { - Icon( - painter = painterResource(id = R.drawable.ic_cross_rounded_24), - contentDescription = "Clean entered value", - tint = TangemTheme.colors.icon.secondary, - ) - } - } - } - - // This is a hack because Compose can't show the keyboard if the input field is not found. - // So first I request focus and make a delay and then show the keyboard. - val keyboardManager = LocalSoftwareKeyboardController.current - LaunchedEffect(keyboardManager) { - focusRequester.requestFocus() - delay(timeMillis = 100) - keyboardManager?.show() - } -} - -@Composable -private fun Hint(value: String) { - if (value.isBlank()) { - Row( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_search_24), - contentDescription = null, - tint = TangemTheme.colors.icon.secondary, - ) - Text( - text = stringResource(id = R.string.common_search), - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing2), - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - // FIXME("Incorrect typography. Replace with typography from design system") - style = TangemTheme.typography.subtitle1.copy(fontSize = 18.sp, fontWeight = FontWeight.Normal), - ) - } - } -} - -@Preview -@Composable -private fun Preview_AddTokensToolbar_EditAccess() { - TangemThemePreview { - TokensListToolbar( - state = Title.Manage( - titleResId = R.string.main_manage_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - onAddCustomTokenClick = {}, - ), - ) - } -} - -@Preview -@Composable -private fun Preview_AddTokensToolbar_ReadAccess() { - TangemThemePreview { - TokensListToolbar( - state = Title.Read( - titleResId = R.string.common_search_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - ), - ) - } -} - -@Preview -@Composable -private fun Preview_AddTokensToolbar_SearchInputField() { - var value by remember { mutableStateOf("") } - - TangemThemePreview { - TokensListToolbar( - state = InputField( - onBackButtonClick = {}, - value = value, - onValueChange = { value = it }, - onCleanButtonClick = { value = "" }, - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListAnalyticsSender.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListAnalyticsSender.kt deleted file mode 100644 index 95d0e9371e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListAnalyticsSender.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.viewmodels - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.ManageTokens - -/** Analytics sender for tokens list screen */ -class TokensListAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) { - - fun sendWhenScreenOpened() { - analyticsEventHandler.send(event = ManageTokens.ScreenOpened()) - } - - fun sendWhenTokenAdded(token: Token) { - analyticsEventHandler.send( - event = ManageTokens.TokenSwitcherChanged( - type = AnalyticsParam.CurrencyType.Token(token), - state = AnalyticsParam.OnOffState.On, - ), - ) - } - - fun sendWhenBlockchainAdded(blockchain: Blockchain) { - analyticsEventHandler.send( - event = ManageTokens.TokenSwitcherChanged( - type = AnalyticsParam.CurrencyType.Blockchain(blockchain), - state = AnalyticsParam.OnOffState.On, - ), - ) - } - - fun sendWhenTokenRemoved(token: Token) { - analyticsEventHandler.send( - event = ManageTokens.TokenSwitcherChanged( - type = AnalyticsParam.CurrencyType.Token(token), - state = AnalyticsParam.OnOffState.Off, - ), - ) - } - - fun sendWhenBlockchainRemoved(blockchain: Blockchain) { - analyticsEventHandler.send( - event = ManageTokens.TokenSwitcherChanged( - type = AnalyticsParam.CurrencyType.Blockchain(blockchain), - state = AnalyticsParam.OnOffState.Off, - ), - ) - } - - fun sendWhenSaveButtonClicked() { - analyticsEventHandler.send(ManageTokens.ButtonSaveChanges()) - } - - fun sendWhenTokenSearched() { - analyticsEventHandler.send(ManageTokens.TokenSearched()) - } - - fun sendWhenAddCustomTokenClicked() { - analyticsEventHandler.send(ManageTokens.ButtonCustomToken()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt deleted file mode 100644 index da83face06..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.viewmodels - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.tokens.TokenWithBlockchain - -internal data class TokensListCryptoCurrencies( - val coins: List, - val tokens: List, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt deleted file mode 100644 index 0cf70b6b4f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.viewmodels - -import arrow.core.Either -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.common.routing.AppRoute -import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.TokenWithBlockchain -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.dispatchDebugErrorNotification -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import timber.log.Timber -import kotlin.properties.Delegates - -/** - * Class that divide a new and legacy logic when user uses tokens list screen - * - * @property getSelectedWalletSyncUseCase use case that returns selected wallet - * @property getCurrenciesUseCase use case that returns crypto currencies of a specified wallet - */ -internal class TokensListMigration( - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, -) { - - private var currentNewCoins: List by Delegates.notNull() - private var currentNewTokens: List by Delegates.notNull() - private var currentUserWallet: UserWallet by Delegates.notNull() - - private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } - - suspend fun getCurrentCryptoCurrencies(): TokensListCryptoCurrencies { - return when (val selectedWalletEither = getSelectedWalletSyncUseCase()) { - is Either.Left -> { - Timber.e(selectedWalletEither.value.toString()) - TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) - } - is Either.Right -> { - currentUserWallet = selectedWalletEither.value - - when ( - val currenciesEither = getCurrenciesUseCase.getSync( - userWalletId = selectedWalletEither.value.walletId, - ) - ) { - is Either.Left -> { - Timber.e(currenciesEither.value.toString()) - TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) - } - is Either.Right -> { - TokensListCryptoCurrencies( - coins = currenciesEither.value - .filterIsInstance() - .filterNot { it.isCustom } - .also { currentNewCoins = it } - .map { Blockchain.fromId(it.network.id.value) }, - tokens = currenciesEither.value - .filterIsInstance() - .filterNot(CryptoCurrency.Token::isCustom) - .also { currentNewTokens = it } - .map { token -> - TokenWithBlockchain( - token = Token( - name = token.name, - symbol = token.symbol, - contractAddress = token.contractAddress, - decimals = token.decimals, - id = token.id.rawCurrencyId, - ), - blockchain = Blockchain.fromId(token.network.id.value), - ) - }, - ) - } - } - } - } - } - - suspend fun onSaveButtonClick( - changedTokensList: MutableList, - changedBlockchainList: List, - ) { - val changedTokens = changedTokensList.mapNotNull { - cryptoCurrencyFactory.createToken( - sdkToken = it.token, - blockchain = it.blockchain, - extraDerivationPath = null, - derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, - ) - } - - val changedCoins = changedBlockchainList.mapNotNull { - cryptoCurrencyFactory.createCoin( - blockchain = it, - extraDerivationPath = null, - derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, - ) - } - - val blockchainsToAdd = changedCoins.filterNot(currentNewCoins::contains) - val blockchainsToRemove = currentNewCoins.filterNot(changedCoins::contains) - - val tokensToAdd = changedTokens.filterNot(currentNewTokens::contains) - val tokensToRemove = currentNewTokens.filterNot { token -> changedTokens.any { it == token } } - - removeCurrenciesIfNeeded( - userWalletId = currentUserWallet.walletId, - currencies = blockchainsToRemove + tokensToRemove, - ) - - val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() - val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() - if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { - store.dispatchDebugErrorNotification(message = "Nothing to save") - navigateToWallet() - return - } - - val currencyList = blockchainsToAdd + tokensToAdd - - derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList) - .onRight { - addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList) - navigateToWallet() - } - .onLeft { Timber.e(it, "Failed to derive public keys") } - } - - private fun navigateToWallet() { - store.dispatchNavigationAction { - val route = AppRoute.Wallet - - if (route in stack) { - popTo(route) - } else { - replaceAll(route) - } - } - } - - private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List) { - if (currencies.isEmpty()) return - val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository) - val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade) - - currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies) - - walletManagersFacade.remove( - userWalletId = userWalletId, - networks = currencies - .filterIsInstance() - .mapTo(hashSetOf(), CryptoCurrency::network), - ) - walletManagersFacade.removeTokens( - userWalletId = userWalletId, - tokens = currencies.filterIsInstance().toSet(), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt deleted file mode 100644 index 736278526e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ /dev/null @@ -1,496 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.viewmodels - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.* -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.getGreyedOutIconRes -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation -import com.tangem.domain.common.extensions.canHandleBlockchain -import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.extensions.supportedTokens -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.TokenWithBlockchain -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.networkNameWithoutTestnet -import com.tangem.tap.common.extensions.getNetworkStandardName -import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType -import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor -import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.features.tokens.impl.domain.models.Token.Network -import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState -import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter -import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState -import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState -import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder -import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.tap.store -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import com.tangem.utils.coroutines.Debouncer -import com.tangem.wallet.R -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import timber.log.Timber -import javax.inject.Inject -import kotlin.properties.Delegates -import com.tangem.blockchain.common.Token as BlockchainToken - -/** - * ViewModel for tokens list screen - * - * @property interactor feature interactor - * @property router feature router - * @property dispatchers coroutine dispatchers provider - * @property getSelectedWalletSyncUseCase use case that returns selected wallet - * @param analyticsEventHandler analytics event handler - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -@HiltViewModel -internal class TokensListViewModel @Inject constructor( - private val interactor: TokensListInteractor, - private val router: TokensListRouter, - private val dispatchers: AppCoroutineDispatcherProvider, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - derivePublicKeysUseCase: DerivePublicKeysUseCase, - addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - analyticsEventHandler: AnalyticsEventHandler, - getCurrenciesUseCase: GetCryptoCurrenciesUseCase, -) : ViewModel(), DefaultLifecycleObserver { - - private val isManageAccess = store.state.tokensState.isManageAccess - private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler) - private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer()) - - /** Screen state */ - var uiState by mutableStateOf(value = getInitialUiState()) - private set - - private var currentTokensList: List by Delegates.notNull() - private var currentBlockchainList: List by Delegates.notNull() - - private var changedTokensList: MutableList = mutableListOf() - private var changedBlockchainList: MutableList = mutableListOf() - - private val tokensListMigration = TokensListMigration( - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - getCurrenciesUseCase = getCurrenciesUseCase, - derivePublicKeysUseCase = derivePublicKeysUseCase, - addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase, - ) - - init { - viewModelScope.launch(dispatchers.main) { - val (currentCoins, currentTokens) = tokensListMigration.getCurrentCryptoCurrencies() - - currentBlockchainList = currentCoins - currentTokensList = currentTokens - - changedBlockchainList = currentCoins.toMutableList() - changedTokensList = currentTokens.toMutableList() - - uiState = uiState.copySealed(tokens = getTokensListBySearchText(text = "")) - } - } - - override fun onCreate(owner: LifecycleOwner) { - if (isManageAccess) analyticsSender.sendWhenScreenOpened() - } - - private fun getInitialUiState(): TokensListStateHolder { - return if (isManageAccess) { - TokensListStateHolder.ManageContent( - toolbarState = getInitialToolbarState(), - isLoading = true, - isDifferentAddressesBlockVisible = isDifferentAddressesBlockVisible(), - tokens = getInitialTokensList(), - onTokensLoadStateChanged = actionsHandler::onTokensLoadStateChanged, - isSavingInProgress = false, - onSaveButtonClick = actionsHandler::onSaveButtonClick, - ) - } else { - TokensListStateHolder.ReadContent( - toolbarState = getInitialToolbarState(), - isLoading = true, - isDifferentAddressesBlockVisible = isDifferentAddressesBlockVisible(), - tokens = getInitialTokensList(), - onTokensLoadStateChanged = actionsHandler::onTokensLoadStateChanged, - ) - } - } - - private fun getInitialToolbarState(): TokensListToolbarState { - return if (isManageAccess) { - TokensListToolbarState.Title.Manage( - titleResId = R.string.add_tokens_title, - onBackButtonClick = actionsHandler::onBackButtonClick, - onSearchButtonClick = actionsHandler::onSearchButtonClick, - onAddCustomTokenClick = actionsHandler::onAddCustomTokenClick, - ) - } else { - TokensListToolbarState.Title.Read( - titleResId = R.string.common_search_tokens, - onBackButtonClick = actionsHandler::onBackButtonClick, - onSearchButtonClick = actionsHandler::onSearchButtonClick, - ) - } - } - - private fun isDifferentAddressesBlockVisible(): Boolean { - return getSelectedWalletSyncUseCase().fold( - ifLeft = { false }, - ifRight = { it.scanResponse.card.useOldStyleDerivation }, - ) - } - - private fun getInitialTokensList(): Flow> { - return flowOf( - value = PagingData.empty( - sourceLoadStates = LoadStates(LoadState.Loading, LoadState.Loading, LoadState.Loading), - mediatorLoadStates = LoadStates(LoadState.Loading, LoadState.Loading, LoadState.Loading), - ), - ) - } - - private fun createManageTokenContent(token: Token): TokenItemState.ManageContent { - val currentTime = System.nanoTime() - return TokenItemState.ManageContent( - fullName = getTokenFullName(token), - iconUrl = token.iconUrl, - networks = token.networks.map(::createManageNetworkContent).toImmutableList(), - composedId = token.id + currentTime, - id = token.id, - name = token.name, - symbol = token.symbol, - ) - } - - private fun createManageNetworkContent(network: Network): NetworkItemState.ManageContent { - return NetworkItemState.ManageContent( - name = network.blockchain.networkNameWithoutTestnet.uppercase(), - protocolName = getNetworkProtocolName(network), - iconResId = mutableIntStateOf(value = getNetworkIconResId(network.address, network.blockchain)), - isMainNetwork = isMainNetwork(network), - isAdded = mutableStateOf( - isAdded(address = network.address, blockchain = network.blockchain), - ), - id = network.id, - address = network.address, - decimalCount = network.decimalCount, - blockchain = network.blockchain, - onToggleClick = actionsHandler::onToggleClick, - onNetworkClick = actionsHandler::onNetworkClick, - ) - } - - private fun createReadTokenContent(token: Token): TokenItemState.ReadContent { - val currentTime = System.nanoTime() - return TokenItemState.ReadContent( - id = token.id, - fullName = getTokenFullName(token), - iconUrl = token.iconUrl, - networks = token.networks.map(::createReadNetworkContent).toImmutableList(), - composedId = token.id + currentTime, - ) - } - - private fun createReadNetworkContent(network: Network): NetworkItemState.ReadContent { - return NetworkItemState.ReadContent( - name = network.blockchain.networkNameWithoutTestnet.uppercase(), - protocolName = getNetworkProtocolName(network), - iconResId = mutableIntStateOf(value = getNetworkIconResId(network.address, network.blockchain)), - isMainNetwork = isMainNetwork(network), - ) - } - - private fun getTokenFullName(token: Token) = "${token.name} (${token.symbol})" - - private fun getNetworkProtocolName(network: Network): String { - return if (network.address == null) { - MAIN_NETWORK_LABEL - } else { - network.blockchain.getNetworkStandardName().uppercase() - } - } - - private fun getNetworkIconResId(address: String?, blockchain: Blockchain): Int { - return if (isAdded(address = address, blockchain = blockchain)) { - getActiveIconRes(blockchain.id) - } else { - getGreyedOutIconRes(blockchain.id) - } - } - - private fun isMainNetwork(network: Network) = network.address == null - - private fun isAdded(address: String?, blockchain: Blockchain?): Boolean { - return if (address != null) { - changedTokensList.any { addedToken -> - address == addedToken.token.contractAddress && blockchain == addedToken.blockchain - } - } else { - changedBlockchainList.contains(blockchain) - } - } - - private fun getTokensListBySearchText(text: String): Flow> { - return interactor.getTokensList(searchText = text, needFilterExcluded = !isManageAccess) - .mapToTokenItemState() - .cachedIn(viewModelScope) - } - - private fun Flow>.mapToTokenItemState(): Flow> { - return map { pagingData -> - pagingData.map { token -> - if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) - } - } - } - - private inner class ActionsHandler( - private val router: TokensListRouter, - private val debouncer: Debouncer, - ) { - - fun onBackButtonClick() { - router.popBackStack() - } - - fun onSearchButtonClick() { - uiState = uiState.copySealed( - toolbarState = TokensListToolbarState.InputField( - onBackButtonClick = this::onBackButtonClick, - value = "", - onValueChange = this::onSearchValueChange, - onCleanButtonClick = this::onCleanButtonClick, - ), - ) - } - - fun onAddCustomTokenClick() { - analyticsSender.sendWhenAddCustomTokenClicked() - router.openAddCustomTokenScreen() - } - - fun onToggleClick(toggledToken: TokenItemState.ManageContent, toggledNetwork: NetworkItemState.ManageContent) { - val blockchain = Blockchain.fromNetworkId( - networkId = if (toggledNetwork.address == null) toggledNetwork.id else toggledToken.id, - ) - if (toggledNetwork.address == null && blockchain != null) { - updateBlockchainItem(toggledNetwork, blockchain) - } else { - updateTokenItem(toggledToken, toggledNetwork) - } - } - - fun onNetworkClick() { - router.showAddressCopiedNotification() - } - - fun onTokensLoadStateChanged(state: LoadState) { - uiState = uiState.copySealed(isLoading = state is LoadState.Loading) - } - - fun onSaveButtonClick() { - val state = uiState as? TokensListStateHolder.ManageContent ?: return - - analyticsSender.sendWhenSaveButtonClicked() - - viewModelScope.launch(dispatchers.main) { - uiState = state.copy(isSavingInProgress = true) - - tokensListMigration.onSaveButtonClick( - changedTokensList = changedTokensList, - changedBlockchainList = changedBlockchainList, - ) - - uiState = state.copy(isSavingInProgress = false) - } - } - - private fun onSearchValueChange(newValue: String) { - val state = requireNotNull(uiState.toolbarState as? TokensListToolbarState.InputField) - uiState = uiState.copySealed(toolbarState = state.copy(value = newValue)) - - debouncer.debounce(waitMs = 800L, coroutineScope = viewModelScope + dispatchers.io) { - uiState = uiState.copySealed(tokens = getSearchedTokensList(newValue)) - } - } - - private fun getSearchedTokensList(searchText: String): Flow> { - analyticsSender.sendWhenTokenSearched() - return getTokensListBySearchText(text = searchText) - } - - private fun onCleanButtonClick() { - val state = requireNotNull(uiState.toolbarState as? TokensListToolbarState.InputField) - - if (state.value.isEmpty()) { - uiState = uiState.copySealed(toolbarState = getInitialToolbarState()) - } else { - onSearchValueChange(newValue = "") - } - } - - private fun updateBlockchainItem(toggledNetwork: NetworkItemState.ManageContent, blockchain: Blockchain) { - val isRemoveAction = changedBlockchainList.contains(blockchain) - - if (isRemoveAction) { - val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain } - val isAddedOnMainScreen = currentBlockchainList.contains(blockchain) - - if (isTokenWithSameBlockchainFound) { - router.openUnableHideMainTokenAlert( - tokenName = blockchain.name, - tokenSymbol = blockchain.currency, - networkName = blockchain.getNetworkName(), - ) - } else if (isAddedOnMainScreen) { - router.openRemoveWalletAlert( - tokenName = blockchain.name, - onOkClick = { - analyticsSender.sendWhenBlockchainAdded(blockchain) - changedBlockchainList.remove(blockchain) - toggledNetwork.changeToggleState() - }, - ) - } else { - analyticsSender.sendWhenBlockchainRemoved(blockchain) - changedBlockchainList.remove(blockchain) - toggledNetwork.changeToggleState() - } - } else { - when (getSupportBlockchainType(blockchain)) { - SupportBlockchainType.SUPPORTED -> { - analyticsSender.sendWhenBlockchainAdded(blockchain) - changedBlockchainList.add(blockchain) - toggledNetwork.changeToggleState() - } - SupportBlockchainType.UNSUPPORTED -> { - router.openUnsupportedNetworkAlert(blockchain) - } - SupportBlockchainType.UNABLE_TO_DETERMINE -> { - router.showGenericErrorAlertAndPopBack() - } - } - } - } - - private fun updateTokenItem( - toggledToken: TokenItemState.ManageContent, - toggledNetwork: NetworkItemState.ManageContent, - ) { - val token = TokenWithBlockchain( - token = BlockchainToken( - id = toggledToken.id, - name = toggledToken.name, - symbol = toggledToken.symbol, - contractAddress = requireNotNull(toggledNetwork.address), - decimals = requireNotNull(toggledNetwork.decimalCount), - ), - blockchain = toggledNetwork.blockchain, - ) - - val isRemoveAction = changedTokensList.contains(token) - - if (isRemoveAction) { - val isAddedOnMainScreen = currentTokensList.contains(token) - - if (isAddedOnMainScreen) { - router.openRemoveWalletAlert( - tokenName = token.token.name, - onOkClick = { - analyticsSender.sendWhenTokenRemoved(token.token) - changedTokensList.remove(token) - toggledNetwork.changeToggleState() - }, - ) - } else { - analyticsSender.sendWhenTokenRemoved(token.token) - changedTokensList.remove(token) - toggledNetwork.changeToggleState() - } - } else { - when (isUnsupportedToken(token.blockchain)) { - SupportTokensState.NetworkTokensUnsupported -> { - router.openNetworkTokensNotSupportAlert(token.blockchain.getNetworkName()) - } - SupportTokensState.SupportedToken -> { - analyticsSender.sendWhenTokenAdded(token.token) - changedTokensList.add(token) - toggledNetwork.changeToggleState() - } - SupportTokensState.UnsupportedCurve -> { - router.openUnsupportedNetworkAlert(token.blockchain) - } - null -> { - Timber.e("Something went wrong in isUnsupportedToken (no scanResponse found)") - } - } - } - } - } - - private fun isUnsupportedToken(blockchain: Blockchain): SupportTokensState? { - return getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { - val cardTypesResolver = it.scanResponse.cardTypesResolver - val supportedTokens = it.scanResponse.card.supportedTokens(cardTypesResolver) - - // refactor this later by moving all this logic in card config - if (blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana)) { - return SupportTokensState.NetworkTokensUnsupported - } - val canHandleToken = it.scanResponse.card.canHandleToken( - supportedTokens = supportedTokens, - blockchain = blockchain, - cardTypesResolver = cardTypesResolver, - ) - if (!canHandleToken) { - return SupportTokensState.UnsupportedCurve - } - return SupportTokensState.SupportedToken - }, - ) - } - - private fun getSupportBlockchainType(blockchain: Blockchain): SupportBlockchainType { - return getSelectedWalletSyncUseCase().fold( - ifLeft = { SupportBlockchainType.UNABLE_TO_DETERMINE }, - ifRight = { - val canHandleBlockchain = it.scanResponse.card.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = it.scanResponse.cardTypesResolver, - ) - if (canHandleBlockchain) { - SupportBlockchainType.SUPPORTED - } else { - SupportBlockchainType.UNSUPPORTED - } - }, - ) - } - - private companion object { - const val MAIN_NETWORK_LABEL = "MAIN" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt deleted file mode 100644 index 389a02219a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.tap.features.tokens.legacy.redux - -import com.tangem.domain.tokens.TokensAction -import com.tangem.tap.common.redux.AppState -import org.rekotlin.Action - -object TokensReducer { - fun reduce(action: Action, state: AppState): TokensState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, state: AppState): TokensState { - if (action !is TokensAction) return state.tokensState - - return when (action) { - is TokensAction.SetArgs.ManageAccess -> state.tokensState.copy(isManageAccess = true) - is TokensAction.SetArgs.ReadAccess -> state.tokensState.copy(isManageAccess = false) - else -> state.tokensState - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt deleted file mode 100644 index 14c5d9d721..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.tap.features.tokens.legacy.redux - -import org.rekotlin.StateType - -data class TokensState(val isManageAccess: Boolean = false) : StateType \ No newline at end of file