Updated on 2026-08-14
This commit is contained in:
commit
3442172603
13 changed files with 493 additions and 424 deletions
|
|
@ -1,15 +1,15 @@
|
|||
package com.tangem.tap.domain.card
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import arrow.fx.coroutines.ResourceScope
|
||||
import arrow.fx.coroutines.resourceScope
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.UserCodeType
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.core.UserCodeRequestPolicy
|
||||
import com.tangem.common.doOnResult
|
||||
import com.tangem.domain.card.ResetCardUseCase
|
||||
import com.tangem.domain.card.models.ResetCardError
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
|
@ -20,36 +20,44 @@ internal class DefaultResetCardUseCase(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
) : ResetCardUseCase {
|
||||
|
||||
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = either {
|
||||
enterRequiredAccessCode(card) {
|
||||
resetToFactorySettings(
|
||||
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = resourceScope {
|
||||
either {
|
||||
withUserCodeRequestPolicy(card)
|
||||
|
||||
tangemSdkManager.resetToFactorySettings(
|
||||
cardId = card.cardId,
|
||||
allowsRequestAccessCodeFromRepository = true,
|
||||
)
|
||||
).bind(raise = this)
|
||||
}
|
||||
.mapToEither()
|
||||
}
|
||||
|
||||
override suspend fun invoke(
|
||||
cardNumber: Int,
|
||||
card: CardDTO,
|
||||
userWalletId: UserWalletId,
|
||||
): Either<ResetCardError, Unit> = either {
|
||||
enterRequiredAccessCode(card) {
|
||||
resetBackupCard(cardNumber = cardNumber, userWalletId = userWalletId)
|
||||
): Either<ResetCardError, Unit> = resourceScope {
|
||||
either {
|
||||
withUserCodeRequestPolicy(card)
|
||||
|
||||
tangemSdkManager.resetBackupCard(
|
||||
cardNumber = cardNumber,
|
||||
userWalletId = userWalletId,
|
||||
).bind(raise = this)
|
||||
}
|
||||
.mapToEither()
|
||||
}
|
||||
|
||||
private suspend fun enterRequiredAccessCode(
|
||||
card: CardDTO,
|
||||
task: suspend TangemSdkManager.() -> CompletionResult<*>,
|
||||
): CompletionResult<*> {
|
||||
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
|
||||
requestMandatoryAccessCodeEntry(card)
|
||||
private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) {
|
||||
install(
|
||||
acquire = {
|
||||
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
|
||||
requestMandatoryAccessCodeEntry(card)
|
||||
|
||||
return tangemSdkManager.task()
|
||||
.doOnResult { tangemSdkManager.setUserCodeRequestPolicy(policyBeforeReset) }
|
||||
policyBeforeReset
|
||||
},
|
||||
release = { prevPolicy, _ ->
|
||||
tangemSdkManager.setUserCodeRequestPolicy(prevPolicy)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestMandatoryAccessCodeEntry(card: CardDTO) {
|
||||
|
|
@ -66,14 +74,19 @@ internal class DefaultResetCardUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CompletionResult<*>.mapToEither(): Either<ResetCardError, Unit> {
|
||||
private fun CompletionResult<*>.bind(raise: Raise<ResetCardError>) {
|
||||
return when (this) {
|
||||
is CompletionResult.Failure -> error.mapToDomainError().left()
|
||||
is CompletionResult.Success -> Unit.right()
|
||||
is CompletionResult.Failure -> {
|
||||
val domainError = error.mapToDomainError()
|
||||
|
||||
raise.raise(domainError)
|
||||
}
|
||||
is CompletionResult.Success -> { /* no-op */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TangemError.mapToDomainError(): ResetCardError {
|
||||
return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.AnotherSdkError
|
||||
return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.SdkError
|
||||
}
|
||||
}
|
||||
|
|
@ -17,5 +17,5 @@ interface CustomTokenInteractor {
|
|||
|
||||
/** Save token [customCurrency] */
|
||||
@Throws(TangemError::class)
|
||||
suspend fun saveToken(customCurrency: CustomCurrency)
|
||||
suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit>
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
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.tokens.utils.CryptoCurrencyFactory
|
||||
|
|
@ -15,7 +17,6 @@ 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
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of custom token interactor
|
||||
|
|
@ -45,16 +46,18 @@ class DefaultCustomTokenInteractor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun saveToken(customCurrency: CustomCurrency) {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
|
||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
||||
|
||||
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
|
||||
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
.onRight {
|
||||
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
override suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit> {
|
||||
return result {
|
||||
val userWallet = getSelectedWalletSyncUseCase().getOrElse {
|
||||
error("Failed to get selected wallet: $it")
|
||||
}
|
||||
.onLeft { Timber.e("Failed to derive public keys: $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? {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package com.tangem.tap.features.customtoken.impl.presentation
|
|||
|
||||
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.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -34,9 +36,12 @@ internal class AddCustomTokenFragment : ComposeFragment() {
|
|||
SystemBarsEffect {
|
||||
setSystemBarsColor(color = statusBarColor)
|
||||
}
|
||||
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
AddCustomTokenScreen(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
stateHolder = viewModel.uiState,
|
||||
stateHolder = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
|
|
@ -49,6 +46,9 @@ 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
|
||||
|
|
@ -85,8 +85,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
private var currentCryptoCurrencies: List<CryptoCurrency> = emptyList()
|
||||
|
||||
/** Screen state */
|
||||
var uiState by mutableStateOf(getInitialUiState())
|
||||
private set
|
||||
val uiState: MutableStateFlow<AddCustomTokenStateHolder> = MutableStateFlow(value = getInitialUiState())
|
||||
|
||||
private var foundToken: FoundToken? = null
|
||||
|
||||
|
|
@ -376,44 +375,48 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
.onSuccess { token ->
|
||||
foundToken = token
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isLoading = false,
|
||||
),
|
||||
networkSelectorField = uiState.form.networkSelectorField.copy(
|
||||
selectedItem = formStateBuilder.createNetworkSelectorItem(
|
||||
blockchain = Blockchain.fromNetworkId(token.network.id)
|
||||
?: Blockchain.Unknown,
|
||||
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,
|
||||
),
|
||||
),
|
||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(
|
||||
value = token.name,
|
||||
isEnabled = false,
|
||||
),
|
||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(
|
||||
value = token.symbol,
|
||||
isEnabled = false,
|
||||
),
|
||||
decimalsInputField = uiState.form.decimalsInputField.copy(
|
||||
value = token.network.decimalCount,
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
foundToken = null
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isLoading = false,
|
||||
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),
|
||||
),
|
||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(isEnabled = true),
|
||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(isEnabled = true),
|
||||
decimalsInputField = uiState.form.decimalsInputField.copy(isEnabled = true),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
Timber.e(it)
|
||||
}
|
||||
|
||||
|
|
@ -423,28 +426,31 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isDerivationPathSelected(): Boolean {
|
||||
return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
|
||||
this != null && this != Blockchain.Unknown ||
|
||||
uiState.form.derivationPathSelectorField?.selectedItem?.type == DerivationPathSelectorType.CUSTOM &&
|
||||
!uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
}
|
||||
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 = uiState.copySealed(
|
||||
warnings = buildSet {
|
||||
when (getCustomTokenType()) {
|
||||
CustomTokenType.TOKEN -> {
|
||||
addAll(getTokenWarningSet())
|
||||
}
|
||||
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)
|
||||
CustomTokenType.BLOCKCHAIN -> {
|
||||
if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomTokenType(): CustomTokenType {
|
||||
|
|
@ -456,24 +462,24 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isAnyTokenFieldsFilled(): Boolean {
|
||||
return with(uiState.form) {
|
||||
return with(uiState.value.form) {
|
||||
contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() ||
|
||||
tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAllTokenFieldsFilled(): Boolean {
|
||||
return with(uiState.form) {
|
||||
return with(uiState.value.form) {
|
||||
contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() &&
|
||||
tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
|
||||
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
address = uiState.value.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
).let {
|
||||
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY
|
||||
|
|
@ -507,66 +513,70 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isNetworkSelected(): Boolean {
|
||||
return uiState.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown
|
||||
return uiState.value.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown
|
||||
}
|
||||
|
||||
private fun updateFloatingButton() {
|
||||
uiState = updateStateWithDerivationError(uiState)
|
||||
uiState.update { state ->
|
||||
updateStateWithDerivationError(state)
|
||||
}
|
||||
if (isCustomTokenAlreadyAdded()) {
|
||||
uiState = uiState.copySealed(
|
||||
warnings = uiState.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
floatingButton = uiState.floatingButton.copy(isEnabled = false),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
warnings = state.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
floatingButton = state.floatingButton.copy(isEnabled = false),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val isCorrectDerivationInput = !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
val state = when {
|
||||
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val error = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
)
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(
|
||||
isEnabled = error is ContractAddressValidatorResult.Success &&
|
||||
isSupportedToken && isCorrectDerivationInput,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
isAnyTokenFieldsFilled() -> {
|
||||
uiState.copySealed(floatingButton = uiState.floatingButton.copy(isEnabled = false))
|
||||
}
|
||||
|
||||
else -> {
|
||||
uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(
|
||||
isEnabled = if (isNetworkSelected()) {
|
||||
!isBlockchainAlreadyAdded() && isCorrectDerivationInput
|
||||
} else {
|
||||
false
|
||||
val isSupportedToken = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifRight = {
|
||||
it.scanResponse.card.canHandleToken(
|
||||
blockchain = networkSelectorValue,
|
||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
uiState = state.copySealed(
|
||||
warnings = state.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
)
|
||||
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 {
|
||||
|
|
@ -599,11 +609,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isTokenAlreadyAdded(): Boolean {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id
|
||||
val contractAddress = convertTokenAddress(
|
||||
blockchain = networkSelectorValue,
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
address = uiState.value.form.contractAddressInputField.value,
|
||||
) ?: return false // invalid address can't be "already added"
|
||||
|
||||
return currentCryptoCurrencies
|
||||
|
|
@ -628,7 +638,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
return currentCryptoCurrencies
|
||||
.filterIsInstance<CryptoCurrency.Coin>()
|
||||
.any { coin ->
|
||||
coin.network.id.value == uiState.form.networkSelectorField.selectedItem.blockchain.id &&
|
||||
coin.network.id.value == uiState.value.form.networkSelectorField.selectedItem.blockchain.id &&
|
||||
coin.network.derivationPath.value == getDerivationPath()?.rawPath
|
||||
}
|
||||
}
|
||||
|
|
@ -637,39 +647,43 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
when {
|
||||
isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
|
||||
val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isError = true,
|
||||
error = TextReference.Res(
|
||||
id = R.string.custom_token_creation_error_invalid_contract_address,
|
||||
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,
|
||||
),
|
||||
),
|
||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(
|
||||
isEnabled = isAnotherTokenFieldsFilled,
|
||||
),
|
||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(
|
||||
isEnabled = isAnotherTokenFieldsFilled,
|
||||
),
|
||||
decimalsInputField = uiState.form.decimalsInputField.copy(
|
||||
isEnabled = isAnotherTokenFieldsFilled,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
!isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false),
|
||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(value = "", isEnabled = false),
|
||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
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),
|
||||
),
|
||||
decimalsInputField = uiState.form.decimalsInputField.copy(value = "", isEnabled = false),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
|
|
@ -677,11 +691,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getDerivationPath(): DerivationPath? {
|
||||
return when (uiState.form.derivationPathSelectorField?.selectedItem?.type) {
|
||||
return when (uiState.value.form.derivationPathSelectorField?.selectedItem?.type) {
|
||||
DerivationPathSelectorType.CUSTOM ->
|
||||
createDerivationPathOrNull(uiState.form.derivationPathInputField?.value ?: "")
|
||||
createDerivationPathOrNull(uiState.value.form.derivationPathInputField?.value ?: "")
|
||||
else ->
|
||||
getDerivationPathForBlockchain(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain)
|
||||
getDerivationPathForBlockchain(uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -704,7 +718,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
val derivationNetwork = if (blockchain == Blockchain.Unknown) {
|
||||
uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
} else {
|
||||
blockchain
|
||||
}
|
||||
|
|
@ -739,13 +753,15 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onContactAddressValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.contractAddressInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val selectedNetwork = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
val validatorResult = ContractAddressValidator.validate(
|
||||
address = enteredValue,
|
||||
blockchain = selectedNetwork,
|
||||
|
|
@ -753,14 +769,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
when (validatorResult) {
|
||||
is ContractAddressValidatorResult.Success -> {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isError = false,
|
||||
isLoading = true,
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||
isError = false,
|
||||
isLoading = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
updateForm(address = enteredValue, selectedNetwork = selectedNetwork)
|
||||
}
|
||||
|
||||
|
|
@ -773,76 +791,92 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onNetworkSelectorItemClick(index: Int) {
|
||||
val selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index))
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
networkSelectorField = uiState.form.networkSelectorField.copy(
|
||||
selectedItem = selectedItem,
|
||||
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,
|
||||
),
|
||||
showTokenFields = selectedItem.blockchain.canHandleTokens() &&
|
||||
// workaround cause in Terra we support only 1 token
|
||||
selectedItem.blockchain != Blockchain.TerraV1,
|
||||
),
|
||||
)
|
||||
onContactAddressValueChange(uiState.form.contractAddressInputField.value)
|
||||
)
|
||||
}
|
||||
|
||||
onContactAddressValueChange(state.form.contractAddressInputField.value)
|
||||
}
|
||||
|
||||
fun onTokenNameValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
tokenNameInputField = state.form.tokenNameInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onTokenSymbolValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onDecimalsValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
decimalsInputField = uiState.form.decimalsInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
decimalsInputField = state.form.decimalsInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onDerivationPathSelectorItemClick(index: Int) {
|
||||
val derivationSelector = requireNotNull(uiState.form.derivationPathSelectorField)
|
||||
val selected = requireNotNull(derivationSelector.items.getOrNull(index))
|
||||
val derivationInputField = requireNotNull(uiState.form.derivationPathInputField)
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
derivationPathSelectorField = derivationSelector.copy(
|
||||
selectedItem = selected,
|
||||
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,
|
||||
),
|
||||
),
|
||||
derivationPathInputField = derivationInputField.copy(
|
||||
showField = selected.type == DerivationPathSelectorType.CUSTOM,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onDerivationPathValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
derivationPathInputField = uiState.form.derivationPathInputField?.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
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.form.networkSelectorField.selectedItem.blockchain
|
||||
val blockchain = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
when (getSupportBlockchainType(blockchain)) {
|
||||
SupportBlockchainType.SUPPORTED -> {
|
||||
/* no-op */
|
||||
|
|
@ -861,15 +895,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
CustomTokenType.TOKEN -> {
|
||||
val contractAddress = convertTokenAddress(
|
||||
blockchain = blockchain,
|
||||
address = foundToken?.network?.contractAddress ?: uiState.form.contractAddressInputField.value,
|
||||
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.form.tokenNameInputField.value,
|
||||
symbol = uiState.form.tokenSymbolInputField.value,
|
||||
name = uiState.value.form.tokenNameInputField.value,
|
||||
symbol = uiState.value.form.tokenSymbolInputField.value,
|
||||
contractAddress = contractAddress,
|
||||
decimals = requireNotNull(uiState.form.decimalsInputField.value.toIntOrNull()),
|
||||
decimals = requireNotNull(uiState.value.form.decimalsInputField.value.toIntOrNull()),
|
||||
id = foundToken?.id,
|
||||
),
|
||||
network = blockchain,
|
||||
|
|
@ -886,78 +921,101 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
analyticsSender.sendWhenAddTokenButtonClicked(currency)
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val oldButtonState = uiState.floatingButton
|
||||
uiState = uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true),
|
||||
)
|
||||
runCatching { featureInteractor.saveToken(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 {
|
||||
uiState = uiState.copySealed(floatingButton = oldButtonState)
|
||||
Timber.e(it)
|
||||
}
|
||||
.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 when (blockchain) {
|
||||
Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address)
|
||||
Blockchain.Cardano -> {
|
||||
cardanoTokenAddressConverter.convertToFingerprint(
|
||||
address = address,
|
||||
symbol = uiState.form.tokenSymbolInputField.value,
|
||||
)
|
||||
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
|
||||
}
|
||||
else -> address
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
private inner class TestActionsHandler {
|
||||
|
||||
fun onClearAddressButtonClick() {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
value = "",
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
error = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onResetButtonClick() {
|
||||
with(uiState.form) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = contractAddressInputField.copy(
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.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 = "",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.domain.card.models
|
||||
|
||||
sealed interface ResetCardError {
|
||||
sealed class ResetCardError {
|
||||
|
||||
data object UserCanceled : ResetCardError
|
||||
data object UserCanceled : ResetCardError()
|
||||
|
||||
data object AnotherSdkError : ResetCardError
|
||||
data object SdkError : ResetCardError()
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import arrow.core.raise.Raise
|
|||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.domain.tokens.error.AddCurrencyError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -32,9 +31,9 @@ class AddCryptoCurrenciesUseCase(
|
|||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param currency Cryptocurrency to add.
|
||||
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
|
||||
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
|
||||
*/
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<AddCurrencyError, Unit> {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Unit> {
|
||||
return invoke(userWalletId, listOf(currency))
|
||||
}
|
||||
|
||||
|
|
@ -47,13 +46,13 @@ class AddCryptoCurrenciesUseCase(
|
|||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param cryptoCurrency Token to add.
|
||||
* @param network Network where we add
|
||||
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
|
||||
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
network: Network,
|
||||
): Either<AddCurrencyError, Unit> = either {
|
||||
): Either<Throwable, Unit> = either {
|
||||
val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network)
|
||||
invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
|
||||
}
|
||||
|
|
@ -66,14 +65,14 @@ class AddCryptoCurrenciesUseCase(
|
|||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param currencies The list of cryptocurrencies to add.
|
||||
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
|
||||
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): Either<AddCurrencyError, Unit> = either {
|
||||
): Either<Throwable, Unit> = either {
|
||||
val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) {
|
||||
raise(AddCurrencyError.DataError(it))
|
||||
raise(it)
|
||||
}
|
||||
val currenciesToAdd = currencies
|
||||
.filterNot(existingCurrencies::contains)
|
||||
|
|
@ -81,7 +80,7 @@ class AddCryptoCurrenciesUseCase(
|
|||
?: return@either
|
||||
|
||||
catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) {
|
||||
raise(AddCurrencyError.DataError(it))
|
||||
raise(it)
|
||||
}
|
||||
|
||||
refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies)
|
||||
|
|
@ -91,7 +90,7 @@ class AddCryptoCurrenciesUseCase(
|
|||
* Refreshes the network statuses for tokens that have corresponding coins in the
|
||||
* [existingCurrencies] list.
|
||||
*/
|
||||
private suspend fun Raise<AddCurrencyError>.refreshUpdatedNetworks(
|
||||
private suspend fun Raise<Throwable>.refreshUpdatedNetworks(
|
||||
userWalletId: UserWalletId,
|
||||
currenciesToAdd: List<CryptoCurrency>,
|
||||
existingCurrencies: List<CryptoCurrency>,
|
||||
|
|
@ -114,7 +113,7 @@ class AddCryptoCurrenciesUseCase(
|
|||
)
|
||||
},
|
||||
) {
|
||||
raise(AddCurrencyError.DataError(it))
|
||||
raise(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.domain.tokens.error
|
||||
|
||||
sealed class AddCurrencyError {
|
||||
|
||||
data class DataError(val cause: Throwable) : AddCurrencyError()
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation
|
|||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
|
@ -61,8 +62,11 @@ internal class SendFragment : ComposeFragment() {
|
|||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
|
||||
val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle()
|
||||
SendScreen(viewModel.uiState, currentState.value)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SendScreen(uiState, currentState.value)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
|
|
@ -123,7 +120,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
|
|
@ -133,7 +130,7 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private val recipientStateFactory = RecipientSendFactory(
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable },
|
||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||
|
|
@ -141,14 +138,14 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private val amountStateFactory = AmountStateFactory(
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
)
|
||||
|
||||
private val feeStateFactory = FeeStateFactory(
|
||||
clickIntents = this,
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
isFeeApproximateUseCase = isFeeApproximateUseCase,
|
||||
|
|
@ -157,13 +154,13 @@ internal class SendViewModel @Inject constructor(
|
|||
private val eventStateFactory = SendEventStateFactory(
|
||||
clickIntents = this,
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
feeStateFactory = feeStateFactory,
|
||||
)
|
||||
|
||||
private val feeNotificationFactory = FeeNotificationFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
clickIntents = this,
|
||||
)
|
||||
|
|
@ -171,7 +168,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val sendNotificationFactory = SendNotificationFactory(
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
|
||||
|
|
@ -186,15 +183,16 @@ internal class SendViewModel @Inject constructor(
|
|||
private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendScreenAnalyticSender(
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrencyProvider = Provider { cryptoCurrency },
|
||||
)
|
||||
}
|
||||
|
||||
// todo convert to StateFlow
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
val uiState: MutableStateFlow<SendUiState> = MutableStateFlow(
|
||||
value = stateFactory.getInitialState(),
|
||||
)
|
||||
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var userWallets: List<AvailableWallet> = emptyList()
|
||||
|
|
@ -272,7 +270,7 @@ internal class SendViewModel @Inject constructor(
|
|||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
uiState = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden)
|
||||
uiState.value = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceHidingJobHolder)
|
||||
|
|
@ -373,15 +371,15 @@ internal class SendViewModel @Inject constructor(
|
|||
feeCryptoCurrencyStatus = feeCurrencyStatus
|
||||
subscribeOnQRScannerResult()
|
||||
when {
|
||||
uiState.sendState?.isSuccess == true -> return
|
||||
uiState.value.sendState?.isSuccess == true -> return
|
||||
transactionId != null && amount != null && destinationAddress != null -> {
|
||||
loadFee()
|
||||
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||
uiState.value = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||
stateRouter.showSend()
|
||||
updateNotifications()
|
||||
}
|
||||
else -> {
|
||||
uiState = stateFactory.getReadyState()
|
||||
uiState.value = stateFactory.getReadyState()
|
||||
getWalletsAndRecent()
|
||||
stateRouter.showRecipient()
|
||||
updateNotifications()
|
||||
|
|
@ -405,9 +403,9 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}.onSuccess { result ->
|
||||
userWallets = result
|
||||
uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
|
||||
uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
|
||||
}.onFailure {
|
||||
uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList())
|
||||
uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -445,7 +443,7 @@ internal class SendViewModel @Inject constructor(
|
|||
pageSize = RECENT_TX_SIZE,
|
||||
).getOrElse { emptyList() }
|
||||
}
|
||||
uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
|
||||
uiState.value = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
|
||||
}
|
||||
|
||||
private fun onStateActive() {
|
||||
|
|
@ -456,7 +454,7 @@ internal class SendViewModel @Inject constructor(
|
|||
SendUiStateType.EditFee,
|
||||
-> loadFee()
|
||||
SendUiStateType.Send -> {
|
||||
uiState = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable)
|
||||
uiState.value = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable)
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
}
|
||||
else -> Unit
|
||||
|
|
@ -469,7 +467,7 @@ internal class SendViewModel @Inject constructor(
|
|||
sendNotificationFactory.create()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getSendNotificationState(notifications = it) }
|
||||
.onEach { uiState.value = stateFactory.getSendNotificationState(notifications = it) }
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(sendNotificationsJobHolder)
|
||||
|
|
@ -479,7 +477,7 @@ internal class SendViewModel @Inject constructor(
|
|||
feeNotificationFactory.create()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) }
|
||||
.onEach { uiState.value = feeStateFactory.getFeeNotificationState(notifications = it) }
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(feeNotificationsJobHolder)
|
||||
|
|
@ -489,7 +487,7 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun popBackStack() = stateRouter.popBackStack()
|
||||
override fun onBackClick() {
|
||||
cancelFeeRequest()
|
||||
stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true)
|
||||
stateRouter.onBackClick(isSuccess = uiState.value.sendState?.isSuccess == true)
|
||||
}
|
||||
|
||||
override fun onCloseClick() {
|
||||
|
|
@ -505,8 +503,8 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onNextClick(isFromEdit: Boolean) {
|
||||
val currentState = stateRouter.currentState.value
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit)
|
||||
sendScreenAnalyticSender.send(currentState.type, uiState)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = isFromEdit)
|
||||
sendScreenAnalyticSender.send(currentState.type, uiState.value)
|
||||
when (currentState.type) {
|
||||
SendUiStateType.Fee,
|
||||
SendUiStateType.EditFee,
|
||||
|
|
@ -531,9 +529,9 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {
|
||||
val recipient = uiState.recipientState?.addressTextField?.value
|
||||
val feeValue = uiState.feeState?.fee?.amount?.value
|
||||
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value
|
||||
val recipient = uiState.value.recipientState?.addressTextField?.value
|
||||
val feeValue = uiState.value.feeState?.fee?.amount?.value
|
||||
val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value
|
||||
|
||||
val receivingAmount = if (amountValue != null && feeValue != null) {
|
||||
checkAndCalculateSubtractedAmount(
|
||||
|
|
@ -541,7 +539,7 @@ internal class SendViewModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
@ -562,20 +560,20 @@ internal class SendViewModel @Inject constructor(
|
|||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
|
||||
private fun onFeeNext(): Boolean {
|
||||
val feeState = uiState.getFeeState(stateRouter.isEditState)
|
||||
val feeState = uiState.value.getFeeState(stateRouter.isEditState)
|
||||
val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||
if (checkIfFeeTooLow(feeSelectorState)) {
|
||||
uiState = eventStateFactory.getFeeTooLowAlert(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
uiState.value = eventStateFactory.getFeeTooLowAlert(
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
return true
|
||||
}
|
||||
return checkIfFeeTooHigh(
|
||||
feeSelectorState = feeSelectorState,
|
||||
onShow = { diff ->
|
||||
uiState = eventStateFactory.getFeeTooHighAlert(
|
||||
uiState.value = eventStateFactory.getFeeTooHighAlert(
|
||||
diff = diff,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -607,20 +605,20 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
// region amount state clicks
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
uiState = amountStateFactory.getOnCurrencyChangedState(isFiat)
|
||||
uiState.value = amountStateFactory.getOnCurrencyChangedState(isFiat)
|
||||
}
|
||||
|
||||
override fun onAmountValueChange(value: String) {
|
||||
uiState = amountStateFactory.getOnAmountValueChange(value)
|
||||
uiState.value = amountStateFactory.getOnAmountValueChange(value)
|
||||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
uiState = amountStateFactory.getOnMaxAmountClick()
|
||||
uiState.value = amountStateFactory.getOnMaxAmountClick()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked)
|
||||
}
|
||||
|
||||
override fun onAmountPasteTriggerDismiss() {
|
||||
uiState = amountStateFactory.getOnAmountPastedTriggerDismiss()
|
||||
uiState.value = amountStateFactory.getOnAmountPastedTriggerDismiss()
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -629,10 +627,10 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
|
||||
viewModelScope.launch {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
uiState.value = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
|
||||
uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
val isValidAddress = validateAddress(value)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
||||
uiState.value = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
||||
type?.let {
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.AddressEntered(
|
||||
|
|
@ -649,11 +647,11 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
|
||||
viewModelScope.launch {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
val recipientState = uiState.getRecipientState(stateRouter.isEditState)
|
||||
uiState.value = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
|
||||
uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
val recipientState = uiState.value.getRecipientState(stateRouter.isEditState)
|
||||
val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty())
|
||||
uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight())
|
||||
uiState.value = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight())
|
||||
}
|
||||
}.saveIn(memoValidationJobHolder)
|
||||
}
|
||||
|
|
@ -675,21 +673,22 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
|
||||
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
|
||||
uiState =
|
||||
uiState.value =
|
||||
recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true)
|
||||
uiState = recipientStateFactory.getOnXAddressMemoState()
|
||||
uiState.value = recipientStateFactory.getOnXAddressMemoState()
|
||||
val isValidAddress = validateAddress(decodedAddress.address)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
|
||||
uiState.value =
|
||||
recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun onEnteredValidAddress(isNotValid: Boolean) {
|
||||
uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid)
|
||||
uiState.value = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid)
|
||||
}
|
||||
|
||||
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
|
||||
val memo = uiState.getRecipientState(stateRouter.isEditState)?.memoTextField?.value
|
||||
val memo = uiState.value.getRecipientState(stateRouter.isEditState)?.memoTextField?.value
|
||||
val isValidMemo = validateMemo(memo)
|
||||
|
||||
val isRecent = type == EnterAddressSource.RecentAddress
|
||||
|
|
@ -701,7 +700,7 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun feeReload() = loadFee()
|
||||
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
||||
uiState.value = feeStateFactory.onFeeSelectedState(feeType)
|
||||
updateFeeNotifications()
|
||||
if (feeType == FeeType.Custom) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked)
|
||||
|
|
@ -709,7 +708,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState = feeStateFactory.onCustomFeeValueChange(index, value)
|
||||
uiState.value = feeStateFactory.onCustomFeeValueChange(index, value)
|
||||
updateFeeNotifications()
|
||||
}
|
||||
|
||||
|
|
@ -725,14 +724,14 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private fun loadFee() {
|
||||
viewModelScope.launch {
|
||||
val isShowStatus = uiState.feeState?.fee == null
|
||||
val isShowStatus = uiState.value.feeState?.fee == null
|
||||
if (isShowStatus) {
|
||||
uiState = feeStateFactory.onFeeOnLoadingState()
|
||||
uiState.value = feeStateFactory.onFeeOnLoadingState()
|
||||
updateNotifications()
|
||||
}
|
||||
val result = callFeeUseCase()?.fold(
|
||||
ifRight = {
|
||||
uiState = feeStateFactory.onFeeOnLoadedState(it)
|
||||
uiState.value = feeStateFactory.onFeeOnLoadedState(it)
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
},
|
||||
ifLeft = {
|
||||
|
|
@ -748,7 +747,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onFeeLoadFailed(isShowStatus: Boolean) {
|
||||
if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState()
|
||||
if (isShowStatus) uiState.value = feeStateFactory.onFeeOnErrorState()
|
||||
}
|
||||
|
||||
private suspend fun checkIfSubtractAvailable() {
|
||||
|
|
@ -767,8 +766,8 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
|
||||
val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation
|
||||
val amountState = uiState.getAmountState(isFromConfirmation) ?: return null
|
||||
val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null
|
||||
val amountState = uiState.value.getAmountState(isFromConfirmation) ?: return null
|
||||
val recipientState = uiState.value.getRecipientState(isFromConfirmation) ?: return null
|
||||
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
|
||||
|
||||
return getFeeUseCase.invoke(
|
||||
|
|
@ -782,10 +781,10 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
// region send state clicks
|
||||
override fun onSendClick() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.value.sendState ?: return
|
||||
if (sendState.isSuccess) popBackStack()
|
||||
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
|
||||
verifyAndSendTransaction()
|
||||
} else {
|
||||
|
|
@ -795,22 +794,22 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun showAmount() {
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||
stateRouter.showAmount(isFromConfirmation = true)
|
||||
setNeverToShowTapHelp()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
|
||||
}
|
||||
|
||||
override fun showRecipient() {
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||
stateRouter.showRecipient(isFromConfirmation = true)
|
||||
uiState = stateFactory.getHiddenTapHelpState()
|
||||
uiState.value = stateFactory.getHiddenTapHelpState()
|
||||
setNeverToShowTapHelp()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
|
||||
}
|
||||
|
||||
override fun showFee() {
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||
stateRouter.showFee(isFromConfirmation = true)
|
||||
setNeverToShowTapHelp()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
|
||||
|
|
@ -821,7 +820,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onExploreClick() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.value.sendState ?: return
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
|
||||
innerRouter.openUrl(sendState.txUrl)
|
||||
}
|
||||
|
|
@ -836,7 +835,7 @@ internal class SendViewModel @Inject constructor(
|
|||
reduceAmountTo: BigDecimal?,
|
||||
clazz: Class<out SendNotification>,
|
||||
) {
|
||||
uiState = when {
|
||||
uiState.value = when {
|
||||
reduceAmountBy != null && reduceAmountByDiff != null -> amountStateFactory.getOnAmountReduceByState(
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
reduceAmountByDiff = reduceAmountByDiff,
|
||||
|
|
@ -845,20 +844,20 @@ internal class SendViewModel @Inject constructor(
|
|||
else -> return
|
||||
}
|
||||
|
||||
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
||||
uiState.value = sendNotificationFactory.dismissNotificationState(clazz)
|
||||
updateNotifications()
|
||||
}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
|
||||
uiState = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
|
||||
uiState.value = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
|
||||
}
|
||||
|
||||
private fun verifyAndSendTransaction() {
|
||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val recipient = uiState.value.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.value.feeState ?: return
|
||||
val fee = feeState.fee ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
val memo = uiState.value.recipientState?.memoTextField?.value
|
||||
val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
val feeValue = fee.amount.value ?: return
|
||||
|
||||
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||
|
|
@ -866,7 +865,7 @@ internal class SendViewModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
|
|
@ -880,10 +879,10 @@ internal class SendViewModel @Inject constructor(
|
|||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it)
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = eventStateFactory.getGenericErrorState(
|
||||
error = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
ifRight = { txData ->
|
||||
|
|
@ -894,21 +893,23 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun sendTransaction(txData: TransactionData) {
|
||||
sendTransactionUseCase(
|
||||
val result = sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
)
|
||||
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
|
||||
result.fold(
|
||||
ifLeft = { error ->
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = eventStateFactory.getSendTransactionErrorState(
|
||||
uiState.value = eventStateFactory.getSendTransactionErrorState(
|
||||
error = error,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
|
||||
},
|
||||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
updateTransactionStatus(txData)
|
||||
addTokenToWalletIfNeeded()
|
||||
scheduleUpdates()
|
||||
|
|
@ -920,7 +921,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private fun addTokenToWalletIfNeeded() {
|
||||
if (cryptoCurrency !is CryptoCurrency.Token) return
|
||||
|
||||
val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return
|
||||
val recipientState = uiState.value.getRecipientState(stateRouter.isEditState) ?: return
|
||||
val destinationAddress = recipientState.addressTextField.value
|
||||
|
||||
val receivingUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return
|
||||
|
|
@ -939,7 +940,7 @@ internal class SendViewModel @Inject constructor(
|
|||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrElse { "" }
|
||||
uiState = stateFactory.getTransactionSendState(txData, txUrl)
|
||||
uiState.value = stateFactory.getTransactionSendState(txData, txUrl)
|
||||
}
|
||||
|
||||
private fun scheduleUpdates() {
|
||||
|
|
@ -957,7 +958,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onCheckFeeUpdate() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.value.sendState ?: return
|
||||
val isSuccess = sendState.isSuccess
|
||||
val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error }
|
||||
|
||||
|
|
@ -965,30 +966,30 @@ internal class SendViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
val feeUpdatedState = callFeeUseCase()?.fold(
|
||||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getFeeUpdatedAlert(
|
||||
fee = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
onFeeNotIncreased = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
verifyAndSendTransaction()
|
||||
},
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getFeeUnreachableErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
uiState = if (feeUpdatedState != null) {
|
||||
uiState.value = if (feeUpdatedState != null) {
|
||||
feeUpdatedState
|
||||
} else {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getFeeUnreachableErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -999,12 +1000,12 @@ internal class SendViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
neverShowTapHelpUseCase()
|
||||
}
|
||||
uiState = stateFactory.getHiddenTapHelpState()
|
||||
uiState.value = stateFactory.getHiddenTapHelpState()
|
||||
}
|
||||
|
||||
private fun showErrorAlert() {
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
uiState.value = eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -538,7 +538,13 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
ifRight = { it },
|
||||
)
|
||||
if (extendedKey.isNotBlank()) {
|
||||
router.share(extendedKey)
|
||||
hapticManager.vibrateMeduim()
|
||||
clipboardManager.setText(text = extendedKey)
|
||||
internalUiState.value = stateFactory.getStateAndTriggerEvent(
|
||||
state = internalUiState.value,
|
||||
errorMessage = resourceReference(R.string.wallet_notification_address_copied),
|
||||
setUiState = { internalUiState.value = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ fun UserWallet.getCardsCount(): Int? {
|
|||
return if (isMultiCurrency) {
|
||||
when (val status = scanResponse.card.backupStatus) {
|
||||
is CardDTO.BackupStatus.Active -> status.cardCount + 1
|
||||
is CardDTO.BackupStatus.CardLinked -> status.cardCount + 1
|
||||
is CardDTO.BackupStatus.NoBackup -> 1
|
||||
is CardDTO.BackupStatus.NoBackup,
|
||||
is CardDTO.BackupStatus.CardLinked,
|
||||
-> 1
|
||||
null -> 1 // Multi-currency wallet without backup function. Example, 4.12
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
|
||||
|
|
@ -108,18 +107,4 @@ internal object WalletAdditionalInfoFactory {
|
|||
WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.getCardsCount(): Int? {
|
||||
return if (isMultiCurrency) {
|
||||
when (val status = scanResponse.card.backupStatus) {
|
||||
is CardDTO.BackupStatus.Active -> status.cardCount + 1
|
||||
is CardDTO.BackupStatus.CardLinked,
|
||||
is CardDTO.BackupStatus.NoBackup,
|
||||
null,
|
||||
-> 1
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue