Updated on 2026-08-14
This commit is contained in:
parent
abec0f1ee8
commit
bfb2338a1a
17 changed files with 613 additions and 516 deletions
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.tap.features.tokens.di
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.tap.features.tokens.data.DefaultTokensListRepository
|
||||
import com.tangem.tap.features.tokens.domain.TokensListRepository
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
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,
|
||||
reduxStateHolder: AppStateHolder,
|
||||
testnetTokensStorage: TestnetTokensStorage,
|
||||
): TokensListRepository {
|
||||
return DefaultTokensListRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
reduxStateHolder = reduxStateHolder,
|
||||
testnetTokensStorage = testnetTokensStorage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,12 @@ 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
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.tokens.presentation.ui.AddTokensScreen
|
||||
import com.tangem.tap.features.tokens.presentation.ui.TokensListScreen
|
||||
import com.tangem.tap.features.tokens.presentation.viewmodels.TokensListViewModel
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
|
@ -36,12 +35,10 @@ internal class TokensListFragment : Fragment() {
|
|||
setContent {
|
||||
isTransitionGroup = true
|
||||
|
||||
val viewModel = hiltViewModel<TokensListViewModel>().apply {
|
||||
subscribeOnReduxEvents(lifecycle = LocalLifecycleOwner.current.lifecycle)
|
||||
}
|
||||
val viewModel = hiltViewModel<TokensListViewModel>()
|
||||
|
||||
TangemTheme {
|
||||
AddTokensScreen(stateHolder = viewModel.uiState)
|
||||
TokensListScreen(stateHolder = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.tap.features.tokens.presentation.models
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.store
|
||||
|
||||
/**
|
||||
* Required data for tokens list screen
|
||||
* FIXME("Necessary to avoid using redux state")
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TokensListArgs {
|
||||
/** Tokens list screen mode */
|
||||
val isManageAccess: Boolean get() = store.state.tokensState.allowToAdd
|
||||
|
||||
/** Tokens list that accessible from the main screen */
|
||||
val mainScreenTokenList: List<TokenWithBlockchain> get() = store.state.tokensState.addedTokens
|
||||
|
||||
/** Blockchains list that accessible from the main screen */
|
||||
val mainScreenBlockchainList: List<Blockchain> get() = store.state.tokensState.addedBlockchains
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
package com.tangem.tap.features.tokens.presentation.states
|
||||
|
||||
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.redux.ContractAddress
|
||||
|
||||
/**
|
||||
|
|
@ -16,7 +20,7 @@ sealed interface NetworkItemState {
|
|||
val protocolName: String
|
||||
|
||||
/** Network icon id from resources */
|
||||
val iconResId: Int
|
||||
val iconResId: MutableState<Int>
|
||||
|
||||
/** Flag that determines if the network is the main network for the token */
|
||||
val isMainNetwork: Boolean
|
||||
|
|
@ -29,35 +33,51 @@ sealed interface NetworkItemState {
|
|||
* @property iconResId network icon id from resources
|
||||
* @property isMainNetwork flag that determines if the network is the main network for the token
|
||||
*/
|
||||
data class ReadAccess(
|
||||
data class ReadContent(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val iconResId: Int,
|
||||
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 networkId network id
|
||||
* @property contractAddress contract address
|
||||
* @property onToggleClick lambda be invoked when switch is been toggled
|
||||
* @property onNetworkClick lambda be invoked when network item is been clicked
|
||||
* @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 ManageAccess(
|
||||
data class ManageContent(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val iconResId: Int,
|
||||
override val iconResId: MutableState<Int>,
|
||||
override val isMainNetwork: Boolean,
|
||||
val isAdded: Boolean,
|
||||
val networkId: String,
|
||||
val contractAddress: ContractAddress?,
|
||||
val onToggleClick: (String, String) -> Unit,
|
||||
val isAdded: MutableState<Boolean>,
|
||||
val id: String,
|
||||
val address: ContractAddress?,
|
||||
val decimalCount: Int?,
|
||||
val blockchain: Blockchain,
|
||||
val onToggleClick: (TokenItemState.ManageContent, ManageContent) -> Unit,
|
||||
val onNetworkClick: () -> Unit,
|
||||
) : NetworkItemState
|
||||
) : 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 blockchain.getGreyedOutIconRes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,24 +25,26 @@ sealed interface TokenItemState {
|
|||
* @property iconUrl token icon url
|
||||
* @property networks list of networks that is available for read
|
||||
*/
|
||||
data class ReadAccess(
|
||||
data class ReadContent(
|
||||
override val name: String,
|
||||
override val iconUrl: String,
|
||||
override val networks: ImmutableList<NetworkItemState.ReadAccess>,
|
||||
override val networks: ImmutableList<NetworkItemState.ReadContent>,
|
||||
) : TokenItemState
|
||||
|
||||
/**
|
||||
* Token item state that is available for read and edit
|
||||
* Token item state that is available for read and manage
|
||||
*
|
||||
* @property name token name
|
||||
* @property iconUrl token icon url
|
||||
* @property networks list of networks is available for read and edit
|
||||
* @property id token id
|
||||
* @property symbol token brief name
|
||||
*/
|
||||
data class ManageAccess(
|
||||
data class ManageContent(
|
||||
override val name: String,
|
||||
override val iconUrl: String,
|
||||
override val networks: ImmutableList<NetworkItemState.ManageAccess>,
|
||||
override val networks: ImmutableList<NetworkItemState.ManageContent>,
|
||||
val id: String,
|
||||
val symbol: String,
|
||||
) : TokenItemState
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.features.tokens.presentation.states
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* State holder for screen with list of tokens
|
||||
|
|
@ -12,47 +14,69 @@ internal sealed interface TokensListStateHolder {
|
|||
/** Toolbar state */
|
||||
val toolbarState: TokensListToolbarState
|
||||
|
||||
/** 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 toolbarState toolbar state
|
||||
* @param tokens tokens list
|
||||
* @param onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed
|
||||
*/
|
||||
fun copySealed(toolbarState: TokensListToolbarState): TokensListStateHolder {
|
||||
fun copySealed(
|
||||
toolbarState: TokensListToolbarState = this.toolbarState,
|
||||
tokens: Flow<PagingData<TokenItemState>> = this.tokens,
|
||||
onTokensLoadStateChanged: (LoadState) -> Unit = this.onTokensLoadStateChanged,
|
||||
): TokensListStateHolder {
|
||||
return when (this) {
|
||||
is ManageAccess -> copy(toolbarState = toolbarState)
|
||||
is Loading -> copy(toolbarState = toolbarState)
|
||||
is ReadAccess -> copy(toolbarState = toolbarState)
|
||||
is ManageContent -> copy(toolbarState, tokens, onTokensLoadStateChanged)
|
||||
is Loading -> copy(toolbarState, tokens, onTokensLoadStateChanged)
|
||||
is ReadContent -> copy(toolbarState, tokens, onTokensLoadStateChanged)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading state
|
||||
*
|
||||
* @property toolbarState toolbar state
|
||||
* @property toolbarState toolbar state
|
||||
* @property tokens tokens list
|
||||
* @property onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed
|
||||
*/
|
||||
data class Loading(override val toolbarState: TokensListToolbarState) : TokensListStateHolder
|
||||
data class Loading(
|
||||
override val toolbarState: TokensListToolbarState,
|
||||
override val tokens: Flow<PagingData<TokenItemState>>,
|
||||
override val onTokensLoadStateChanged: (LoadState) -> Unit,
|
||||
) : TokensListStateHolder
|
||||
|
||||
/**
|
||||
* State screen that is available only for read
|
||||
*
|
||||
* @property toolbarState toolbar state
|
||||
* @property tokens tokens list
|
||||
* @property toolbarState toolbar state
|
||||
* @property tokens tokens list
|
||||
* @property onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed
|
||||
*/
|
||||
data class ReadAccess(
|
||||
data class ReadContent(
|
||||
override val toolbarState: TokensListToolbarState,
|
||||
override val tokens: ImmutableList<TokenItemState.ReadAccess>,
|
||||
) : TokensListStateHolder, TokensListVisibility
|
||||
override val tokens: Flow<PagingData<TokenItemState>>,
|
||||
override val onTokensLoadStateChanged: (LoadState) -> Unit,
|
||||
) : TokensListStateHolder
|
||||
|
||||
/**
|
||||
* State screen that is available for read and edit
|
||||
* State screen that is available for read and manage
|
||||
*
|
||||
* @property toolbarState toolbar state
|
||||
* @property tokens tokens list
|
||||
* @property onSaveButtonClick callback to be invoked when SaveButton is being clicked
|
||||
* @property toolbarState toolbar state
|
||||
* @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 ManageAccess(
|
||||
data class ManageContent(
|
||||
override val toolbarState: TokensListToolbarState,
|
||||
override val tokens: ImmutableList<TokenItemState.ManageAccess>,
|
||||
override val tokens: Flow<PagingData<TokenItemState>>,
|
||||
override val onTokensLoadStateChanged: (LoadState) -> Unit,
|
||||
val onSaveButtonClick: () -> Unit,
|
||||
) : TokensListStateHolder, TokensListVisibility
|
||||
) : TokensListStateHolder
|
||||
}
|
||||
|
|
@ -6,39 +6,39 @@ sealed interface TokensListToolbarState {
|
|||
/** Callback to be invoked when BackButton is being clicked */
|
||||
val onBackButtonClick: () -> Unit
|
||||
|
||||
/** Callback to be invoked when SearchButton is being clicked */
|
||||
val onSearchButtonClick: () -> 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 titleResId toolbar title id from resources
|
||||
* @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 ReadAccess(
|
||||
override val titleResId: Int,
|
||||
data class Read(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
override val titleResId: Int,
|
||||
override val onSearchButtonClick: () -> Unit,
|
||||
) : Title
|
||||
|
||||
/**
|
||||
* Title state that is available for read and edit
|
||||
* Title state that is available for read and manage
|
||||
*
|
||||
* @property titleResId toolbar title id from resources
|
||||
* @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 ManageAccess(
|
||||
override val titleResId: Int,
|
||||
data class Manage(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
override val titleResId: Int,
|
||||
override val onSearchButtonClick: () -> Unit,
|
||||
val onAddCustomTokenClick: () -> Unit,
|
||||
) : Title
|
||||
|
|
@ -47,15 +47,13 @@ sealed interface TokensListToolbarState {
|
|||
/**
|
||||
* Toolbar state as input field
|
||||
*
|
||||
* @property onBackButtonClick callback to be invoked when BackButton is being clicked
|
||||
* @property onSearchButtonClick callback to be invoked when SearchButton 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
|
||||
* @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 SearchInputField(
|
||||
data class InputField(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
override val onSearchButtonClick: () -> Unit,
|
||||
val value: String,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val onCleanButtonClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.presentation.states
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/** Marker interface for states that have a list of tokens */
|
||||
sealed interface TokensListVisibility {
|
||||
|
||||
/** Tokens list */
|
||||
val tokens: ImmutableList<TokenItemState>
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ internal fun BriefNetworksList(
|
|||
internal fun BriefNetworkItem(model: NetworkItemState) {
|
||||
Box(modifier = Modifier.size(size = TangemTheme.dimens.size20)) {
|
||||
Image(
|
||||
painter = painterResource(id = model.iconResId),
|
||||
painter = painterResource(id = model.iconResId.value),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.unit.sp
|
|||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.tokens.presentation.states.NetworkItemState
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokenItemState
|
||||
import kotlinx.collections.immutable.ImmutableCollection
|
||||
|
||||
/**
|
||||
|
|
@ -43,7 +44,7 @@ import kotlinx.collections.immutable.ImmutableCollection
|
|||
@Composable
|
||||
internal fun DetailedNetworksList(
|
||||
isExpanded: Boolean,
|
||||
tokenId: String?,
|
||||
token: TokenItemState,
|
||||
networks: ImmutableCollection<NetworkItemState>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -56,7 +57,7 @@ internal fun DetailedNetworksList(
|
|||
Column {
|
||||
networks.forEachIndexed { index, network ->
|
||||
key(network.name) {
|
||||
DetailedNetworkItem(model = network, tokenId = tokenId, isLastItem = networks.size - 1 == index)
|
||||
DetailedNetworkItem(token = token, network = network, isLastItem = networks.size - 1 == index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,18 +66,18 @@ internal fun DetailedNetworksList(
|
|||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun DetailedNetworkItem(model: NetworkItemState, tokenId: String?, isLastItem: Boolean) {
|
||||
private fun DetailedNetworkItem(token: TokenItemState, network: NetworkItemState, isLastItem: Boolean) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val itemHeight = TangemTheme.dimens.size50
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
enabled = model is NetworkItemState.ManageAccess,
|
||||
enabled = network is NetworkItemState.ManageContent,
|
||||
onLongClick = {
|
||||
if (model is NetworkItemState.ManageAccess && model.contractAddress != null) {
|
||||
clipboardManager.setText(AnnotatedString(text = model.contractAddress))
|
||||
model.onNetworkClick()
|
||||
if (network is NetworkItemState.ManageContent && network.address != null) {
|
||||
clipboardManager.setText(AnnotatedString(text = network.address))
|
||||
network.onNetworkClick()
|
||||
}
|
||||
},
|
||||
onClick = {},
|
||||
|
|
@ -90,14 +91,19 @@ private fun DetailedNetworkItem(model: NetworkItemState, tokenId: String?, isLas
|
|||
) {
|
||||
NetworkItemArrow(itemHeight = itemHeight, isLastItem = isLastItem)
|
||||
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing16))
|
||||
BriefNetworkItem(model = model)
|
||||
BriefNetworkItem(model = network)
|
||||
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6))
|
||||
NetworkTitle(model = model)
|
||||
NetworkTitle(model = network)
|
||||
|
||||
if (model is NetworkItemState.ManageAccess) {
|
||||
if (network is NetworkItemState.ManageContent) {
|
||||
Switch(
|
||||
checked = model.isAdded,
|
||||
onCheckedChange = { model.onToggleClick(requireNotNull(tokenId), model.networkId) },
|
||||
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 = TangemColorPalette.Meadow,
|
||||
|
|
@ -125,7 +131,7 @@ private fun RowScope.NetworkTitle(model: NetworkItemState) {
|
|||
},
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
color = if (model is NetworkItemState.ManageAccess && model.isAdded) {
|
||||
color = if (model is NetworkItemState.ManageContent && model.isAdded.value) {
|
||||
TangemColorPalette.Black
|
||||
} else {
|
||||
TangemColorPalette.Dark2
|
||||
|
|
@ -139,7 +145,7 @@ private fun Preview_DetailedNetworksList_ManageAccess() {
|
|||
TangemTheme {
|
||||
DetailedNetworksList(
|
||||
isExpanded = true,
|
||||
tokenId = null,
|
||||
token = TokenListPreviewData.createManageToken(),
|
||||
networks = TokenListPreviewData.createManageNetworksList(),
|
||||
)
|
||||
}
|
||||
|
|
@ -151,7 +157,7 @@ private fun Preview_DetailedNetworksList_ReadAccess() {
|
|||
TangemTheme {
|
||||
DetailedNetworksList(
|
||||
isExpanded = true,
|
||||
tokenId = null,
|
||||
token = TokenListPreviewData.createReadToken(),
|
||||
networks = TokenListPreviewData.createReadNetworksList(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import com.tangem.wallet.R
|
|||
*/
|
||||
@Composable
|
||||
internal fun TokenItem(model: TokenItemState) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
|
|
@ -101,7 +101,7 @@ internal fun TokenItem(model: TokenItemState) {
|
|||
|
||||
DetailedNetworksList(
|
||||
isExpanded = isExpanded,
|
||||
tokenId = (model as? TokenItemState.ManageAccess)?.id,
|
||||
token = model,
|
||||
networks = model.networks,
|
||||
modifier = Modifier.constrainAs(detailedNetworksList) {
|
||||
top.linkTo(anchor = availableNetworksText.bottom, margin = spacing16)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.features.tokens.presentation.ui
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.tokens.presentation.states.NetworkItemState
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokenItemState
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -11,62 +13,67 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
*/
|
||||
object TokenListPreviewData {
|
||||
|
||||
fun createManageToken(): TokenItemState.ManageAccess {
|
||||
return TokenItemState.ManageAccess(
|
||||
fun createManageToken(): TokenItemState.ManageContent {
|
||||
return TokenItemState.ManageContent(
|
||||
name = "Tether (USDT)",
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png",
|
||||
networks = createManageNetworksList(),
|
||||
id = "",
|
||||
symbol = "",
|
||||
)
|
||||
}
|
||||
|
||||
fun createReadToken(): TokenItemState.ReadAccess {
|
||||
return TokenItemState.ReadAccess(
|
||||
fun createReadToken(): TokenItemState.ReadContent {
|
||||
return TokenItemState.ReadContent(
|
||||
name = "Tether (USDT)",
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png",
|
||||
networks = createReadNetworksList(),
|
||||
)
|
||||
}
|
||||
|
||||
fun createManageNetworksList(): ImmutableList<NetworkItemState.ManageAccess> {
|
||||
fun createManageNetworksList(): ImmutableList<NetworkItemState.ManageContent> {
|
||||
return persistentListOf(
|
||||
NetworkItemState.ManageAccess(
|
||||
NetworkItemState.ManageContent(
|
||||
name = "Ethereum",
|
||||
protocolName = "MAIN",
|
||||
iconResId = R.drawable.ic_eth_no_color,
|
||||
iconResId = mutableStateOf(R.drawable.ic_eth_no_color),
|
||||
isMainNetwork = true,
|
||||
isAdded = true,
|
||||
networkId = "",
|
||||
contractAddress = null,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
address = null,
|
||||
onToggleClick = { _, _ -> },
|
||||
onNetworkClick = {},
|
||||
decimalCount = null,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
NetworkItemState.ManageAccess(
|
||||
NetworkItemState.ManageContent(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = R.drawable.ic_bsc_no_color,
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_no_color),
|
||||
isMainNetwork = false,
|
||||
isAdded = false,
|
||||
networkId = "",
|
||||
contractAddress = null,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
address = null,
|
||||
onToggleClick = { _, _ -> },
|
||||
onNetworkClick = {},
|
||||
decimalCount = null,
|
||||
blockchain = Blockchain.BSC,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createReadNetworksList(): ImmutableList<NetworkItemState.ReadAccess> {
|
||||
fun createReadNetworksList(): ImmutableList<NetworkItemState.ReadContent> {
|
||||
return persistentListOf(
|
||||
NetworkItemState.ReadAccess(
|
||||
NetworkItemState.ReadContent(
|
||||
name = "Ethereum",
|
||||
protocolName = "MAIN",
|
||||
iconResId = R.drawable.ic_eth_no_color,
|
||||
iconResId = mutableStateOf(R.drawable.ic_eth_no_color),
|
||||
isMainNetwork = true,
|
||||
),
|
||||
NetworkItemState.ReadAccess(
|
||||
NetworkItemState.ReadContent(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = R.drawable.ic_bsc_no_color,
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_no_color),
|
||||
isMainNetwork = false,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,58 +12,55 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.compose.extensions.OnBottomReached
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokenItemState
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokensListStateHolder
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokensListToolbarState
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokensListVisibility
|
||||
import com.tangem.tap.features.tokens.redux.TokensAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddTokensScreen(stateHolder: TokensListStateHolder) {
|
||||
internal fun TokensListScreen(stateHolder: TokensListStateHolder) {
|
||||
BackHandler(onBack = stateHolder.toolbarState.onBackButtonClick)
|
||||
|
||||
Scaffold(
|
||||
topBar = { AddTokensToolbar(state = stateHolder.toolbarState) },
|
||||
topBar = { TokensListToolbar(state = stateHolder.toolbarState) },
|
||||
floatingActionButton = {
|
||||
if (stateHolder is TokensListStateHolder.ManageAccess) {
|
||||
if (stateHolder is TokensListStateHolder.ManageContent) {
|
||||
SaveChangesButton(onClick = stateHolder.onSaveButtonClick)
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
) { scaffoldPadding ->
|
||||
// This is a hack because AnimatedContent trigger recomposition if stateHolder content is changed
|
||||
AnimatedVisibility(
|
||||
visible = stateHolder is TokensListVisibility,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
if (stateHolder is TokensListVisibility) {
|
||||
TokensListContent(
|
||||
stateHolder = stateHolder,
|
||||
scaffoldPadding = scaffoldPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
val tokens = stateHolder.tokens.collectAsLazyPagingItems()
|
||||
|
||||
TokensListContent(
|
||||
tokens = tokens,
|
||||
scaffoldPadding = scaffoldPadding,
|
||||
)
|
||||
|
||||
stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = stateHolder is TokensListStateHolder.Loading,
|
||||
|
|
@ -87,24 +84,24 @@ private fun LoadingContent() {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun TokensListContent(stateHolder: TokensListVisibility, scaffoldPadding: PaddingValues) {
|
||||
val state = rememberLazyListState().apply {
|
||||
OnBottomReached(loadMoreThreshold = 40) {
|
||||
store.dispatch(TokensAction.LoadMore(scanResponse = store.state.globalState.scanResponse))
|
||||
}
|
||||
private fun TokensListContent(tokens: LazyPagingItems<TokenItemState>, scaffoldPadding: PaddingValues) {
|
||||
val state = rememberLazyListState()
|
||||
|
||||
if (state.isScrollInProgress) {
|
||||
LocalSoftwareKeyboardController.current?.hide()
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(scaffoldPadding),
|
||||
state,
|
||||
state = state,
|
||||
) {
|
||||
items(
|
||||
items = stateHolder.tokens,
|
||||
key = TokenItemState::name,
|
||||
itemContent = { TokenItem(model = it) },
|
||||
)
|
||||
items(items = tokens, key = TokenItemState::name) {
|
||||
it?.let { TokenItem(model = it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,16 +119,18 @@ private fun SaveChangesButton(onClick: () -> Unit) {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddTokensScreen_Loading() {
|
||||
private fun Preview_TokensListScreen_Loading() {
|
||||
TangemTheme {
|
||||
AddTokensScreen(
|
||||
TokensListScreen(
|
||||
stateHolder = TokensListStateHolder.Loading(
|
||||
toolbarState = TokensListToolbarState.Title.ManageAccess(
|
||||
toolbarState = TokensListToolbarState.Title.Manage(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
onAddCustomTokenClick = {},
|
||||
),
|
||||
tokens = emptyFlow(),
|
||||
onTokensLoadStateChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -139,21 +138,28 @@ private fun Preview_AddTokensScreen_Loading() {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddTokensScreen_ManageAccess() {
|
||||
private fun Preview_TokensListScreen_Manage() {
|
||||
TangemTheme {
|
||||
AddTokensScreen(
|
||||
stateHolder = TokensListStateHolder.ManageAccess(
|
||||
toolbarState = TokensListToolbarState.Title.ManageAccess(
|
||||
TokensListScreen(
|
||||
stateHolder = TokensListStateHolder.ManageContent(
|
||||
toolbarState = TokensListToolbarState.Title.Manage(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
onAddCustomTokenClick = {},
|
||||
),
|
||||
tokens = persistentListOf(
|
||||
TokenListPreviewData.createManageToken(),
|
||||
TokenListPreviewData.createManageToken(),
|
||||
),
|
||||
tokens = flow {
|
||||
emit(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TokenListPreviewData.createManageToken(),
|
||||
TokenListPreviewData.createManageToken(),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onSaveButtonClick = {},
|
||||
onTokensLoadStateChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -161,19 +167,26 @@ private fun Preview_AddTokensScreen_ManageAccess() {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddTokensScreen_ReadAccess() {
|
||||
private fun Preview_TokensListScreen_Read() {
|
||||
TangemTheme {
|
||||
AddTokensScreen(
|
||||
stateHolder = TokensListStateHolder.ReadAccess(
|
||||
toolbarState = TokensListToolbarState.Title.ReadAccess(
|
||||
TokensListScreen(
|
||||
stateHolder = TokensListStateHolder.ReadContent(
|
||||
toolbarState = TokensListToolbarState.Title.Read(
|
||||
titleResId = R.string.search_tokens_title,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
),
|
||||
tokens = persistentListOf(
|
||||
TokenListPreviewData.createReadToken(),
|
||||
TokenListPreviewData.createReadToken(),
|
||||
),
|
||||
tokens = flow {
|
||||
emit(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TokenListPreviewData.createManageToken(),
|
||||
TokenListPreviewData.createManageToken(),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onTokensLoadStateChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ import androidx.compose.ui.unit.sp
|
|||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokensListToolbarState
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokensListToolbarState.SearchInputField
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokensListToolbarState.InputField
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokensListToolbarState.Title
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -43,7 +43,7 @@ import kotlinx.coroutines.delay
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddTokensToolbar(state: TokensListToolbarState) {
|
||||
internal fun TokensListToolbar(state: TokensListToolbarState) {
|
||||
TopAppBar(backgroundColor = TangemTheme.colors.background.secondary) {
|
||||
IconButton(onClick = state.onBackButtonClick) {
|
||||
Icon(
|
||||
|
|
@ -55,7 +55,7 @@ internal fun AddTokensToolbar(state: TokensListToolbarState) {
|
|||
|
||||
when (state) {
|
||||
is Title -> TitleContent(state = state, modifier = Modifier.weight(1f))
|
||||
is SearchInputField -> InputContent(state = state, modifier = Modifier.weight(1f))
|
||||
is InputField -> InputContent(state = state, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ private fun TitleContent(state: Title, modifier: Modifier = Modifier) {
|
|||
)
|
||||
}
|
||||
|
||||
if (state is Title.ManageAccess) {
|
||||
if (state is Title.Manage) {
|
||||
IconButton(onClick = state.onAddCustomTokenClick) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_plus_24),
|
||||
|
|
@ -91,8 +91,9 @@ private fun TitleContent(state: Title, modifier: Modifier = Modifier) {
|
|||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun InputContent(state: SearchInputField, modifier: Modifier = Modifier) {
|
||||
private fun InputContent(state: InputField, modifier: Modifier = Modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
|
|
@ -100,7 +101,7 @@ private fun InputContent(state: SearchInputField, modifier: Modifier = Modifier)
|
|||
modifier = modifier.focusRequester(focusRequester),
|
||||
textStyle = TangemTheme.typography.subtitle1.copy(fontWeight = FontWeight.Normal),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { state.onSearchButtonClick() }),
|
||||
keyboardActions = KeyboardActions(onSearch = { keyboardController?.hide() }),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
cursorBrush = SolidColor(value = TangemTheme.colors.stroke.secondary),
|
||||
|
|
@ -168,8 +169,8 @@ private fun Hint(value: String) {
|
|||
@Composable
|
||||
private fun Preview_AddTokensToolbar_EditAccess() {
|
||||
TangemTheme {
|
||||
AddTokensToolbar(
|
||||
state = Title.ManageAccess(
|
||||
TokensListToolbar(
|
||||
state = Title.Manage(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
|
|
@ -183,8 +184,8 @@ private fun Preview_AddTokensToolbar_EditAccess() {
|
|||
@Composable
|
||||
private fun Preview_AddTokensToolbar_ReadAccess() {
|
||||
TangemTheme {
|
||||
AddTokensToolbar(
|
||||
state = Title.ReadAccess(
|
||||
TokensListToolbar(
|
||||
state = Title.Read(
|
||||
titleResId = R.string.search_tokens_title,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
|
|
@ -199,10 +200,9 @@ private fun Preview_AddTokensToolbar_SearchInputField() {
|
|||
var value by remember { mutableStateOf("") }
|
||||
|
||||
TangemTheme {
|
||||
AddTokensToolbar(
|
||||
state = SearchInputField(
|
||||
TokensListToolbar(
|
||||
state = InputField(
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
onCleanButtonClick = { value = "" },
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.tap.features.tokens.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 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -3,27 +3,26 @@ package com.tangem.tap.features.tokens.presentation.viewmodels
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.map
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.domain.common.extensions.canHandleToken
|
||||
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.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchNotification
|
||||
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.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.tokens.Contract
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.domain.TokensListInteractor
|
||||
import com.tangem.tap.features.tokens.domain.models.Token
|
||||
import com.tangem.tap.features.tokens.domain.models.Token.Network
|
||||
import com.tangem.tap.features.tokens.presentation.models.TokensListArgs
|
||||
import com.tangem.tap.features.tokens.presentation.router.TokensListRouter
|
||||
import com.tangem.tap.features.tokens.presentation.states.NetworkItemState
|
||||
import com.tangem.tap.features.tokens.presentation.states.TokenItemState
|
||||
|
|
@ -31,317 +30,328 @@ import com.tangem.tap.features.tokens.presentation.states.TokensListStateHolder
|
|||
import com.tangem.tap.features.tokens.presentation.states.TokensListToolbarState
|
||||
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.redux.TokensAction
|
||||
import com.tangem.tap.features.tokens.redux.TokensState
|
||||
import com.tangem.tap.features.tokens.ui.compose.fullName
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
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.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
import com.tangem.blockchain.common.Token as BlockchainToken
|
||||
|
||||
/**
|
||||
* ViewModel for add tokens screen
|
||||
* ViewModel for tokens list screen
|
||||
*
|
||||
* @property router feature router
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property interactor feature interactor
|
||||
* @property router feature router
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property reduxStateHolder redux state holder
|
||||
* @param analyticsEventHandler analytics event handler
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@HiltViewModel
|
||||
internal class TokensListViewModel @Inject constructor(
|
||||
private val interactor: TokensListInteractor,
|
||||
private val router: TokensListRouter,
|
||||
private val dispatchers: AppCoroutineDispatcherProvider,
|
||||
private val reduxStateHolder: AppStateHolder,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel() {
|
||||
|
||||
private val uiStateBuilder = UiStateBuilder()
|
||||
private val args = TokensListArgs()
|
||||
private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler)
|
||||
private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer())
|
||||
|
||||
var uiState by mutableStateOf(uiStateBuilder.getInitialUiState())
|
||||
/** Screen state */
|
||||
var uiState by mutableStateOf(value = getInitialUiState())
|
||||
private set
|
||||
|
||||
private val reduxSubscriber = ReduxSubscriber()
|
||||
private var debounceJob: Job? = null
|
||||
private val addedTokenList: MutableList<TokenWithBlockchain> = args.mainScreenTokenList.toMutableList()
|
||||
private val addedBlockchainList: MutableList<Blockchain> = args.mainScreenBlockchainList.toMutableList()
|
||||
|
||||
private var originalAddedTokenList: List<TokenWithBlockchain>? = null
|
||||
private var originalAddedBlockchainList: List<Blockchain>? = null
|
||||
private var addedTokenList: List<TokenWithBlockchain>? = null
|
||||
private var addedBlockchainList: List<Blockchain>? = null
|
||||
|
||||
fun subscribeOnReduxEvents(lifecycle: Lifecycle) {
|
||||
lifecycle.addObserver(reduxSubscriber)
|
||||
private fun getInitialUiState(): TokensListStateHolder {
|
||||
return TokensListStateHolder.Loading(
|
||||
toolbarState = getInitialToolbarState(),
|
||||
tokens = getInitialTokensList(),
|
||||
onTokensLoadStateChanged = actionsHandler::onTokensLoadStateChanged,
|
||||
)
|
||||
}
|
||||
|
||||
private object AnalyticsSender {
|
||||
fun sendWhenTokenAdded(token: Token) {
|
||||
Analytics.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Token(token),
|
||||
state = AnalyticsParam.OnOffState.On,
|
||||
),
|
||||
private fun getInitialToolbarState(): TokensListToolbarState {
|
||||
return if (args.isManageAccess) {
|
||||
TokensListToolbarState.Title.Manage(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = actionsHandler::onBackButtonClick,
|
||||
onSearchButtonClick = actionsHandler::onSearchButtonClick,
|
||||
onAddCustomTokenClick = actionsHandler::onAddCustomTokenClick,
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenBlockchainAdded(blockchain: Blockchain) {
|
||||
Analytics.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Blockchain(blockchain),
|
||||
state = AnalyticsParam.OnOffState.On,
|
||||
),
|
||||
} else {
|
||||
TokensListToolbarState.Title.Read(
|
||||
titleResId = R.string.search_tokens_title,
|
||||
onBackButtonClick = actionsHandler::onBackButtonClick,
|
||||
onSearchButtonClick = actionsHandler::onSearchButtonClick,
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenTokenRemoved(token: Token) {
|
||||
Analytics.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Token(token),
|
||||
state = AnalyticsParam.OnOffState.Off,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenBlockchainRemoved(blockchain: Blockchain) {
|
||||
Analytics.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Blockchain(blockchain),
|
||||
state = AnalyticsParam.OnOffState.Off,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenSaveButtonClicked() {
|
||||
Analytics.send(ManageTokens.ButtonSaveChanges())
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ReduxSubscriber : DefaultLifecycleObserver, StoreSubscriber<TokensState> {
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
store.subscribe(
|
||||
subscriber = this,
|
||||
transform = { state ->
|
||||
state
|
||||
.skipRepeats { old, new -> old.tokensState == new.tokensState }
|
||||
.select(AppState::tokensState)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
store.unsubscribe(subscriber = this)
|
||||
}
|
||||
|
||||
override fun newState(state: TokensState) {
|
||||
val isNotOriginalListsInitialized = originalAddedTokenList == null || originalAddedBlockchainList == null
|
||||
val isNotCurrentListsInitialized = addedTokenList == null || addedBlockchainList == null
|
||||
|
||||
if (isNotOriginalListsInitialized || isNotCurrentListsInitialized) {
|
||||
originalAddedTokenList = state.addedTokens
|
||||
originalAddedBlockchainList = state.addedBlockchains
|
||||
addedTokenList = state.addedTokens
|
||||
addedBlockchainList = state.addedBlockchains
|
||||
private fun getInitialTokensList(searchText: String = ""): Flow<PagingData<TokenItemState>> {
|
||||
return interactor.getTokensList(searchText = searchText).map {
|
||||
it.map { token ->
|
||||
if (args.isManageAccess) createManageTokenContent(token) else createReadTokenContent(token)
|
||||
}
|
||||
|
||||
uiStateBuilder.updateStateByReduxState(state)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class UiStateBuilder {
|
||||
private fun createManageTokenContent(token: Token): TokenItemState.ManageContent {
|
||||
return TokenItemState.ManageContent(
|
||||
name = getTokenName(token),
|
||||
iconUrl = token.iconUrl,
|
||||
networks = token.networks.map(::createManageNetworkContent).toImmutableList(),
|
||||
id = token.id,
|
||||
symbol = token.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
fun getInitialUiState(): TokensListStateHolder {
|
||||
return TokensListStateHolder.Loading(toolbarState = getToolbarState())
|
||||
private fun createManageNetworkContent(network: Network): NetworkItemState.ManageContent {
|
||||
return NetworkItemState.ManageContent(
|
||||
name = network.blockchain.fullNameWithoutTestnet.uppercase(),
|
||||
protocolName = getNetworkProtocolName(network),
|
||||
iconResId = mutableStateOf(
|
||||
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 {
|
||||
return TokenItemState.ReadContent(
|
||||
name = getTokenName(token),
|
||||
iconUrl = token.iconUrl,
|
||||
networks = token.networks.map(::createReadNetworkContent).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createReadNetworkContent(network: Network): NetworkItemState.ReadContent {
|
||||
return NetworkItemState.ReadContent(
|
||||
name = network.blockchain.fullNameWithoutTestnet.uppercase(),
|
||||
protocolName = getNetworkProtocolName(network),
|
||||
iconResId = mutableStateOf(getNetworkIconResId(network.address, network.blockchain)),
|
||||
isMainNetwork = isMainNetwork(network),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTokenName(token: Token) = "${token.name} (${token.symbol})"
|
||||
|
||||
private fun getNetworkProtocolName(network: Network): String {
|
||||
return if (network.address == null) {
|
||||
MAIN_NETWORK_LABEL
|
||||
} else {
|
||||
network.blockchain.getNetworkName().uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNetworkIconResId(address: String?, blockchain: Blockchain): Int {
|
||||
return if (isAdded(address = address, blockchain = blockchain)) {
|
||||
getActiveIconRes(blockchain.id)
|
||||
} else {
|
||||
blockchain.getGreyedOutIconRes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMainNetwork(network: Network) = network.address == null
|
||||
|
||||
private fun isAdded(address: String?, blockchain: Blockchain?): Boolean {
|
||||
return if (address != null) {
|
||||
addedTokenList.any { addedToken ->
|
||||
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain
|
||||
}
|
||||
} else {
|
||||
addedBlockchainList.contains(blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ActionsHandler(
|
||||
private val router: TokensListRouter,
|
||||
private val debouncer: Debouncer,
|
||||
) {
|
||||
|
||||
fun onBackButtonClick() {
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
fun updateStateByReduxState(state: TokensState) {
|
||||
uiState = if (state.currencies.isEmpty()) {
|
||||
TokensListStateHolder.Loading(toolbarState = getToolbarState(reduxState = state))
|
||||
} else if (state.allowToAdd) {
|
||||
TokensListStateHolder.ManageAccess(
|
||||
toolbarState = getToolbarState(reduxState = state),
|
||||
tokens = state.currencies.map(::toManageTokenItemState).toImmutableList(),
|
||||
onSaveButtonClick = {
|
||||
val notNullAddedTokens = addedTokenList ?: return@ManageAccess
|
||||
val notNullAddedBlockchains = addedBlockchainList ?: return@ManageAccess
|
||||
fun onSearchButtonClick() {
|
||||
uiState = uiState.copySealed(
|
||||
toolbarState = TokensListToolbarState.InputField(
|
||||
onBackButtonClick = this::onBackButtonClick,
|
||||
value = "",
|
||||
onValueChange = this::onSearchValueChange,
|
||||
onCleanButtonClick = this::onCleanButtonClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AnalyticsSender.sendWhenSaveButtonClicked()
|
||||
store.dispatch(TokensAction.SaveChanges(notNullAddedTokens, notNullAddedBlockchains))
|
||||
},
|
||||
)
|
||||
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 {
|
||||
TokensListStateHolder.ReadAccess(
|
||||
toolbarState = getToolbarState(state),
|
||||
tokens = state.currencies.map(::toReadTokenItemState).toImmutableList(),
|
||||
)
|
||||
updateTokenItem(toggledToken, toggledNetwork)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getToolbarState(reduxState: TokensState = store.state.tokensState): TokensListToolbarState {
|
||||
val toolbarState = uiState.toolbarState
|
||||
|
||||
return if (reduxState.searchInput != null && toolbarState is TokensListToolbarState.SearchInputField) {
|
||||
TokensListToolbarState.SearchInputField(
|
||||
onBackButtonClick = router::popBackStack,
|
||||
onSearchButtonClick = ::onSearchButtonClick,
|
||||
value = toolbarState.value,
|
||||
onValueChange = ::onSearchValueChange,
|
||||
onCleanButtonClick = ::onCleanButtonClick,
|
||||
)
|
||||
} else if (reduxState.allowToAdd) {
|
||||
TokensListToolbarState.Title.ManageAccess(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = router::popBackStack,
|
||||
onSearchButtonClick = ::onSearchButtonClick,
|
||||
onAddCustomTokenClick = router::openAddCustomTokenScreen,
|
||||
)
|
||||
} else {
|
||||
TokensListToolbarState.Title.ReadAccess(
|
||||
titleResId = R.string.search_tokens_title,
|
||||
onBackButtonClick = router::popBackStack,
|
||||
onSearchButtonClick = ::onSearchButtonClick,
|
||||
)
|
||||
}
|
||||
// FIXME("Necessary to avoid using redux actions")
|
||||
fun onNetworkClick() {
|
||||
store.dispatchNotification(R.string.contract_address_copied_message)
|
||||
}
|
||||
|
||||
private fun onSearchButtonClick() {
|
||||
uiState = when (val state = uiState.toolbarState) {
|
||||
is TokensListToolbarState.Title -> {
|
||||
uiState.copySealed(
|
||||
toolbarState = TokensListToolbarState.SearchInputField(
|
||||
onBackButtonClick = uiState.toolbarState.onBackButtonClick,
|
||||
onSearchButtonClick = uiState.toolbarState.onSearchButtonClick,
|
||||
value = "",
|
||||
onValueChange = ::onSearchValueChange,
|
||||
onCleanButtonClick = ::onCleanButtonClick,
|
||||
),
|
||||
)
|
||||
fun onTokensLoadStateChanged(state: LoadState) {
|
||||
uiState = when (state) {
|
||||
is LoadState.NotLoading -> {
|
||||
analyticsSender.sendWhenTokenSearched()
|
||||
|
||||
if (args.isManageAccess) {
|
||||
TokensListStateHolder.ManageContent(
|
||||
toolbarState = uiState.toolbarState,
|
||||
tokens = uiState.tokens,
|
||||
onTokensLoadStateChanged = uiState.onTokensLoadStateChanged,
|
||||
onSaveButtonClick = this::onSaveButtonClick,
|
||||
)
|
||||
} else {
|
||||
TokensListStateHolder.ReadContent(
|
||||
toolbarState = uiState.toolbarState,
|
||||
tokens = uiState.tokens,
|
||||
onTokensLoadStateChanged = uiState.onTokensLoadStateChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
is TokensListToolbarState.SearchInputField -> {
|
||||
store.dispatch(TokensAction.SetSearchInput(searchInput = state.value))
|
||||
uiState.copySealed(toolbarState = getToolbarState())
|
||||
else -> {
|
||||
TokensListStateHolder.Loading(
|
||||
toolbarState = uiState.toolbarState,
|
||||
tokens = uiState.tokens,
|
||||
onTokensLoadStateChanged = uiState.onTokensLoadStateChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchValueChange(newValue: String) {
|
||||
(uiState.toolbarState as? TokensListToolbarState.SearchInputField)?.let { state ->
|
||||
uiState = uiState.copySealed(toolbarState = state.copy(value = newValue))
|
||||
}
|
||||
val state = requireNotNull(uiState.toolbarState as? TokensListToolbarState.InputField)
|
||||
uiState = uiState.copySealed(toolbarState = state.copy(value = newValue))
|
||||
|
||||
debounceJob?.cancel()
|
||||
debounceJob = viewModelScope.launch(dispatchers.io) {
|
||||
delay(timeMillis = 800L)
|
||||
store.dispatch(TokensAction.SetSearchInput(searchInput = newValue))
|
||||
debouncer.debounce(waitMs = 800L, coroutineScope = viewModelScope + dispatchers.io) {
|
||||
uiState = uiState.copySealed(tokens = getInitialTokensList(newValue))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCleanButtonClick() {
|
||||
(uiState.toolbarState as? TokensListToolbarState.SearchInputField)?.let { state ->
|
||||
if (state.value.isEmpty()) {
|
||||
uiState = uiState.copySealed(toolbarState = getToolbarState())
|
||||
} else {
|
||||
onSearchValueChange(newValue = "")
|
||||
}
|
||||
val state = requireNotNull(uiState.toolbarState as? TokensListToolbarState.InputField)
|
||||
|
||||
if (state.value.isEmpty()) {
|
||||
uiState = uiState.copySealed(toolbarState = getInitialToolbarState())
|
||||
} else {
|
||||
onSearchValueChange(newValue = "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onToggleClick(tokenId: String, networkId: String) {
|
||||
(uiState as? TokensListStateHolder.ManageAccess)?.let { state ->
|
||||
val switchedToken = state.tokens.firstOrNull { it.id == tokenId }
|
||||
val contractAddress = switchedToken?.networks
|
||||
?.firstOrNull { it.networkId == networkId }
|
||||
?.contractAddress
|
||||
private fun updateBlockchainItem(toggledNetwork: NetworkItemState.ManageContent, blockchain: Blockchain) {
|
||||
val isRemoveAction = addedBlockchainList.contains(blockchain)
|
||||
|
||||
val blockchain = Blockchain.fromNetworkId(
|
||||
networkId = if (contractAddress == null) networkId else tokenId,
|
||||
)
|
||||
if (isRemoveAction) {
|
||||
val isTokenWithSameBlockchainFound = addedTokenList.any { it.blockchain == blockchain }
|
||||
val isAddedOnMainScreen = args.mainScreenBlockchainList.contains(blockchain)
|
||||
|
||||
if (blockchain != null && contractAddress == null) {
|
||||
onBlockchainToggleClick(blockchain)
|
||||
} else if (contractAddress != null) {
|
||||
onTokenToggleClick(tokenId, networkId, switchedToken, contractAddress)
|
||||
}
|
||||
|
||||
updateTokensList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBlockchainToggleClick(blockchain: Blockchain) {
|
||||
val isRemoveAction = addedBlockchainList?.contains(blockchain)
|
||||
|
||||
if (isRemoveAction == true) {
|
||||
val isTokenWithSameBlockchainFound = addedTokenList?.any { it.blockchain == blockchain }
|
||||
val isAddedOnMainScreen = originalAddedBlockchainList?.contains(blockchain)
|
||||
|
||||
if (isTokenWithSameBlockchainFound == true) {
|
||||
if (isTokenWithSameBlockchainFound) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.TokensAreLinkedDialog(
|
||||
currencyTitle = blockchain.name,
|
||||
currencySymbol = blockchain.currency,
|
||||
),
|
||||
)
|
||||
} else if (isAddedOnMainScreen == true) {
|
||||
} else if (isAddedOnMainScreen) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.RemoveWalletDialog(
|
||||
currencyTitle = blockchain.name,
|
||||
onOk = {
|
||||
AnalyticsSender.sendWhenBlockchainAdded(blockchain)
|
||||
addedBlockchainList = addedBlockchainList?.minus(blockchain)
|
||||
updateTokensList()
|
||||
analyticsSender.sendWhenBlockchainAdded(blockchain)
|
||||
addedBlockchainList.remove(blockchain)
|
||||
toggledNetwork.changeToggleState()
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
AnalyticsSender.sendWhenBlockchainRemoved(blockchain)
|
||||
addedBlockchainList = addedBlockchainList?.minus(blockchain)
|
||||
analyticsSender.sendWhenBlockchainRemoved(blockchain)
|
||||
addedBlockchainList.remove(blockchain)
|
||||
toggledNetwork.changeToggleState()
|
||||
}
|
||||
} else {
|
||||
AnalyticsSender.sendWhenBlockchainAdded(blockchain)
|
||||
addedBlockchainList = addedBlockchainList?.plus(blockchain)
|
||||
analyticsSender.sendWhenBlockchainAdded(blockchain)
|
||||
addedBlockchainList.add(blockchain)
|
||||
toggledNetwork.changeToggleState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTokenToggleClick(
|
||||
tokenId: String,
|
||||
networkId: String,
|
||||
switchedToken: TokenItemState.ManageAccess,
|
||||
contractAddress: String,
|
||||
private fun updateTokenItem(
|
||||
toggledToken: TokenItemState.ManageContent,
|
||||
toggledNetwork: NetworkItemState.ManageContent,
|
||||
) {
|
||||
val currency = store.state.tokensState.currencies.firstOrNull { it.id == tokenId } ?: return
|
||||
val contract = currency.contracts.firstOrNull { it.networkId == networkId } ?: return
|
||||
val token = TokenWithBlockchain(
|
||||
token = Token(
|
||||
id = switchedToken.id,
|
||||
name = switchedToken.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = contractAddress,
|
||||
decimals = requireNotNull(contract.decimalCount),
|
||||
token = BlockchainToken(
|
||||
id = toggledToken.id,
|
||||
name = toggledToken.name,
|
||||
symbol = toggledToken.symbol,
|
||||
contractAddress = requireNotNull(toggledNetwork.address),
|
||||
decimals = requireNotNull(toggledNetwork.decimalCount),
|
||||
),
|
||||
blockchain = contract.blockchain,
|
||||
blockchain = toggledNetwork.blockchain,
|
||||
)
|
||||
val isUnsupportedToken = !store.state.tokensState.canHandleToken(token)
|
||||
val isRemoveAction = addedTokenList?.contains(token)
|
||||
val isAddedOnMainScreen = originalAddedTokenList?.contains(token)
|
||||
if (isRemoveAction == true) {
|
||||
if (isAddedOnMainScreen == true) {
|
||||
|
||||
val isRemoveAction = addedTokenList.contains(token)
|
||||
|
||||
if (isRemoveAction) {
|
||||
val isAddedOnMainScreen = args.mainScreenTokenList.contains(token)
|
||||
|
||||
if (isAddedOnMainScreen) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.RemoveWalletDialog(
|
||||
currencyTitle = token.token.name,
|
||||
onOk = {
|
||||
AnalyticsSender.sendWhenTokenRemoved(token.token)
|
||||
addedTokenList = addedTokenList?.minus(token)
|
||||
updateTokensList()
|
||||
analyticsSender.sendWhenTokenRemoved(token.token)
|
||||
addedTokenList.remove(token)
|
||||
toggledNetwork.changeToggleState()
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
AnalyticsSender.sendWhenTokenRemoved(token.token)
|
||||
addedTokenList = addedTokenList?.minus(token)
|
||||
analyticsSender.sendWhenTokenRemoved(token.token)
|
||||
addedTokenList.remove(token)
|
||||
toggledNetwork.changeToggleState()
|
||||
}
|
||||
} else {
|
||||
val isUnsupportedToken =
|
||||
!(reduxStateHolder.scanResponse?.card?.canHandleToken(token.blockchain) ?: false)
|
||||
|
||||
if (isUnsupportedToken) {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
|
|
@ -350,100 +360,16 @@ internal class TokensListViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
} else {
|
||||
AnalyticsSender.sendWhenTokenAdded(token.token)
|
||||
addedTokenList = addedTokenList?.plus(token)
|
||||
analyticsSender.sendWhenTokenAdded(token.token)
|
||||
addedTokenList.add(token)
|
||||
toggledNetwork.changeToggleState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokensList() {
|
||||
(uiState as? TokensListStateHolder.ManageAccess)?.let { state ->
|
||||
val tokens = store.state.tokensState.currencies.mapNotNull { currency ->
|
||||
state.tokens.firstOrNull { it.id == currency.id }?.copy(
|
||||
networks = currency.contracts.mapNotNull { contract ->
|
||||
state.tokens.firstOrNull { it.id == currency.id }
|
||||
?.networks?.firstOrNull { it.networkId == contract.networkId }
|
||||
?.copy(
|
||||
isAdded = contract.isAdded(),
|
||||
iconResId = if (contract.isAdded()) {
|
||||
getActiveIconRes(contract.blockchain.id)
|
||||
} else {
|
||||
contract.blockchain.getGreyedOutIconRes()
|
||||
},
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
uiState = state.copy(tokens = tokens)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toManageTokenItemState(currency: Currency): TokenItemState.ManageAccess {
|
||||
return TokenItemState.ManageAccess(
|
||||
name = currency.fullName,
|
||||
iconUrl = currency.iconUrl,
|
||||
networks = currency.contracts.map(::toManageNetworkItemState).toImmutableList(),
|
||||
id = currency.id,
|
||||
)
|
||||
}
|
||||
|
||||
private fun toManageNetworkItemState(contract: Contract): NetworkItemState.ManageAccess {
|
||||
val isAdded = contract.isAdded()
|
||||
return NetworkItemState.ManageAccess(
|
||||
name = contract.blockchain.fullNameWithoutTestnet.uppercase(),
|
||||
protocolName = if (contract.address == null) {
|
||||
MAIN_NETWORK_LABEL
|
||||
} else {
|
||||
contract.blockchain.getNetworkName().uppercase()
|
||||
},
|
||||
iconResId = if (isAdded) {
|
||||
getActiveIconRes(contract.blockchain.id)
|
||||
} else {
|
||||
contract.blockchain.getGreyedOutIconRes()
|
||||
},
|
||||
isMainNetwork = contract.address == null,
|
||||
isAdded = isAdded,
|
||||
networkId = contract.networkId,
|
||||
contractAddress = contract.address,
|
||||
onToggleClick = ::onToggleClick,
|
||||
onNetworkClick = { store.dispatchNotification(R.string.contract_address_copied_message) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun toReadTokenItemState(currency: Currency): TokenItemState.ReadAccess {
|
||||
return TokenItemState.ReadAccess(
|
||||
name = currency.fullName,
|
||||
iconUrl = currency.iconUrl,
|
||||
networks = currency.contracts.map(::toReadNetworkItemState).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun toReadNetworkItemState(contract: Contract): NetworkItemState.ReadAccess {
|
||||
return NetworkItemState.ReadAccess(
|
||||
name = contract.blockchain.fullNameWithoutTestnet.uppercase(),
|
||||
protocolName = if (contract.address == null) {
|
||||
MAIN_NETWORK_LABEL
|
||||
} else {
|
||||
contract.blockchain.getNetworkName().uppercase()
|
||||
},
|
||||
iconResId = if (contract.isAdded()) {
|
||||
getActiveIconRes(contract.blockchain.id)
|
||||
} else {
|
||||
contract.blockchain.getGreyedOutIconRes()
|
||||
},
|
||||
isMainNetwork = contract.address == null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Contract.isAdded(): Boolean {
|
||||
return if (address != null) {
|
||||
addedTokenList?.any { addedToken ->
|
||||
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain
|
||||
}
|
||||
} else {
|
||||
addedBlockchainList?.contains(blockchain)
|
||||
} ?: false
|
||||
private fun onSaveButtonClick() {
|
||||
analyticsSender.sendWhenSaveButtonClicked()
|
||||
store.dispatch(TokensAction.SaveChanges(addedTokenList, addedBlockchainList))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,35 +113,30 @@ class TokensMiddleware {
|
|||
val scanResponse = store.state.globalState.scanResponse ?: return@launch
|
||||
|
||||
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
|
||||
scanResponse.card.derivationStyle,
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
)
|
||||
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
|
||||
scanResponse.card.derivationStyle,
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
)
|
||||
|
||||
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
|
||||
val blockchainsToRemove =
|
||||
currentBlockchains.filter { !action.addedBlockchains.contains(it) }
|
||||
val blockchainsToAdd = action.addedBlockchains.filterNot(currentBlockchains::contains)
|
||||
val blockchainsToRemove = currentBlockchains.filterNot(action.addedBlockchains::contains)
|
||||
|
||||
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it) }
|
||||
val tokensToRemove = currentTokens.filter { token ->
|
||||
!action.addedTokens.any { it.token == token.token }
|
||||
}
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
val tokensToAdd = action.addedTokens.filterNot(currentTokens::contains)
|
||||
val tokensToRemove = currentTokens.filterNot { token -> action.addedTokens.any { it.token == token.token } }
|
||||
|
||||
removeCurrenciesIfNeeded(
|
||||
convertToCurrencies(
|
||||
currencies = convertToCurrencies(
|
||||
blockchains = blockchainsToRemove,
|
||||
tokens = tokensToRemove,
|
||||
derivationStyle = derivationStyle,
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
if (tokensToAdd.isEmpty() && tokensToRemove.isEmpty() &&
|
||||
blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
||||
) {
|
||||
store.dispatchDebugErrorNotification("Nothing to save")
|
||||
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
|
||||
}
|
||||
|
|
@ -149,8 +144,9 @@ class TokensMiddleware {
|
|||
val currencyList = convertToCurrencies(
|
||||
blockchains = blockchainsToAdd,
|
||||
tokens = tokensToAdd,
|
||||
derivationStyle = derivationStyle,
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
)
|
||||
|
||||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||
submitAdd(it, currencyList)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue