Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-16 21:04:10 +08:00
parent a49ecbcabb
commit 855ee45378
30 changed files with 112 additions and 1440 deletions

View file

@ -12,6 +12,7 @@ import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
@ -20,7 +21,6 @@ import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
@ -51,7 +51,6 @@ import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.BuildConfig
@ -128,9 +127,6 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var networkConnectionManager: NetworkConnectionManager
@Inject
lateinit var tokensListFeatureToggles: TokensListFeatureToggles
@Inject
lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
@ -149,7 +145,6 @@ class TapApplication : Application(), ImageLoaderFactory {
daggerGraphState = DaggerGraphState(
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
tokensListFeatureToggles = tokensListFeatureToggles,
customTokenFeatureToggles = customTokenFeatureToggles,
),
),

View file

@ -29,7 +29,6 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.tokens.legacy.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
@ -148,12 +147,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::tokensListFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) TokensListFragment() else AddTokensFragment()
}
AppScreen.AddTokens -> TokensListFragment()
AppScreen.AddCustomToken -> {
val featureToggles = store.state.daggerGraphState.get(

View file

@ -101,7 +101,7 @@ data class AppState(
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
TokensMiddleware.tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,

View file

@ -91,8 +91,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
onSearchTokensClick = {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies())
store.dispatch(TokensAction.SetArgs.ReadAccess)
},
)
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.features.tokens.api.featuretoggles
/**
* TokensList feature toggles
*
[REDACTED_AUTHOR]
*/
interface TokensListFeatureToggles {
/** Availability of redesigned screen (internal feature) */
val isRedesignedScreenEnabled: Boolean
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.tokens.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
import com.tangem.tap.features.tokens.impl.featuretoggles.DefaultTokensListFeatureToggles
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 TokensListFeatureTogglesModule {
@Provides
@Singleton
fun providesTokensListFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokensListFeatureToggles {
return DefaultTokensListFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.features.tokens.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
/**
* Default implementation of TokensList feature toggles
*
* @property featureTogglesManager manager for getting information about the availability of feature toggles
*
[REDACTED_AUTHOR]
*/
internal class DefaultTokensListFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensListFeatureToggles {
override val isRedesignedScreenEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_TOKEN_LIST_SCREEN_ENABLED")
}

View file

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
@ -35,7 +36,9 @@ internal class TokensListFragment : Fragment() {
setContent {
isTransitionGroup = true
val viewModel = hiltViewModel<TokensListViewModel>()
val viewModel = hiltViewModel<TokensListViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(this)
}
TangemTheme {
TokensListScreen(stateHolder = viewModel.uiState)

View file

@ -12,7 +12,7 @@ import com.tangem.tap.store
*/
class TokensListArgs {
/** Tokens list screen mode */
val isManageAccess: Boolean get() = store.state.tokensState.allowToAdd
val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess
/** Tokens list that accessible from the main screen */
val mainScreenTokenList: List<TokenWithBlockchain> get() = store.state.tokensState.addedTokens

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.MutableState
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
/**
* Network item state
@ -62,7 +61,7 @@ sealed interface NetworkItemState {
override val isMainNetwork: Boolean,
val isAdded: MutableState<Boolean>,
val id: String,
val address: ContractAddress?,
val address: String?,
val decimalCount: Int?,
val blockchain: Blockchain,
val onToggleClick: (TokenItemState.ManageContent, ManageContent) -> Unit,

View file

@ -9,6 +9,10 @@ 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(

View file

@ -3,6 +3,8 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import androidx.compose.runtime.getValue
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.LoadState
@ -59,7 +61,7 @@ internal class TokensListViewModel @Inject constructor(
private val dispatchers: AppCoroutineDispatcherProvider,
private val reduxStateHolder: AppStateHolder,
analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel() {
) : ViewModel(), DefaultLifecycleObserver {
private val args = TokensListArgs()
private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler)
@ -72,6 +74,10 @@ internal class TokensListViewModel @Inject constructor(
private val changedTokensList: MutableList<TokenWithBlockchain> = args.mainScreenTokenList.toMutableList()
private val changedBlockchainList: MutableList<Blockchain> = args.mainScreenBlockchainList.toMutableList()
override fun onCreate(owner: LifecycleOwner) {
if (args.isManageAccess) analyticsSender.sendWhenScreenOpened()
}
private fun getInitialUiState(): TokensListStateHolder {
return if (args.isManageAccess) {
TokensListStateHolder.ManageContent(

View file

@ -1,174 +0,0 @@
package com.tangem.tap.features.tokens.legacy
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.tokens.legacy.ui.compose.CurrenciesScreen
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentAddTokensBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.rekotlin.StoreSubscriber
@AndroidEntryPoint
internal class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber<TokensState> {
private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind)
private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
private var searchInput = mutableStateOf("")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
addBackPressHandler(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
toolbar.setNavigationOnClickListener { handleOnBackPressed() }
val onSaveChanges = { tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain> ->
Analytics.send(ManageTokens.ButtonSaveChanges())
store.dispatch(TokensAction.SaveChanges(tokens, blockchains))
}
val onNetworkItemClicked = { contractAddress: ContractAddress ->
context?.copyToClipboard(contractAddress)
store.dispatchNotification(R.string.contract_address_copied_message)
}
val onLoadMore = {
store.dispatch(TokensAction.LoadMore(scanResponse = store.state.globalState.scanResponse))
}
cvCurrencies.setContent {
TangemTheme {
CurrenciesScreen(
tokensState = tokensState,
onSaveChanges = onSaveChanges,
onNetworkItemClick = onNetworkItemClicked,
onLoadMore = onLoadMore,
)
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state
.skipRepeats { oldState, newState -> oldState.tokensState == newState.tokensState }
.select { it.tokensState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onDestroy() {
store.dispatch(TokensAction.ResetState)
super.onDestroy()
}
@Suppress("MagicNumber")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_popular_tokens, menu)
val addCustomTokenAllowed = store.state.tokensState.allowToAdd
menu.findItem(R.id.menu_navigate_add_custom_token).isVisible = addCustomTokenAllowed
val menuItem = menu.findItem(R.id.menu_search)
val searchView: SearchView = menuItem.actionView as SearchView
searchView.queryHint = searchView.getString(R.string.common_search)
searchView.maxWidth = android.R.attr.width
searchView.inputtedTextAsFlow()
.debounce(800)
.distinctUntilChanged()
.onEach {
searchInput.value = it.lowercase()
dispatchOnMain(TokensAction.SetSearchInput(searchInput.value))
}
.launchIn(mainScope)
return super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_search -> true
R.id.menu_navigate_add_custom_token -> {
Analytics.send(ManageTokens.ButtonCustomToken())
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun configureTransitions() {
super.configureTransitions()
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
override fun newState(state: TokensState) {
if (activity == null || view == null) return
val toolbarTitle =
if (state.allowToAdd) R.string.main_manage_tokens else R.string.search_tokens_title
tokensState.value = state
binding.toolbar.title = getString(toolbarTitle)
}
private fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
awaitClose { watcher }
}
}

View file

@ -2,37 +2,22 @@ package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.tokens.Currency
import org.rekotlin.Action
sealed class TokensAction : Action {
sealed interface TokensAction : Action {
object ResetState : TokensAction()
/** Single way to pass data to the screen */
sealed interface SetArgs : TokensAction {
data class AllowToAddTokens(val allow: Boolean) : TokensAction()
data class ManageAccess(val wallets: List<WalletDataModel>, val derivationStyle: DerivationStyle?) : SetArgs
data class LoadCurrencies(val scanResponse: ScanResponse? = null) : TokensAction() {
data class Success(val currencies: List<Currency>, val loadMore: Boolean) : TokensAction()
object Failure : TokensAction()
object ReadAccess : SetArgs
}
data class LoadMore(
val scanResponse: ScanResponse? = null,
) : TokensAction()
// TODO: [REDACTED_TASK_KEY] Remove this action
data class SaveChanges(val tokens: List<TokenWithBlockchain>, val blockchains: List<Blockchain>) : TokensAction
data class SetAddedCurrencies(
val wallets: List<WalletDataModel>,
val derivationStyle: DerivationStyle?,
) : TokensAction()
data class SaveChanges(
val addedTokens: List<TokenWithBlockchain>,
val addedBlockchains: List<Blockchain>,
) : TokensAction()
object PrepareAndNavigateToAddCustomToken : TokensAction()
data class SetSearchInput(val searchInput: String) : TokensAction()
// TODO: Remove this action in 4.7 release
object PrepareAndNavigateToAddCustomToken : TokensAction
}

View file

@ -8,13 +8,10 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.features.addCustomToken.CustomCurrency
@ -32,132 +29,76 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("LargeClass")
class TokensMiddleware {
object TokensMiddleware {
val tokensMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action.scanResponse)
is TokensAction.SaveChanges -> handleSaveChanges(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken()
is TokensAction.SetSearchInput -> {
Analytics.send(ManageTokens.TokenSearched())
handleLoadCurrencies(
scanResponse = store.state.globalState.scanResponse,
newSearchInput = action.searchInput,
)
}
is TokensAction.LoadMore -> {
if (store.state.tokensState.needToLoadMore &&
store.state.tokensState.currencies.isNotEmpty()
) {
handleLoadCurrencies(action.scanResponse)
}
}
}
next(action)
}
}
}
private fun handleLoadCurrencies(scanResponse: ScanResponse?, newSearchInput: String? = null) {
val tokensState = store.state.tokensState
val supportedBlockchains: List<Blockchain> = scanResponse?.card?.supportedBlockchains()
?: Blockchain.values().toList().filterNot(Blockchain::isTestnet)
val loadCoinsService = LoadAvailableCoinsService(
tangemTechApi = store.state.domainNetworks.tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
assetReader = store.state.daggerGraphState.get(DaggerGraphState::assetReader),
)
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
scope.launch {
val result = loadCoinsService.getSupportedTokens(
isTestNet = scanResponse?.card?.isTestCard ?: false,
supportedBlockchains = supportedBlockchains,
page = if (newSearchInput == null) tokensState.pageToLoad else 0,
searchInput = if (newSearchInput == null) tokensState.searchInput else newSearchInput.ifBlank { null },
val scanResponse = store.state.globalState.scanResponse ?: return@launch
val currentTokens = store.state.tokensState.addedTokens
val currentBlockchains = store.state.tokensState.addedBlockchains
val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains)
val blockchainsToRemove =
store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains)
val tokensToAdd = action.tokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } }
removeCurrenciesIfNeeded(
currencies = convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
derivationStyle = scanResponse.card.derivationStyle,
),
)
store.dispatchOnMain(
action = when (result) {
is Result.Success -> {
TokensAction.LoadCurrencies.Success(
currencies = result.data.currencies.filter(supportedBlockchains.toSet()),
loadMore = result.data.moreAvailable,
)
}
is Result.Failure -> {
TokensAction.LoadCurrencies.Failure
}
},
)
}
}
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) = scope.launch {
val scanResponse = store.state.globalState.scanResponse ?: return@launch
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
derivationStyle = scanResponse.card.derivationStyle,
)
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
derivationStyle = scanResponse.card.derivationStyle,
)
val blockchainsToAdd = action.addedBlockchains.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.addedBlockchains::contains)
val tokensToAdd = action.addedTokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.addedTokens.any { it.token == token.token } }
removeCurrenciesIfNeeded(
currencies = convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
val currencyList = convertToCurrencies(
blockchains = blockchainsToAdd,
tokens = tokensToAdd,
derivationStyle = scanResponse.card.derivationStyle,
),
)
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
val currencyList = convertToCurrencies(
blockchains = blockchainsToAdd,
tokens = tokensToAdd,
derivationStyle = scanResponse.card.derivationStyle,
)
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, currencyList) {
submitAdd(it, currencyList)
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, currencyList) {
submitAdd(it, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitAdd(scanResponse, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitAdd(scanResponse, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}

View file

@ -1,6 +1,11 @@
package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.Currency.Token
import org.rekotlin.Action
object TokensReducer {
@ -10,47 +15,41 @@ object TokensReducer {
private fun internalReduce(action: Action, state: AppState): TokensState {
if (action !is TokensAction) return state.tokensState
val tokensState = state.tokensState
return when (action) {
is TokensAction.ResetState -> TokensState()
is TokensAction.LoadCurrencies -> {
val loadingState = if (tokensState.currencies.isEmpty()) {
LoadCoinsState.LOADING
} else {
LoadCoinsState.LOADED
}
tokensState.copy(
scanResponse = action.scanResponse,
loadCoinsState = loadingState,
)
}
is TokensAction.LoadCurrencies.Success -> {
tokensState.copy(
currencies = tokensState.currencies + action.currencies,
needToLoadMore = action.loadMore,
pageToLoad = tokensState.pageToLoad + 1,
loadCoinsState = LoadCoinsState.LOADED,
)
}
is TokensAction.SetAddedCurrencies -> {
tokensState.copy(
is TokensAction.SetArgs.ManageAccess -> {
state.tokensState.copy(
isManageAccess = true,
addedWallets = action.wallets,
addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle),
addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle),
addedWallets = action.wallets,
)
}
is TokensAction.AllowToAddTokens -> {
tokensState.copy(allowToAdd = action.allow)
is TokensAction.SetArgs.ReadAccess -> {
state.tokensState.copy(isManageAccess = false)
}
is TokensAction.SetSearchInput -> {
tokensState.copy(
searchInput = action.searchInput.ifBlank { null },
needToLoadMore = true,
currencies = emptyList(),
pageToLoad = 0,
loadCoinsState = LoadCoinsState.LOADING,
)
}
else -> tokensState
else -> state.tokensState
}
}
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return mapNotNull { walletDataModel ->
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) {
null
} else {
(walletDataModel.currency as? Currency.Blockchain)?.blockchain
}
}.distinct()
}
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
derivationStyle: DerivationStyle?,
): List<TokenWithBlockchain> {
return mapNotNull { walletDataModel ->
if (walletDataModel.currency !is Token) return@mapNotNull null
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain)
}.distinct()
}

View file

@ -1,76 +1,16 @@
package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.tokens.Currency
import org.rekotlin.StateType
import com.tangem.tap.features.wallet.models.Currency.Token as CurrencyToken
data class TokensState(
val isManageAccess: Boolean = false,
val addedWallets: List<WalletDataModel> = emptyList(),
val addedTokens: List<TokenWithBlockchain> = emptyList(),
val addedBlockchains: List<Blockchain> = emptyList(),
val currencies: List<Currency> = emptyList(),
val searchInput: String? = null,
val allowToAdd: Boolean = true,
val scanResponse: ScanResponse? = null,
val needToLoadMore: Boolean = true,
val pageToLoad: Int = 0,
val loadCoinsState: LoadCoinsState = LoadCoinsState.LOADING,
) : StateType {
) : StateType
fun canHandleToken(token: TokenWithBlockchain): Boolean {
return scanResponse?.card?.canHandleToken(token.blockchain) ?: false
}
}
typealias ContractAddress = String
fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
derivationStyle: DerivationStyle?,
): List<TokenWithBlockchain> {
return mapNotNull {
if (it.currency !is CurrencyToken) return@mapNotNull null
if (it.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
TokenWithBlockchain(it.currency.token, it.currency.blockchain)
}.distinct()
}
fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return mapNotNull {
if (it.currency.isCustomCurrency(derivationStyle)) {
null
} else {
(it.currency as? com.tangem.tap.features.wallet.models.Currency.Blockchain)?.blockchain
}
}.distinct()
}
data class TokenWithBlockchain(
val token: Token,
val blockchain: Blockchain,
)
fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency> {
if (supportedBlockchains == null) return this
return map { currency ->
currency.copy(
contracts = currency.contracts.filter { contract ->
supportedBlockchains.contains(contract.blockchain) &&
(contract.blockchain.canHandleTokens() || contract.address == null)
},
)
}.filterNot { currency ->
currency.contracts.isEmpty() && !supportedBlockchains.contains(Blockchain.fromNetworkId(currency.id))
}
}
enum class LoadCoinsState {
LOADING, LOADED,
}
// TODO: [REDACTED_TASK_KEY] Remove this class
data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain)

View file

@ -1,232 +0,0 @@
@file:Suppress("MagicNumber")
package com.tangem.tap.features.tokens.legacy.ui.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.compose.extensions.addAndNotify
import com.tangem.tap.common.compose.extensions.removeAndNotify
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
import com.tangem.tap.features.tokens.legacy.redux.LoadCoinsState
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@Suppress("LongMethod")
@Composable
fun CurrenciesScreen(
tokensState: MutableState<TokensState>,
onSaveChanges: (List<TokenWithBlockchain>, List<Blockchain>) -> Unit,
onNetworkItemClick: (ContractAddress) -> Unit,
onLoadMore: () -> Unit,
) {
val tokensAddedOnMainScreen = remember { tokensState.value.addedTokens }
val blockchainsAddedOnMainScreen = remember { tokensState.value.addedBlockchains }
val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) }
val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) }
val keyboardState by keyboardAsState()
val onAddCurrencyToggleClick = { currency: Currency, token: TokenWithBlockchain? ->
val blockchain = Blockchain.fromNetworkId(currency.id)
if (blockchain != null && token == null) {
toggleBlockchain(
blockchain,
blockchainsAddedOnMainScreen,
addedBlockchainsState,
addedTokensState.value,
)
} else if (token != null) {
toggleToken(
token,
tokensAddedOnMainScreen,
addedTokensState,
tokensState.value,
)
}
}
val statusBarColor = colorResource(id = R.color.backgroundLightGray)
SystemBarsEffect {
setSystemBarsColor(color = statusBarColor)
}
Scaffold(
floatingActionButton = {
if (tokensState.value.allowToAdd) {
SaveChangesButton(keyboardState) {
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
}
}
},
floatingActionButtonPosition = FabPosition.Center,
) {
AnimatedVisibility(
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADING,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = Color(0xFF1ACE80))
}
}
AnimatedVisibility(
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADED,
enter = fadeIn(animationSpec = tween(1000)),
exit = fadeOut(animationSpec = tween(1000)),
) {
Column {
val showHeader = tokensState.value.scanResponse?.card?.useOldStyleDerivation == true
ListOfCurrencies(
header = { if (showHeader) CurrenciesWarning() },
currencies = tokensState.value.currencies,
addedTokens = addedTokensState.value,
addedBlockchains = addedBlockchainsState.value,
allowToAdd = tokensState.value.allowToAdd,
onAddCurrencyToggle = { currency, token ->
onAddCurrencyToggleClick(currency, token)
},
onNetworkItemClick = onNetworkItemClick,
onLoadMore = onLoadMore,
)
}
}
}
}
private fun toggleBlockchain(
blockchain: Blockchain,
blockchainsAddedOnMainScreen: List<Blockchain>,
addedBlockchainsState: MutableState<List<Blockchain>>,
addedTokens: List<TokenWithBlockchain>,
) {
val isTryingToRemove = addedBlockchainsState.value.contains(blockchain)
val isAddedOnMainScreen = blockchainsAddedOnMainScreen.contains(blockchain)
val isTokenWithSameBlockchainFound = addedTokens.any { it.blockchain == blockchain }
val analyticsCurrencyTypeParam = AnalyticsParam.CurrencyType.Blockchain(blockchain)
if (isTryingToRemove) {
if (isTokenWithSameBlockchainFound) {
store.dispatchDialogShow(
WalletDialog.TokensAreLinkedDialog(
currencyTitle = blockchain.name,
currencySymbol = blockchain.currency,
),
)
} else if (isAddedOnMainScreen) {
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = blockchain.name,
onOk = {
analyticsCurrencyTypeParam.sendOn()
addedBlockchainsState.removeAndNotify(blockchain)
},
),
)
} else {
analyticsCurrencyTypeParam.sendOff()
addedBlockchainsState.removeAndNotify(blockchain)
}
} else {
analyticsCurrencyTypeParam.sendOn()
addedBlockchainsState.addAndNotify(blockchain)
}
}
private fun toggleToken(
token: TokenWithBlockchain,
tokensAddedOnMainScreen: List<TokenWithBlockchain>,
addedTokensState: MutableState<List<TokenWithBlockchain>>,
tokensState: TokensState,
) {
val isTryingToRemove = addedTokensState.value.contains(token)
val isAddedOnMainScreen = tokensAddedOnMainScreen.contains(token)
val isUnsupportedToken = !tokensState.canHandleToken(token)
val analyticsCurrencyTypeParam = AnalyticsParam.CurrencyType.Token(token.token)
if (isTryingToRemove) {
if (isAddedOnMainScreen) {
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = token.token.name,
onOk = {
analyticsCurrencyTypeParam.sendOff()
addedTokensState.removeAndNotify(token)
},
),
)
} else {
analyticsCurrencyTypeParam.sendOff()
addedTokensState.removeAndNotify(token)
}
} else {
if (isUnsupportedToken) {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.alert_manage_tokens_unsupported_message,
),
)
} else {
analyticsCurrencyTypeParam.sendOn()
addedTokensState.addAndNotify(token)
}
}
}
private fun AnalyticsParam.CurrencyType.sendOn() {
Analytics.send(ManageTokens.TokenSwitcherChanged(this, AnalyticsParam.OnOffState.On))
}
private fun AnalyticsParam.CurrencyType.sendOff() {
Analytics.send(ManageTokens.TokenSwitcherChanged(this, AnalyticsParam.OnOffState.Off))
}
@Composable
fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {
PrimaryButton(
modifier = Modifier
.padding(bottom = keyboardState.height)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.common_save_changes),
onClick = onSaveChanges,
)
}

View file

@ -1,56 +0,0 @@
package com.tangem.tap.features.tokens.legacy.ui.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
@Suppress("MagicNumber")
@Composable
fun CurrenciesWarning() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(16.dp)
.background(
color = Color(0xFFF2F2F2),
shape = RoundedCornerShape(10.dp),
),
) {
val warning = stringResource(id = R.string.alert_manage_tokens_addresses_message)
val end = warning.indexOf(" ")
val spanStyles = listOf(
AnnotatedString.Range(
SpanStyle(fontWeight = FontWeight.Bold),
start = 0,
end = end,
),
)
Text(
text = AnnotatedString(text = warning, spanStyles = spanStyles),
textAlign = TextAlign.Start,
color = Color(0xFF848488),
modifier = Modifier.padding(
top = 8.dp,
bottom = 8.dp,
start = 16.dp,
end = 16.dp,
),
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
)
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.features.tokens.legacy.ui.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun CurrencyPlaceholderIcon(id: String) {
val letterColor: Color = Color.White
val circleColor: Color = Color.Black
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(circleColor, shape = CircleShape),
) {
Text(
text = id.firstOrNull()?.titlecase() ?: "",
textAlign = TextAlign.Center,
color = letterColor,
modifier = Modifier.padding(4.dp),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
)
}
}

View file

@ -1,74 +0,0 @@
package com.tangem.tap.features.tokens.legacy.ui.compose
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.compose.extensions.HideKeyboardOnScroll
import com.tangem.tap.common.compose.extensions.OnBottomReached
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem.CurrencyItem
@Suppress("LongParameterList")
@Composable
fun ListOfCurrencies(
header: @Composable () -> Unit,
currencies: List<Currency>,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClick: (ContractAddress) -> Unit,
onLoadMore: () -> Unit,
) {
val expandedCurrencies = remember { mutableStateOf(listOf("")) }
val onCurrencyClick = { currencyId: String ->
val mutableList = expandedCurrencies.value.toMutableList()
if (mutableList.contains(currencyId)) {
mutableList.remove(currencyId)
} else {
mutableList.add(currencyId)
}
expandedCurrencies.value = mutableList
}
val listState = rememberLazyListState()
listState.HideKeyboardOnScroll()
listState.OnBottomReached(loadMoreThreshold = 40) { onLoadMore() }
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(bottom = 90.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
item { header() }
itemsIndexed(currencies) { _, currency ->
CurrencyItem(
currency = currency,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
allowToAdd = allowToAdd,
isExpanded = expandedCurrencies.value.contains(currency.id),
onCurrencyClick = { onCurrencyClick(currency.id) },
onAddCurrencyToggle = onAddCurrencyToggle,
onNetworkItemClick = onNetworkItemClick,
)
}
}
}
val Currency.fullName: String
get() = "${this.name} (${this.symbol})"

View file

@ -1,63 +0,0 @@
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.domain.tokens.Contract
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
@Suppress("LongParameterList")
@Composable
fun CurrencyExpandedContent(
currency: Currency,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
isExpanded: Boolean,
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClick: (ContractAddress) -> Unit,
) {
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
val blockchains = currency.contracts.map { it.blockchain }
Column(modifier = Modifier.fillMaxWidth()) {
blockchains.mapIndexed { index, blockchain ->
val currencyContract = currency.contracts
.firstOrNull { it.blockchain == blockchain }
?: return@mapIndexed
val added = if (currencyContract.address != null) {
addedTokens.any(currencyContract::isInclude)
} else {
addedBlockchains.contains(blockchain)
}
NetworkItem(
currency = currency,
contract = currencyContract,
blockchain = blockchain,
allowToAdd = allowToAdd,
added = added,
index = index,
size = blockchains.size,
onAddCurrencyToggle = onAddCurrencyToggle,
onNetworkItemClick = onNetworkItemClick,
)
}
}
}
}
private fun Contract.isInclude(addedToken: TokenWithBlockchain): Boolean =
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain

View file

@ -1,40 +0,0 @@
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
@Suppress("LongParameterList")
@Composable
fun CurrencyItem(
currency: Currency,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
isExpanded: Boolean,
onCurrencyClick: () -> Unit,
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClick: (ContractAddress) -> Unit,
) {
Column {
CurrencyItemHeader(
currency = currency,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
isExpanded = isExpanded,
onCurrencyClick = onCurrencyClick,
)
CurrencyExpandedContent(
currency = currency,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
allowToAdd = allowToAdd,
isExpanded = isExpanded,
onAddCurrencyToggle = onAddCurrencyToggle,
onNetworkItemClick = onNetworkItemClick,
)
}
}

View file

@ -1,213 +0,0 @@
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.domain.tokens.Contract
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.legacy.ui.compose.CurrencyPlaceholderIcon
import com.tangem.tap.features.tokens.legacy.ui.compose.fullName
import com.tangem.wallet.R
@Suppress("LongMethod", "MagicNumber")
@Composable
fun CurrencyItemHeader(
currency: Currency,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
isExpanded: Boolean,
onCurrencyClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 62.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onCurrencyClick,
),
) {
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
.align(Alignment.CenterVertically)
.clip(RoundedCornerShape(6.dp)),
) {
SubcomposeAsyncImage(
modifier = Modifier
.size(46.dp),
model = ImageRequest.Builder(LocalContext.current)
.data(currency.iconUrl)
.crossfade(true)
.build(),
contentDescription = currency.id,
loading = { CurrencyPlaceholderIcon(currency.id) },
error = { CurrencyPlaceholderIcon(currency.id) },
)
}
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
) {
Text(
text = currency.fullName,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF1C1C1E),
)
Spacer(modifier = Modifier.size(6.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(20.dp),
) {
if (isExpanded) {
Text(
modifier = Modifier.align(Alignment.TopStart),
text = stringResource(id = R.string.currency_subtitle_expanded),
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF8E8E93),
)
} else {
NetworksRow(
currency = currency,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
)
}
}
}
val expandCollapseArrow = if (isExpanded) R.drawable.ic_arrow_extended else R.drawable.ic_arrow_collapsed
Image(
painter = painterResource(id = expandCollapseArrow),
contentDescription = null,
Modifier
.padding(20.dp)
.align(Alignment.CenterVertically),
)
}
}
@Composable
private fun NetworksRow(
currency: Currency,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
) {
Row {
if (currency.contracts.isNotEmpty()) {
currency.contracts.map { contract ->
if (contract.address == null) {
BlockchainNetworkItem(
blockchain = contract.blockchain,
isMainNetwork = true,
addedBlockchains = addedBlockchains,
)
} else {
val isAdded = addedTokens.any(contract::isInclude)
val iconResId = if (isAdded) {
getActiveIconRes(contract.blockchain.id)
} else {
contract.blockchain.getGreyedOutIconRes()
}
Image(
modifier = Modifier
.size(20.dp),
painter = painterResource(id = iconResId),
contentDescription = null,
)
Spacer(Modifier.size(5.dp))
}
}
} else {
BlockchainNetworkItem(
blockchain = Blockchain.fromNetworkId(currency.id),
isMainNetwork = false,
addedBlockchains = addedBlockchains,
)
}
}
}
@Suppress("MagicNumber")
@Composable
private fun BlockchainNetworkItem(
blockchain: Blockchain?,
isMainNetwork: Boolean,
addedBlockchains: List<Blockchain>,
) {
val added = addedBlockchains.contains(blockchain)
val icon = if (added) blockchain?.let { getActiveIconRes(it.id) } else blockchain?.getGreyedOutIconRes()
if (icon != null) {
Box(
Modifier
.size(20.dp),
) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp),
)
if (isMainNetwork) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(7.dp)
.clip(CircleShape)
.background(Color.White),
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(5.dp)
.clip(CircleShape)
.background(Color(0xFF1ACE80)),
)
}
}
}
Spacer(modifier = Modifier.size(5.dp))
}
}
private fun Contract.isInclude(addedToken: TokenWithBlockchain): Boolean =
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain

View file

@ -1,186 +0,0 @@
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getNetworkName
import com.tangem.tap.domain.tokens.Contract
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.impl.presentation.ui.NetworkItemArrow
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.legacy.ui.compose.CurrencyPlaceholderIcon
@Suppress("LongParameterList", "LongMethod", "MagicNumber")
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NetworkItem(
currency: Currency,
contract: Contract,
blockchain: Blockchain,
allowToAdd: Boolean,
added: Boolean,
index: Int,
size: Int,
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClick: (ContractAddress) -> Unit,
) {
val rowHeight = 53.dp
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(rowHeight)
.combinedClickable(
enabled = allowToAdd,
onLongClick = {
contract.address?.let { onNetworkItemClick(it) }
},
onClick = {},
indication = null,
interactionSource = remember { MutableInteractionSource() },
),
) {
Box(
Modifier
.width(78.dp)
.padding(start = 38.dp),
) {
NetworkItemArrow(
itemHeight = rowHeight,
isLastItem = index == size - 1,
)
}
Box(
modifier = Modifier
.align(Alignment.CenterVertically),
) {
SubcomposeAsyncImage(
model = if (added) getActiveIconRes(blockchain.id) else blockchain.getGreyedOutIconRes(),
contentDescription = blockchain.fullName,
loading = { CurrencyPlaceholderIcon(blockchain.id) },
error = { CurrencyPlaceholderIcon(blockchain.id) },
modifier = Modifier
.size(20.dp),
)
if (contract.address == null) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(7.dp)
.clip(CircleShape)
.background(Color.White),
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(5.dp)
.clip(CircleShape)
.background(Color(0xFF1ACE80)),
)
}
}
}
Spacer(Modifier.width(6.dp))
Text(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
text = prepareNetworkNameSpannableText(
blockchain = blockchain,
contractAddress = contract.address,
),
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
color = if (added) Color.Black else Color(0xFF848488),
)
if (allowToAdd) {
val token = if (contract.address != null) {
Token(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
contractAddress = contract.address,
decimals = contract.decimalCount!!,
)
} else {
null
}
val tokenWithBlockchain = token?.let { TokenWithBlockchain(it, contract.blockchain) }
val currencyToSave = if (contract.address == null) {
currency.copy(id = contract.networkId)
} else {
currency
}
Switch(
modifier = Modifier
.fillMaxHeight()
.padding(start = 16.dp, end = 16.dp),
checked = added,
onCheckedChange = { onAddCurrencyToggle(currencyToSave, tokenWithBlockchain) },
colors = SwitchDefaults.colors(
checkedThumbColor = Color(0xFF1ACE80),
),
)
}
}
}
@Suppress("MagicNumber")
@Composable
private fun prepareNetworkNameSpannableText(blockchain: Blockchain, contractAddress: String?): AnnotatedString {
val blockchainName = blockchain.fullNameWithoutTestnet.uppercase()
val additionalText =
if (contractAddress == null) "MAIN" else blockchain.getNetworkName().uppercase()
val text = "$blockchainName $additionalText"
val startOfAdditionalText =
if (additionalText.isNotBlank()) text.indexOf(additionalText) else text.length
val spanStyles = listOf(
AnnotatedString.Range(
SpanStyle(
fontWeight = FontWeight.Normal,
color = if (contractAddress != null) Color(0xFF8E8E93) else Color(0xFF1ACE80),
),
start = startOfAdditionalText,
end = text.length,
),
)
return AnnotatedString(text = text, spanStyles = spanStyles)
}

View file

@ -6,7 +6,6 @@ import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.getQuantityString
@ -108,16 +107,13 @@ class MultiWalletView : WalletView() {
binding.btnAddToken.setOnClickListener {
val card = store.state.globalState.scanResponse!!.card
Analytics.send(Portfolio.ButtonManageTokens())
store.dispatch(TokensAction.LoadCurrencies(scanResponse = store.state.globalState.scanResponse))
store.dispatch(TokensAction.AllowToAddTokens(true))
store.dispatch(
TokensAction.SetAddedCurrencies(
TokensAction.SetArgs.ManageAccess(
wallets = state.walletsDataFromStores,
derivationStyle = card.derivationStyle,
),
)
Analytics.send(ManageTokens.ScreenOpened())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)
@ -175,7 +171,8 @@ class MultiWalletView : WalletView() {
fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle),
)
}
else -> { /* no-op */ }
else -> Unit
}
}

View file

@ -4,14 +4,12 @@ import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.features.tester.api.TesterRouter
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
import org.rekotlin.StateType
data class DaggerGraphState(
val assetReader: AssetReader? = null,
val testerRouter: TesterRouter? = null,
val networkConnectionManager: NetworkConnectionManager? = null,
val tokensListFeatureToggles: TokensListFeatureToggles? = null,
val customTokenFeatureToggles: CustomTokenFeatureToggles? = null,
) : StateType {

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/menu_popular_tokens"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/main_manage_tokens" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/cv_currencies"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_search"
android:icon="@drawable/ic_search"
android:title="@string/common_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always" />
<item
android:id="@+id/menu_navigate_add_custom_token"
android:icon="@drawable/ic_add"
android:title="@string/common_add"
app:iconTint="@color/darkGray2"
app:showAsAction="always" />
</menu>

View file

@ -3,12 +3,8 @@
"name": "OPTIMISM_SWAP_FEATURE_ENABLED",
"version": "4.3.1"
},
{
"name": "REDESIGNED_TOKEN_LIST_SCREEN_ENABLED",
"version": "4.4.0"
},
{
"name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED",
"version": "undefined"
"version": "4.6.0"
}
]