Updated on 2026-08-14
This commit is contained in:
parent
738d08802e
commit
3b5b923cc2
36 changed files with 0 additions and 3007 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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 = "" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.redux
|
||||
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class TokensState(val isManageAccess: Boolean = false) : StateType
|
||||
Loading…
Add table
Add a link
Reference in a new issue