diff --git a/app/src/main/java/com/tangem/tap/features/tokens/di/TokensListRepositoryModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/di/TokensListRepositoryModule.kt new file mode 100644 index 0000000000..e71a3a6531 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/di/TokensListRepositoryModule.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/TokensListFragment.kt index 0c66bbcbea..55615e908b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/TokensListFragment.kt @@ -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().apply { - subscribeOnReduxEvents(lifecycle = LocalLifecycleOwner.current.lifecycle) - } + val viewModel = hiltViewModel() TangemTheme { - AddTokensScreen(stateHolder = viewModel.uiState) + TokensListScreen(stateHolder = viewModel.uiState) } } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/models/TokensListArgs.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/models/TokensListArgs.kt new file mode 100644 index 0000000000..de2b9a97bf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/models/TokensListArgs.kt @@ -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 get() = store.state.tokensState.addedTokens + + /** Blockchains list that accessible from the main screen */ + val mainScreenBlockchainList: List get() = store.state.tokensState.addedBlockchains +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/NetworkItemState.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/NetworkItemState.kt index c11142b85a..c6967c78e4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/NetworkItemState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/NetworkItemState.kt @@ -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 /** 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, 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, override val isMainNetwork: Boolean, - val isAdded: Boolean, - val networkId: String, - val contractAddress: ContractAddress?, - val onToggleClick: (String, String) -> Unit, + val isAdded: MutableState, + 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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemState.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemState.kt index 5ce98ed4f0..e3ab7e2553 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemState.kt @@ -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, + override val networks: ImmutableList, ) : 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, + override val networks: ImmutableList, val id: String, + val symbol: String, ) : TokenItemState } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListStateHolder.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListStateHolder.kt index 0cf5c4d03a..c4049ddfcc 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListStateHolder.kt @@ -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> + + /** 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> = 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>, + 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, - ) : TokensListStateHolder, TokensListVisibility + override val tokens: Flow>, + 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, + override val tokens: Flow>, + override val onTokensLoadStateChanged: (LoadState) -> Unit, val onSaveButtonClick: () -> Unit, - ) : TokensListStateHolder, TokensListVisibility + ) : TokensListStateHolder } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListToolbarState.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListToolbarState.kt index 532c0739cf..0fff69aabd 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListToolbarState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListToolbarState.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListVisibility.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListVisibility.kt deleted file mode 100644 index 4062066ad3..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListVisibility.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt index e44b3def87..bd3605c70e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt @@ -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), ) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt index e404aec20e..bdf39d31e9 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt @@ -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, 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(), ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt index ba0b7959ec..bd6b760fd5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenListPreviewData.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenListPreviewData.kt index 8eb5cd50ee..efed29fd37 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenListPreviewData.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenListPreviewData.kt @@ -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 { + fun createManageNetworksList(): ImmutableList { 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 { + fun createReadNetworksList(): ImmutableList { 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, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokensListScreen.kt similarity index 60% rename from app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensScreen.kt rename to app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokensListScreen.kt index a691d057e6..977e5705bf 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokensListScreen.kt @@ -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, 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 = {}, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokensListToolbar.kt similarity index 91% rename from app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensToolbar.kt rename to app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokensListToolbar.kt index a79d8cb8b4..f3bb8bbafd 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokensListToolbar.kt @@ -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 = "" }, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/TokensListAnalyticsSender.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/TokensListAnalyticsSender.kt new file mode 100644 index 0000000000..615f039a8f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/TokensListAnalyticsSender.kt @@ -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()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/TokensListViewModel.kt index a56973ee89..d09d68b5d5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/TokensListViewModel.kt @@ -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 = args.mainScreenTokenList.toMutableList() + private val addedBlockchainList: MutableList = args.mainScreenBlockchainList.toMutableList() - private var originalAddedTokenList: List? = null - private var originalAddedBlockchainList: List? = null - private var addedTokenList: List? = null - private var addedBlockchainList: List? = 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 { - - 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> { + 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)) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 9b3be6dca7..eee906b7d1 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -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)