diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 81d40b5307..9ad974cdcd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.legacy import com.tangem.domain.redux.LegacyAction import com.tangem.tap.common.feedback.RateCanBeBetterEmail +import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store @@ -20,6 +21,11 @@ internal object LegacyMiddleware { GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup), ) } + is LegacyAction.SendEmailTransactionFailed -> { + store.state.globalState.feedbackManager?.sendEmail( + SendTransactionFailedEmail(action.errorMessage), + ) + } } next(action) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 5429ab3030..fdd81bd7d9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -31,13 +31,13 @@ internal object TransactionDomainModule { @ViewModelScoped fun provideSendTransactionUseCase( isDemoCardUseCase: IsDemoCardUseCase, - walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, ): SendTransactionUseCase { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade, + transactionRepository = transactionRepository, ) } diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 12777954f8..dc64c4dac6 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -136,6 +136,7 @@ Перевод Я понял Недоступно + Произошла ошибка. Пожалуйста, попробуйте снова. Да Адрес контракта скопирован! Доступные сети @@ -448,8 +449,9 @@ Сумма Вычесть из суммы отправки Сумма к получению %s + Поддержка Транзакция не выполнена - Причина: %1$s\Код:%2$s + Причина: %1$s\nКод: %2$s %1$s в %2$s Адрес Код назначения @@ -528,7 +530,6 @@ Подтвердить Ошибка: %s Вы отправляете - Произошла ошибка. Пожалуйста, попробуйте еще раз. Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index c7ef19cf31..a1d2ee341a 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -327,7 +327,6 @@ 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 批准 錯誤: %s - 有錯誤。請再試一遍 賦予權限 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 585432c7dd..b0bd11e551 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -135,6 +135,7 @@ Transfer I understand Unreachable + There was an error. Please try again. Yes Contract address copied! Available networks @@ -447,8 +448,9 @@ Amount Subtract from send amount The recipient will receive %s + Support The transaction is not completed - Reason: %1$s\nCode:%2$s + Reason: %1$s\nCode: %2$s Confirm %1$s at %2$s Address @@ -519,6 +521,7 @@ Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Invalid address Transaction sent + %s (%s) Buy now I have a promo code… Tangem Wallet @@ -545,7 +548,6 @@ Approve Error: %s You swap - There was an error. Please try again. Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index e5cf1cec6c..7c515253c2 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionExtras +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository @@ -44,6 +41,21 @@ internal class DefaultTransactionRepository( ) } + override suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + (walletManager as TransactionSender).send(txData, signer) + } + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) if (memo == null) return null diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index f715061ac7..826ba5a966 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -13,4 +13,9 @@ sealed interface LegacyAction : Action { * BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup */ data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction + + /** + * Sending an email to support when sending transaction failed + */ + data class SendEmailTransactionFailed(val errorMessage: String) : LegacyAction } \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 571b6c0f50..f0345df888 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -13,9 +13,13 @@ dependencies { implementation(deps.arrow.core) implementation(projects.core.utils) + implementation(projects.core.ui) /** Tangem SDKs */ implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { + exclude(module = "joda-time") + } implementation(deps.tangem.blockchain) implementation(projects.domain.models) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 6c38bc0e7d..70c8b7d0ae 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -1,8 +1,10 @@ package com.tangem.domain.transaction import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.CommonSigner import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -17,4 +19,11 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): TransactionData? + + suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt index 8ed9705902..2d5536cefb 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -1,5 +1,7 @@ package com.tangem.domain.transaction.error +import com.tangem.core.ui.extensions.TextReference + sealed class SendTransactionError { object DemoCardError : SendTransactionError() @@ -8,9 +10,12 @@ sealed class SendTransactionError { data class NetworkError(val message: String?) : SendTransactionError() - data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError() + object UserCancelledError : SendTransactionError() - data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + + data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError() + data class UnknownError(val ex: Exception? = null) : SendTransactionError() companion object { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 1e710033ae..c8ed1fe905 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -8,19 +8,23 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.SimpleResult 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 import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.R +import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.sdk.extensions.localizedDescriptionRes class SendTransactionUseCase( private val isDemoCardUseCase: IsDemoCardUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, - private val walletManagersFacade: WalletManagersFacade, + private val transactionRepository: TransactionRepository, ) { suspend operator fun invoke( txData: TransactionData, @@ -37,7 +41,7 @@ class SendTransactionUseCase( if (isDemoCardUseCase(cardId = userWallet.cardId)) { SendTransactionError.DemoCardError.left() } else { - walletManagersFacade.sendTransaction( + transactionRepository.sendTransaction( txData = txData, signer = signer, userWalletId = userWallet.walletId, @@ -64,29 +68,28 @@ class SendTransactionUseCase( private fun handleError(result: SimpleResult.Failure): SendTransactionError { if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message) val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() - when (error) { + return when (error) { is BlockchainSdkError.WrappedTangemError -> { - val errorByCode = mapErrorByCode(error) - if (errorByCode != null) { - return errorByCode + if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + val tangemError = error.tangemError + if (tangemError is TangemSdkError) { + 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) + } else { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } } - val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError() - if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError - return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause) } else -> { - return SendTransactionError.TangemSdkError(error.code, error.cause) - } - } - } - - private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? { - return when (error.code) { - USER_CANCELLED_ERROR_CODE -> { - return SendTransactionError.UserCancelledError - } - else -> { - null + SendTransactionError.BlockchainSdkError( + code = error.code, + message = error.customMessage, + ) } } } diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 53a6d8a573..46295abadb 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(deps.lifecycle.compose) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.reKotlin) /** Compose */ implementation(deps.compose.accompanist.systemUiController) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt new file mode 100644 index 0000000000..ff2aac684f --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -0,0 +1,47 @@ +package com.tangem.features.send.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.core.ui.extensions.wrappedList +import com.tangem.features.send.impl.R + +@Immutable +internal sealed class SendAlertState { + + 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? = resourceReference(id = R.string.send_alert_transaction_failed_title), + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.send_alert_button_request_support) + } + + data class TransactionError( + val code: String, + val cause: String?, + val causeTextReference: TextReference? = null, + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + 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.send_alert_button_request_support) + } + + data class DemoMode( + override val onConfirmClick: () -> Unit, + ) : SendAlertState() { + override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) + override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt new file mode 100644 index 0000000000..2fdb687d96 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class SendEvent { + + data class ShowSnackBar(val text: TextReference) : SendEvent() + + data class ShowAlert(val alert: SendAlertState) : SendEvent() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt new file mode 100644 index 0000000000..8bd869cb7c --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -0,0 +1,50 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider + +/** + * Factory to produce event state for [SendUiState] + * + * @param currentStateProvider [Provider] of [SendUiState] + * @param clickIntents [SendClickIntents] + */ +internal class SendEventStateFactory( + val currentStateProvider: Provider, + val clickIntents: SendClickIntents, +) { + private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) } + + fun onConsumeEventState(): SendUiState { + return currentStateProvider().copy(event = consumedEvent()) + } + + fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + val event = error?.let { + sendTransactionErrorConverter.convert(error)?.let { + triggeredEvent(SendEvent.ShowAlert(it), onConsume) + } + } + return state.copy( + event = event ?: consumedEvent(), + ) + } + + fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + return state.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert( + SendAlertState.GenericError( + onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) }, + ), + ), + onConsume = onConsume, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 70fac6ad0d..4fea850423 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -44,7 +45,6 @@ internal class SendStateFactory( ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } private val customFeeFieldConverter by lazy { @@ -92,6 +92,7 @@ internal class SendStateFactory( fun getInitialState(): SendUiState = SendUiState( clickIntents = clickIntents, currentState = MutableStateFlow(SendUiStateType.Amount), + event = consumedEvent(), ) fun getReadyState(): SendUiState { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt new file mode 100644 index 0000000000..14f9cc265d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter + +internal class SendTransactionAlertConverter( + private val clickIntents: SendClickIntents, +) : Converter { + override fun convert(value: SendTransactionError): SendAlertState? { + return when (value) { + SendTransactionError.DemoCardError -> SendAlertState.DemoMode( + onConfirmClick = { clickIntents.popBackStack() }, + ) + is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError( + code = value.code.toString(), + cause = null, + causeTextReference = value.messageReference, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError( + code = value.code.toString(), + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> SendAlertState.TransactionError( + code = "", + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> SendAlertState.TransactionError( + code = "", + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> SendAlertState.TransactionError( + code = "", + cause = value.ex?.localizedMessage, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, + ) + else -> null + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index fb2918c11d..3db22025d6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable import androidx.paging.PagingData import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.event.StateEvent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent @@ -31,6 +32,7 @@ internal data class SendUiState( val sendState: SendStates.SendState = SendStates.SendState(), val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), val currentState: MutableStateFlow, + val event: StateEvent, ) @Stable diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt new file mode 100644 index 0000000000..84e638eb8a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -0,0 +1,73 @@ +package com.tangem.features.send.impl.presentation.ui + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +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.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendAlertState +import com.tangem.features.send.impl.presentation.state.SendEvent + +@Composable +internal fun SendEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { + val resources = LocalContext.current.resources + var alertConfig by remember { mutableStateOf(value = null) } + + alertConfig?.let { + SendAlert(state = it, onDismiss = { alertConfig = null }) + } + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is SendEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is SendEvent.ShowAlert -> { + alertConfig = value.alert + } + } + }, + ) +} + +@Composable +internal fun SendAlert(state: SendAlertState, 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, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index eae5388921..8fe5c5f53d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -28,6 +30,7 @@ import com.tangem.features.send.impl.presentation.ui.send.SendContent internal fun SendScreen(uiState: SendUiState) { val currentState = uiState.currentState.collectAsStateWithLifecycle() val isSuccess = uiState.sendState.isSuccess + val snackbarHostState = remember { SnackbarHostState() } BackHandler { uiState.clickIntents.onBackClick() } Column( modifier = Modifier @@ -65,6 +68,11 @@ internal fun SendScreen(uiState: SendUiState) { ) SendNavigationButtons(uiState) } + + SendEventEffect( + event = uiState.event, + snackbarHostState = snackbarHostState, + ) } @Composable diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index f79bf242b5..43a116ee65 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.impl.presentation.state.fee.FeeType +@Suppress("TooManyFunctions") interface SendClickIntents { fun popBackStack() @@ -16,6 +17,8 @@ interface SendClickIntents { fun onQrCodeScanClick() + fun onFailedTxEmailClick(errorMessage: String) + fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) // region Amount diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 133e865e26..d4861a2e74 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -8,8 +8,11 @@ import androidx.paging.PagingData import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase @@ -31,10 +34,6 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.SendNotificationFactory -import com.tangem.features.send.impl.presentation.state.SendStateFactory -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fee.getFee @@ -70,6 +69,7 @@ internal class SendViewModel @Inject constructor( private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, + private val reduxStateHolder: ReduxStateHolder, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, savedStateHandle: SavedStateHandle, @@ -98,6 +98,11 @@ internal class SendViewModel @Inject constructor( getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, ) + private val eventStateFactory = SendEventStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + ) + private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, @@ -106,6 +111,7 @@ internal class SendViewModel @Inject constructor( walletManagersFacade = walletManagersFacade, ) + // todo convert to StateFlow var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) private set @@ -139,7 +145,9 @@ internal class SendViewModel @Inject constructor( getCurrenciesStatusUpdates(owner, wallet) }, ifLeft = { - // todo add error handling [[REDACTED_JIRA]] + uiState = eventStateFactory.getGenericErrorState( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) return@launch }, ) @@ -331,6 +339,10 @@ internal class SendViewModel @Inject constructor( override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name) + override fun onFailedTxEmailClick(errorMessage: String) { + reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage)) + } + override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) = innerRouter.openTokenDetails(userWalletId, currency) // endregion @@ -433,8 +445,8 @@ internal class SendViewModel @Inject constructor( val sendState = uiState.sendState if (sendState.isSuccess) popBackStack() - uiState = stateFactory.getSendingStateUpdate(true) - viewModelScope.launch(dispatchers.io) { verifyAndSendTransaction() } + uiState = stateFactory.getSendingStateUpdate(isSending = true) + verifyAndSendTransaction() } override fun showAmount() = stateRouter.showAmount(isFromSend = true) @@ -445,7 +457,7 @@ internal class SendViewModel @Inject constructor( override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl) - private suspend fun verifyAndSendTransaction() { + private fun verifyAndSendTransaction() { val recipient = uiState.recipientState?.addressTextField?.value ?: return val feeState = uiState.feeState ?: return val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return @@ -454,41 +466,46 @@ internal class SendViewModel @Inject constructor( val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency) - // todo add error handling [[REDACTED_JIRA]] - // val transactionErrors = walletManagersFacade.validateTransaction( - // amount = amountToSend, - // fee = fee.amount, - // userWalletId = userWalletId, - // network = cryptoCurrency.network, - // ) + viewModelScope.launch(dispatchers.main) { + createTransactionUseCase( + amount = amountToSend, + fee = fee, + memo = memo, + destination = recipient, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + Timber.e(it) + uiState = eventStateFactory.getGenericErrorState( + error = it, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ifRight = { txData -> + sendTransaction(txData) + }, + ) + } + } - createTransactionUseCase( - amount = amountToSend, - fee = fee, - memo = memo, - destination = recipient, - userWalletId = userWalletId, + private suspend fun sendTransaction(txData: TransactionData) { + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, network = cryptoCurrency.network, ).fold( - ifLeft = { - Timber.e(it) - // todo add error handling [[REDACTED_JIRA]] - }, - ifRight = { txData -> - sendTransactionUseCase( - txData = txData, - userWallet = userWallet, - network = cryptoCurrency.network, - ).fold( - ifLeft = { - uiState = stateFactory.getSendingStateUpdate(false) - // todo add error handling [[REDACTED_JIRA]] - }, - ifRight = { - uiState = stateFactory.getTransactionSendState(txData) - }, + ifLeft = { error -> + uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState = eventStateFactory.getSendTransactionErrorState( + error = error, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) }, + ifRight = { + uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState = stateFactory.getTransactionSendState(txData) + }, ) } // endregion diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index d299f7824e..bfa910c669 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder @@ -118,13 +119,13 @@ class SwapDomainModule { @Singleton fun provideSendTransactionUseCase( @SwapScope isDemoCardUseCase: IsDemoCardUseCase, - walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, ): SendTransactionUseCase { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade, + transactionRepository = transactionRepository, ) } 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 d0d611881d..90e2f34c73 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 @@ -553,7 +553,7 @@ internal class StateBuilder( resourceReference(R.string.warning_express_refresh_required_title) }, subtitle = if (dataError is DataError.UnknownError) { - resourceReference(R.string.swapping_generic_error) + resourceReference(R.string.common_unknown_error) } else { resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) }, 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 bb85650ace..3b42cf78a6 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 @@ -109,7 +109,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi val message = if (state.alert.type == GenericWarningType.NETWORK) { stringResource(id = R.string.disclaimer_error_loading) } else { - state.alert.message?.resolveReference() ?: stringResource(id = R.string.swapping_generic_error) + state.alert.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error) } SimpleOkDialog( message = message, @@ -338,7 +338,7 @@ private fun SwapWarnings(warnings: List) { } else { it.resolveReference() } - } ?: stringResource(id = R.string.swapping_generic_error) + } ?: stringResource(id = R.string.common_unknown_error) RefreshableWaringCard( title = stringResource(id = R.string.common_warning), description = message, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 2fcde13f2c..3d15ad3489 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ spr-client = "3.6.2" # region Tangem tangemBlockchainSdk = "develop-458" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-318" +tangemCardSdk = "develop-319" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem