Updated on 2026-08-14
This commit is contained in:
parent
33de3e7ae5
commit
fa056925a0
29 changed files with 0 additions and 2883 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CoinsResponse.Coin, FoundToken> {
|
||||
|
||||
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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<Unit>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<Unit> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenViewModel>().apply {
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||
}
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
AddCustomTokenScreen(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.systemBarsPadding(),
|
||||
stateHolder = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokensCategoryBlock>,
|
||||
val onTestTokenClick: (String) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Tokens category model
|
||||
*
|
||||
* @property name category name
|
||||
* @property items category items
|
||||
*/
|
||||
data class TokensCategoryBlock(val name: String, val items: List<TestTokenItem>)
|
||||
|
||||
/**
|
||||
* 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<SelectorItem>
|
||||
|
||||
/** 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<SelectorItem.Title>,
|
||||
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<SelectorItem.TitleWithSubtitle>,
|
||||
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,
|
||||
)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
/** Custom token type */
|
||||
enum class CustomTokenType { TOKEN, BLOCKCHAIN }
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
internal enum class SupportBlockchainType {
|
||||
|
||||
SUPPORTED, UNSUPPORTED, UNABLE_TO_DETERMINE
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<AppRoute.Wallet>() }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenWarning>
|
||||
|
||||
/** 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<AddCustomTokenWarning> = 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<AddCustomTokenWarning>,
|
||||
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<AddCustomTokenWarning>,
|
||||
override val floatingButton: AddCustomTokenFloatingButton,
|
||||
val testBlock: AddCustomTokenTestBlock,
|
||||
val bottomSheet: AddCustomTokenChooseTokenBottomSheet,
|
||||
) : AddCustomTokenStateHolder
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenWarning> {
|
||||
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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenStateHolder>(
|
||||
collection = listOf(
|
||||
AddCustomTokenPreviewData.createContent(),
|
||||
AddCustomTokenPreviewData.createTestContent(),
|
||||
),
|
||||
)
|
||||
|
|
@ -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<TestTokenItem>, 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenFloatingButton>(
|
||||
listOf(
|
||||
AddCustomTokenFloatingButton(isEnabled = true, showProgress = false, onClick = {}),
|
||||
AddCustomTokenFloatingButton(isEnabled = false, showProgress = false, onClick = {}),
|
||||
),
|
||||
)
|
||||
|
|
@ -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<AddCustomTokenForm>(
|
||||
collection = listOf(
|
||||
AddCustomTokenPreviewData.createDefaultForm(),
|
||||
AddCustomTokenPreviewData.createDefaultForm().copy(derivationPathSelectorField = null),
|
||||
AddCustomTokenPreviewData.createDefaultForm().let { form ->
|
||||
form.copy(contractAddressInputField = form.contractAddressInputField.copy(isLoading = true))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenWarning>) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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() })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue