diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt index b134e9d549..c67e77ecd9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt @@ -50,29 +50,17 @@ internal fun StakingEventEffect(event: StateEvent, snackbarHostSta @Composable internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton: DialogButtonUM - val dismissButton: DialogButtonUM? - - val onActionClick = state.onConfirmClick - if (onActionClick != null) { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = { - onActionClick() - onDismiss() - }, - ) - dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), - onClick = onDismiss, - ) - } else { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = onDismiss, - ) - dismissButton = null - } + val confirmButton = DialogButtonUM( + title = state.confirmButtonText.resolveReference(), + onClick = { + state.onConfirmClick() + onDismiss() + }, + ) + val dismissButton = DialogButtonUM( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ) BasicDialog( message = state.message.resolveReference(), diff --git a/features/swap/domain/models/build.gradle.kts b/features/swap/domain/models/build.gradle.kts index 2ec7133686..431789f3d2 100644 --- a/features/swap/domain/models/build.gradle.kts +++ b/features/swap/domain/models/build.gradle.kts @@ -12,6 +12,7 @@ android { dependencies { /** Domain */ implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) /** Core modules */ implementation(projects.core.utils) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index bd29c42170..6db16f52c8 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.domain.models.ui -import com.tangem.feature.swap.domain.models.DataError +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.feature.swap.domain.models.ExpressDataError import java.math.BigDecimal sealed class SwapTransactionState { @@ -15,17 +16,13 @@ sealed class SwapTransactionState { val timestamp: Long, ) : SwapTransactionState() - data object UserCancelled : SwapTransactionState() - - data object BlockchainError : SwapTransactionState() - - data object TangemSdkError : SwapTransactionState() - - data object NetworkError : SwapTransactionState() - - data object UnknownError : SwapTransactionState() - - data class ExpressError(val dataError: DataError) : SwapTransactionState() - data object DemoMode : SwapTransactionState() + + sealed class Error : SwapTransactionState() { + data class TransactionError(val error: SendTransactionError?) : Error() + + data class ExpressError(val error: ExpressDataError) : Error() + + data object UnknownError : Error() + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 2a1a873a96..541327a97a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -22,7 +22,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount @@ -227,7 +226,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ), ).getOrElse { Timber.e(it, "Failed to create approveTransaction") - return SwapTransactionState.UnknownError + return SwapTransactionState.Error.UnknownError } val result = sendTransactionUseCase( @@ -243,16 +242,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( timestamp = System.currentTimeMillis(), ) }, - ifLeft = { - when (it) { - SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError - is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode - else -> SwapTransactionState.UnknownError - } - }, + ifLeft = { SwapTransactionState.Error.TransactionError(it) }, ) } @@ -591,7 +581,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( """.trimIndent(), ) - val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return SwapTransactionState.UnknownError val cardId = userWallet.scanResponse.card.cardId if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode @@ -693,12 +682,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( hash = dataToSign, ).getOrElse { Timber.e(it, "Failed to create swap dex tx data") - return SwapTransactionState.UnknownError + return SwapTransactionState.Error.UnknownError } val result = sendTransactionUseCase( txData = txData, - userWallet = getUserWalletUseCase(userWalletId).getOrElse { return SwapTransactionState.UnknownError }, + userWallet = userWallet, network = currencyToSendStatus.currency.network, ) return result.fold( @@ -738,7 +727,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( timestamp = System.currentTimeMillis(), ) }, - ifLeft = { handleSendTxError(it) }, + ifLeft = { SwapTransactionState.Error.TransactionError(it) }, ) } @@ -773,14 +762,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(), refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value, refundExtraId = null, // currently always null - ).getOrElse { return SwapTransactionState.ExpressError(it) } + ).getOrElse { return SwapTransactionState.Error.ExpressError(it) } val exchangeDataCex = - exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError + exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError val cardId = userWallet.scanResponse.card.cardId - if (isDemoCardUseCase(cardId)) return SwapTransactionState.UnknownError + if (isDemoCardUseCase(cardId)) return SwapTransactionState.Error.UnknownError val txData = createTransactionUseCase( amount = amount.value.convertToSdkAmount(currencyToSend.currency), @@ -794,11 +783,11 @@ internal class SwapInteractorImpl @AssistedInject constructor( network = currencyToSend.currency.network, ).getOrElse { Timber.e(it, "Failed to create swap CEX tx data") - return SwapTransactionState.UnknownError + return SwapTransactionState.Error.UnknownError } if (txData.extras == null && exchangeDataCex.txExtraId != null) { - return SwapTransactionState.UnknownError + return SwapTransactionState.Error.UnknownError } val result = sendTransactionUseCase( @@ -809,9 +798,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val derivationPath = currencyToSend.currency.network.derivationPath.value return result.fold( - ifLeft = { - handleSendTxError(it) - }, + ifLeft = { SwapTransactionState.Error.TransactionError(it) }, ifRight = { txHash -> repository.exchangeSent( txId = exchangeDataCex.txId, @@ -856,17 +843,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } - private fun handleSendTxError(txError: SendTransactionError?): SwapTransactionState { - return when (txError) { - SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError - is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode - else -> SwapTransactionState.UnknownError - } - } - private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee { val feeAmountValue = fee.feeValue val feeAmount = Amount( diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index bea28f028c..3b0dbe2d6d 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.core.decompose) // For Route supertype /** Domain modules **/ + implementation(projects.domain.models) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) @@ -31,13 +32,17 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.settings) + implementation(projects.domain.staking) + implementation(projects.domain.feedback) + + /** Feature modules */ implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) - implementation(projects.domain.staking) /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt new file mode 100644 index 0000000000..ad158cc32e --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.swap.converters + +import com.tangem.common.ui.alerts.TransactionErrorAlertConverter +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.feature.swap.domain.models.ui.SwapTransactionState +import com.tangem.feature.swap.models.SwapAlertUM +import com.tangem.feature.swap.utils.getExpressErrorMessage +import com.tangem.utils.converter.Converter + +internal class SwapTransactionErrorStateConverter( + private val onDismiss: () -> Unit, + private val onSupportClick: (String) -> Unit, +) : Converter { + override fun convert(value: SwapTransactionState.Error): AlertUM? { + return when (value) { + is SwapTransactionState.Error.TransactionError -> { + when (val error = value.error) { + is SendTransactionError.UserCancelledError -> return null + null -> SwapAlertUM.GenericError(onDismiss) + else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error) + } + } + is SwapTransactionState.Error.ExpressError -> { + SwapAlertUM.ExpressErrorAlert( + message = getExpressErrorMessage(value.error), + onConfirmClick = { onSupportClick(value.error.code.toString()) }, + ) + } + SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss) + } + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt new file mode 100644 index 0000000000..f99c9278ea --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.swap.models + +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference + +sealed class SwapAlertUM : AlertUM { + + data class GenericError( + override val onConfirmClick: (() -> Unit), + override val message: TextReference = resourceReference(R.string.common_unknown_error), + ) : SwapAlertUM() { + override val title: TextReference? = null + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_support) + } + + data class ExpressErrorAlert( + override val message: TextReference = resourceReference(R.string.common_unknown_error), + override val onConfirmClick: (() -> Unit), + ) : SwapAlertUM() { + override val title: TextReference? = null + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_support) + } + + data class FeesAlert( + override val message: TextReference, + override val onConfirmClick: (() -> Unit), + ) : SwapAlertUM() { + override val title: TextReference = resourceReference( + com.tangem.feature.swap.presentation.R.string.swapping_alert_title, + ) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_ok) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 003f258f6f..8b21ec6919 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -6,18 +6,21 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState +import com.tangem.feature.swap.models.states.events.SwapEvent -data class SwapStateHolder( +internal data class SwapStateHolder( val sendCardData: SwapCardState, val receiveCardData: SwapCardState, val blockchainId: String, // not the same as networkId, its local id in app val warnings: List = emptyList(), - val alert: SwapWarning.GenericWarning? = null, + val event: StateEvent = consumedEvent(), val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, @@ -110,7 +113,6 @@ sealed interface SwapWarning { data class GenericWarning( val title: TextReference? = null, val message: TextReference? = null, - val type: GenericWarningType = GenericWarningType.OTHER, val onClick: () -> Unit, ) : SwapWarning @@ -133,10 +135,6 @@ sealed interface SwapWarning { } } -enum class GenericWarningType { - NETWORK, OTHER -} - enum class ChangeCardsButtonState { ENABLED, DISABLED, UPDATE_IN_PROGRESS } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt new file mode 100644 index 0000000000..1741d69029 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.swap.models.states.events + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.alerts.models.AlertUM + +@Immutable +internal sealed class SwapEvent { + data class ShowAlert(val alert: AlertUM) : SwapEvent() + + data class ShowShareDialog(val txUrl: String) : SwapEvent() +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 34bf5fb2ec..2575e2d24b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -2,15 +2,19 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -19,7 +23,10 @@ import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* +import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.utils.getExpressErrorMessage +import com.tangem.feature.swap.utils.getExpressErrorTitle import com.tangem.feature.swap.viewmodels.SwapProcessDataState import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN @@ -1045,81 +1052,43 @@ internal class StateBuilder( ) } - fun createErrorTransaction( + fun createErrorTransactionAlert( uiState: SwapStateHolder, - swapTransactionState: SwapTransactionState, - onAlertClick: () -> Unit, + error: SwapTransactionState.Error, + onDismiss: () -> Unit, + onSupportClick: (String) -> Unit, ): SwapStateHolder { + val errorAlert = SwapTransactionErrorStateConverter( + onSupportClick = onSupportClick, + onDismiss = onDismiss, + ).convert(error) return uiState.copy( - alert = SwapWarning.GenericWarning( - message = if (swapTransactionState is SwapTransactionState.ExpressError) { - getProviderErrorMessage(swapTransactionState.dataError) - } else { - null - }, - onClick = onAlertClick, - type = if (swapTransactionState is SwapTransactionState.NetworkError) { - GenericWarningType.NETWORK - } else { - GenericWarningType.OTHER - }, - ), + event = errorAlert?.let { + triggeredEvent( + data = SwapEvent.ShowAlert(errorAlert), + onConsume = onDismiss, + ) + } ?: consumedEvent(), changeCardsButtonState = ChangeCardsButtonState.ENABLED, ) } - fun createDemoModeAlert(uiState: SwapStateHolder, onAlertClick: () -> Unit): SwapStateHolder { + fun createDemoModeAlert(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder { return uiState.copy( - alert = SwapWarning.GenericWarning( - title = resourceReference(id = R.string.warning_demo_mode_title), - message = resourceReference(id = R.string.warning_demo_mode_message), - onClick = onAlertClick, - type = GenericWarningType.OTHER, + event = triggeredEvent( + data = SwapEvent.ShowAlert(AlertDemoModeUM(onDismiss)), + onConsume = onDismiss, ), changeCardsButtonState = ChangeCardsButtonState.ENABLED, ) } - private fun getProviderErrorMessage(dataError: DataError): TextReference { - return when (dataError) { - is DataError.SwapsAreUnavailableNowError -> resourceReference( - id = R.string.express_error_swap_unavailable, - formatArgs = wrappedList(dataError.code), - ) - is DataError.ExchangeNotPossibleError -> resourceReference( - id = R.string.warning_express_pair_unavailable_message, - formatArgs = wrappedList(dataError.code), - ) - is DataError.UnknownError -> resourceReference(R.string.common_unknown_error) - is DataError.ExchangeProviderNotActiveError, - is DataError.ExchangeProviderNotFoundError, - is DataError.ExchangeProviderNotAvailableError, - is DataError.ExchangeProviderProviderInternalError, - -> resourceReference( - id = R.string.express_error_swap_pair_unavailable, - formatArgs = wrappedList(dataError.code), - ) - else -> resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString())) - } - } - - private fun getProviderErrorTitle(dataError: DataError): TextReference { - return when (dataError) { - is DataError.ExchangeNotPossibleError -> resourceReference( - id = R.string.warning_express_pair_unavailable_title, - formatArgs = wrappedList(dataError.code), - ) - is DataError.UnknownError -> resourceReference(R.string.common_error) - else -> resourceReference(R.string.warning_express_refresh_required_title) - } - } - fun createAlert( uiState: SwapStateHolder, isPriceImpact: Boolean, token: String, providerType: ExchangeProviderType, - onAlertClick: () -> Unit, + onDismiss: () -> Unit, ): SwapStateHolder { val message = when (providerType) { ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token)) @@ -1136,29 +1105,38 @@ internal class StateBuilder( } } return uiState.copy( - alert = SwapWarning.GenericWarning( - title = resourceReference(R.string.swapping_alert_title), - message = message, - onClick = onAlertClick, - type = GenericWarningType.OTHER, + event = triggeredEvent( + SwapEvent.ShowAlert( + SwapAlertUM.FeesAlert( + message = message, + onConfirmClick = onDismiss, + ), + ), + onConsume = onDismiss, ), changeCardsButtonState = ChangeCardsButtonState.ENABLED, ) } - fun addAlert(uiState: SwapStateHolder, message: TextReference? = null, onClick: () -> Unit): SwapStateHolder { + fun addAlert( + uiState: SwapStateHolder, + message: TextReference = resourceReference(R.string.common_unknown_error), + onDismiss: () -> Unit = { clearAlert(uiState) }, + ): SwapStateHolder { return uiState.copy( - alert = SwapWarning.GenericWarning( - message = message, - onClick = onClick, + event = triggeredEvent( + SwapEvent.ShowAlert( + SwapAlertUM.GenericError(onDismiss, message), + ), + onConsume = onDismiss, ), ) } - fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null) + fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent()) fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { - val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList() + val renewWarnings = uiState.warnings.toMutableList() renewWarnings.add( SwapWarning.GenericWarning( message = message, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt new file mode 100644 index 0000000000..c356624cdb --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.swap.ui + +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.DialogButtonUM +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.shareText +import com.tangem.feature.swap.models.states.events.SwapEvent +import com.tangem.feature.swap.presentation.R + +@Composable +internal fun SwapEventEffect(event: StateEvent) { + val context = LocalContext.current + var alertConfig by remember { mutableStateOf(value = null) } + + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(key1 = alertConfig) { + keyboardController?.hide() + } + + alertConfig?.let { + SwapAlert(state = it, onDismiss = { alertConfig = null }) + } + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is SwapEvent.ShowAlert -> { + alertConfig = value.alert + } + is SwapEvent.ShowShareDialog -> { + context.shareText(value.txUrl) + } + } + }, + ) +} + +@Composable +internal fun SwapAlert(state: AlertUM, onDismiss: () -> Unit) { + val confirmButton = DialogButtonUM( + title = state.confirmButtonText.resolveReference(), + onClick = { + state.onConfirmClick() + onDismiss() + }, + ) + val dismissButton = DialogButtonUM( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ) + + BasicDialog( + message = state.message.resolveReference(), + confirmButton = confirmButton, + onDismissDialog = onDismiss, + title = state.title?.resolveReference(), + dismissButton = dismissButton, + ) +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index f64eb3344e..318e1dd104 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -101,22 +101,9 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi ) } - if (state.alert != null) { - val message = if (state.alert.type == GenericWarningType.NETWORK) { - stringResource(id = R.string.disclaimer_error_loading) - } else { - state.alert.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error) - } - BasicDialog( - title = state.alert.title?.resolveReference(), - message = message, - confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), - onClick = state.alert.onClick, - ), - onDismissDialog = state.alert.onClick, - ) - } + SwapEventEffect( + event = state.event, + ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt new file mode 100644 index 0000000000..e288546e37 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.swap.utils + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.presentation.R + +internal fun getExpressErrorMessage(expressDataError: ExpressDataError): TextReference { + return when (expressDataError) { + is ExpressDataError.SwapsAreUnavailableNowError -> resourceReference( + id = R.string.express_error_swap_unavailable, + formatArgs = wrappedList(expressDataError.code), + ) + is ExpressDataError.ExchangeNotPossibleError -> resourceReference( + id = R.string.warning_express_pair_unavailable_message, + formatArgs = wrappedList(expressDataError.code), + ) + is ExpressDataError.UnknownError -> resourceReference(R.string.common_unknown_error) + is ExpressDataError.ExchangeProviderNotActiveError, + is ExpressDataError.ExchangeProviderNotFoundError, + is ExpressDataError.ExchangeProviderNotAvailableError, + is ExpressDataError.ExchangeProviderProviderInternalError, + -> resourceReference( + id = R.string.express_error_swap_pair_unavailable, + formatArgs = wrappedList(expressDataError.code), + ) + else -> resourceReference(R.string.express_error_code, wrappedList(expressDataError.code.toString())) + } +} + +internal fun getExpressErrorTitle(expressDataError: ExpressDataError): TextReference { + return when (expressDataError) { + is ExpressDataError.ExchangeNotPossibleError -> resourceReference( + id = R.string.warning_express_pair_unavailable_title, + formatArgs = wrappedList(expressDataError.code), + ) + is ExpressDataError.UnknownError -> resourceReference(R.string.common_error) + else -> resourceReference(R.string.warning_express_refresh_required_title) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index cc9b9d3132..e88d4698d1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -19,13 +19,20 @@ import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor @@ -64,7 +71,6 @@ typealias SuccessLoadedSwapData = Map @Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class SwapViewModel @Inject constructor( - private val swapInteractorFactory: SwapInteractor.Factory, private val blockchainInteractor: BlockchainInteractor, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -73,6 +79,11 @@ internal class SwapViewModel @Inject constructor( private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val feedbackManager: FeedbackManager, + swapInteractorFactory: SwapInteractor.Factory, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -90,6 +101,7 @@ internal class SwapViewModel @Inject constructor( private val swapInteractor = swapInteractorFactory.create(userWalletId) private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus + private var userWallet: UserWallet by Delegates.notNull() private var isBalanceHidden = true @@ -123,7 +135,10 @@ internal class SwapViewModel @Inject constructor( private val isUserResolvableError: (SwapState) -> Boolean = { it is SwapState.SwapError && - (it.error is DataError.ExchangeTooSmallAmountError || it.error is DataError.ExchangeTooBigAmountError) + ( + it.error is ExpressDataError.ExchangeTooSmallAmountError || + it.error is ExpressDataError.ExchangeTooBigAmountError + ) } private val fromTokenBalanceJobHolder = JobHolder() @@ -136,9 +151,11 @@ internal class SwapViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { val cryptoCurrencyStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCryptoCurrency.id).getOrNull() - if (cryptoCurrencyStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back) + val wallet = getUserWalletUseCase(userWalletId).getOrNull() + if (cryptoCurrencyStatus == null || wallet == null) { + uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) } else { + userWallet = wallet initialCryptoCurrencyStatus = cryptoCurrencyStatus initTokens(isInitiallyReversed) } @@ -573,20 +590,19 @@ internal class SwapViewModel @Inject constructor( swapRouter.openScreen(SwapNavScreen.Success) } - is SwapTransactionState.UserCancelled -> { - startLoadingQuotesFromLastState() - } - is SwapTransactionState.DemoMode -> { - startLoadingQuotesFromLastState() + SwapTransactionState.DemoMode -> { uiState = stateBuilder.createDemoModeAlert(uiState) { uiState = stateBuilder.clearAlert(uiState) } } - else -> { + is SwapTransactionState.Error -> { startLoadingQuotesFromLastState() - uiState = stateBuilder.createErrorTransaction(uiState, it) { - uiState = stateBuilder.clearAlert(uiState) - } + uiState = stateBuilder.createErrorTransactionAlert( + uiState = uiState, + error = it, + onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, + onSupportClick = ::onFailedTxEmailClick, + ) } } }.onFailure { @@ -657,23 +673,28 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.dismissBottomSheet(uiState) startLoadingQuotesFromLastState(isSilent = true) } - is SwapTransactionState.UserCancelled -> Unit - else -> { - uiState = stateBuilder.createErrorTransaction(uiState, it) { + is SwapTransactionState.Error -> { + uiState = stateBuilder.createErrorTransactionAlert( + uiState = uiState, + error = it, + onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, + onSupportClick = ::onFailedTxEmailClick, + ) + } + SwapTransactionState.DemoMode -> { + uiState = stateBuilder.createDemoModeAlert(uiState) { uiState = stateBuilder.clearAlert(uiState) } } } }.onFailure { makeDefaultAlert() } - }.onFailure { showGenericError(it.message.orEmpty()) } + }.onFailure { + Timber.e(it.message.orEmpty()) + makeDefaultAlert() + } } } - private fun showGenericError(message: String) { - makeDefaultAlert(resourceReference(R.string.common_unknown_error)) - Timber.e(message) - } - private fun onSearchEntered(searchQuery: String) { viewModelScope.launch(dispatchers.io) { val tokenDataState = dataState.tokensDataState ?: return@launch @@ -880,15 +901,11 @@ internal class SwapViewModel @Inject constructor( } private fun makeDefaultAlert() { - uiState = stateBuilder.addAlert(uiState) { - uiState = stateBuilder.clearAlert(uiState) - } + uiState = stateBuilder.addAlert(uiState) } private fun makeDefaultAlert(message: TextReference) { - uiState = stateBuilder.addAlert(uiState, message) { - uiState = stateBuilder.clearAlert(uiState) - } + uiState = stateBuilder.addAlert(uiState, message) } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1130,7 +1147,8 @@ internal class SwapViewModel @Inject constructor( toToken.currency.id.value } - return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList() + return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers + ?: emptyList() } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { @@ -1218,6 +1236,33 @@ internal class SwapViewModel @Inject constructor( analyticsEventHandler.send(event = event) } + private fun onFailedTxEmailClick(errorMessage: String) { + viewModelScope.launch { + val network = initialCryptoCurrencyStatus.currency.network + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } + + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage, + blockchainId = network.id.value, + derivationPath = network.derivationPath.value, + destinationAddress = dataState.swapDataModel?.transaction?.txTo.orEmpty(), + tokenSymbol = initialCryptoCurrency.symbol, + amount = dataState.amount.orEmpty(), + fee = dataState.selectedFee?.feeCryptoFormatted.orEmpty(), + ), + ) + + val email = FeedbackEmailType.SwapProblem( + cardInfo = cardInfo, + providerName = dataState.selectedProvider?.name.orEmpty(), + txId = dataState.swapDataModel?.transaction?.txId.orEmpty(), + ) + + feedbackManager.sendEmail(email) + } + } + private companion object { const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L