diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 3083d7a5df..0220fa2823 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -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) + } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index fa752cc4bd..ebbe1dc50a 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -30,4 +30,8 @@ class Debouncer { fun release() { debounceJob?.cancel() } + + companion object { + const val DEFAULT_WAIT_TIME_MS = 500L + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt index 86a31f0fea..f17fe671b9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt @@ -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 { + 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" diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt index d4f2d9a9a2..e083c266df 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt @@ -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) + } + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/FoundTokenConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/FoundTokenConverter.kt new file mode 100644 index 0000000000..78ce164eb6 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/FoundTokenConverter.kt @@ -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 { + + 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(), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/FoundToken.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/FoundToken.kt new file mode 100644 index 0000000000..cf81c58e8e --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/FoundToken.kt @@ -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, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AreTokensSupportedByNetworkUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AreTokensSupportedByNetworkUseCase.kt new file mode 100644 index 0000000000..fbb83e52d9 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AreTokensSupportedByNetworkUseCase.kt @@ -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 { + 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 { + return either { + catch( + block = { repository.areTokensSupportedByNetwork(networkId) }, + catch = { throwable -> raise(throwable) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FindTokenByContractAddressUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FindTokenByContractAddressUseCase.kt new file mode 100644 index 0000000000..809e8ca270 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FindTokenByContractAddressUseCase.kt @@ -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 { + return either { + catch( + block = { repository.findToken(contractAddress, networkId) }, + catch = { throwable -> raise(throwable) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworksSupportedByWallet.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworksSupportedByWallet.kt new file mode 100644 index 0000000000..e014cb48af --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworksSupportedByWallet.kt @@ -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> { + return either { + catch( + block = { + repository.getSupportedNetworks(userWalletId) + }, + catch = { throwable -> raise(throwable) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ValidateContractAddressUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ValidateContractAddressUseCase.kt new file mode 100644 index 0000000000..361a79c45c --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ValidateContractAddressUseCase.kt @@ -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 { + return either { + catch( + block = { + if (address.isEmpty()) raise(AddCustomTokenError.FieldIsEmpty) + + if (!tokensListRepository.validateAddress(networkId, address)) { + raise(AddCustomTokenError.InvalidContractAddress) + } + }, + catch = { + AddCustomTokenError.InvalidContractAddress + }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt index 6361ebdd73..1d0f6946b1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt @@ -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 + + fun areTokensSupportedByNetwork(networkId: String): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt index 992b9b8fcc..24d0605cda 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt @@ -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> + + /** + * 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 } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt index b898add2be..87b216af73 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt @@ -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() - // 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) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt index 73783601c0..12c7db4c53 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt @@ -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) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt index e8e7018826..6d79799bc2 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt @@ -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) + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt index 44a0529872..6a585d7148 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt @@ -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, } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt index 71e95a8bc2..14e49a02b6 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt @@ -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) } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt index 6b90d9f12b..0359b28aa1 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt @@ -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, + ) } } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/AddCustomTokenState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/AddCustomTokenState.kt new file mode 100644 index 0000000000..5c37331d11 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/AddCustomTokenState.kt @@ -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, + val addTokenButton: ButtonState, + val showChooseWalletScreen: Boolean = false, + val event: StateEvent = consumedEvent(), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/AddCustomTokenWarning.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/AddCustomTokenWarning.kt new file mode 100644 index 0000000000..fdca691ce8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/AddCustomTokenWarning.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ButtonState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ButtonState.kt new file mode 100644 index 0000000000..0b1412cc0b --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ButtonState.kt @@ -0,0 +1,6 @@ +package com.tangem.managetokens.presentation.customtokens.state + +internal data class ButtonState( + val isEnabled: Boolean, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ChooseDerivationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ChooseDerivationState.kt new file mode 100644 index 0000000000..c9bb1eefc4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ChooseDerivationState.kt @@ -0,0 +1,13 @@ +package com.tangem.managetokens.presentation.customtokens.state + +import kotlinx.collections.immutable.ImmutableList + +internal data class ChooseDerivationState( + val derivations: ImmutableList, + val selectedDerivation: Derivation?, + val enterCustomDerivationState: EnterCustomDerivationState?, + val onChooseDerivationClick: () -> Unit, + val onCloseChoosingDerivationClick: () -> Unit, + val onEnterCustomDerivation: () -> Unit, + val show: Boolean = false, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ChooseNetworkState.kt new file mode 100644 index 0000000000..1a09c93ec3 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/ChooseNetworkState.kt @@ -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, + val selectedNetwork: NetworkItemState?, + val onChooseNetworkClick: () -> Unit, + val onCloseChoosingNetworkClick: () -> Unit, + val show: Boolean = false, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/CustomTokenData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/CustomTokenData.kt new file mode 100644 index 0000000000..a586ec848a --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/CustomTokenData.kt @@ -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() + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/Derivation.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/Derivation.kt new file mode 100644 index 0000000000..cb06fa67b7 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/Derivation.kt @@ -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, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/EnterCustomDerivationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/EnterCustomDerivationState.kt new file mode 100644 index 0000000000..aefe21b295 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/EnterCustomDerivationState.kt @@ -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, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/TextFieldState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/TextFieldState.kt new file mode 100644 index 0000000000..090d4a2a22 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/TextFieldState.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt new file mode 100644 index 0000000000..94f5828c6c --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt @@ -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 { + + 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 + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/ContractAddressToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/ContractAddressToCustomTokenDataConverter.kt new file mode 100644 index 0000000000..b6ab812200 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/ContractAddressToCustomTokenDataConverter.kt @@ -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 { + 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, + ), + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/CustomTokensStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/CustomTokensStateFactory.kt new file mode 100644 index 0000000000..8a72ab97bf --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/CustomTokensStateFactory.kt @@ -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, + 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, + allUserWallets: List, + selectedWalletId: UserWalletId?, + supportedNetworks: List, + ): 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): List { + 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): 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, + ): 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, + allUserWallets: List, + 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, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/FoundTokenToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/FoundTokenToCustomTokenDataConverter.kt new file mode 100644 index 0000000000..ebe6b1bee2 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/FoundTokenToCustomTokenDataConverter.kt @@ -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 { + 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, + ), + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/NetworkToNetworkItemStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/NetworkToNetworkItemStateConverter.kt new file mode 100644 index 0000000000..dee07b42c5 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/NetworkToNetworkItemStateConverter.kt @@ -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 { + 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, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensClickIntents.kt new file mode 100644 index 0000000000..6601f3e270 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensClickIntents.kt @@ -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() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensViewModel.kt new file mode 100644 index 0000000000..cc265a1bc2 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensViewModel.kt @@ -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): 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 { + 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() + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt index 27810cf7f7..01af86d1f2 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt @@ -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 ), diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt index 02887479e7..475f9a8687 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt @@ -55,7 +55,7 @@ internal fun TokensList(tokens: LazyPagingItems, addCustomTokenB if (addCustomTokenButton.isVisible) { item { - AddCustomTokenButton(onButtonClick = { addCustomTokenButton.onClick }) + AddCustomTokenButton(onButtonClick = addCustomTokenButton.onClick) } } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt index 581bb496f7..ae04c6d7cd 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt @@ -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)) } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt index 593da2a10a..feeab80123 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt @@ -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().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(viewModelStoreOwner).apply { + router = this@DefaultManageTokensRouter + } + // CustomTokensScreen(state = viewModel.uiState) + // TODO: enable in [REDACTED_JIRA] + } + composable( + ManageTokensRoute.CustomTokens.ChooseNetwork.route, + ) { + val viewModel = hiltViewModel(viewModelStoreOwner).apply { + router = this@DefaultManageTokensRouter + } + // ChooseNetworkCustomScreen( + // state = viewModel.uiState.chooseNetworkState, + // ) + // TODO: enable in [REDACTED_JIRA] + } + composable( + ManageTokensRoute.CustomTokens.ChooseDerivation.route, + ) { + val viewModel = hiltViewModel(viewModelStoreOwner).apply { + router = this@DefaultManageTokensRouter + } + // ChooseDerivationScreen( + // state = requireNotNull(viewModel.uiState.chooseDerivationState), + // ) + // TODO: enable in [REDACTED_JIRA] + } + composable( + ManageTokensRoute.CustomTokens.ChooseWallet.route, + ) { + val viewModel = hiltViewModel(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) + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/InnerManageTokensRouter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/InnerManageTokensRouter.kt new file mode 100644 index 0000000000..29d0cd4c9e --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/InnerManageTokensRouter.kt @@ -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() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensRoute.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensRoute.kt new file mode 100644 index 0000000000..0e6a358743 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensRoute.kt @@ -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") + } +} \ No newline at end of file