Updated on 2026-08-14
This commit is contained in:
commit
bbcdc778fa
40 changed files with 1453 additions and 30 deletions
|
|
@ -254,4 +254,36 @@ internal object TokensDomainModule {
|
|||
): CheckCurrencyCompatibilityUseCase {
|
||||
return CheckCurrencyCompatibilityUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideFindTokenByContractAddressUseCase(
|
||||
tokensListRepository: TokensListRepository,
|
||||
): FindTokenByContractAddressUseCase {
|
||||
return FindTokenByContractAddressUseCase(repository = tokensListRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideValidateContractAddressUseCase(
|
||||
tokensListRepository: TokensListRepository,
|
||||
): ValidateContractAddressUseCase {
|
||||
return ValidateContractAddressUseCase(tokensListRepository = tokensListRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideAreTokensSupportedByNetworkUseCase(
|
||||
repository: NetworksCompatibilityRepository,
|
||||
): AreTokensSupportedByNetworkUseCase {
|
||||
return AreTokensSupportedByNetworkUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetNetworksSupportedByWallet(
|
||||
repository: NetworksCompatibilityRepository,
|
||||
): GetNetworksSupportedByWallet {
|
||||
return GetNetworksSupportedByWallet(repository = repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,4 +30,8 @@ class Debouncer {
|
|||
fun release() {
|
||||
debounceJob?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_WAIT_TIME_MS = 500L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.tokens.utils.getNetwork
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.extensions.canHandleBlockchain
|
||||
import com.tangem.domain.common.extensions.canHandleToken
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.supportedTokens
|
||||
import com.tangem.domain.common.extensions.*
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
|
@ -58,6 +58,23 @@ internal class DefaultNetworksCompatibilityRepository(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
|
||||
return Blockchain.values()
|
||||
.filter { blockchain ->
|
||||
scanResponse.card.supportedBlockchains(scanResponse.cardTypesResolver).contains(blockchain)
|
||||
}
|
||||
.sortedBy(Blockchain::fullName)
|
||||
.mapNotNull { blockchain ->
|
||||
getNetwork(blockchain, null, scanResponse.derivationStyleProvider)
|
||||
}
|
||||
}
|
||||
|
||||
override fun areTokensSupportedByNetwork(networkId: String): Boolean {
|
||||
return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false
|
||||
}
|
||||
|
||||
private suspend fun getWalletOrThrow(userWalletId: UserWalletId): UserWallet {
|
||||
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Requested UserWallet not found"
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@ package com.tangem.data.tokens.repository
|
|||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.tokens.paging.CoinsPagingSource
|
||||
import com.tangem.data.tokens.utils.FoundTokenConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Default repository implementation for managing operations related to a complete set of tokens
|
||||
|
|
@ -42,4 +48,32 @@ internal class DefaultTokensListRepository(
|
|||
},
|
||||
).flow
|
||||
}
|
||||
|
||||
override suspend fun findToken(contractAddress: String, networkId: String): FoundToken? {
|
||||
return withContext(dispatchers.io) {
|
||||
val foundCoin = tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = networkId,
|
||||
).getOrThrow().coins.firstNotNullOfOrNull { coin ->
|
||||
val tokenNetwork = coin.networks.filter { network ->
|
||||
network.contractAddress != null && network.decimalCount != null &&
|
||||
network.contractAddress?.equals(contractAddress, ignoreCase = true) == true &&
|
||||
networkId == network.networkId
|
||||
}
|
||||
if (tokenNetwork.isNotEmpty()) {
|
||||
coin.copy(networks = tokenNetwork)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
foundCoin?.let { FoundTokenConverter.convert(foundCoin) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun validateAddress(contractAddress: String, networkId: String): Boolean {
|
||||
return when (val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown) {
|
||||
Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet -> true
|
||||
else -> blockchain.validateAddress(contractAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object FoundTokenConverter : Converter<CoinsResponse.Coin, FoundToken> {
|
||||
|
||||
override fun convert(value: CoinsResponse.Coin): FoundToken {
|
||||
return FoundToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
contractAddress = requireNotNull(value.networks.first().contractAddress),
|
||||
decimals = requireNotNull(value.networks.first().decimalCount).intValueExact(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
/**
|
||||
* Found token model
|
||||
*
|
||||
* @property id id
|
||||
* @property name name
|
||||
* @property symbol symbol
|
||||
* @property decimals decimals
|
||||
* @property contractAddress contractAddress
|
||||
*/
|
||||
data class FoundToken(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val decimals: Int,
|
||||
val contractAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class AreTokensSupportedByNetworkUseCase(private val repository: NetworksCompatibilityRepository) {
|
||||
|
||||
suspend operator fun invoke(networkId: String, userWalletId: UserWalletId?): Either<Throwable, Boolean> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
if (userWalletId == null) {
|
||||
repository.areTokensSupportedByNetwork(networkId)
|
||||
} else {
|
||||
repository.areTokensSupportedByNetwork(networkId, userWalletId)
|
||||
}
|
||||
},
|
||||
catch = { throwable -> raise(throwable) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun invoke(networkId: String): Either<Throwable, Boolean> {
|
||||
return either {
|
||||
catch(
|
||||
block = { repository.areTokensSupportedByNetwork(networkId) },
|
||||
catch = { throwable -> raise(throwable) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
|
||||
class FindTokenByContractAddressUseCase(private val repository: TokensListRepository) {
|
||||
suspend operator fun invoke(contractAddress: String, networkId: String): Either<Throwable, FoundToken?> {
|
||||
return either {
|
||||
catch(
|
||||
block = { repository.findToken(contractAddress, networkId) },
|
||||
catch = { throwable -> raise(throwable) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class GetNetworksSupportedByWallet(
|
||||
private val repository: NetworksCompatibilityRepository,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, List<Network>> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
repository.getSupportedNetworks(userWalletId)
|
||||
},
|
||||
catch = { throwable -> raise(throwable) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
|
||||
class ValidateContractAddressUseCase(private val tokensListRepository: TokensListRepository) {
|
||||
operator fun invoke(address: String, networkId: String): Either<AddCustomTokenError, Unit> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
if (address.isEmpty()) raise(AddCustomTokenError.FieldIsEmpty)
|
||||
|
||||
if (!tokensListRepository.validateAddress(networkId, address)) {
|
||||
raise(AddCustomTokenError.InvalidContractAddress)
|
||||
}
|
||||
},
|
||||
catch = {
|
||||
AddCustomTokenError.InvalidContractAddress
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface NetworksCompatibilityRepository {
|
||||
|
|
@ -12,4 +13,9 @@ interface NetworksCompatibilityRepository {
|
|||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
suspend fun isNetworkSupported(networkId: String, userWalletId: UserWalletId): Boolean
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network>
|
||||
|
||||
fun areTokensSupportedByNetwork(networkId: String): Boolean
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -18,4 +19,25 @@ interface TokensListRepository {
|
|||
* @throws com.tangem.datasource.api.common.response.ApiResponseError
|
||||
*/
|
||||
fun getTokens(searchText: String?): Flow<PagingData<Token>>
|
||||
|
||||
/**
|
||||
* Retrieves a token information with the specified contract address on the provided network.
|
||||
*
|
||||
* @param contractAddress contract address of the token
|
||||
* @param networkId network of the token
|
||||
* @return [FoundToken] object containing token information or null if no token is found with the provided
|
||||
* contract address
|
||||
*/
|
||||
@Throws
|
||||
suspend fun findToken(contractAddress: String, networkId: String): FoundToken?
|
||||
|
||||
/**
|
||||
* Validates a contract address on a particular network.
|
||||
*
|
||||
* @param contractAddress contract address of the token
|
||||
* @param networkId network of the token
|
||||
* @return [Boolean] true is address is valid (possible on the network), false if its format is not
|
||||
* supported on the network
|
||||
*/
|
||||
fun validateAddress(contractAddress: String, networkId: String): Boolean
|
||||
}
|
||||
|
|
@ -2,15 +2,12 @@ package com.tangem.managetokens
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen
|
||||
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel
|
||||
import com.tangem.managetokens.presentation.router.InnerManageTokensRouter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -23,23 +20,18 @@ internal class ManageTokensFragment : ComposeFragment() {
|
|||
@Inject
|
||||
lateinit var manageTokensRouter: ManageTokensRouter
|
||||
|
||||
// private val internalManageTokensRouter: InnerManageTokensRouter
|
||||
// get() = requireNotNull(manageTokensRouter as? InnerManageTokensRouter) {
|
||||
// "internalManageTokensRouter should be instance of InnerManageTokensRouter"
|
||||
// }
|
||||
private val innerManageTokensRouter: InnerManageTokensRouter
|
||||
get() = requireNotNull(manageTokensRouter as? InnerManageTokensRouter) {
|
||||
"internalManageTokensRouter should be instance of InnerManageTokensRouter"
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val viewModel = hiltViewModel<ManageTokensViewModel>()
|
||||
// viewModel.router = [REDACTED_EMAIL]
|
||||
//
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
val systemBarsColor = TangemTheme.colors.background.secondary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
|
||||
ManageTokensScreen(state = viewModel.uiState)
|
||||
innerManageTokensRouter.Initialize(viewModelStoreOwner = this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.managetokens.di
|
||||
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.managetokens.presentation.router.DefaultManageTokensRouter
|
||||
import dagger.Module
|
||||
|
|
@ -14,7 +15,7 @@ internal object ManageTokensRouterModule {
|
|||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideManageTokensRouter(): ManageTokensRouter {
|
||||
return DefaultManageTokensRouter()
|
||||
fun provideManageTokensRouter(reduxNavController: ReduxNavController): ManageTokensRouter {
|
||||
return DefaultManageTokensRouter(reduxNavController)
|
||||
}
|
||||
}
|
||||
|
|
@ -49,4 +49,8 @@ internal sealed class AlertState {
|
|||
formatArgs = wrappedList(tokenName, networkName),
|
||||
)
|
||||
}
|
||||
|
||||
object TokenAlreadyAdded : AlertState() {
|
||||
override val message: TextReference = resourceReference(R.string.custom_token_validation_error_already_added)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ internal sealed class ChooseWalletState {
|
|||
val selectedWallet: WalletState?,
|
||||
val onChooseWalletClick: () -> Unit,
|
||||
val onCloseChoosingWalletClick: () -> Unit,
|
||||
val show: Boolean = false,
|
||||
) : ChooseWalletState()
|
||||
|
||||
object NoSelection : ChooseWalletState()
|
||||
|
|
@ -19,13 +20,10 @@ internal sealed class ChooseWalletState {
|
|||
get() = when (type) {
|
||||
ChooseWalletWarning.SINGLE_CURRENCY ->
|
||||
TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title)
|
||||
ChooseWalletWarning.WALLET_INCOMPATIBLE ->
|
||||
TextReference.Res(R.string.manage_tokens_wallet_does_not_supported_blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ChooseWalletWarning {
|
||||
SINGLE_CURRENCY,
|
||||
WALLET_INCOMPATIBLE,
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ internal fun Alert(state: AlertState, onDismiss: () -> Unit) {
|
|||
is AlertState.TokensUnsupported,
|
||||
is AlertState.TokensUnsupportedBlockchainByCard,
|
||||
is AlertState.CannotHideNetworkWithTokens,
|
||||
is AlertState.TokenAlreadyAdded,
|
||||
-> DefaultAlert(state, onDismiss)
|
||||
is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.managetokens.presentation.common.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
|
|
@ -24,11 +25,23 @@ import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
|||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun NetworkItem(state: NetworkItemState, tokenState: TokenItemState.Loaded?, modifier: Modifier = Modifier) {
|
||||
internal fun NetworkItem(
|
||||
state: NetworkItemState,
|
||||
tokenState: TokenItemState.Loaded?,
|
||||
modifier: Modifier = Modifier,
|
||||
isSelected: Boolean = false,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
|
||||
.then(
|
||||
if (state is NetworkItemState.Selectable) {
|
||||
Modifier.clickable { state.onNetworkClick(state) }
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -55,6 +68,12 @@ internal fun NetworkItem(state: NetworkItemState, tokenState: TokenItemState.Loa
|
|||
},
|
||||
checked = state.isAdded.value,
|
||||
)
|
||||
} else if (state is NetworkItemState.Selectable && isSelected) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_check_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
internal data class AddCustomTokenState(
|
||||
val chooseWalletState: ChooseWalletState,
|
||||
val chooseNetworkState: ChooseNetworkState,
|
||||
val chooseDerivationState: ChooseDerivationState?,
|
||||
val tokenData: CustomTokenData?,
|
||||
val warnings: ImmutableSet<AddCustomTokenWarning>,
|
||||
val addTokenButton: ButtonState,
|
||||
val showChooseWalletScreen: Boolean = false,
|
||||
val event: StateEvent<Event> = consumedEvent(),
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
/**
|
||||
* Warning model of add custom token screen
|
||||
*
|
||||
* @property title warning description
|
||||
*/
|
||||
internal sealed class AddCustomTokenWarning(val title: TextReference, val subtitle: TextReference? = null) {
|
||||
|
||||
object PotentialScamToken : AddCustomTokenWarning(
|
||||
title = resourceReference(R.string.custom_token_validation_error_not_found_title),
|
||||
subtitle = resourceReference(R.string.custom_token_validation_error_not_found_description),
|
||||
)
|
||||
|
||||
object InvalidContractAddress : AddCustomTokenWarning(
|
||||
title = resourceReference(R.string.custom_token_creation_error_invalid_contract_address),
|
||||
)
|
||||
|
||||
object WrongDecimals : AddCustomTokenWarning(
|
||||
title =
|
||||
resourceReference(R.string.custom_token_creation_error_wrong_decimals, wrappedList(MAXIMUM_DECIMAL_NUMBER)),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val MAXIMUM_DECIMAL_NUMBER = 30
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
internal data class ButtonState(
|
||||
val isEnabled: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ChooseDerivationState(
|
||||
val derivations: ImmutableList<Derivation>,
|
||||
val selectedDerivation: Derivation?,
|
||||
val enterCustomDerivationState: EnterCustomDerivationState?,
|
||||
val onChooseDerivationClick: () -> Unit,
|
||||
val onCloseChoosingDerivationClick: () -> Unit,
|
||||
val onEnterCustomDerivation: () -> Unit,
|
||||
val show: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ChooseNetworkState(
|
||||
val networks: ImmutableList<NetworkItemState>,
|
||||
val selectedNetwork: NetworkItemState?,
|
||||
val onChooseNetworkClick: () -> Unit,
|
||||
val onCloseChoosingNetworkClick: () -> Unit,
|
||||
val show: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
internal data class CustomTokenData(
|
||||
val contractAddressTextField: TextFieldState,
|
||||
val nameTextField: TextFieldState,
|
||||
val symbolTextField: TextFieldState,
|
||||
val decimalsTextField: TextFieldState,
|
||||
) {
|
||||
|
||||
fun isRequiredInformationProvided(): Boolean {
|
||||
return contractAddressTextField.isInputValid() && nameTextField.isInputValid() &&
|
||||
symbolTextField.isInputValid() && decimalsTextField.isInputValid()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
internal data class Derivation(
|
||||
val networkName: String,
|
||||
val standardType: String?,
|
||||
val path: String,
|
||||
val networkId: String?,
|
||||
val onDerivationSelected: (Derivation) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
internal data class EnterCustomDerivationState(
|
||||
val value: String,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val confirmButtonEnabled: Boolean,
|
||||
val derivationIncorrect: Boolean,
|
||||
val onConfirmButtonClick: () -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
|
||||
internal sealed class TextFieldState {
|
||||
object Loading : TextFieldState()
|
||||
|
||||
data class Editable(
|
||||
val value: String,
|
||||
val isEnabled: Boolean,
|
||||
val error: AddCustomTokenWarning? = null,
|
||||
val onValueChange: (String) -> Unit,
|
||||
) : TextFieldState()
|
||||
|
||||
fun isInputValid(): Boolean = this is Editable && value.isNotBlank() && error == null
|
||||
|
||||
fun copySealed(
|
||||
value: String = (this as? Editable)?.value ?: "",
|
||||
isEnabled: Boolean = (this as? Editable)?.isEnabled ?: true,
|
||||
error: AddCustomTokenWarning? = (this as? Editable)?.error,
|
||||
onValueChange: (String) -> Unit = (this as? Editable)?.onValueChange ?: {},
|
||||
): TextFieldState {
|
||||
return when (this) {
|
||||
is Editable -> this.copy(
|
||||
value = value,
|
||||
isEnabled = isEnabled,
|
||||
error = error,
|
||||
onValueChange = onValueChange,
|
||||
)
|
||||
is Loading -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.managetokens.presentation.customtokens.state.AddCustomTokenState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class AddCustomTokenStateToCryptoCurrencyConverter(
|
||||
private val derivationStyleProvider: DerivationStyleProvider,
|
||||
) : Converter<AddCustomTokenState, CryptoCurrency> {
|
||||
|
||||
override fun convert(value: AddCustomTokenState): CryptoCurrency {
|
||||
val derivationPath = value.chooseDerivationState?.selectedDerivation?.path
|
||||
val token = parseTokenOrNull(value.tokenData)
|
||||
|
||||
val cryptoCurrency = if (token != null) {
|
||||
CryptoCurrencyFactory().createToken(
|
||||
token = token,
|
||||
networkId = value.chooseNetworkState.selectedNetwork?.id ?: "",
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
extraDerivationPath = derivationPath,
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyFactory().createCoin(
|
||||
networkId = value.chooseNetworkState.selectedNetwork?.id ?: "",
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
extraDerivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
return requireNotNull(cryptoCurrency) {
|
||||
"Unless network is not Unknown blockchain, CryptoCurrency cannot be null"
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
private fun parseTokenOrNull(tokenData: CustomTokenData?): CryptoCurrencyFactory.Token? {
|
||||
val contractAddress = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value
|
||||
val symbol = (tokenData?.symbolTextField as? TextFieldState.Editable)?.value
|
||||
val name = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value
|
||||
val decimals = (tokenData?.decimalsTextField as? TextFieldState.Editable)?.value?.toIntOrNull()
|
||||
return if (
|
||||
!contractAddress.isNullOrBlank() && !symbol.isNullOrBlank() && !name.isNullOrBlank() && decimals != null
|
||||
) {
|
||||
CryptoCurrencyFactory.Token(
|
||||
symbol = symbol,
|
||||
name = name,
|
||||
contractAddress = contractAddress,
|
||||
decimals = decimals,
|
||||
id = null,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
|
||||
import com.tangem.managetokens.presentation.customtokens.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class ContractAddressToCustomTokenDataConverter(
|
||||
private val clickIntents: CustomTokensClickIntents,
|
||||
) : Converter<String, CustomTokenData> {
|
||||
override fun convert(value: String): CustomTokenData {
|
||||
return CustomTokenData(
|
||||
contractAddressTextField = TextFieldState.Editable(
|
||||
value = value,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.managetokens.presentation.common.state.*
|
||||
import com.tangem.managetokens.presentation.customtokens.state.*
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
|
||||
internal class CustomTokensStateFactory(
|
||||
private val currentStateProvider: Provider<AddCustomTokenState>,
|
||||
private val clickIntents: CustomTokensClickIntents,
|
||||
) {
|
||||
|
||||
fun getInitialState(): AddCustomTokenState {
|
||||
return AddCustomTokenState(
|
||||
chooseWalletState = ChooseWalletState.NoSelection,
|
||||
chooseNetworkState = ChooseNetworkState(
|
||||
networks = persistentListOf(),
|
||||
selectedNetwork = null,
|
||||
onChooseNetworkClick = clickIntents::onChooseNetworkClick,
|
||||
onCloseChoosingNetworkClick = clickIntents::onCloseChoosingNetworkClick,
|
||||
),
|
||||
chooseDerivationState = ChooseDerivationState(
|
||||
derivations = persistentListOf(),
|
||||
selectedDerivation = null,
|
||||
enterCustomDerivationState = null,
|
||||
onChooseDerivationClick = clickIntents::onChooseDerivationClick,
|
||||
onCloseChoosingDerivationClick = clickIntents::onCloseChoosingDerivationClick,
|
||||
onEnterCustomDerivation = clickIntents::onEnterCustomDerivation,
|
||||
),
|
||||
tokenData = null,
|
||||
warnings = persistentSetOf(),
|
||||
addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFullState(
|
||||
suitableUserWallets: List<UserWallet>,
|
||||
allUserWallets: List<UserWallet>,
|
||||
selectedWalletId: UserWalletId?,
|
||||
supportedNetworks: List<Network>,
|
||||
): AddCustomTokenState {
|
||||
val derivations = getListOfDerivations(supportedNetworks)
|
||||
|
||||
val chooseDerivationState = createChooseDerivationState(derivations)
|
||||
val networkConverter = NetworkToNetworkItemStateConverter(clickIntents::onNetworkSelected)
|
||||
val networks = supportedNetworks.map { networkConverter.convert(it) }
|
||||
|
||||
val chooseWalletState = getNewChooseWalletState(allUserWallets, suitableUserWallets, selectedWalletId)
|
||||
|
||||
return AddCustomTokenState(
|
||||
chooseWalletState = chooseWalletState,
|
||||
chooseNetworkState = ChooseNetworkState(
|
||||
networks = networks.toPersistentList(),
|
||||
selectedNetwork = null,
|
||||
onChooseNetworkClick = clickIntents::onChooseNetworkClick,
|
||||
onCloseChoosingNetworkClick = clickIntents::onBack,
|
||||
),
|
||||
chooseDerivationState = chooseDerivationState,
|
||||
tokenData = null,
|
||||
warnings = persistentSetOf(),
|
||||
addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getListOfDerivations(supportedNetworks: List<Network>): List<Derivation> {
|
||||
return supportedNetworks.mapNotNull { network ->
|
||||
network.derivationPath.value?.let { rawPath ->
|
||||
Derivation(
|
||||
networkName = network.name,
|
||||
standardType = network.standardType.name,
|
||||
path = rawPath,
|
||||
networkId = network.backendId,
|
||||
onDerivationSelected = clickIntents::onDerivationSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChooseDerivationState(derivations: List<Derivation>): ChooseDerivationState? {
|
||||
return if (derivations.isNotEmpty()) {
|
||||
ChooseDerivationState(
|
||||
derivations = derivations.toPersistentList(),
|
||||
selectedDerivation = null,
|
||||
enterCustomDerivationState = null,
|
||||
onChooseDerivationClick = clickIntents::onChooseDerivationClick,
|
||||
onCloseChoosingDerivationClick = clickIntents::onBack,
|
||||
onEnterCustomDerivation = clickIntents::onEnterCustomDerivation,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWithNewWalletSelected(
|
||||
selectedWalletId: UserWalletId,
|
||||
supportedNetworks: List<Network>,
|
||||
): AddCustomTokenState {
|
||||
val derivations = getListOfDerivations(supportedNetworks)
|
||||
|
||||
val chooseDerivationState = createChooseDerivationState(derivations)
|
||||
val networkConverter = NetworkToNetworkItemStateConverter(clickIntents::onNetworkSelected)
|
||||
val networks = supportedNetworks.map { networkConverter.convert(it) }
|
||||
|
||||
val currentWalletState = requireNotNull(
|
||||
currentStateProvider().chooseWalletState as? ChooseWalletState.Choose,
|
||||
) {
|
||||
"If user wallet was chosen, ChooseWalletState type must be Choose"
|
||||
}
|
||||
val selectedWalletState = currentWalletState.wallets.find { it.walletId == selectedWalletId.stringValue }
|
||||
val chooseWalletState = currentWalletState.copy(selectedWallet = selectedWalletState)
|
||||
|
||||
return AddCustomTokenState(
|
||||
chooseWalletState = chooseWalletState,
|
||||
chooseNetworkState = ChooseNetworkState(
|
||||
networks = networks.toPersistentList(),
|
||||
selectedNetwork = null,
|
||||
onChooseNetworkClick = clickIntents::onChooseNetworkClick,
|
||||
onCloseChoosingNetworkClick = clickIntents::onBack,
|
||||
),
|
||||
chooseDerivationState = chooseDerivationState,
|
||||
tokenData = null,
|
||||
warnings = persistentSetOf(),
|
||||
addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNewChooseWalletState(
|
||||
suitableUserWallets: List<UserWallet>,
|
||||
allUserWallets: List<UserWallet>,
|
||||
selectedWalletId: UserWalletId?,
|
||||
): ChooseWalletState {
|
||||
val chooseWalletState = if (suitableUserWallets.size == 1) {
|
||||
ChooseWalletState.NoSelection
|
||||
} else if (suitableUserWallets.isEmpty() && allUserWallets.all { !it.isMultiCurrency }) {
|
||||
ChooseWalletState.Warning(ChooseWalletWarning.SINGLE_CURRENCY)
|
||||
} else {
|
||||
var selectedWalletState: WalletState? = null
|
||||
ChooseWalletState.Choose(
|
||||
wallets = suitableUserWallets.map { wallet ->
|
||||
val walletState = WalletState(
|
||||
walletId = wallet.walletId.stringValue,
|
||||
artworkUrl = wallet.artworkUrl,
|
||||
onSelected = clickIntents::onWalletSelected,
|
||||
walletName = wallet.name,
|
||||
)
|
||||
if (wallet.walletId.stringValue == selectedWalletId?.stringValue) {
|
||||
selectedWalletState = walletState
|
||||
}
|
||||
walletState
|
||||
}.toPersistentList(),
|
||||
selectedWallet = requireNotNull(selectedWalletState),
|
||||
onChooseWalletClick = clickIntents::onChooseWalletClick,
|
||||
onCloseChoosingWalletClick = clickIntents::onCloseChoosingWalletClick,
|
||||
)
|
||||
}
|
||||
return chooseWalletState
|
||||
}
|
||||
|
||||
fun removeTokenAddressError(): AddCustomTokenState {
|
||||
return addTokenAddressFieldError(null)
|
||||
}
|
||||
|
||||
private fun addTokenAddressFieldError(error: AddCustomTokenWarning?): AddCustomTokenState {
|
||||
val tokenData = currentStateProvider().tokenData ?: return currentStateProvider()
|
||||
val contractAddressField = tokenData.contractAddressTextField.copySealed(error = error)
|
||||
return currentStateProvider().copy(tokenData = tokenData.copy(contractAddressTextField = contractAddressField))
|
||||
}
|
||||
|
||||
fun getStateAndTriggerEvent(
|
||||
state: AddCustomTokenState,
|
||||
event: Event,
|
||||
setUiState: (AddCustomTokenState) -> Unit,
|
||||
): AddCustomTokenState {
|
||||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = event,
|
||||
onConsume = {
|
||||
val currentState = currentStateProvider()
|
||||
setUiState(currentState.copy(event = consumedEvent()))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun updateStateOnNetworkSelected(networkItemState: NetworkItemState, supportsTokens: Boolean): AddCustomTokenState {
|
||||
val uiState = currentStateProvider()
|
||||
val tokenData = if (supportsTokens) {
|
||||
uiState.tokenData
|
||||
?: CustomTokenData(
|
||||
contractAddressTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return uiState.copy(
|
||||
chooseNetworkState = uiState.chooseNetworkState.copy(selectedNetwork = networkItemState),
|
||||
tokenData = tokenData,
|
||||
addTokenButton = uiState.addTokenButton.copy(isEnabled = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun updateOnCustomDerivationSelected(): AddCustomTokenState {
|
||||
val uiState = currentStateProvider()
|
||||
return uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = null,
|
||||
selectedDerivation = Derivation(
|
||||
networkName = "",
|
||||
path = uiState.chooseDerivationState.enterCustomDerivationState?.value ?: "",
|
||||
networkId = null,
|
||||
standardType = null,
|
||||
onDerivationSelected = clickIntents::onDerivationSelected,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun updateStateOnEnterCustomDerivation(): AddCustomTokenState {
|
||||
val customDerivationState = EnterCustomDerivationState(
|
||||
value = "",
|
||||
onValueChange = clickIntents::onCustomDerivationChange,
|
||||
confirmButtonEnabled = false,
|
||||
derivationIncorrect = false,
|
||||
onConfirmButtonClick = clickIntents::onCustomDerivationSelected,
|
||||
onDismiss = clickIntents::onCustomDerivationDialogDismissed,
|
||||
)
|
||||
val uiState = currentStateProvider()
|
||||
return uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = customDerivationState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState {
|
||||
return currentStateProvider().copy(
|
||||
tokenData = CustomTokenData(
|
||||
contractAddressTextField = TextFieldState.Editable(
|
||||
value = contractAddress,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
),
|
||||
nameTextField = TextFieldState.Loading,
|
||||
symbolTextField = TextFieldState.Loading,
|
||||
decimalsTextField = TextFieldState.Loading,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun handleAddressError(error: AddCustomTokenError): AddCustomTokenState {
|
||||
val uiState = currentStateProvider()
|
||||
return when (error) {
|
||||
AddCustomTokenError.InvalidContractAddress -> {
|
||||
addTokenAddressFieldError(AddCustomTokenWarning.InvalidContractAddress)
|
||||
.copy(
|
||||
addTokenButton = uiState.addTokenButton.copy(isEnabled = false),
|
||||
warnings = uiState.warnings
|
||||
.filterNot { it is AddCustomTokenWarning.PotentialScamToken }
|
||||
.toPersistentSet(),
|
||||
)
|
||||
}
|
||||
AddCustomTokenError.FieldIsEmpty ->
|
||||
removeTokenAddressError()
|
||||
.copy(
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.managetokens.presentation.customtokens.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class FoundTokenToCustomTokenDataConverter(
|
||||
private val clickIntents: CustomTokensClickIntents,
|
||||
) : Converter<FoundToken, CustomTokenData> {
|
||||
override fun convert(value: FoundToken): CustomTokenData {
|
||||
return CustomTokenData(
|
||||
contractAddressTextField = TextFieldState.Editable(
|
||||
value = value.contractAddress,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = value.name,
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = value.symbol,
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = value.decimals.toString(),
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
|
||||
import com.tangem.core.ui.extensions.getActiveIconResByNetworkId
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class NetworkToNetworkItemStateConverter(
|
||||
private val onNetworkItemSelected: (NetworkItemState) -> Unit,
|
||||
) : Converter<Network, NetworkItemState> {
|
||||
override fun convert(value: Network): NetworkItemState {
|
||||
return NetworkItemState.Selectable(
|
||||
name = value.name,
|
||||
protocolName = value.standardType.name,
|
||||
iconResId = getActiveIconResByNetworkId(value.backendId),
|
||||
id = value.backendId,
|
||||
onNetworkClick = onNetworkItemSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.viewmodels
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.Derivation
|
||||
|
||||
internal interface CustomTokensClickIntents {
|
||||
|
||||
fun onNetworkSelected(networkItemState: NetworkItemState)
|
||||
|
||||
fun onChooseNetworkClick()
|
||||
|
||||
fun onCloseChoosingNetworkClick()
|
||||
|
||||
fun onWalletSelected(walletId: String)
|
||||
|
||||
fun onChooseWalletClick()
|
||||
|
||||
fun onCloseChoosingWalletClick()
|
||||
|
||||
fun onContractAddressChange(input: String)
|
||||
|
||||
fun onTokenNameChange(input: String)
|
||||
|
||||
fun onSymbolChange(input: String)
|
||||
|
||||
fun onDecimalsChange(input: String)
|
||||
|
||||
fun onDerivationSelected(derivation: Derivation)
|
||||
|
||||
fun onChooseDerivationClick()
|
||||
|
||||
fun onCloseChoosingDerivationClick()
|
||||
|
||||
fun onEnterCustomDerivation()
|
||||
|
||||
fun onCustomDerivationChange(input: String)
|
||||
|
||||
fun onCustomDerivationSelected()
|
||||
|
||||
fun onCustomDerivationDialogDismissed()
|
||||
|
||||
fun onAddCustomButtonClick()
|
||||
|
||||
fun onBack()
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.viewmodels
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.HDWalletError
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.*
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.AddCustomTokenStateToCryptoCurrencyConverter
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.ContractAddressToCustomTokenDataConverter
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.CustomTokensStateFactory
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.FoundTokenToCustomTokenDataConverter
|
||||
import com.tangem.managetokens.presentation.router.InnerManageTokensRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import com.tangem.utils.coroutines.Debouncer.Companion.DEFAULT_WAIT_TIME_MS
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class CustomTokensViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val selectWalletUseCase: SelectWalletUseCase,
|
||||
private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
private val findTokenByContractAddressUseCase: FindTokenByContractAddressUseCase,
|
||||
private val validateContractAddressUseCase: ValidateContractAddressUseCase,
|
||||
private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet,
|
||||
private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase,
|
||||
) : ViewModel(), CustomTokensClickIntents, DefaultLifecycleObserver {
|
||||
|
||||
private val debouncer = Debouncer()
|
||||
|
||||
private val stateFactory = CustomTokensStateFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
clickIntents = this,
|
||||
)
|
||||
|
||||
var router: InnerManageTokensRouter by Delegates.notNull()
|
||||
|
||||
var uiState: AddCustomTokenState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
getWalletsUseCase()
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { userWallets ->
|
||||
val suitableUserWallets = userWallets.filter { it.isMultiCurrency && !it.isLocked }
|
||||
val selectedWalletId = selectSuitableWallet(suitableUserWallets)
|
||||
val networks = selectedWalletId?.let { getSupportedNetworks(selectedWalletId) } ?: emptyList()
|
||||
withContext(dispatchers.main) {
|
||||
uiState = stateFactory.getFullState(
|
||||
allUserWallets = suitableUserWallets,
|
||||
suitableUserWallets = userWallets,
|
||||
selectedWalletId = selectedWalletId,
|
||||
supportedNetworks = networks,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun selectSuitableWallet(suitableUserWallets: List<UserWallet>): UserWalletId? {
|
||||
val selectedWallet = getSelectedWalletSyncUseCase().getOrNull()
|
||||
val selectedWalletId = if (walletSupportsAddingTokens(selectedWallet) && suitableUserWallets.isNotEmpty()) {
|
||||
val walletId = suitableUserWallets.first().walletId
|
||||
selectWalletUseCase(walletId)
|
||||
walletId
|
||||
} else {
|
||||
selectedWallet?.walletId
|
||||
}
|
||||
return selectedWalletId
|
||||
}
|
||||
|
||||
private fun walletSupportsAddingTokens(userWallet: UserWallet?): Boolean {
|
||||
return userWallet != null && userWallet.isMultiCurrency && !userWallet.isLocked
|
||||
}
|
||||
|
||||
private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
return getNetworksSupportedByWallet(userWalletId).fold(
|
||||
ifLeft = { emptyList() },
|
||||
ifRight = { it },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNetworkSelected(networkItemState: NetworkItemState) {
|
||||
selectNetwork(networkItemState)
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
private fun selectNetwork(networkItemState: NetworkItemState) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
|
||||
val supportsTokens = areTokensSupportedByNetworkUseCase(
|
||||
networkId = networkItemState.id,
|
||||
userWalletId = selectedWalletId,
|
||||
).getOrNull() ?: false
|
||||
withContext(dispatchers.main) {
|
||||
uiState = stateFactory.updateStateOnNetworkSelected(networkItemState, supportsTokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChooseNetworkClick() {
|
||||
router.openCustomTokensChooseNetwork()
|
||||
}
|
||||
|
||||
override fun onCloseChoosingNetworkClick() {
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onWalletSelected(walletId: String) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val userWalletId = UserWalletId(walletId)
|
||||
selectWalletUseCase(userWalletId)
|
||||
val supportedNetworks = getSupportedNetworks(UserWalletId(walletId))
|
||||
|
||||
withContext(dispatchers.main) {
|
||||
uiState = stateFactory.updateWithNewWalletSelected(
|
||||
selectedWalletId = userWalletId,
|
||||
supportedNetworks = supportedNetworks,
|
||||
)
|
||||
router.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChooseWalletClick() {
|
||||
router.openCustomTokensChooseWallet()
|
||||
}
|
||||
|
||||
override fun onCloseChoosingWalletClick() {
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onContractAddressChange(input: String) {
|
||||
uiState = uiState.copy(
|
||||
tokenData = uiState.tokenData?.copy(
|
||||
contractAddressTextField = TextFieldState.Editable(
|
||||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onContractAddressChange,
|
||||
),
|
||||
),
|
||||
)
|
||||
debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) {
|
||||
uiState.chooseNetworkState.selectedNetwork?.let { networkItemState ->
|
||||
validateContractAddressUseCase(input, networkItemState.id).fold(
|
||||
ifRight = {
|
||||
uiState = stateFactory.removeTokenAddressError()
|
||||
.copy(
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
warnings = uiState.warnings
|
||||
.filterNot { it is AddCustomTokenWarning.PotentialScamToken }.toPersistentSet(),
|
||||
)
|
||||
fetchTokenInformation(contractAddress = input, networkId = networkItemState.id)
|
||||
},
|
||||
ifLeft = { error ->
|
||||
uiState = stateFactory.handleAddressError(error)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchTokenInformation(contractAddress: String, networkId: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState = stateFactory.updateStateOnLoadingTokenInfo(contractAddress)
|
||||
withContext(dispatchers.io) {
|
||||
findTokenByContractAddressUseCase(
|
||||
contractAddress = contractAddress,
|
||||
networkId = networkId,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
val tokenData = ContractAddressToCustomTokenDataConverter(this@CustomTokensViewModel)
|
||||
.convert(contractAddress)
|
||||
uiState = uiState.copy(
|
||||
tokenData = tokenData,
|
||||
warnings = (uiState.warnings + AddCustomTokenWarning.PotentialScamToken).toPersistentSet(),
|
||||
)
|
||||
},
|
||||
ifRight = { token ->
|
||||
val tokenData = if (token != null) {
|
||||
FoundTokenToCustomTokenDataConverter(this@CustomTokensViewModel).convert(token)
|
||||
} else {
|
||||
ContractAddressToCustomTokenDataConverter(this@CustomTokensViewModel).convert(
|
||||
contractAddress,
|
||||
)
|
||||
}
|
||||
uiState = uiState.copy(tokenData = tokenData)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTokenNameChange(input: String) {
|
||||
uiState = uiState.copy(
|
||||
tokenData = uiState.tokenData?.copy(
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onTokenNameChange,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSymbolChange(input: String) {
|
||||
uiState = uiState.copy(
|
||||
tokenData = uiState.tokenData?.copy(
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onSymbolChange,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDecimalsChange(input: String) {
|
||||
val correctInput = input.toIntOrNull()
|
||||
val error = if (input.isNotBlank() && correctInput == null) {
|
||||
AddCustomTokenWarning.WrongDecimals
|
||||
} else {
|
||||
null
|
||||
}
|
||||
uiState = uiState.copy(
|
||||
tokenData = uiState.tokenData?.copy(
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onDecimalsChange,
|
||||
error = error,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDerivationSelected(derivation: Derivation) {
|
||||
uiState =
|
||||
uiState.copy(chooseDerivationState = uiState.chooseDerivationState?.copy(selectedDerivation = derivation))
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onChooseDerivationClick() {
|
||||
router.openCustomTokensChooseDerivation()
|
||||
}
|
||||
|
||||
override fun onCloseChoosingDerivationClick() {
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onCustomDerivationChange(input: String) {
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
|
||||
value = input,
|
||||
),
|
||||
),
|
||||
)
|
||||
debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) {
|
||||
val path = createDerivationPathOrNull(input)
|
||||
val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
|
||||
confirmButtonEnabled = path != null,
|
||||
derivationIncorrect = input.isNotBlank() && path == null,
|
||||
)
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = enterDerivationState,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
|
||||
return try {
|
||||
DerivationPath(rawPath)
|
||||
} catch (error: HDWalletError) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCustomDerivationSelected() {
|
||||
uiState = stateFactory.updateOnCustomDerivationSelected()
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onEnterCustomDerivation() {
|
||||
uiState = stateFactory.updateStateOnEnterCustomDerivation()
|
||||
}
|
||||
|
||||
override fun onCustomDerivationDialogDismissed() {
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAddCustomButtonClick() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return@launch
|
||||
val cryptoCurrency = AddCustomTokenStateToCryptoCurrencyConverter(
|
||||
selectedWallet.scanResponse.derivationStyleProvider,
|
||||
).convert(uiState)
|
||||
val alreadyAdded =
|
||||
getCurrenciesUseCase(selectedWallet.walletId).getOrNull()?.any { it == cryptoCurrency }
|
||||
if (alreadyAdded == true) {
|
||||
uiState = stateFactory.getStateAndTriggerEvent(
|
||||
state = uiState,
|
||||
event = Event.ShowAlert(AlertState.TokenAlreadyAdded),
|
||||
setUiState = { uiState = it },
|
||||
)
|
||||
} else {
|
||||
addCryptoCurrenciesUseCase(selectedWallet.walletId, currency = cryptoCurrency)
|
||||
withContext(dispatchers.main) { router.popBackStack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBack() {
|
||||
router.popBackStack()
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ internal class NetworkToNetworkItemStateConverter(
|
|||
currencies = addedCurrencies,
|
||||
)
|
||||
return NetworkItemState.Toggleable(
|
||||
name = network.name.uppercase(),
|
||||
name = network.name,
|
||||
iconResId = mutableIntStateOf(
|
||||
getNetworkIconResId(isAdded, network.networkId), // todo
|
||||
),
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ internal fun TokensList(tokens: LazyPagingItems<TokenItemState>, addCustomTokenB
|
|||
|
||||
if (addCustomTokenButton.isVisible) {
|
||||
item {
|
||||
AddCustomTokenButton(onButtonClick = { addCustomTokenButton.onClick })
|
||||
AddCustomTokenButton(onButtonClick = addCustomTokenButton.onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,11 @@ import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState
|
|||
import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.factory.*
|
||||
import com.tangem.managetokens.presentation.router.InnerManageTokensRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import com.tangem.utils.coroutines.Debouncer.Companion.DEFAULT_WAIT_TIME_MS
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -65,6 +67,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
clickIntents = this,
|
||||
)
|
||||
|
||||
var router: InnerManageTokensRouter by Delegates.notNull()
|
||||
|
||||
var uiState: ManageTokensState by mutableStateOf(stateFactory.getInitialState(flowOf(PagingData.from(emptyList()))))
|
||||
private set
|
||||
|
||||
|
|
@ -178,13 +182,13 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAddCustomTokensButtonClick() {
|
||||
TODO("Not yet implemented") // TODO: add when custom tokens are implemented
|
||||
router.openCustomTokensScreen()
|
||||
}
|
||||
|
||||
override fun onSearchQueryChange(query: String) {
|
||||
uiState = uiState.copy(searchBarState = uiState.searchBarState.copy(query = query))
|
||||
|
||||
debouncer.debounce(waitMs = 500L, coroutineScope = viewModelScope + dispatchers.io) {
|
||||
debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) {
|
||||
val state = stateFactory.showAddCustomTokensButton(query.isNotBlank())
|
||||
uiState = state.copy(tokens = getInitialTokensList(query))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,120 @@
|
|||
package com.tangem.managetokens.presentation.router
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navigation
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.managetokens.ManageTokensFragment
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensViewModel
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen
|
||||
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
internal class DefaultManageTokensRouter(
|
||||
private val reduxNavController: ReduxNavController,
|
||||
) : InnerManageTokensRouter {
|
||||
|
||||
private var navController: NavHostController by Delegates.notNull()
|
||||
|
||||
internal class DefaultManageTokensRouter : ManageTokensRouter {
|
||||
override fun getEntryFragment(): Fragment = ManageTokensFragment()
|
||||
|
||||
@Composable
|
||||
override fun Initialize(viewModelStoreOwner: ViewModelStoreOwner) {
|
||||
NavHost(
|
||||
navController = rememberNavController().apply { navController = this },
|
||||
startDestination = ManageTokensRoute.ManageTokens.route,
|
||||
) {
|
||||
composable(ManageTokensRoute.ManageTokens.route) {
|
||||
val viewModel = hiltViewModel<ManageTokensViewModel>().apply { router = this@DefaultManageTokensRouter }
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
ManageTokensScreen(state = viewModel.uiState)
|
||||
}
|
||||
|
||||
navigation(
|
||||
startDestination = ManageTokensRoute.CustomTokens.Main.route,
|
||||
route = ManageTokensRoute.CustomTokens.route,
|
||||
) {
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.Main.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
// CustomTokensScreen(state = viewModel.uiState)
|
||||
// TODO: enable in [REDACTED_JIRA]
|
||||
}
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.ChooseNetwork.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
// ChooseNetworkCustomScreen(
|
||||
// state = viewModel.uiState.chooseNetworkState,
|
||||
// )
|
||||
// TODO: enable in [REDACTED_JIRA]
|
||||
}
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.ChooseDerivation.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
// ChooseDerivationScreen(
|
||||
// state = requireNotNull(viewModel.uiState.chooseDerivationState),
|
||||
// )
|
||||
// TODO: enable in [REDACTED_JIRA]
|
||||
}
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.ChooseWallet.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
// CustomTokensChooseWalletScreen(
|
||||
// state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose,
|
||||
// )
|
||||
// TODO: enable in [REDACTED_JIRA]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popBackStack(screen: AppScreen?) {
|
||||
if (screen != null) {
|
||||
reduxNavController.navigate(action = NavigationAction.PopBackTo(screen))
|
||||
} else {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
override fun openManageTokensScreen() {
|
||||
navController.navigate(ManageTokensRoute.ManageTokens.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensScreen() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensChooseNetwork() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.ChooseNetwork.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensChooseDerivation() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.ChooseDerivation.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensChooseWallet() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.ChooseWallet.route)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.managetokens.presentation.router
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
|
||||
internal interface InnerManageTokensRouter : ManageTokensRouter {
|
||||
/**
|
||||
* Initialize router
|
||||
**/
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun Initialize(viewModelStoreOwner: ViewModelStoreOwner)
|
||||
|
||||
/** Pop back stack */
|
||||
fun popBackStack(screen: AppScreen? = null)
|
||||
|
||||
/** Open manage tokens screen */
|
||||
fun openManageTokensScreen()
|
||||
|
||||
/** Open custom tokens screen */
|
||||
fun openCustomTokensScreen()
|
||||
|
||||
fun openCustomTokensChooseNetwork()
|
||||
|
||||
fun openCustomTokensChooseDerivation()
|
||||
|
||||
fun openCustomTokensChooseWallet()
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.managetokens.presentation.router
|
||||
|
||||
/**
|
||||
* Manage Tokens screens
|
||||
*
|
||||
* @property route route string representation
|
||||
*
|
||||
*/
|
||||
internal sealed class ManageTokensRoute(val route: String) {
|
||||
|
||||
object ManageTokens : ManageTokensRoute(route = "manage_tokens")
|
||||
|
||||
object CustomTokens : ManageTokensRoute(route = "manage_tokens/custom_tokens") {
|
||||
object Main : ManageTokensRoute(CustomTokens.route + "/main")
|
||||
object ChooseNetwork : ManageTokensRoute(CustomTokens.route + "/choose_network")
|
||||
object ChooseWallet : ManageTokensRoute(CustomTokens.route + "/choose_wallet")
|
||||
object ChooseDerivation : ManageTokensRoute(CustomTokens.route + "/choose_derivation")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue