From e2dcb369a37230d6e50755dca49e928affc3fd48 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 3 Apr 2023 19:53:43 +0800 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/common/extensions/Navigation.kt | 2 +- .../tokens/di/AddTokensRouterModule.kt | 21 ++++ .../{ui => presentation}/AddTokensFragment.kt | 30 ++--- .../presentation/router/AddTokensRouter.kt | 11 ++ .../router/DefaultAddTokensRouter.kt | 25 ++++ .../states/AddTokensNetworkItemState.kt | 55 +++++++++ .../states/AddTokensStateHolder.kt | 56 +++++++++ .../states/AddTokensToolbarState.kt | 63 ++++++++++ .../presentation/states/TokenItemModel.kt | 14 +++ .../states/TokensListVisibility.kt | 8 ++ .../viewmodels/AddTokensViewModel.kt | 113 ++++++++++++++++++ 11 files changed, 383 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/di/AddTokensRouterModule.kt rename app/src/main/java/com/tangem/tap/features/tokens/{ui => presentation}/AddTokensFragment.kt (89%) create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/router/AddTokensRouter.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/router/DefaultAddTokensRouter.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensNetworkItemState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensStateHolder.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensToolbarState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemModel.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListVisibility.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/AddTokensViewModel.kt diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 6cb707b071..0755764b78 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -26,7 +26,7 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.shop.ui.ShopFragment import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment -import com.tangem.tap.features.tokens.ui.AddTokensFragment +import com.tangem.tap.features.tokens.presentation.AddTokensFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment diff --git a/app/src/main/java/com/tangem/tap/features/tokens/di/AddTokensRouterModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/di/AddTokensRouterModule.kt new file mode 100644 index 0000000000..31e1d6acb3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/di/AddTokensRouterModule.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.tokens.di + +import com.tangem.tap.features.tokens.presentation.router.AddTokensRouter +import com.tangem.tap.features.tokens.presentation.router.DefaultAddTokensRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +/** +[REDACTED_AUTHOR] + */ +@Module +@InstallIn(ViewModelComponent::class) +internal object AddTokensRouterModule { + + @Provides + @ViewModelScoped + fun provideAddTokensRouter(): AddTokensRouter = DefaultAddTokensRouter() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/AddTokensFragment.kt similarity index 89% rename from app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt rename to app/src/main/java/com/tangem/tap/features/tokens/presentation/AddTokensFragment.kt index 955d0e73fd..c2e0aa1302 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/AddTokensFragment.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.tokens.ui +package com.tangem.tap.features.tokens.presentation import android.os.Bundle import android.view.Menu @@ -31,6 +31,7 @@ import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentAddTokensBinding +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -40,7 +41,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.rekotlin.StoreSubscriber -class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber { +@AndroidEntryPoint +internal class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber { private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind) private var tokensState: MutableState = mutableStateOf(store.state.tokensState) @@ -155,18 +157,18 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc tokensState.value = state binding.toolbar.title = getString(toolbarTitle) } -} -fun SearchView.inputtedTextAsFlow(): Flow = callbackFlow { - val watcher = setOnQueryTextListener( - object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean = false + private fun SearchView.inputtedTextAsFlow(): Flow = callbackFlow { + val watcher = setOnQueryTextListener( + object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean = false - override fun onQueryTextChange(newText: String?): Boolean { - trySend(newText ?: "") - return false - } - }, - ) - awaitClose { watcher } + override fun onQueryTextChange(newText: String?): Boolean { + trySend(newText ?: "") + return false + } + }, + ) + awaitClose { watcher } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/router/AddTokensRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/router/AddTokensRouter.kt new file mode 100644 index 0000000000..c0a4d91316 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/router/AddTokensRouter.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.tokens.presentation.router + +/** Add tokens feature router */ +internal interface AddTokensRouter { + + /** Return to last screen */ + fun popBackStack() + + /** Open screen for adding custom token */ + fun openAddCustomTokenScreen() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/router/DefaultAddTokensRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/router/DefaultAddTokensRouter.kt new file mode 100644 index 0000000000..c2543a86cb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/router/DefaultAddTokensRouter.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.features.tokens.presentation.router + +import com.tangem.core.analytics.Analytics +import com.tangem.tap.common.analytics.events.ManageTokens +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.tokens.redux.TokensAction +import com.tangem.tap.store + +/** + * Router implementation for add tokens feature + * +[REDACTED_AUTHOR] + */ +internal class DefaultAddTokensRouter : AddTokensRouter { + + override fun popBackStack() { + store.dispatch(NavigationAction.PopBackTo()) + store.dispatch(TokensAction.ResetState) + } + + override fun openAddCustomTokenScreen() { + Analytics.send(ManageTokens.ButtonCustomToken()) + store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensNetworkItemState.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensNetworkItemState.kt new file mode 100644 index 0000000000..c0df838d4d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensNetworkItemState.kt @@ -0,0 +1,55 @@ +package com.tangem.tap.features.tokens.presentation.states + +/** + * Network item state + * +[REDACTED_AUTHOR] + */ +sealed interface AddTokensNetworkItemState { + + /** Network name */ + val name: String + + /** Network protocol name */ + val protocolName: String + + /** Network icon id from resources */ + val iconResId: Int + + /** Flag that determines if the network is the main network for the token */ + val isMainNetwork: Boolean + + /** + * Network item state that is available for read + * + * @property name network name + * @property iconResId network icon id from resources + * @property isMainNetwork flag that determines if the network is the main network for the token + */ + data class ReadAccess( + override val name: String, + override val protocolName: String, + override val iconResId: Int, + override val isMainNetwork: Boolean, + ) : AddTokensNetworkItemState + + /** + * Network item state that is available for read and edit + * + * @property name network 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 onToggleClick lambda be invoked when switch is been toggled + */ + data class EditAccess( + override val name: String, + override val protocolName: String, + override val iconResId: Int, + override val isMainNetwork: Boolean, + val isAdded: Boolean, + val networkId: String, + val onToggleClick: (String) -> Unit, + ) : AddTokensNetworkItemState +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensStateHolder.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensStateHolder.kt new file mode 100644 index 0000000000..2db2b8bc59 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensStateHolder.kt @@ -0,0 +1,56 @@ +package com.tangem.tap.features.tokens.presentation.states + +/** + * State holder for adding token screen + * +[REDACTED_AUTHOR] + */ +internal sealed interface AddTokensStateHolder { + + /** Toolbar state */ + val toolbarState: AddTokensToolbarState + + /** + * Util function that allow to make a copy + * + * @param toolbarState toolbar state + */ + fun copySealed(toolbarState: AddTokensToolbarState): AddTokensStateHolder { + return when (this) { + is ManageAccess -> copy(toolbarState = toolbarState) + is Loading -> copy(toolbarState = toolbarState) + is ReadAccess -> copy(toolbarState = toolbarState) + } + } + + /** + * Loading state + * + * @property toolbarState toolbar state + */ + data class Loading(override val toolbarState: AddTokensToolbarState) : AddTokensStateHolder + + /** + * State screen that is available only for read + * + * @property toolbarState toolbar state + * @property tokens tokens list + */ + data class ReadAccess( + override val toolbarState: AddTokensToolbarState, + override val tokens: List, + ) : AddTokensStateHolder, TokensListVisibility + + /** + * State screen that is available for read and edit + * + * @property toolbarState toolbar state + * @property tokens tokens list + * @property onSaveButtonClick callback to be invoked when SaveButton is being clicked + */ + data class ManageAccess( + override val toolbarState: AddTokensToolbarState, + override val tokens: List, + val onSaveButtonClick: () -> Unit, + ) : AddTokensStateHolder, TokensListVisibility +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensToolbarState.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensToolbarState.kt new file mode 100644 index 0000000000..8a21094acc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/AddTokensToolbarState.kt @@ -0,0 +1,63 @@ +package com.tangem.tap.features.tokens.presentation.states + +/** Toolbar state */ +sealed interface AddTokensToolbarState { + + /** 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 : AddTokensToolbarState { + + /** Toolbar title id from resources */ + val titleResId: Int + + /** + * 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 onSearchButtonClick callback to be invoked when SearchButton is being clicked + */ + data class ReadAccess( + override val titleResId: Int, + override val onBackButtonClick: () -> Unit, + override val onSearchButtonClick: () -> Unit, + ) : Title + + /** + * Title state that is available for read and edit + * + * @property titleResId toolbar title id from resources + * @property onBackButtonClick callback to be invoked when BackButton is being clicked + * @property onSearchButtonClick callback to be invoked when SearchButton is being clicked + * @property onAddCustomTokenClick callback to be invoked when AddCustomTokenButton is being clicked + */ + data class EditAccess( + override val titleResId: Int, + override val onBackButtonClick: () -> Unit, + override val onSearchButtonClick: () -> Unit, + val onAddCustomTokenClick: () -> Unit, + ) : Title + } + + /** + * Toolbar state as input field + * + * @property onBackButtonClick callback to be invoked when BackButton is being clicked + * @property 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 + */ + data class SearchInputField( + override val onBackButtonClick: () -> Unit, + override val onSearchButtonClick: () -> Unit, + val value: String, + val onValueChange: (String) -> Unit, + val onCleanButtonClick: () -> Unit, + ) : AddTokensToolbarState +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemModel.kt new file mode 100644 index 0000000000..07e0af08f0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokenItemModel.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.tokens.presentation.states + +/** + * Token model + * + * @property name token name + * @property iconUrl token icon url + * @property networks list of networks + */ +data class TokenItemModel( + val name: String, + val iconUrl: String, + val networks: List, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..b1bd56a96e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/states/TokensListVisibility.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.tokens.presentation.states + +/** Marker interface for states that have a list of tokens */ +sealed interface TokensListVisibility { + + /** Tokens list */ + val tokens: List +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/AddTokensViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/AddTokensViewModel.kt new file mode 100644 index 0000000000..828ace57c6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/viewmodels/AddTokensViewModel.kt @@ -0,0 +1,113 @@ +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.LifecycleOwner +import androidx.lifecycle.ViewModel +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.tokens.presentation.router.AddTokensRouter +import com.tangem.tap.features.tokens.presentation.states.AddTokensStateHolder +import com.tangem.tap.features.tokens.presentation.states.AddTokensToolbarState +import com.tangem.tap.features.tokens.redux.TokensAction +import com.tangem.tap.features.tokens.redux.TokensState +import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.lifecycle.HiltViewModel +import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +/** + * ViewModel for add tokens screen + * + * @property router feature router + * +[REDACTED_AUTHOR] + */ +@HiltViewModel +internal class AddTokensViewModel @Inject constructor( + private val router: AddTokensRouter, +) : ViewModel(), DefaultLifecycleObserver, StoreSubscriber { + + var uiState by mutableStateOf(getInitialUiState()) + private set + + init { + loadContent() + } + + override fun onStop(owner: LifecycleOwner) { + store.unsubscribe(subscriber = this) + } + + override fun onDestroy(owner: LifecycleOwner) { + store.dispatch(TokensAction.ResetState) + } + + override fun newState(state: TokensState) { + uiState = uiState.copySealed( + toolbarState = getToolbarState(hasEditAccess = state.allowToAdd), + ) + } + + private fun loadContent() { + store.subscribe(subscriber = this) { state -> + state + .skipRepeats { old, new -> old.tokensState == new.tokensState } + .select(AppState::tokensState) + } + } + + private fun getInitialUiState(): AddTokensStateHolder { + return AddTokensStateHolder.Loading(toolbarState = getToolbarState(store.state.tokensState.allowToAdd)) + } + + private fun getToolbarState(hasEditAccess: Boolean): AddTokensToolbarState { + return if (hasEditAccess) { + AddTokensToolbarState.Title.EditAccess( + titleResId = R.string.main_manage_tokens, + onBackButtonClick = router::popBackStack, + onSearchButtonClick = ::onSearchButtonClick, + onAddCustomTokenClick = router::openAddCustomTokenScreen, + ) + } else { + AddTokensToolbarState.Title.ReadAccess( + titleResId = R.string.search_tokens_title, + onBackButtonClick = router::popBackStack, + onSearchButtonClick = ::onSearchButtonClick, + ) + } + } + + private fun onSearchButtonClick() { + uiState = when (val state = uiState.toolbarState) { + is AddTokensToolbarState.Title -> { + uiState.copySealed( + toolbarState = AddTokensToolbarState.SearchInputField( + onBackButtonClick = uiState.toolbarState.onBackButtonClick, + onSearchButtonClick = ::onSearchButtonClick, + value = "", + onValueChange = ::onValueChange, + onCleanButtonClick = { onValueChange(newValue = "") }, + ), + ) + } + is AddTokensToolbarState.SearchInputField -> { + store.dispatch(TokensAction.SetSearchInput(state.value)) + uiState.copySealed( + getToolbarState(store.state.tokensState.allowToAdd), + ) + } + } + } + + private fun onValueChange(newValue: String) { + val state = requireNotNull( + value = uiState.toolbarState as? AddTokensToolbarState.SearchInputField, + lazyMessage = { "Impossible to change value in another state" }, + ) + + uiState = uiState.copySealed(toolbarState = state.copy(value = newValue)) + } +} \ No newline at end of file