Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-20 16:35:04 +05:00
parent ddecdf444d
commit 78e022c1d2
21 changed files with 317 additions and 46 deletions

View file

@ -31,6 +31,8 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.transaction.models)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}

View file

@ -0,0 +1,49 @@
package com.tangem.common.ui.alerts
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.utils.converter.Converter
class SendTransactionAlertConverter(
private val popBackStack: () -> Unit,
private val onFailedTxEmailClick: (String) -> Unit,
) : Converter<SendTransactionError, AlertUM?> {
override fun convert(value: SendTransactionError): AlertUM? {
return when (value) {
is SendTransactionError.DemoCardError -> AlertDemoModeUM(
onConfirmClick = popBackStack,
)
is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = null,
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
onConfirmClick = { onFailedTxEmailClick(value.code.toString()) },
)
is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = value.message,
onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
)
is SendTransactionError.DataError -> AlertTransactionErrorUM(
code = "",
cause = value.message,
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.NetworkError -> AlertTransactionErrorUM(
code = value.code.orEmpty(),
cause = value.message.orEmpty(),
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.UnknownError -> AlertTransactionErrorUM(
code = "",
cause = value.ex?.localizedMessage,
onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
)
else -> null
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
data class AlertDemoModeUM(
override val onConfirmClick: () -> Unit,
) : AlertUM {
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
}

View file

@ -0,0 +1,21 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
data class AlertTransactionErrorUM(
val code: String,
val cause: String?,
val causeTextReference: TextReference? = null,
override val onConfirmClick: (() -> Unit)? = null,
) : AlertUM {
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
override val message: TextReference = resourceReference(
id = R.string.send_alert_transaction_failed_text,
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}

View file

@ -0,0 +1,12 @@
package com.tangem.common.ui.alerts.models
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
interface AlertUM {
val title: TextReference?
val message: TextReference
val confirmButtonText: TextReference
val onConfirmClick: (() -> Unit)?
}

View file

@ -1,7 +1,5 @@
package com.tangem.domain.transaction.error
import com.tangem.core.ui.extensions.TextReference
sealed class SendTransactionError {
data object DemoCardError : SendTransactionError()
@ -16,7 +14,7 @@ sealed class SendTransactionError {
data class CreateAccountUnderfunded(val amount: String) : SendTransactionError()
data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError()
data class TangemSdkError(val code: Int, val messageRes: Int, val args: List<Any>) : SendTransactionError()
data class UnknownError(val ex: Exception? = null) : SendTransactionError()

View file

@ -10,7 +10,6 @@ import com.tangem.blockchain.common.transaction.TransactionSendResult
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.network.ResultChecker
import com.tangem.common.core.TangemSdkError
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
@ -133,8 +132,7 @@ class SendTransactionUseCase(
val resource = tangemError.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
val textReference = resourceReference(resId, wrappedList(resArgs))
SendTransactionError.TangemSdkError(tangemError.code, textReference)
SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs))
}
is BlockchainSdkError.WrappedTangemError -> {
parseWrappedError(tangemError) // todo remove when sdk errors are revised

View file

@ -71,6 +71,7 @@ dependencies {
implementation(projects.domain.txhistory)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.transaction)
implementation(projects.domain.transaction.models)
implementation(projects.domain.card)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)

View file

@ -1,5 +1,7 @@
package com.tangem.features.send.impl.presentation.state
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.converter.Converter
@ -15,7 +17,7 @@ internal class SendTransactionAlertConverter(
is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError(
code = value.code.toString(),
cause = null,
causeTextReference = value.messageReference,
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) },
)
is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError(

View file

@ -61,6 +61,7 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.transaction)
implementation(projects.domain.transaction.models)
implementation(projects.domain.txhistory)
/** Common */

View file

@ -1,24 +0,0 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.staking.impl.R
@Immutable
internal sealed class StakingAlertState {
abstract val title: TextReference?
abstract val message: TextReference
open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
open val onConfirmClick: (() -> Unit)? = null
data class GenericError(
override val title: TextReference? = TODO(),
override val onConfirmClick: () -> Unit,
) : StakingAlertState() {
override val message: TextReference = resourceReference(R.string.common_unknown_error)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}
}

View file

@ -3,8 +3,10 @@ package com.tangem.features.staking.impl.presentation.state
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer
@ -46,6 +48,16 @@ internal class StakingStateController @Inject constructor() {
mutableUiState.update(function = titleTransformer::transform)
}
fun updateEvent(event: StakingEvent?) {
mutableUiState.update {
it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent())
}
}
private fun dismissAlert() {
mutableUiState.update { it.copy(event = consumedEvent()) }
}
private fun getInitialState(): StakingUiState {
return StakingUiState(
title = TextReference.EMPTY,

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList

View file

@ -0,0 +1,29 @@
package com.tangem.features.staking.impl.presentation.state.events
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.staking.impl.R
@Immutable
internal sealed class StakingAlertUM : AlertUM {
data class GenericError(
override val onConfirmClick: () -> Unit,
) : StakingAlertUM() {
override val title: TextReference = resourceReference(R.string.common_error)
override val message: TextReference = resourceReference(R.string.common_unknown_error)
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support)
}
data class StakingError(
val code: String,
override val onConfirmClick: () -> Unit,
) : StakingAlertUM() {
override val title: TextReference = resourceReference(R.string.common_error)
override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code))
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state
package com.tangem.features.staking.impl.presentation.state.events
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.extensions.TextReference
@Immutable
@ -8,5 +9,5 @@ internal sealed class StakingEvent {
data class ShowSnackBar(val text: TextReference) : StakingEvent()
data class ShowAlert(val alert: StakingAlertState) : StakingEvent()
data class ShowAlert(val alert: AlertUM) : StakingEvent()
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.staking.impl.presentation.state.events
import com.tangem.common.ui.alerts.SendTransactionAlertConverter
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.features.staking.impl.presentation.state.StakingStateController
internal class StakingEventFactory(
private val stateController: StakingStateController,
private val popBackStack: () -> Unit,
private val onFailedTxEmailClick: (String) -> Unit,
) {
fun createGenericErrorAlert(error: String) {
val alert = StakingEvent.ShowAlert(
StakingAlertUM.GenericError(
onConfirmClick = { onFailedTxEmailClick(error) },
),
)
stateController.updateEvent(alert)
}
fun createSendTransactionErrorAlert(error: SendTransactionError?) {
val alert = error?.let {
SendTransactionAlertConverter(
popBackStack = popBackStack,
onFailedTxEmailClick = onFailedTxEmailClick,
).convert(error)
}?.let {
StakingEvent.ShowAlert(it)
}
stateController.updateEvent(alert)
}
fun createStakingErrorAlert(error: StakingError) {
val alert = StakingEvent.ShowAlert(
StakingAlertUM.StakingError(
code = error.toString(),
onConfirmClick = { onFailedTxEmailClick(error.toString()) },
),
)
stateController.updateEvent(alert)
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
@Suppress("TooManyFunctions")
object StakingClickIntentsStub : StakingClickIntents {
override fun onBackClick() {}
@ -46,5 +47,7 @@ object StakingClickIntentsStub : StakingClickIntents {
override fun onShareClick() {}
override fun onFailedTxEmailClick(errorMessage: String) {}
override fun onActiveStake(activeStake: BalanceState) {}
}

View file

@ -0,0 +1,79 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
@Composable
internal fun StakingEventEffect(event: StateEvent<StakingEvent>, snackbarHostState: SnackbarHostState) {
val resources = LocalContext.current.resources
var alertConfig by remember { mutableStateOf<AlertUM?>(value = null) }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(key1 = alertConfig) {
keyboardController?.hide()
}
alertConfig?.let {
StakingAlert(state = it, onDismiss = { alertConfig = null })
}
EventEffect(
event = event,
onTrigger = { value ->
when (value) {
is StakingEvent.ShowSnackBar -> {
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
}
is StakingEvent.ShowAlert -> {
alertConfig = value.alert
}
}
},
)
}
@Composable
internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) {
val confirmButton: DialogButton
val dismissButton: DialogButton?
val onActionClick = state.onConfirmClick
if (onActionClick != null) {
confirmButton = DialogButton(
title = state.confirmButtonText.resolveReference(),
onClick = {
onActionClick()
onDismiss()
},
)
dismissButton = DialogButton(
title = stringResource(id = R.string.common_cancel),
onClick = onDismiss,
)
} else {
confirmButton = DialogButton(
title = state.confirmButtonText.resolveReference(),
onClick = onDismiss,
)
dismissButton = null
}
BasicDialog(
message = state.message.resolveReference(),
confirmButton = confirmButton,
onDismissDialog = onDismiss,
title = state.title?.resolveReference(),
dismissButton = dismissButton,
)
}

View file

@ -8,6 +8,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -32,6 +33,8 @@ import kotlinx.coroutines.flow.withIndex
@Composable
internal fun StakingScreen(uiState: StakingUiState) {
val snackbarHostState = remember { SnackbarHostState() }
BackHandler(onBack = uiState.clickIntents::onPrevClick)
Column(
modifier = Modifier
@ -58,6 +61,11 @@ internal fun StakingScreen(uiState: StakingUiState) {
)
StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig)
}
StakingEventEffect(
event = uiState.event,
snackbarHostState = snackbarHostState,
)
}
@Composable

View file

@ -43,4 +43,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onExploreClick()
fun onShareClick()
fun onFailedTxEmailClick(errorMessage: String)
}

View file

@ -43,6 +43,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory
import com.tangem.features.staking.impl.presentation.state.transformers.*
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer
@ -64,7 +65,7 @@ import java.math.BigDecimal
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LargeClass", "LongParameterList")
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@HiltViewModel
internal class StakingViewModel @Inject constructor(
private val stateController: StakingStateController,
@ -122,6 +123,13 @@ internal class StakingViewModel @Inject constructor(
private var userWallet: UserWallet by Delegates.notNull()
private var appCurrency: AppCurrency by Delegates.notNull()
private val stakingEventFactory: StakingEventFactory
get() = StakingEventFactory(
stateController = stateController,
popBackStack = stakingStateRouter::onBackClick,
onFailedTxEmailClick = ::onFailedTxEmailClick,
)
private var stakingApproval: StakingApproval = StakingApproval.Empty
private val allowanceTaskScheduler = SingleTaskScheduler<BigDecimal>()
@ -164,6 +172,8 @@ internal class StakingViewModel @Inject constructor(
val amountState = value.amountState as? AmountState.Data ?: error("No amount provided")
val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value")
val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided")
val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address")
val stakingTransaction = getStakingTransactionUseCase(
userWalletId = userWalletId,
@ -172,15 +182,16 @@ internal class StakingViewModel @Inject constructor(
actionCommonType = value.actionType,
integrationId = yield.id,
amount = amountValue,
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address"),
address = defaultAddress,
validatorAddress = validatorState.chosenValidator.address,
token = yield.token,
passthrough = pendingAction?.passthrough,
type = pendingAction?.type,
),
).getOrElse {
error(it)
Timber.e(it.toString())
stakingEventFactory.createStakingErrorAlert(it)
return@launch
}
stakingTransaction
@ -190,8 +201,11 @@ internal class StakingViewModel @Inject constructor(
networkId = cryptoCurrencyStatus.currency.network.id.value,
fee = fee,
transactionId = transaction.id,
).getOrNull() ?: error("No constructed transaction")
).getOrElse {
Timber.e(it.toString())
stakingEventFactory.createStakingErrorAlert(it)
return@launch
}
sendStakingTransaction(
transactionId = constructedTransaction.id,
gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"),
@ -296,8 +310,7 @@ internal class StakingViewModel @Inject constructor(
userWallet = userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
).getOrElse {
// TODO staking error
return
return stakingEventFactory.createGenericErrorAlert(it.toString())
}
stateController.update(
@ -407,7 +420,7 @@ internal class StakingViewModel @Inject constructor(
fee = TransactionFee.Single(fee),
),
)
// TODO staking error
stakingEventFactory.createGenericErrorAlert(error.message ?: error.toString())
return@launch
},
ifRight = { it },
@ -427,7 +440,7 @@ internal class StakingViewModel @Inject constructor(
fee = TransactionFee.Single(fee),
),
)
// TODO staking error
stakingEventFactory.createSendTransactionErrorAlert(error)
},
ifRight = {
stateController.update(SetApprovalInProgressTransformer)
@ -489,6 +502,10 @@ internal class StakingViewModel @Inject constructor(
// TODO staking [REDACTED_TASK_KEY]
}
override fun onFailedTxEmailClick(errorMessage: String) {
// TODO staking sending feedback email [REDACTED_TASK_KEY]
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
innerRouter = router
this.stakingStateRouter = stateRouter
@ -505,7 +522,8 @@ internal class StakingViewModel @Inject constructor(
userWallet = wallet
},
ifLeft = {
// TODO staking error
Timber.e(it.toString())
stakingEventFactory.createGenericErrorAlert(it.toString())
},
)
getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold(
@ -530,7 +548,8 @@ internal class StakingViewModel @Inject constructor(
)
},
ifLeft = {
// TODO staking error
Timber.e(it.toString())
stakingEventFactory.createGenericErrorAlert(it.toString())
},
)
}
@ -579,7 +598,7 @@ internal class StakingViewModel @Inject constructor(
pendingActionList = pendingActionList,
),
)
// todo add error dialog
stakingEventFactory.createSendTransactionErrorAlert(error)
},
ifRight = { txHash ->
submitHash(transactionId, txHash)