Updated on 2026-08-14
This commit is contained in:
parent
ae5a46d436
commit
e2dcb369a3
11 changed files with 383 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue