diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt deleted file mode 100644 index e9b45a2623..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.data - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter -import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository -import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken -import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -/** - * Default implementation of custom token repository - * - * @property tangemTechApi TangemTech API - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * -[REDACTED_AUTHOR] - */ -class DefaultCustomTokenRepository( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, -) : CustomTokenRepository { - - override suspend fun findToken(address: String, networkId: String?): FoundToken { - val scanResponse = requireNotNull(reduxStateHolder.scanResponse) - val supportedTokenNetworkIds = requireNotNull(scanResponse.card) - .supportedBlockchains(scanResponse.cardTypesResolver) - .filter(Blockchain::canHandleTokens) - .map(Blockchain::toNetworkId) - - return withContext(dispatchers.io) { - val foundCoin = tangemTechApi.getCoins( - contractAddress = address, - networkIds = networkId ?: supportedTokenNetworkIds.joinToString(separator = ","), - ) - .getOrThrow() - .coins.firstNotNullOfOrNull { coin -> - val networksWithTheSameAddress = coin.networks.filter { network -> - (network.contractAddress != null || network.decimalCount != null) && - network.contractAddress?.equals(address, ignoreCase = true) == true && - supportedTokenNetworkIds.contains(network.networkId) - } - - if (networksWithTheSameAddress.isNotEmpty()) { - coin.copy(networks = networksWithTheSameAddress) - } else { - null - } - } - - foundCoin?.let(FoundTokenConverter::convert) ?: error("Token not found") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt deleted file mode 100644 index 33271e9fb6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.data.converters - -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken -import com.tangem.utils.converter.Converter - -/** - * Converter between data model [CoinsResponse.Coin] and domain model [FoundToken] - * -[REDACTED_AUTHOR] - */ -object FoundTokenConverter : Converter { - - override fun convert(value: CoinsResponse.Coin): FoundToken { - return FoundToken( - id = value.id, - name = value.name, - symbol = value.symbol, - isActive = value.active, - network = value.networks.firstOrNull()?.let { network -> - FoundToken.Network( - id = network.networkId, - contractAddress = requireNotNull(network.contractAddress), - decimalCount = requireNotNull(network.decimalCount).toString(), - ) - } ?: error("Found token networks is empty"), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt deleted file mode 100644 index 61dc90c2dc..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.di - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository -import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor -import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor -import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -/** -[REDACTED_AUTHOR] - */ -@Module -@InstallIn(ViewModelComponent::class) -internal object CustomTokenInteractorModule { - - @Provides - @ViewModelScoped - fun provideCustomTokenInteractor( - tangemTechApi: TangemTechApi, - appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider, - reduxStateHolder: AppStateHolder, - getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - derivePublicKeysUseCase: DerivePublicKeysUseCase, - ): CustomTokenInteractor { - return DefaultCustomTokenInteractor( - featureRepository = DefaultCustomTokenRepository( - tangemTechApi = tangemTechApi, - dispatchers = appCoroutineDispatcherProvider, - reduxStateHolder = reduxStateHolder, - ), - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - derivePublicKeysUseCase = derivePublicKeysUseCase, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenRouterModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenRouterModule.kt deleted file mode 100644 index 0c8dec0948..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenRouterModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.di - -import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter -import com.tangem.tap.features.customtoken.impl.presentation.routers.DefaultCustomTokenRouter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -/** -[REDACTED_AUTHOR] - */ -@Module -@InstallIn(ViewModelComponent::class) -internal object CustomTokenRouterModule { - - @Provides - @ViewModelScoped - fun provideAddCustomTokenRouter(): CustomTokenRouter = DefaultCustomTokenRouter() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt deleted file mode 100644 index 22bea5bf11..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.domain - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.core.TangemError -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken - -/** - * Custom token interactor - * -[REDACTED_AUTHOR] - */ -interface CustomTokenInteractor { - - /** Find token by [address] and [blockchain] */ - suspend fun findToken(address: String, blockchain: Blockchain): FoundToken - - /** Save token [customCurrency] */ - @Throws(TangemError::class) - suspend fun saveToken(customCurrency: CustomCurrency): Result -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt deleted file mode 100644 index cb1959d157..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.domain - -import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken - -/** - * Custom token repository - * -[REDACTED_AUTHOR] - */ -interface CustomTokenRepository { - - /** - * Find token by [address] and [networkId] - * - * @throws com.tangem.datasource.api.common.response.ApiResponseError - * */ - suspend fun findToken(address: String, networkId: String?): FoundToken -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt deleted file mode 100644 index bd9cc89301..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.domain - -import arrow.core.getOrElse -import arrow.core.raise.result -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.domain.model.Currency -import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store - -/** - * Default implementation of custom token interactor - * - * @property featureRepository feature repository - * -[REDACTED_AUTHOR] - */ -class DefaultCustomTokenInteractor( - private val featureRepository: CustomTokenRepository, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, -) : CustomTokenInteractor { - - // TODO: Move to DI - private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) { - val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository) - val networksRepository = store.inject(DaggerGraphState::networksRepository) - - AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository) - } - - override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken { - return featureRepository.findToken( - address = address, - networkId = if (blockchain != Blockchain.Unknown) blockchain.toNetworkId() else null, - ) - } - - override suspend fun saveToken(customCurrency: CustomCurrency): Result { - return result { - val userWallet = getSelectedWalletSyncUseCase().getOrElse { - error("Failed to get selected wallet: $it") - } - - val currency = Currency.fromCustomCurrency(customCurrency) - val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse)) - - derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind() - addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind() - } - } - - private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? { - val cryptoCurrencyFactory = CryptoCurrencyFactory() - - return when (this) { - is Currency.Blockchain -> { - cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = derivationPath, - derivationStyleProvider = scanResponse.derivationStyleProvider, - ) - } - is Currency.Token -> { - cryptoCurrencyFactory.createToken( - sdkToken = token, - blockchain = blockchain, - extraDerivationPath = derivationPath, - derivationStyleProvider = scanResponse.derivationStyleProvider, - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt deleted file mode 100644 index 5b39f2dd52..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.domain.models - -/** - * Found token model - * - * @property id id - * @property name name - * @property symbol symbol - * @property isActive flag that determines status of token - * @property network network - * -[REDACTED_AUTHOR] - */ -data class FoundToken( - val id: String, - val name: String, - val symbol: String, - val isActive: Boolean, - val network: Network, -) { - - /** - * Found token network - * - * @property id id - * @property contractAddress address - * @property decimalCount decimal count - */ - data class Network(val id: String, val contractAddress: String, val decimalCount: String) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt deleted file mode 100644 index 443a8af290..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen -import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -/** - * Add custom token screen - * -[REDACTED_AUTHOR] - */ -@AndroidEntryPoint -internal class AddCustomTokenFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - @Composable - override fun ScreenContent(modifier: Modifier) { - val viewModel = hiltViewModel().apply { - LocalLifecycleOwner.current.lifecycle.addObserver(this) - } - val state by viewModel.uiState.collectAsStateWithLifecycle() - - AddCustomTokenScreen( - modifier = Modifier - .background(TangemTheme.colors.background.primary) - .systemBarsPadding(), - stateHolder = state, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt deleted file mode 100644 index 6ba979beab..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt +++ /dev/null @@ -1,324 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.models - -import androidx.compose.foundation.text.KeyboardOptions -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.wallet.R - -/** - * Toolbar model of add custom token screen - * - * @property title title - * @property onBackButtonClick lambda be invoked when back button is been pressed - */ -internal data class AddCustomTokensToolbar(val title: TextReference, val onBackButtonClick: () -> Unit) - -/** - * Model of block with fields for testing - * - * @property chooseTokenButtonText choose token button text - * @property clearButtonText clear button text - * @property resetButtonText reset button text - * @property onClearAddressButtonClick lambda be invoked when clear address button is been pressed - * @property onResetButtonClick lambda be invoked when reset form fields button is been pressed - */ -internal data class AddCustomTokenTestBlock( - val chooseTokenButtonText: String, - val clearButtonText: String, - val resetButtonText: String, - val onClearAddressButtonClick: () -> Unit, - val onResetButtonClick: () -> Unit, -) - -/** - * Bottom sheet model for choose custom token - * - * @property categoriesBlocks tokens categories - * @property onTestTokenClick lambda be invoked when token is been pressed - */ -internal data class AddCustomTokenChooseTokenBottomSheet( - val categoriesBlocks: List, - val onTestTokenClick: (String) -> Unit, -) { - - /** - * Tokens category model - * - * @property name category name - * @property items category items - */ - data class TokensCategoryBlock(val name: String, val items: List) - - /** - * Test token model - * - * @property name token name - * @property address token address - */ - data class TestTokenItem(val name: String, val address: String) -} - -/** - * Form with fields model of add custom token screen - * - * @property contractAddressInputField input field to enter the contract address - * @property networkSelectorField selector field to select the token network - * @property tokenNameInputField input field to enter the token name - * @property tokenSymbolInputField input field to enter the token symbol - * @property decimalsInputField input field to enter the token decimals - * @property derivationPathSelectorField selector field to select the derivation path - * @property derivationPathInputField input field for a custom derivation path - * @property showTokenFields if token fields should be shown - */ -internal data class AddCustomTokenForm( - val contractAddressInputField: AddCustomTokenInputField.ContactAddress, - val networkSelectorField: AddCustomTokenSelectorField.Network, - val tokenNameInputField: AddCustomTokenInputField.TokenName, - val tokenSymbolInputField: AddCustomTokenInputField.TokenSymbol, - val decimalsInputField: AddCustomTokenInputField.Decimals, - val derivationPathSelectorField: AddCustomTokenSelectorField.DerivationPath?, - val derivationPathInputField: AddCustomTokenInputField.DerivationPath?, - val showTokenFields: Boolean = false, -) - -/** Base input field model of add custom token screen */ -internal sealed interface AddCustomTokenInputField { - - /** Current value */ - val value: String - - /** Lambda be invoked when value is been changed */ - val onValueChange: (String) -> Unit - - /** Keyboard options */ - val keyboardOptions: KeyboardOptions - - /** Label */ - val label: TextReference - - /** Placeholder (hint) */ - val placeholder: TextReference - - /** - * Input field model to enter the contract address - * - * @property value current value - * @property onValueChange lambda be invoked when value is been changed - * @property keyboardOptions keyboard options - * @property label label - * @property placeholder placeholder (hint) - * @property isLoading flag that determine the processing of current value - * @property isError flag that determine if current value has error - * @property error error description - */ - data class ContactAddress( - override val value: String, - override val onValueChange: (String) -> Unit, - override val keyboardOptions: KeyboardOptions, - override val label: TextReference, - override val placeholder: TextReference, - val isLoading: Boolean, - val isError: Boolean, - val error: TextReference? = null, - ) : AddCustomTokenInputField - - /** - * Input field model to enter the token name - * - * @property value current value - * @property onValueChange lambda be invoked when value is been changed - * @property keyboardOptions keyboard options - * @property label label - * @property placeholder placeholder (hint) - * @property isEnabled input availability - */ - data class TokenName( - override val value: String, - override val onValueChange: (String) -> Unit, - override val keyboardOptions: KeyboardOptions, - override val label: TextReference, - override val placeholder: TextReference, - val isEnabled: Boolean, - ) : AddCustomTokenInputField - - /** - * Input field model to enter the token symbol - * - * @property value current value - * @property onValueChange lambda be invoked when value is been changed - * @property keyboardOptions keyboard options - * @property label label - * @property placeholder placeholder (hint) - * @property isEnabled input availability - */ - data class TokenSymbol( - override val value: String, - override val onValueChange: (String) -> Unit, - override val keyboardOptions: KeyboardOptions, - override val label: TextReference, - override val placeholder: TextReference, - val isEnabled: Boolean, - ) : AddCustomTokenInputField - - /** - * Input field model to enter the token decimals - * - * @property value current value - * @property onValueChange lambda be invoked when value is been changed - * @property keyboardOptions keyboard options - * @property label label - * @property placeholder placeholder (hint) - * @property isEnabled input availability - */ - data class Decimals( - override val value: String, - override val onValueChange: (String) -> Unit, - override val keyboardOptions: KeyboardOptions, - override val label: TextReference, - override val placeholder: TextReference, - val isEnabled: Boolean, - ) : AddCustomTokenInputField - - /** - * Input field model to enter a custom derivation path - * - * @property value current value - * @property onValueChange lambda be invoked when value is been changed - * @property keyboardOptions keyboard options - * @property label label - * @property placeholder placeholder (hint) - * @property showField whether the field should be shown - */ - data class DerivationPath( - override val value: String, - override val onValueChange: (String) -> Unit, - override val keyboardOptions: KeyboardOptions, - override val label: TextReference, - override val placeholder: TextReference, - val showField: Boolean = false, - ) : AddCustomTokenInputField -} - -/** Base selector field model of add custom token screen */ -internal sealed interface AddCustomTokenSelectorField { - - /** Label */ - val label: TextReference - - /** Selected menu item */ - val selectedItem: SelectorItem - - /** Menu items */ - val items: List - - /** Lambda be invoked when menu item is been selected */ - val onMenuItemClick: (Int) -> Unit - - /** - * Network selector model - * - * @property label label - * @property selectedItem selected menu item - * @property items menu items - * @property onMenuItemClick lambda be invoked when menu item is been selected - */ - data class Network( - override val label: TextReference, - override val selectedItem: SelectorItem.Title, - override val items: List, - override val onMenuItemClick: (Int) -> Unit, - ) : AddCustomTokenSelectorField - - /** - * Derivation path selector model - * - * @property label label - * @property selectedItem selected menu item - * @property items menu items - * @property onMenuItemClick lambda be invoked when menu item is been selected - * @property isEnabled selection availability - */ - data class DerivationPath( - override val label: TextReference, - override val selectedItem: SelectorItem.TitleWithSubtitle, - override val items: List, - override val onMenuItemClick: (Int) -> Unit, - val isEnabled: Boolean, - ) : AddCustomTokenSelectorField - - /** Base menu item model */ - sealed interface SelectorItem { - - /** Title */ - val title: TextReference - - /** Blockchain */ - val blockchain: Blockchain - - /** - * Menu item with title - * - * @property title title text - * @property blockchain blockchain - */ - data class Title(override val title: TextReference, override val blockchain: Blockchain) : SelectorItem - - /** - * Menu item with title ans subtitle - * - * @property title title text - * @property blockchain blockchain - * @property subtitle subtitle text - */ - data class TitleWithSubtitle( - override val title: TextReference, - override val blockchain: Blockchain, - val subtitle: TextReference, - val type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN, - ) : SelectorItem - } -} - -enum class DerivationPathSelectorType { - DEFAULT, CUSTOM, BLOCKCHAIN -} - -/** - * Warning model of add custom token screen - * - * @property description warning description - */ -internal sealed class AddCustomTokenWarning(val description: TextReference) { - - /** Potential scam warning */ - data object PotentialScamToken : AddCustomTokenWarning( - description = TextReference.Res(R.string.custom_token_validation_error_not_found), - ) - - /** Token already added warning */ - data object TokenAlreadyAdded : AddCustomTokenWarning( - description = TextReference.Res(R.string.custom_token_validation_error_already_added), - ) - - /** Unsupported token warning */ - data class UnsupportedToken(val networkName: String) : AddCustomTokenWarning( - description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message, networkName), - ) - - data object WrongDerivationPath : AddCustomTokenWarning( - description = TextReference.Res(R.string.custom_token_invalid_derivation_path), - ) -} - -/** - * Floating button of add custom token screen - * - * @property isEnabled button availability - * @property showProgress whether circle progress indication is enabled - * @property onClick lambda be invoked when button is been pressed - */ -internal data class AddCustomTokenFloatingButton( - val isEnabled: Boolean, - val showProgress: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/CustomTokenType.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/CustomTokenType.kt deleted file mode 100644 index b36fe58ce3..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/CustomTokenType.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.models - -/** Custom token type */ -enum class CustomTokenType { TOKEN, BLOCKCHAIN } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/SupportBlockchainType.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/SupportBlockchainType.kt deleted file mode 100644 index 27215046bd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/SupportBlockchainType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.models - -internal enum class SupportBlockchainType { - - SUPPORTED, UNSUPPORTED, UNABLE_TO_DETERMINE -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt deleted file mode 100644 index 1c016c4993..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.routers - -import com.tangem.blockchain.common.Blockchain - -/** - * Custom token feature router - * -[REDACTED_AUTHOR] - */ -internal interface CustomTokenRouter { - - /** Return to last screen */ - fun popBackStack() - - /** Open wallet (main) screen */ - fun openWalletScreen() - - /** Open alert if solana network is unsupported - * - * @param blockchain blockchain to show alert - */ - fun openUnsupportedNetworkAlert(blockchain: Blockchain) - - fun showGenericErrorAlertAndPopBack() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt deleted file mode 100644 index 348c3492ce..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.routers - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter -import com.tangem.common.routing.utils.popTo -import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.store -import com.tangem.wallet.R - -/** Default implementation of custom token feature router */ -internal class DefaultCustomTokenRouter : CustomTokenRouter { - - override fun popBackStack() { - store.dispatchNavigationAction(AppRouter::pop) - } - - override fun openWalletScreen() { - store.dispatchNavigationAction { popTo() } - } - - override fun openUnsupportedNetworkAlert(blockchain: Blockchain) { - val alert = AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.alert_manage_tokens_unsupported_curve_message, - args = listOf(blockchain.getNetworkName()), - ) - store.dispatchDialogShow(alert) - } - - override fun showGenericErrorAlertAndPopBack() { - val alert = AppDialog.SimpleOkDialogRes( - headerId = R.string.common_error, - messageId = R.string.common_unknown_error, - onOk = { store.dispatchNavigationAction(AppRouter::pop) }, - ) - store.dispatchDialogShow(alert) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/states/AddCustomTokenStateHolder.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/states/AddCustomTokenStateHolder.kt deleted file mode 100644 index 32eba291ef..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/states/AddCustomTokenStateHolder.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.states - -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar - -/** - * State holder of add custom token screen - * -[REDACTED_AUTHOR] - */ -internal sealed interface AddCustomTokenStateHolder { - - /** Lambda be invoked when system back action is been called */ - val onBackButtonClick: () -> Unit - - /** Toolbar model */ - val toolbar: AddCustomTokensToolbar - - /** Form model */ - val form: AddCustomTokenForm - - /** Warnings */ - val warnings: Set - - /** Floating button model */ - val floatingButton: AddCustomTokenFloatingButton - - /** - * Util function that allow to make a copy - * - * @param onBackButtonClick lambda be invoked when system back action is been called - * @param toolbar toolbar model - * @param form form model - * @param warnings warnings - * @param floatingButton floating button model - */ - fun copySealed( - onBackButtonClick: () -> Unit = this.onBackButtonClick, - toolbar: AddCustomTokensToolbar = this.toolbar, - form: AddCustomTokenForm = this.form, - warnings: Set = this.warnings, - floatingButton: AddCustomTokenFloatingButton = this.floatingButton, - ): AddCustomTokenStateHolder { - return when (this) { - is Content -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton) - is TestContent -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton) - } - } - - /** - * Content state - * - * @property onBackButtonClick lambda be invoked when system back action is been called - * @property toolbar toolbar model - * @property form form model - * @property warnings warnings - * @property floatingButton floating button model - */ - data class Content( - override val onBackButtonClick: () -> Unit, - override val toolbar: AddCustomTokensToolbar, - override val form: AddCustomTokenForm, - override val warnings: Set, - override val floatingButton: AddCustomTokenFloatingButton, - ) : AddCustomTokenStateHolder - - /** - * Content state with fields for testing - * - * @property onBackButtonClick lambda be invoked when system back action is been called - * @property toolbar toolbar model - * @property form form model - * @property warnings warnings - * @property floatingButton floating button model - * @property testBlock test block model - * @property bottomSheet bottom sheet model - */ - data class TestContent( - override val onBackButtonClick: () -> Unit, - override val toolbar: AddCustomTokensToolbar, - override val form: AddCustomTokenForm, - override val warnings: Set, - override val floatingButton: AddCustomTokenFloatingButton, - val testBlock: AddCustomTokenTestBlock, - val bottomSheet: AddCustomTokenChooseTokenBottomSheet, - ) : AddCustomTokenStateHolder -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt deleted file mode 100644 index afff4bf8e5..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.FabPosition -import androidx.compose.material.Scaffold -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings - -/** - * Add custom token content - * - * @param state screen state - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, modifier: Modifier = Modifier) { - BackHandler(onBack = state.onBackButtonClick) - - var floatingButtonHeight by remember { mutableStateOf(0.dp) } - Scaffold( - modifier = modifier, - topBar = { - AddCustomTokenToolbar( - title = state.toolbar.title, - onBackButtonClick = state.toolbar.onBackButtonClick, - ) - }, - floatingActionButton = { - val density = LocalDensity.current - val verticalPadding = TangemTheme.dimens.spacing32 - AddCustomTokenFloatingButton( - model = state.floatingButton, - modifier = Modifier.onSizeChanged { - floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding } - }, - ) - }, - floatingActionButtonPosition = FabPosition.Center, - ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(paddingValues = it) - .padding(bottom = floatingButtonHeight) - .fillMaxSize(), - ) { - AddCustomTokenForm(model = state.form) - - AddCustomTokenWarnings(warnings = state.warnings) - } - } -} - -@Preview -@Composable -private fun Preview_AddCustomTokenContent() { - TangemThemePreview { - AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt deleted file mode 100644 index ba7d22727c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.customtoken.impl.presentation.models.* -import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -internal object AddCustomTokenPreviewData { - - fun createWarnings(): Set { - return setOf( - AddCustomTokenWarning.PotentialScamToken, - AddCustomTokenWarning.TokenAlreadyAdded, - AddCustomTokenWarning.UnsupportedToken(networkName = "Solana"), - ) - } - - fun createDefaultForm(): AddCustomTokenForm { - return AddCustomTokenForm( - contractAddressInputField = AddCustomTokenInputField.ContactAddress( - value = "", - onValueChange = {}, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_contract_address_input_title), - placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000"), - isLoading = false, - isError = false, - error = null, - ), - networkSelectorField = AddCustomTokenSelectorField.Network( - label = TextReference.Res(R.string.custom_token_network_input_title), - selectedItem = AddCustomTokenSelectorField.SelectorItem.Title( - title = TextReference.Res(R.string.custom_token_network_input_not_selected), - blockchain = Blockchain.Unknown, - ), - items = emptyList(), - onMenuItemClick = {}, - ), - tokenNameInputField = AddCustomTokenInputField.TokenName( - value = "", - onValueChange = {}, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_name_input_title), - placeholder = TextReference.Res(id = R.string.custom_token_name_input_placeholder), - isEnabled = false, - ), - tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol( - value = "", - onValueChange = {}, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old), - placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder), - isEnabled = false, - ), - decimalsInputField = AddCustomTokenInputField.Decimals( - value = "", - onValueChange = {}, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_decimals_input_title), - placeholder = TextReference.Str(value = "8"), - isEnabled = false, - ), - derivationPathSelectorField = AddCustomTokenSelectorField.DerivationPath( - label = TextReference.Res(R.string.custom_token_derivation_path_input_title), - selectedItem = AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle( - title = TextReference.Res(R.string.custom_token_derivation_path_default), - subtitle = TextReference.Res(R.string.custom_token_derivation_path_default), - blockchain = Blockchain.Unknown, - ), - items = emptyList(), - onMenuItemClick = {}, - isEnabled = true, - ), - derivationPathInputField = null, - ) - } - - fun createTestContent(): AddCustomTokenStateHolder.TestContent { - return AddCustomTokenStateHolder.TestContent( - onBackButtonClick = {}, - toolbar = AddCustomTokensToolbar( - title = TextReference.Res(R.string.add_custom_token_title), - onBackButtonClick = {}, - ), - form = createDefaultForm(), - warnings = createWarnings(), - floatingButton = AddCustomTokenFloatingButton( - isEnabled = false, - showProgress = false, - onClick = {}, - ), - testBlock = AddCustomTokenTestBlock( - chooseTokenButtonText = "Choose token", - clearButtonText = "Clear address", - resetButtonText = "Reset", - onClearAddressButtonClick = {}, - onResetButtonClick = {}, - ), - bottomSheet = AddCustomTokenChooseTokenBottomSheet(categoriesBlocks = emptyList(), onTestTokenClick = {}), - ) - } - - fun createContent(): AddCustomTokenStateHolder.Content { - return AddCustomTokenStateHolder.Content( - onBackButtonClick = {}, - toolbar = AddCustomTokensToolbar( - title = TextReference.Res(R.string.add_custom_token_title), - onBackButtonClick = {}, - ), - form = createDefaultForm(), - warnings = createWarnings(), - floatingButton = AddCustomTokenFloatingButton( - isEnabled = false, - showProgress = false, - onClick = {}, - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt deleted file mode 100644 index d2e3729321..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui - -import android.content.res.Configuration -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder - -/** - * Add custom token screen - * - * @param stateHolder state holder - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifier: Modifier = Modifier) { - when (stateHolder) { - is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(stateHolder, modifier) - is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(stateHolder, modifier) - } -} - -@Preview(showSystemUi = true) -@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AddCustomTokenScreen( - @PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder, -) { - TangemThemePreview { - AddCustomTokenScreen(stateHolder) - } -} - -private class AddCustomTokenScreenProvider : CollectionPreviewParameterProvider( - collection = listOf( - AddCustomTokenPreviewData.createContent(), - AddCustomTokenPreviewData.createTestContent(), - ), -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt deleted file mode 100644 index f5b7759a82..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock -import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar -import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -/** - * Add custom token content for testing - * - * @param state screen state - * -[REDACTED_AUTHOR] - */ -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) { - val coroutineScope = rememberCoroutineScope() - val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current), - ) - - BackHandler( - onBack = { - onBackButtonClicked( - coroutineScope = coroutineScope, - bottomSheetScaffoldState = bottomSheetScaffoldState, - defaultAction = state.onBackButtonClick, - ) - }, - ) - - var floatingButtonHeight by remember { mutableStateOf(0.dp) } - BottomSheetScaffold( - modifier = modifier, - sheetContent = { - SheetContent( - coroutineScope = coroutineScope, - bottomSheetScaffoldState = bottomSheetScaffoldState, - model = state.bottomSheet, - ) - }, - scaffoldState = bottomSheetScaffoldState, - topBar = { - AddCustomTokenToolbar( - title = state.toolbar.title, - onBackButtonClick = { - onBackButtonClicked( - coroutineScope, - bottomSheetScaffoldState, - defaultAction = state.toolbar.onBackButtonClick, - ) - }, - ) - }, - floatingActionButton = { - val density = LocalDensity.current - val verticalPadding = TangemTheme.dimens.spacing32 - AddCustomTokenFloatingButton( - model = state.floatingButton, - modifier = Modifier.onSizeChanged { - floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding } - }, - ) - }, - floatingActionButtonPosition = FabPosition.Center, - sheetBackgroundColor = TangemTheme.colors.background.secondary, - sheetPeekHeight = TangemTheme.dimens.size0, - backgroundColor = TangemTheme.colors.background.primary, - ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(paddingValues = it) - .padding(bottom = floatingButtonHeight) - .fillMaxSize(), - ) { - TestBlock( - state.testBlock, - coroutineScope, - bottomSheetScaffoldState, - ) - - AddCustomTokenForm(model = state.form) - - AddCustomTokenWarnings(warnings = state.warnings) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -private fun onBackButtonClicked( - coroutineScope: CoroutineScope, - bottomSheetScaffoldState: BottomSheetScaffoldState, - defaultAction: () -> Unit, -) { - coroutineScope.launch { - if (bottomSheetScaffoldState.bottomSheetState.isExpanded) { - bottomSheetScaffoldState.bottomSheetState.collapse() - } else { - defaultAction() - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun SheetContent( - model: AddCustomTokenChooseTokenBottomSheet, - coroutineScope: CoroutineScope, - bottomSheetScaffoldState: BottomSheetScaffoldState, -) { - Column( - modifier = Modifier - .fillMaxWidth() - .height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16), - ) { - Hand() - - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - ) { - model.categoriesBlocks.forEachIndexed { index, categoryBlock -> - key(categoryBlock) { - Column { - TokensList( - title = categoryBlock.name, - tokens = categoryBlock.items, - onTestTokenClick = { address -> - model.onTestTokenClick(address) - coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() } - }, - ) - - if (model.categoriesBlocks.lastIndex != index) { - Divider() - SpacerH8() - } - } - } - } - } - } -} - -@Composable -private fun TokensList(title: String, tokens: List, onTestTokenClick: (String) -> Unit) { - Text( - text = title, - modifier = Modifier.padding( - horizontal = TangemTheme.dimens.spacing24, - vertical = TangemTheme.dimens.spacing8, - ), - maxLines = 1, - style = TangemTheme.typography.h3, - ) - - tokens.forEach { token -> - key(token) { - PrimaryButton( - text = token.name, - onClick = { onTestTokenClick(token.address) }, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .padding(bottom = TangemTheme.dimens.spacing8) - .fillMaxWidth(), - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) -@Composable -private fun TestBlock( - model: AddCustomTokenTestBlock, - coroutineScope: CoroutineScope, - bottomSheetScaffoldState: BottomSheetScaffoldState, -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16) - .padding(top = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), - ) { - val softwareKeyboardController = LocalSoftwareKeyboardController.current - PrimaryButton( - text = model.chooseTokenButtonText, - onClick = { - softwareKeyboardController?.hide() - coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.expand() } - }, - modifier = Modifier.fillMaxWidth(), - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - verticalAlignment = Alignment.CenterVertically, - ) { - PrimaryButton( - text = model.clearButtonText, - onClick = model.onClearAddressButtonClick, - modifier = Modifier.weight(1f), - ) - PrimaryButton( - text = model.resetButtonText, - onClick = model.onResetButtonClick, - modifier = Modifier.weight(1f), - ) - } - } -} - -@Preview -@Composable -private fun Preview_AddCustomTokenTestContent() { - TangemThemePreview { - AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt deleted file mode 100644 index 3cc1d4847a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui.components - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton -import com.tangem.wallet.R - -/** - * Add custom token floating button. Attached above the keyboard. - * - * @param model button model - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, modifier: Modifier = Modifier) { - PrimaryButtonIconStart( - modifier = modifier - .imePadding() - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource(id = R.string.custom_token_add_token), - iconResId = R.drawable.ic_plus_24, - enabled = model.isEnabled, - showProgress = model.showProgress, - onClick = model.onClick, - ) -} - -@Preview -@Composable -private fun Preview_AddCustomTokenFloatingButton( - @PreviewParameter(AddCustomTokenFloatingButtonProvider::class) model: AddCustomTokenFloatingButton, -) { - TangemThemePreview { - AddCustomTokenFloatingButton(model) - } -} - -private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider( - listOf( - AddCustomTokenFloatingButton(isEnabled = true, showProgress = false, onClick = {}), - AddCustomTokenFloatingButton(isEnabled = false, showProgress = false, onClick = {}), - ), -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt deleted file mode 100644 index 58da365d81..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt +++ /dev/null @@ -1,212 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui.components - -import androidx.compose.animation.* -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.common.compose.TangemTextFieldsDefault -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField -import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData -import com.tangem.tap.features.details.ui.cardsettings.resolveReference - -/** - * Add custom token form - * - * @param model component model - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun AddCustomTokenForm(model: AddCustomTokenForm) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing16), - shape = RoundedCornerShape(TangemTheme.dimens.radius8), - backgroundColor = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation4, - ) { - Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - if (model.showTokenFields) InputField(model = model.contractAddressInputField) - SelectorField(model = model.networkSelectorField) - if (model.showTokenFields) InputField(model = model.tokenNameInputField) - if (model.showTokenFields) InputField(model = model.tokenSymbolInputField) - if (model.showTokenFields) InputField(model = model.decimalsInputField) - model.derivationPathSelectorField?.let { SelectorField(model = it) } - if (model.derivationPathInputField?.showField == true) InputField(model = model.derivationPathInputField) - } - } -} - -@Composable -private fun InputField(model: AddCustomTokenInputField) { - Column { - val isError = (model as? AddCustomTokenInputField.ContactAddress)?.isError ?: false - - TextField(model, isError) - - (model as? AddCustomTokenInputField.ContactAddress)?.error?.resolveReference()?.let { - AnimatedVisibility( - visible = isError, - enter = fadeIn() + slideInVertically(), - exit = slideOutVertically() + fadeOut(), - ) { - Text( - text = it, - color = MaterialTheme.colors.error, - style = TangemTheme.typography.body2, - ) - } - } - } -} - -@Composable -private fun TextField(model: AddCustomTokenInputField, isError: Boolean) { - Box { - val isEnabled = when (model) { - is AddCustomTokenInputField.ContactAddress -> true - is AddCustomTokenInputField.Decimals -> model.isEnabled - is AddCustomTokenInputField.TokenName -> model.isEnabled - is AddCustomTokenInputField.TokenSymbol -> model.isEnabled - is AddCustomTokenInputField.DerivationPath -> true - } - - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = model.value, - onValueChange = model.onValueChange, - keyboardOptions = model.keyboardOptions, - label = { - Text( - text = model.label.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor( - enabled = isEnabled, - error = isError, - interactionSource = remember { MutableInteractionSource() }, - ).value, - ) - }, - placeholder = { - Text( - text = model.placeholder.resolveReference(), - style = TangemTheme.typography.body1, - color = TangemTextFieldsDefault.defaultTextFieldColors - .placeholderColor(enabled = isEnabled) - .value, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - singleLine = true, - enabled = isEnabled, - isError = isError, - colors = TangemTextFieldsDefault.defaultTextFieldColors, - ) - - AnimatedVisibility( - visible = (model as? AddCustomTokenInputField.ContactAddress)?.isLoading ?: false, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(horizontal = TangemTheme.dimens.spacing6) - .padding(bottom = TangemTheme.dimens.spacing6), - ) { - LinearProgressIndicator(color = TangemTheme.colors.icon.primary1) - } - } -} - -/** - * Be careful with this function – ExposedDropdownMenuBox can crash the app if it is open and user clicks system back - * button. It was fixed in compose-material 1.6.4. - */ -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun SelectorField(model: AddCustomTokenSelectorField) { - var isExpanded by remember { mutableStateOf(value = false) } - - ExposedDropdownMenuBox( - expanded = isExpanded, - onExpandedChange = { isExpanded = !isExpanded }, - ) { - val isEnabled = (model as? AddCustomTokenSelectorField.DerivationPath)?.isEnabled ?: true - OutlinedTextField( - value = when (val item = model.selectedItem) { - is AddCustomTokenSelectorField.SelectorItem.Title -> item.title - is AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle -> item.subtitle - }.resolveReference(), - modifier = Modifier.fillMaxWidth(), - onValueChange = {}, - readOnly = true, - enabled = isEnabled, - label = { Text(text = model.label.resolveReference()) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) }, - colors = TangemTextFieldsDefault.defaultTextFieldColors, - ) - - ExposedDropdownMenu( - expanded = isExpanded && isEnabled, - onDismissRequest = { isExpanded = false }, - ) { - model.items.forEachIndexed { index, item -> - DropdownMenuItem( - onClick = { - model.onMenuItemClick(index) - isExpanded = false - }, - ) { - Column { - Text(text = item.title.resolveReference()) - - val subtitle = (item as? AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle) - ?.subtitle?.resolveReference() - - if (!subtitle.isNullOrBlank()) { - Text( - text = subtitle, - color = TangemTheme.colors.text.secondary, - maxLines = 1, - style = TangemTheme.typography.caption2, - ) - } - } - } - } - } - } -} - -@Preview -@Composable -private fun Preview_AddCustomTokenForm(@PreviewParameter(AddCustomTokenFormProvider::class) model: AddCustomTokenForm) { - TangemThemePreview { - AddCustomTokenForm(model) - } -} - -private class AddCustomTokenFormProvider : CollectionPreviewParameterProvider( - collection = listOf( - AddCustomTokenPreviewData.createDefaultForm(), - AddCustomTokenPreviewData.createDefaultForm().copy(derivationPathSelectorField = null), - AddCustomTokenPreviewData.createDefaultForm().let { form -> - form.copy(contractAddressInputField = form.contractAddressInputField.copy(isLoading = true)) - }, - ), -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt deleted file mode 100644 index 030f130a1a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui.components - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.tap.features.details.ui.cardsettings.resolveReference - -/** - * Add custom token toolbar - * - * @param title title - * @param onBackButtonClick lambda be invoked when BackButton is been pressed - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () -> Unit) { - val toolbarElevation = if (isSystemInDarkTheme()) { - TangemTheme.dimens.elevation0 - } else { - AppBarDefaults.TopAppBarElevation - } - TopAppBar( - backgroundColor = TangemTheme.colors.background.primary, - elevation = toolbarElevation, - ) { - IconButton(onClick = onBackButtonClick) { - Icon( - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - tint = TangemTheme.colors.icon.secondary, - ) - } - - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing26)) - - Text( - text = title.resolveReference(), - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.h3, - ) - } -} - -@Preview -@Composable -internal fun Preview_AddCustomTokenToolbar() { - TangemThemePreview { - AddCustomTokenToolbar(title = TextReference.Res(R.string.add_custom_token_title), onBackButtonClick = {}) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt deleted file mode 100644 index a323a8a982..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Card -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning -import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData -import com.tangem.tap.features.details.ui.cardsettings.resolveReference -import com.tangem.wallet.R - -/** - * Add custom token warnings - * - * @param warnings warnings descriptions set - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun AddCustomTokenWarnings(warnings: Set) { - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - warnings.forEach { warning -> - key(warning) { AddCustomTokenWarning(warning) } - } - } -} - -@Composable -private fun AddCustomTokenWarning(warning: AddCustomTokenWarning) { - Card( - modifier = Modifier.fillMaxSize(), - shape = TangemTheme.shapes.roundedCornersSmall2, - backgroundColor = TangemColorPalette.Tangerine, - contentColor = TangemColorPalette.White, - elevation = TangemTheme.dimens.elevation4, - ) { - // FIXME("Incorrect typography. Replace with typography from design system") - Column( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Text( - text = stringResource(id = R.string.common_warning), - maxLines = 1, - style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.Bold), - ) - Text( - text = warning.description.resolveReference(), - fontSize = 13.sp, - lineHeight = 18.sp, - ) - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AddCustomTokenWarnings() { - TangemThemePreview { - AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt deleted file mode 100644 index 966af2bd6a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.validators - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.address.AddressService -import com.tangem.common.card.EllipticCurve -import com.tangem.domain.tokens.error.AddCustomTokenError - -/** - * Validator of contract address - * -[REDACTED_AUTHOR] - */ -object ContractAddressValidator { - - /** Validate a [address] using [blockchain] */ - fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult { - return when { - address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FIELD_IS_EMPTY) - validateAddress(blockchain, address) -> ContractAddressValidatorResult.Success - else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.INVALID_CONTRACT_ADDRESS) - } - } - - private fun validateAddress(blockchain: Blockchain, address: String): Boolean { - return when (blockchain) { - Blockchain.Unknown, - Blockchain.Binance, - Blockchain.BinanceTestnet, - -> SuccessAddressValidator.validate(address) - Blockchain.Cardano -> blockchain.validateContractAddress(address) - else -> blockchain.validateAddress(address) - } - } - - private object SuccessAddressValidator : AddressService() { - override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String { - throw UnsupportedOperationException() - } - - override fun validate(address: String): Boolean = true - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidatorResult.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidatorResult.kt deleted file mode 100644 index daf1b4d159..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidatorResult.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.validators - -import com.tangem.domain.tokens.error.AddCustomTokenError - -/** - * Result of validation contract address - * -[REDACTED_AUTHOR] - */ -sealed interface ContractAddressValidatorResult { - - /** Success */ - object Success : ContractAddressValidatorResult - - /** - * Error - * - * @property type type of error - */ - data class Error(val type: AddCustomTokenError) : ContractAddressValidatorResult -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenAnalyticsSender.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenAnalyticsSender.kt deleted file mode 100644 index 55886b316a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenAnalyticsSender.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.viewmodels - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.tap.common.analytics.events.ManageTokens - -/** Analytics sender for tokens list screen */ -class AddCustomTokenAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) { - - fun sendWhenScreenOpened() { - analyticsEventHandler.send(ManageTokens.CustomToken.ScreenOpened) - } - - fun sendWhenAddTokenButtonClicked(customCurrency: CustomCurrency) { - analyticsEventHandler.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt deleted file mode 100644 index 0fadd8ea5b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ /dev/null @@ -1,1048 +0,0 @@ -package com.tangem.tap.features.customtoken.impl.presentation.viewmodels - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter -import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.blockchainsdk.utils.isSupportedInApp -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.extensions.canHandleBlockchain -import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.error.AddCustomTokenError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor -import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken -import com.tangem.tap.features.customtoken.impl.presentation.models.* -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TokensCategoryBlock -import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField.SelectorItem -import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter -import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder -import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidator -import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching -import com.tangem.wallet.BuildConfig -import com.tangem.wallet.R -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.flow.updateAndGet -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -/** - * ViewModel for add custom token screen - * - * @param analyticsEventHandler analytics event handler - * @param featureRouter feature router - * @property featureInteractor feature interactor - * @property getSelectedWalletSyncUseCase use case that returns selected wallet - * @property dispatchers coroutine dispatchers provider - * -[REDACTED_AUTHOR] - */ -@Suppress("LargeClass", "LongParameterList") -@HiltViewModel -internal class AddCustomTokenViewModel @Inject constructor( - analyticsEventHandler: AnalyticsEventHandler, - featureRouter: CustomTokenRouter, - getCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val featureInteractor: CustomTokenInteractor, - private val dispatchers: AppCoroutineDispatcherProvider, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, -) : ViewModel(), DefaultLifecycleObserver { - - private val analyticsSender = AddCustomTokenAnalyticsSender(analyticsEventHandler) - private val actionsHandler = ActionsHandler(featureRouter) - private val testActionsHandler = TestActionsHandler() - private val formStateBuilder = FormStateBuilder() - private val hederaAddressConverter = HederaTokenAddressConverter() - private val cardanoTokenAddressConverter = CardanoTokenAddressConverter() - - private var currentCryptoCurrencies: List = emptyList() - - /** Screen state */ - val uiState: MutableStateFlow = MutableStateFlow(value = getInitialUiState()) - - private var foundToken: FoundToken? = null - - init { - viewModelScope.launch(dispatchers.main) { - currentCryptoCurrencies = getSelectedWalletSyncUseCase().fold( - ifLeft = { emptyList() }, - ifRight = { selectedWallet -> - getCurrenciesUseCase.getSync(selectedWallet.walletId).fold( - ifLeft = { emptyList() }, - ifRight = { it }, - ) - }, - ) - } - } - - override fun onCreate(owner: LifecycleOwner) { - analyticsSender.sendWhenScreenOpened() - } - - private fun getInitialUiState(): AddCustomTokenStateHolder { - return if (BuildConfig.TEST_ACTION_ENABLED) { - AddCustomTokenStateHolder.TestContent( - onBackButtonClick = actionsHandler::onBackButtonClick, - toolbar = createToolbar(), - form = formStateBuilder.createForm(), - warnings = emptySet(), - floatingButton = createFloatingButton(), - testBlock = AddCustomTokenTestBlock( - chooseTokenButtonText = "Choose token", - clearButtonText = "Clear address", - resetButtonText = "Reset", - onClearAddressButtonClick = testActionsHandler::onClearAddressButtonClick, - onResetButtonClick = testActionsHandler::onResetButtonClick, - ), - bottomSheet = AddCustomTokenChooseTokenBottomSheet( - categoriesBlocks = listOf( - TokensCategoryBlock(name = "Common", items = COMMON_TOKENS), - TokensCategoryBlock(name = "Solana", items = SOLANA_TOKENS), - ), - onTestTokenClick = actionsHandler::onContactAddressValueChange, - ), - ) - } else { - AddCustomTokenStateHolder.Content( - onBackButtonClick = actionsHandler::onBackButtonClick, - toolbar = createToolbar(), - form = formStateBuilder.createForm(), - warnings = emptySet(), - floatingButton = createFloatingButton(), - ) - } - } - - private fun createToolbar(): AddCustomTokensToolbar { - return AddCustomTokensToolbar( - title = TextReference.Res(R.string.add_custom_token_title), - onBackButtonClick = actionsHandler::onBackButtonClick, - ) - } - - private fun createFloatingButton(): AddCustomTokenFloatingButton { - return AddCustomTokenFloatingButton( - isEnabled = true, - showProgress = false, - onClick = actionsHandler::onAddCustomTokenClick, - ) - } - - private inner class FormStateBuilder { - - fun createForm(): AddCustomTokenForm { - return AddCustomTokenForm( - contractAddressInputField = createContractAddressInputField(), - networkSelectorField = createNetworkSelectorField(), - tokenNameInputField = createTokenNameInputField(), - tokenSymbolInputField = createTokenSymbolInputField(), - decimalsInputField = createDecimalsInputField(), - derivationPathSelectorField = createDerivationPathsSelectorField(), - derivationPathInputField = createDerivationPathInputField(), - ) - } - - fun createDerivationPathSelectorAdditionalItem( - blockchain: Blockchain, - type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN, - derivationPath: String, - ): SelectorItem.TitleWithSubtitle { - return when (type) { - DerivationPathSelectorType.DEFAULT -> SelectorItem.TitleWithSubtitle( - title = TextReference.Res(R.string.custom_token_derivation_path_default), - subtitle = TextReference.Res(R.string.custom_token_derivation_path_default), - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.DEFAULT, - ) - DerivationPathSelectorType.CUSTOM -> SelectorItem.TitleWithSubtitle( - title = TextReference.Res(R.string.custom_token_custom_derivation), - subtitle = TextReference.Res(R.string.custom_token_custom_derivation), - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.CUSTOM, - ) - DerivationPathSelectorType.BLOCKCHAIN -> SelectorItem.TitleWithSubtitle( - title = TextReference.Str(derivationPath), - subtitle = TextReference.Str(blockchain.getNetworkName()), - blockchain = blockchain, - ) - } - } - - fun createNetworkSelectorItem(blockchain: Blockchain): SelectorItem.Title { - return if (blockchain == Blockchain.Unknown) { - SelectorItem.Title( - title = TextReference.Res(R.string.custom_token_network_input_not_selected), - blockchain = Blockchain.Unknown, - ) - } else { - SelectorItem.Title( - title = TextReference.Str(blockchain.getNetworkName()), - blockchain = blockchain, - ) - } - } - - private fun createContractAddressInputField(): AddCustomTokenInputField.ContactAddress { - return AddCustomTokenInputField.ContactAddress( - value = "", - onValueChange = actionsHandler::onContactAddressValueChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_contract_address_input_title), - placeholder = TextReference.Str(value = CONTRACT_ADDRESS_PLACEHOLDER), - isLoading = false, - isError = false, - error = null, - ) - } - - private fun createNetworkSelectorField(): AddCustomTokenSelectorField.Network { - val selectorItems = getNetworkSelectorItems() - return AddCustomTokenSelectorField.Network( - label = TextReference.Res(R.string.custom_token_network_input_title), - selectedItem = requireNotNull(selectorItems.firstOrNull()), - items = selectorItems, - onMenuItemClick = actionsHandler::onNetworkSelectorItemClick, - ) - } - - private fun getNetworkSelectorItems(): List { - val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown) - val scanResponse = getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { it.scanResponse }, - ) - val derivationStyle = scanResponse?.derivationStyleProvider?.getDerivationStyle() - return listOf(defaultNetwork) + Blockchain.entries - .filter { blockchain -> - scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver) - ?.contains(blockchain) == true && isDerivationPathNotEmpty(derivationStyle, blockchain) - } - .sortedBy(Blockchain::fullName) - .map(::createNetworkSelectorItem) - } - - private fun isDerivationPathNotEmpty(derivationStyle: DerivationStyle?, blockchain: Blockchain): Boolean { - // derivationStyle is null for cards without HD wallets, always return true - return if (derivationStyle != null) { - blockchain.derivationPath(derivationStyle)?.rawPath?.isNotEmpty() == true - } else { - true - } - } - - private fun createTokenNameInputField(): AddCustomTokenInputField.TokenName { - return AddCustomTokenInputField.TokenName( - value = "", - onValueChange = actionsHandler::onTokenNameValueChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_name_input_title), - placeholder = TextReference.Res(id = R.string.custom_token_name_input_placeholder), - isEnabled = false, - ) - } - - private fun createTokenSymbolInputField(): AddCustomTokenInputField.TokenSymbol { - return AddCustomTokenInputField.TokenSymbol( - value = "", - onValueChange = actionsHandler::onTokenSymbolValueChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old), - placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder), - isEnabled = false, - ) - } - - private fun createDecimalsInputField(): AddCustomTokenInputField.Decimals { - return AddCustomTokenInputField.Decimals( - value = "", - onValueChange = actionsHandler::onDecimalsValueChange, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_decimals_input_title), - placeholder = TextReference.Str(value = DECIMALS_PLACEHOLDER), - isEnabled = false, - ) - } - - private fun createDerivationPathsSelectorField(): AddCustomTokenSelectorField.DerivationPath? { - return getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { - if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - - val selectorItems = getDerivationPathsSelectorItems(it.scanResponse.derivationStyleProvider) - AddCustomTokenSelectorField.DerivationPath( - label = TextReference.Res(R.string.custom_token_derivation_path_input_title), - selectedItem = requireNotNull(selectorItems.firstOrNull()), - items = selectorItems, - onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick, - isEnabled = true, - ) - }, - ) - } - - private fun getDerivationPathsSelectorItems( - derivationStyleProvider: DerivationStyleProvider?, - ): List { - val derivationStyle = derivationStyleProvider?.getDerivationStyle() - return listOf( - createDerivationPathSelectorAdditionalItem( - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.DEFAULT, - derivationPath = "", - ), - createDerivationPathSelectorAdditionalItem( - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.CUSTOM, - derivationPath = "", - ), - ) + Blockchain.entries - .filter { blockchain -> - blockchain.isSupportedInApp() && !blockchain.isTestnet() - } - .sortedBy(Blockchain::fullName) - .mapNotNull { - val derivationPath = if (derivationStyle != null) { - it.derivationPath(derivationStyle)?.rawPath - } else { - // derivationStyle is null for cards without HDWallet, use DerivationStyle.V1 - it.derivationPath(DerivationStyle.V1)?.rawPath - } - if (derivationPath?.isNotEmpty() == true) { - createDerivationPathSelectorAdditionalItem( - blockchain = it, - derivationPath = derivationPath, - ) - } else { - null - } - } - } - - private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? { - return getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { - if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - - AddCustomTokenInputField.DerivationPath( - value = "", - onValueChange = actionsHandler::onDerivationPathValueChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - label = TextReference.Res(R.string.custom_token_custom_derivation), - placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER), - ) - }, - ) - } - } - - private fun updateForm(address: String, selectedNetwork: Blockchain) { - viewModelScope.launch(dispatchers.main) { - runCatching(dispatchers.io) { - val tokenAddress = convertTokenAddress(selectedNetwork, address) - ?: error("TokenAddress is invalid") - - featureInteractor.findToken(address = tokenAddress, blockchain = selectedNetwork) - } - .onSuccess { token -> - foundToken = token - uiState.update { state -> - state.copySealed( - form = state.form.copy( - contractAddressInputField = state.form.contractAddressInputField.copy( - isLoading = false, - ), - networkSelectorField = state.form.networkSelectorField.copy( - selectedItem = formStateBuilder.createNetworkSelectorItem( - blockchain = Blockchain.fromNetworkId(token.network.id) - ?: Blockchain.Unknown, - ), - ), - tokenNameInputField = state.form.tokenNameInputField.copy( - value = token.name, - isEnabled = false, - ), - tokenSymbolInputField = state.form.tokenSymbolInputField.copy( - value = token.symbol, - isEnabled = false, - ), - decimalsInputField = state.form.decimalsInputField.copy( - value = token.network.decimalCount, - isEnabled = false, - ), - ), - ) - } - } - .onFailure { - foundToken = null - uiState.update { state -> - state.copySealed( - form = state.form.copy( - contractAddressInputField = state.form.contractAddressInputField.copy( - isLoading = false, - ), - tokenNameInputField = state.form.tokenNameInputField.copy(isEnabled = true), - tokenSymbolInputField = state.form.tokenSymbolInputField.copy(isEnabled = true), - decimalsInputField = state.form.decimalsInputField.copy(isEnabled = true), - ), - ) - } - Timber.e(it) - } - - updateWarnings() - updateFloatingButton() - } - } - - private fun isDerivationPathSelected(): Boolean { - val blockchain = uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain - val selectorType = uiState.value.form.derivationPathSelectorField?.selectedItem?.type - - return blockchain != null && blockchain != Blockchain.Unknown || - selectorType == DerivationPathSelectorType.CUSTOM && - !uiState.value.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) - } - - private fun updateWarnings() { - uiState.update { state -> - state.copySealed( - warnings = buildSet { - when (getCustomTokenType()) { - CustomTokenType.TOKEN -> { - addAll(getTokenWarningSet()) - } - - CustomTokenType.BLOCKCHAIN -> { - if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded) - if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken) - } - } - }, - ) - } - } - - private fun getCustomTokenType(): CustomTokenType { - return if (isAnyTokenFieldsFilled() || isAllTokenFieldsFilled()) { - CustomTokenType.TOKEN - } else { - CustomTokenType.BLOCKCHAIN - } - } - - private fun isAnyTokenFieldsFilled(): Boolean { - return with(uiState.value.form) { - contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() || - tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty() - } - } - - private fun isAllTokenFieldsFilled(): Boolean { - return with(uiState.value.form) { - contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() && - tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty() - } - } - - private fun getTokenWarningSet(): Set { - val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain - - val isContractAddressFieldEmpty = ContractAddressValidator.validate( - address = uiState.value.form.contractAddressInputField.value, - blockchain = networkSelectorValue, - ).let { - it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY - } - - val isSupportedToken = if (!isNetworkSelected()) { - true - } else { - getSelectedWalletSyncUseCase().fold( - ifLeft = { false }, - ifRight = { - it.scanResponse.card.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = it.scanResponse.cardTypesResolver, - ) - }, - ) - } - - return buildSet { - if (!isSupportedToken && !isContractAddressFieldEmpty) { - add(AddCustomTokenWarning.UnsupportedToken(networkSelectorValue.getNetworkName())) - } - if (isCustomTokenAlreadyAdded()) { - add(AddCustomTokenWarning.TokenAlreadyAdded) - } - if (foundToken == null && isAnyTokenFieldsFilled() || foundToken?.isActive == false) { - add(AddCustomTokenWarning.PotentialScamToken) - } - } - } - - private fun isNetworkSelected(): Boolean { - return uiState.value.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown - } - - private fun updateFloatingButton() { - uiState.update { state -> - updateStateWithDerivationError(state) - } - if (isCustomTokenAlreadyAdded()) { - uiState.update { state -> - state.copySealed( - warnings = state.warnings + AddCustomTokenWarning.TokenAlreadyAdded, - floatingButton = state.floatingButton.copy(isEnabled = false), - ) - } - return - } - - uiState.update { state -> - val isCorrectDerivationInput = !state.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) - val updatedState = when { - isAllTokenFieldsFilled() && isNetworkSelected() -> { - val networkSelectorValue = state.form.networkSelectorField.selectedItem.blockchain - val error = ContractAddressValidator.validate( - address = state.form.contractAddressInputField.value, - blockchain = networkSelectorValue, - ) - - val isSupportedToken = getSelectedWalletSyncUseCase().fold( - ifLeft = { false }, - ifRight = { - it.scanResponse.card.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = it.scanResponse.cardTypesResolver, - ) - }, - ) - - state.copySealed( - floatingButton = state.floatingButton.copy( - isEnabled = error is ContractAddressValidatorResult.Success && - isSupportedToken && isCorrectDerivationInput, - ), - ) - } - isAnyTokenFieldsFilled() -> { - state.copySealed(floatingButton = state.floatingButton.copy(isEnabled = false)) - } - else -> { - state.copySealed( - floatingButton = state.floatingButton.copy( - isEnabled = if (isNetworkSelected()) { - !isBlockchainAlreadyAdded() && isCorrectDerivationInput - } else { - false - }, - ), - ) - } - } - - updatedState.copySealed( - warnings = updatedState.warnings - AddCustomTokenWarning.TokenAlreadyAdded, - ) - } - } - - private fun updateStateWithDerivationError(uiState: AddCustomTokenStateHolder): AddCustomTokenStateHolder { - val updatedWarnings = if (isWrongDerivationPathEntered( - derivationPathSelectorType = uiState.form.derivationPathSelectorField?.selectedItem?.type, - derivationPath = getDerivationPath(), - ) - ) { - uiState.warnings + AddCustomTokenWarning.WrongDerivationPath - } else { - uiState.warnings - AddCustomTokenWarning.WrongDerivationPath - } - return uiState.copySealed( - warnings = updatedWarnings, - ) - } - - private fun isWrongDerivationPathEntered( - derivationPathSelectorType: DerivationPathSelectorType?, - derivationPath: DerivationPath?, - ): Boolean { - return derivationPathSelectorType == DerivationPathSelectorType.CUSTOM && derivationPath == null - } - - private fun isCustomTokenAlreadyAdded(): Boolean { - return when (getCustomTokenType()) { - CustomTokenType.TOKEN -> isTokenAlreadyAdded() - CustomTokenType.BLOCKCHAIN -> isBlockchainAlreadyAdded() - } - } - - private fun isTokenAlreadyAdded(): Boolean { - val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain - val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id - val contractAddress = convertTokenAddress( - blockchain = networkSelectorValue, - address = uiState.value.form.contractAddressInputField.value, - ) ?: return false // invalid address can't be "already added" - - return currentCryptoCurrencies - .filterIsInstance() - .any { token -> - val sameId = if (!token.isCustom) { - // todo after move foundToken to CryptoCurrency model, use only id - foundToken?.id == token.id.rawCurrencyId - } else { - true - } - - val sameAddress = contractAddress.equals(token.contractAddress, ignoreCase = true) - val sameBlockchain = networkId == token.network.id.value - val isSameDerivationPath = getDerivationPath()?.rawPath == token.network.derivationPath.value - - sameId && sameAddress && sameBlockchain && isSameDerivationPath - } - } - - private fun isBlockchainAlreadyAdded(): Boolean { - return currentCryptoCurrencies - .filterIsInstance() - .any { coin -> - coin.network.id.value == uiState.value.form.networkSelectorField.selectedItem.blockchain.id && - coin.network.derivationPath.value == getDerivationPath()?.rawPath - } - } - - private fun handleContractAddressErrorValidation(type: AddCustomTokenError) { - when { - isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> { - val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled() - uiState.update { state -> - state.copySealed( - form = state.form.copy( - contractAddressInputField = state.form.contractAddressInputField.copy( - isError = true, - error = TextReference.Res( - id = R.string.custom_token_creation_error_invalid_contract_address, - ), - ), - tokenNameInputField = state.form.tokenNameInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - tokenSymbolInputField = state.form.tokenSymbolInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - decimalsInputField = state.form.decimalsInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - ), - ) - } - } - - !isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - contractAddressInputField = state.form.contractAddressInputField.copy(isError = false), - tokenNameInputField = state.form.tokenNameInputField.copy(value = "", isEnabled = false), - tokenSymbolInputField = state.form.tokenSymbolInputField.copy( - value = "", - isEnabled = false, - ), - decimalsInputField = state.form.decimalsInputField.copy(value = "", isEnabled = false), - ), - ) - } - } - - else -> Unit - } - } - - private fun getDerivationPath(): DerivationPath? { - return when (uiState.value.form.derivationPathSelectorField?.selectedItem?.type) { - DerivationPathSelectorType.CUSTOM -> - createDerivationPathOrNull(uiState.value.form.derivationPathInputField?.value ?: "") - else -> - getDerivationPathForBlockchain(uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain) - } - } - - private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { - return try { - DerivationPath(rawPath) - } catch (error: Throwable) { - null - } - } - - private fun getDerivationPathForBlockchain(blockchain: Blockchain?): DerivationPath? { - if (blockchain == null) return null - - val derivationStyle = getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { - it.scanResponse.derivationStyleProvider.getDerivationStyle() - }, - ) - - val derivationNetwork = if (blockchain == Blockchain.Unknown) { - uiState.value.form.networkSelectorField.selectedItem.blockchain - } else { - blockchain - } - return derivationNetwork.derivationPath(derivationStyle) - } - - private fun getSupportBlockchainType(blockchain: Blockchain): SupportBlockchainType { - return getSelectedWalletSyncUseCase().fold( - ifLeft = { SupportBlockchainType.UNABLE_TO_DETERMINE }, - ifRight = { - val canHandleBlockchain = it.scanResponse.card.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = it.scanResponse.cardTypesResolver, - ) - if (canHandleBlockchain) { - SupportBlockchainType.SUPPORTED - } else { - SupportBlockchainType.UNSUPPORTED - } - }, - ) - } - - private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) { - - fun onBackButtonClick() { - viewModelScope.launch(dispatchers.main) { - // need delay before close, cause crashed in compose PopUpMenu as - delay(timeMillis = 100) - featureRouter.popBackStack() - } - } - - fun onContactAddressValueChange(enteredValue: String) { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - contractAddressInputField = state.form.contractAddressInputField.copy(value = enteredValue), - ), - ) - } - - val selectedNetwork = uiState.value.form.networkSelectorField.selectedItem.blockchain - val validatorResult = ContractAddressValidator.validate( - address = enteredValue, - blockchain = selectedNetwork, - ) - - when (validatorResult) { - is ContractAddressValidatorResult.Success -> { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - contractAddressInputField = state.form.contractAddressInputField.copy( - isError = false, - isLoading = true, - ), - ), - ) - } - updateForm(address = enteredValue, selectedNetwork = selectedNetwork) - } - - is ContractAddressValidatorResult.Error -> { - handleContractAddressErrorValidation(type = validatorResult.type) - updateWarnings() - updateFloatingButton() - } - } - } - - fun onNetworkSelectorItemClick(index: Int) { - val state = uiState.updateAndGet { state -> - val selectedItem = requireNotNull(state.form.networkSelectorField.items.getOrNull(index)) - state.copySealed( - form = state.form.copy( - networkSelectorField = state.form.networkSelectorField.copy( - selectedItem = selectedItem, - ), - showTokenFields = selectedItem.blockchain.canHandleTokens() && - // workaround cause in Terra we support only 1 token - selectedItem.blockchain != Blockchain.TerraV1, - ), - ) - } - - onContactAddressValueChange(state.form.contractAddressInputField.value) - } - - fun onTokenNameValueChange(enteredValue: String) { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - tokenNameInputField = state.form.tokenNameInputField.copy(value = enteredValue), - ), - ) - } - - updateFloatingButton() - } - - fun onTokenSymbolValueChange(enteredValue: String) { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - tokenSymbolInputField = state.form.tokenSymbolInputField.copy(value = enteredValue), - ), - ) - } - updateFloatingButton() - } - - fun onDecimalsValueChange(enteredValue: String) { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - decimalsInputField = state.form.decimalsInputField.copy(value = enteredValue), - ), - ) - } - updateFloatingButton() - } - - fun onDerivationPathSelectorItemClick(index: Int) { - uiState.update { state -> - val derivationSelector = requireNotNull(state.form.derivationPathSelectorField) - val selected = requireNotNull(derivationSelector.items.getOrNull(index)) - val derivationInputField = requireNotNull(state.form.derivationPathInputField) - - state.copySealed( - form = state.form.copy( - derivationPathSelectorField = derivationSelector.copy( - selectedItem = selected, - ), - derivationPathInputField = derivationInputField.copy( - showField = selected.type == DerivationPathSelectorType.CUSTOM, - ), - ), - ) - } - - updateFloatingButton() - } - - fun onDerivationPathValueChange(enteredValue: String) { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - derivationPathInputField = state.form.derivationPathInputField?.copy(value = enteredValue), - ), - ) - } - updateFloatingButton() - } - - fun onAddCustomTokenClick() { - if (!isNetworkSelected()) return - val blockchain = uiState.value.form.networkSelectorField.selectedItem.blockchain - when (getSupportBlockchainType(blockchain)) { - SupportBlockchainType.SUPPORTED -> { - /* no-op */ - } - SupportBlockchainType.UNSUPPORTED -> { - featureRouter.openUnsupportedNetworkAlert(blockchain) - return - } - SupportBlockchainType.UNABLE_TO_DETERMINE -> { - featureRouter.showGenericErrorAlertAndPopBack() - return - } - } - - val currency = when (getCustomTokenType()) { - CustomTokenType.TOKEN -> { - val contractAddress = convertTokenAddress( - blockchain = blockchain, - address = foundToken?.network?.contractAddress ?: uiState.value.form.contractAddressInputField - .value, - ) ?: error("Contract address is invalid") // impossible to add a token with invalid address - - CustomCurrency.CustomToken( - token = Token( - name = uiState.value.form.tokenNameInputField.value, - symbol = uiState.value.form.tokenSymbolInputField.value, - contractAddress = contractAddress, - decimals = requireNotNull(uiState.value.form.decimalsInputField.value.toIntOrNull()), - id = foundToken?.id, - ), - network = blockchain, - derivationPath = getDerivationPath(), - ) - } - CustomTokenType.BLOCKCHAIN -> { - CustomCurrency.CustomBlockchain( - network = blockchain, - derivationPath = getDerivationPath(), - ) - } - } - - analyticsSender.sendWhenAddTokenButtonClicked(currency) - - viewModelScope.launch { - uiState.update { state -> - state.copySealed( - floatingButton = state.floatingButton.copy( - isEnabled = false, - showProgress = true, - ), - ) - } - - val result = featureInteractor.saveToken(currency) - - uiState.update { state -> - state.copySealed( - floatingButton = state.floatingButton.copy( - isEnabled = true, - showProgress = false, - ), - ) - } - - result - .onSuccess { featureRouter.openWalletScreen() } - .onFailure { Timber.e(it, "Unable to save custom token") } - } - } - } - - /** Convert [address] to single address for specific [blockchain] or return null if invalid */ - private fun convertTokenAddress(blockchain: Blockchain, address: String): String? { - return runCatching { - when (blockchain) { - Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) - Blockchain.Cardano -> { - // TODO: [REDACTED_JIRA] - cardanoTokenAddressConverter.convertToFingerprint( - address = address, - symbol = uiState.value.form.tokenSymbolInputField.value, - ) - } - else -> address - } - } - .getOrNull() - } - - private inner class TestActionsHandler { - - fun onClearAddressButtonClick() { - uiState.update { state -> - state.copySealed( - form = state.form.copy( - contractAddressInputField = state.form.contractAddressInputField.copy( - value = "", - isLoading = false, - isError = false, - error = null, - ), - ), - ) - } - } - - fun onResetButtonClick() { - uiState.update { state -> - with(state.form) { - state.copySealed( - form = state.form.copy( - contractAddressInputField = contractAddressInputField.copy( - value = "", - isLoading = false, - isError = false, - error = null, - ), - networkSelectorField = networkSelectorField.copy( - selectedItem = formStateBuilder.createNetworkSelectorItem( - blockchain = Blockchain.Unknown, - ), - ), - tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false), - tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false), - decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false), - derivationPathSelectorField = derivationPathSelectorField?.copy( - isEnabled = true, - selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem( - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.DEFAULT, - derivationPath = "", - ), - ), - ), - ) - } - } - } - } - - private companion object { - val COMMON_TOKENS = persistentListOf( - TestTokenItem(name = "USDC on ETH", address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), - TestTokenItem(name = "BUSD on ETH", address = "0x4fabb145d64652a948d72533023f6e7a623c7c53"), - TestTokenItem(name = "ETH on AVALANCHE", address = "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15"), - TestTokenItem(name = "USDC on ETH (invalid - cut address)", address = "0xa0b86991c6218b36c1d1"), - TestTokenItem(name = "Custom EVM", address = "0x1111111111111111112111111111111111111113"), - TestTokenItem( - name = "Supported by several networks", - address = "0xa1faa113cbe53436df28ff0aee54275c13b40975", - ), - TestTokenItem(name = "Invalid", address = "!@#_ _-%%^&&*((){P P2iOWsdfFQLA"), - ) - - val SOLANA_TOKENS = persistentListOf( - TestTokenItem(name = "USDT (full)", address = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), - TestTokenItem( - name = "USDT (valid - 2/3 of address)", - address = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben", - ), - TestTokenItem(name = "USDT (invalid - 1/3 of address)", address = "Es9vMFrzaCERmJ"), - TestTokenItem(name = "ETH (full)", address = "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk"), - ) - const val DERIVATION_PATH_PLACEHOLDER = "m/44'/0'/0'/0/0" - const val DECIMALS_PLACEHOLDER = "8" - const val CONTRACT_ADDRESS_PLACEHOLDER = "0x0000000000000000000000000000000000000000" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 2eb4a736b8..04e0e5f804 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -17,7 +17,6 @@ import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter -import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment @@ -67,9 +66,6 @@ internal class ChildFactory @Inject constructor( is AppRoute.AccessCodeRecovery -> { route.asFragmentChild(Provider { AccessCodeRecoveryFragment() }) } - is AppRoute.AddCustomToken -> { - route.asFragmentChild(Provider { AddCustomTokenFragment() }) - } is AppRoute.AppCurrencySelector -> { route.asFragmentChild(Provider { AppCurrencySelectorFragment() }) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index b1f4c7a19b..398cb634a8 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -191,9 +191,6 @@ sealed class AppRoute(val path: String) : Route { } } - @Serializable - data object AddCustomToken : AppRoute(path = "/add_custom_token") - @Serializable data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions")