Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-03 19:53:43 +08:00
parent ae5a46d436
commit e2dcb369a3
11 changed files with 383 additions and 15 deletions

View file

@ -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

View file

@ -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()
}

View file

@ -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<TokensState> {
@AndroidEntryPoint
internal class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber<TokensState> {
private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind)
private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
@ -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<String> = callbackFlow {
val watcher = setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
private fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
awaitClose { watcher }
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
awaitClose { watcher }
}
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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<TokenItemModel>,
) : 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<TokenItemModel>,
val onSaveButtonClick: () -> Unit,
) : AddTokensStateHolder, TokensListVisibility
}

View file

@ -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
}

View file

@ -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<AddTokensNetworkItemState>,
)

View file

@ -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<TokenItemModel>
}

View file

@ -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<TokensState> {
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))
}
}