Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-25 18:34:28 +04:00
parent 738d08802e
commit 3b5b923cc2
36 changed files with 0 additions and 3007 deletions

View file

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

View file

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

View file

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

View file

@ -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<PagingData<Token>> {
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
}
}

View file

@ -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<Int, Token>() {
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, Token>): 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<Int>): LoadResult<Int, Token> {
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) },
)
}
}

View file

@ -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<Int, Token>() {
override fun getRefreshKey(state: PagingState<Int, Token>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Token> {
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)
},
)
}
}

View file

@ -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<CoinsResponse, List<Token>> {
override fun convert(value: CoinsResponse): List<Token> {
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/"
}
}

View file

@ -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<TestnetTokensConfig, List<Token>> {
private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded = false)
override fun convert(value: TestnetTokensConfig): List<Token> {
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(),
)
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<PagingData<Token>> {
return repository.getAvailableTokens(
searchText = searchText.ifBlank(defaultValue = { null }),
needFilterExcluded = needFilterExcluded,
)
}
}

View file

@ -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<PagingData<Token>>
}

View file

@ -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<PagingData<Token>>
}

View file

@ -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<Network>,
) {
/**
* 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?,
)
}

View file

@ -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<TokensListViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(this)
}
TokensListScreen(
stateHolder = viewModel.uiState,
)
}
}

View file

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

View file

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

View file

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

View file

@ -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<Int>
/** 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<Int>,
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<Int>,
override val isMainNetwork: Boolean,
val isAdded: MutableState<Boolean>,
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)
}
}
}

View file

@ -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<NetworkItemState>
/** 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<NetworkItemState.ReadContent>,
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<NetworkItemState.ManageContent>,
override val composedId: String,
val name: String,
val symbol: String,
) : TokenItemState
}

View file

@ -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<PagingData<TokenItemState>>
/** 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<PagingData<TokenItemState>> = 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<PagingData<TokenItemState>>,
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<PagingData<TokenItemState>>,
override val onTokensLoadStateChanged: (LoadState) -> Unit,
val onSaveButtonClick: () -> Unit,
val isSavingInProgress: Boolean,
) : TokensListStateHolder
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<NetworkItemState.ManageContent> {
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<NetworkItemState.ReadContent> {
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,
),
)
}
}

View file

@ -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<TokenItemState>,
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<TokensListStateHolder>(
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 = {},
),
),
)

View file

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

View file

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

View file

@ -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<Blockchain>,
val tokens: List<TokenWithBlockchain>,
)

View file

@ -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<CryptoCurrency.Coin> by Delegates.notNull()
private var currentNewTokens: List<CryptoCurrency.Token> 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<CryptoCurrency.Coin>()
.filterNot { it.isCustom }
.also { currentNewCoins = it }
.map { Blockchain.fromId(it.network.id.value) },
tokens = currenciesEither.value
.filterIsInstance<CryptoCurrency.Token>()
.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<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
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<CryptoCurrency>) {
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<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -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<TokenWithBlockchain> by Delegates.notNull()
private var currentBlockchainList: List<Blockchain> by Delegates.notNull()
private var changedTokensList: MutableList<TokenWithBlockchain> = mutableListOf()
private var changedBlockchainList: MutableList<Blockchain> = 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<PagingData<TokenItemState>> {
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<PagingData<TokenItemState>> {
return interactor.getTokensList(searchText = text, needFilterExcluded = !isManageAccess)
.mapToTokenItemState()
.cachedIn(viewModelScope)
}
private fun Flow<PagingData<Token>>.mapToTokenItemState(): Flow<PagingData<TokenItemState>> {
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<PagingData<TokenItemState>> {
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"
}
}

View file

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

View file

@ -1,5 +0,0 @@
package com.tangem.tap.features.tokens.legacy.redux
import org.rekotlin.StateType
data class TokensState(val isManageAccess: Boolean = false) : StateType