Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-08 16:26:42 +01:00
commit 3442172603
13 changed files with 493 additions and 424 deletions

View file

@ -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
}
}

View file

@ -17,5 +17,5 @@ interface CustomTokenInteractor {
/** Save token [customCurrency] */
@Throws(TangemError::class)
suspend fun saveToken(customCurrency: CustomCurrency)
suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit>
}

View file

@ -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? {

View file

@ -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,
)
}
}

View file

@ -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 {

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.domain.tokens.error
sealed class AddCurrencyError {
data class DataError(val cause: Throwable) : AddCurrencyError()
}

View file

@ -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() {

View file

@ -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

View file

@ -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 },
)
}
}
}

View file

@ -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 {

View file

@ -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
}
}
}