diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt index bdc2fdb4f5..f347f480b2 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -1,9 +1,12 @@ package com.tangem.data.pay.datasource import arrow.core.Either +import arrow.core.left import arrow.core.raise.either import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.pay.model.WithdrawalSignatureResult import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.TangemPayInitialCredentials @@ -28,12 +31,21 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( .bind() } - override suspend fun getWithdrawalSignature(cardId: String, hash: String): Either { + override suspend fun getWithdrawalSignature( + cardId: String, + hash: String, + ): Either { return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) { - is CompletionResult.Failure<*> -> Either.Left( - IllegalStateException("TangemPay sign hash failed: ${signResult.error}"), - ) - is CompletionResult.Success -> Either.Right(signResult.data) + is CompletionResult.Failure<*> -> { + if (signResult.error is TangemSdkError.UserCancelled) { + Either.Right(WithdrawalSignatureResult.Cancelled) + } else { + signResult.error.left() + } + } + is CompletionResult.Success -> { + Either.Right(WithdrawalSignatureResult.Success(signResult.data)) + } } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt index 1b6c73d66d..78dec8de97 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt @@ -11,7 +11,9 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.pay.model.WithdrawalSignatureResult import com.tangem.domain.pay.repository.TangemPaySwapRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -39,7 +41,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, - ): Either { + ): Either { val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) return requestHelper.makeSafeRequest(userWalletId) { authHeader -> @@ -48,25 +50,35 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( }.map { data -> val result = data.result if (result == null) return Either.Left(VisaApiError.WithdrawalDataError) - val signature = authDataSource.getWithdrawalSignature(cardId = getCardId(userWalletId), hash = result.hash) - .getOrNull() - if (signature == null) return Either.Left(VisaApiError.SignWithdrawError) + val signatureResult = authDataSource.getWithdrawalSignature( + cardId = getCardId(userWalletId), + hash = result.hash, + ).getOrNull() - requestHelper.makeSafeRequest(userWalletId) { authHeader -> - val request = WithdrawRequest( - amountInCents = amountInCents, - recipientAddress = receiverAddress, - adminSalt = result.salt, - senderAddress = result.senderAddress, - adminSignature = signature, - ) - tangemPayApi.withdraw(authHeader = authHeader, body = request) - } - .mapLeft { return Either.Left(VisaApiError.WithdrawError) } - .map { response -> - val orderId = response.result?.orderId - if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId) + return when (signatureResult) { + is WithdrawalSignatureResult.Cancelled -> { + Either.Right(WithdrawalResult.Cancelled) } + is WithdrawalSignatureResult.Success -> { + requestHelper.makeSafeRequest(userWalletId) { authHeader -> + val request = WithdrawRequest( + amountInCents = amountInCents, + recipientAddress = receiverAddress, + adminSalt = result.salt, + senderAddress = result.senderAddress, + adminSignature = signatureResult.signature, + ) + tangemPayApi.withdraw(authHeader = authHeader, body = request) + } + .mapLeft { return Either.Left(VisaApiError.WithdrawError) } + .map { response -> + val orderId = response.result?.orderId + if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId) + WithdrawalResult.Success + } + } + null -> return Either.Left(VisaApiError.SignWithdrawError) + } } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt index a18dfc3b7e..3ccde5d629 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.repository.TangemPaySwapRepository import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import java.math.BigDecimal @@ -18,7 +19,7 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor( cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, - ): Either { + ): Either { return repository.withdraw( userWalletId = userWalletId, cryptoAmount = cryptoAmount, diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 75eb04a10a..5786ee9dac 100644 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -72,5 +72,11 @@ sealed interface FeedbackEmailType { val item: TangemPayTxHistoryItem, override val walletMetaInfo: WalletMetaInfo, ) : Visa() + + data class Withdrawal( + override val walletMetaInfo: WalletMetaInfo, + val providerName: String, + val txId: String, + ) : Visa() } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 97cf3eac5d..04e4d5db8f 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -74,6 +74,7 @@ class SendFeedbackEmailUseCase( is FeedbackEmailType.Visa.Activation, is FeedbackEmailType.Visa.DirectUserRequest, is FeedbackEmailType.Visa.FailedIssueCard, + is FeedbackEmailType.Visa.Withdrawal, -> { append(resources.getStringSafe(R.string.feedback_data_collection_message)) skipLine() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 1984abe218..12bd80d66a 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -37,6 +37,7 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.Visa.FailedIssueCard -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails) is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayRequestBody(type.walletMetaInfo, type.item) + is FeedbackEmailType.Visa.Withdrawal -> addTangemPayWithdrawalRequestBody(type) } return build() @@ -51,6 +52,33 @@ internal class EmailMessageBodyResolver( addTangemPayTxInfo(item) } + private suspend fun FeedbackDataBuilder.addTangemPayWithdrawalRequestBody( + type: FeedbackEmailType.Visa.Withdrawal, + ) { + addUserWalletMetaInfo(type.walletMetaInfo) + addDelimiter() + + val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" } + val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) + val blockchainInfo = blockchainError?.let { + feedbackRepository.getBlockchainInfo( + userWalletId = userWalletId, + blockchainId = blockchainError.blockchainId, + derivationPath = blockchainError.derivationPath, + ) + } + + if (blockchainInfo != null) { + addBlockchainError(blockchainInfo, blockchainError) + addDelimiter() + } + + addSwapInfo(providerName = type.providerName, txId = type.txId) + addDelimiter() + + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + private suspend fun FeedbackDataBuilder.addVisaRequestBody( walletMetaInfo: WalletMetaInfo, visaTxDetails: VisaTxDetails, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 349abcde4b..7d42928a99 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -25,6 +25,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.Visa.Dispute, is FeedbackEmailType.Visa.DisputeV2, is FeedbackEmailType.Visa.FailedIssueCard, + is FeedbackEmailType.Visa.Withdrawal, -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index c12948f861..6e01c9989f 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -43,6 +43,8 @@ internal class EmailSubjectResolver(private val resources: Resources) { is FeedbackEmailType.Visa.Dispute, is FeedbackEmailType.Visa.DisputeV2, -> "[Visa] [DISPUTE] {auto-filled subject}" + is FeedbackEmailType.Visa.Withdrawal, + -> "[Visa] [WITHDRAWAL] {auto-filled subject}" } } } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalResult.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalResult.kt new file mode 100644 index 0000000000..a6a7f4b528 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalResult.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.pay + +enum class WithdrawalResult { + Cancelled, Success +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt index 93748e8365..0ff292096a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt @@ -1,6 +1,7 @@ package com.tangem.domain.pay.datasource import arrow.core.Either +import com.tangem.domain.pay.model.WithdrawalSignatureResult import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.TangemPayInitialCredentials @@ -10,5 +11,5 @@ interface TangemPayAuthDataSource { suspend fun refreshAuthTokens(refreshToken: String): Either - suspend fun getWithdrawalSignature(cardId: String, hash: String): Either + suspend fun getWithdrawalSignature(cardId: String, hash: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt new file mode 100644 index 0000000000..42f55abd15 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay.model + +sealed class WithdrawalSignatureResult { + + data class Success(val signature: String) : WithdrawalSignatureResult() + + data object Cancelled : WithdrawalSignatureResult() +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt index c0cbbac415..d27ac30af7 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal interface TangemPaySwapRepository { @@ -13,5 +14,5 @@ interface TangemPaySwapRepository { receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, - ): Either + ): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt index a7d5c6d02c..910f50090c 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal interface TangemPayWithdrawUseCase { @@ -13,5 +14,5 @@ interface TangemPayWithdrawUseCase { cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, - ): Either + ): Either } \ No newline at end of file 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 2d46805dd4..7ad787a146 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 @@ -55,5 +55,7 @@ sealed class SwapTransactionState { data class ExpressError(val error: ExpressDataError) : Error() data object UnknownError : Error() + + data class TangemPayWithdrawalError(val txId: String) : Error() } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt index ad158cc32e..646e3a3218 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt @@ -28,6 +28,9 @@ internal class SwapTransactionErrorStateConverter( ) } SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss) + is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.GenericError( + onConfirmClick = { onSupportClick(value.txId) }, + ) } } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 78fee3f6f0..ffa4642c46 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -38,6 +38,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -806,31 +807,38 @@ internal class SwapModel @Inject constructor( ) .onLeft { startLoadingQuotesFromLastState() - makeDefaultAlert() + onTangemPayWithdrawalError(swapTransactionState.storeData.txExternalId) } - .onRight { - val txUrl = swapTransactionState.storeData.txExternalUrl - swapInteractor.storeSwapTransaction( - currencyToSend = swapTransactionState.storeData.currencyToSend, - currencyToGet = swapTransactionState.storeData.currencyToGet, - fromAccount = swapTransactionState.storeData.fromAccount, - toAccount = swapTransactionState.storeData.toAccount, - amount = swapTransactionState.storeData.amount, - swapProvider = swapTransactionState.storeData.swapProvider, - swapDataModel = swapTransactionState.storeData.swapDataModel, - txExternalUrl = txUrl, - timestamp = System.currentTimeMillis(), - txExternalId = swapTransactionState.storeData.txExternalId, - averageDuration = null, - ) - uiState = stateBuilder.createTangemPayWithdrawalSuccessState( - uiState = uiState, - swapTransactionState = swapTransactionState, - dataState = dataState, - txUrl = txUrl.orEmpty(), - onExploreClick = { if (txUrl != null) urlOpener.openUrl(txUrl) }, - ) - swapRouter.openScreen(SwapNavScreen.Success) + .onRight { result: WithdrawalResult -> + when (result) { + WithdrawalResult.Cancelled -> { + startLoadingQuotesFromLastState() + } + WithdrawalResult.Success -> { + val txUrl = swapTransactionState.storeData.txExternalUrl + swapInteractor.storeSwapTransaction( + currencyToSend = swapTransactionState.storeData.currencyToSend, + currencyToGet = swapTransactionState.storeData.currencyToGet, + fromAccount = swapTransactionState.storeData.fromAccount, + toAccount = swapTransactionState.storeData.toAccount, + amount = swapTransactionState.storeData.amount, + swapProvider = swapTransactionState.storeData.swapProvider, + swapDataModel = swapTransactionState.storeData.swapDataModel, + txExternalUrl = txUrl, + timestamp = System.currentTimeMillis(), + txExternalId = swapTransactionState.storeData.txExternalId, + averageDuration = null, + ) + uiState = stateBuilder.createTangemPayWithdrawalSuccessState( + uiState = uiState, + swapTransactionState = swapTransactionState, + dataState = dataState, + txUrl = txUrl.orEmpty(), + onExploreClick = { if (txUrl != null) urlOpener.openUrl(txUrl) }, + ) + swapRouter.openScreen(SwapNavScreen.Success) + } + } } } @@ -1627,6 +1635,29 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(event = event) } + private fun onTangemPayWithdrawalError(txId: String?) { + uiState = stateBuilder.createErrorTransactionAlert( + uiState = uiState, + error = SwapTransactionState.Error.TangemPayWithdrawalError(txId.orEmpty()), + onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, + onSupportClick = ::onTangemPaySupportClick, + isReverseSwapPossible = isReverseSwapPossible(), + ) + } + + private fun onTangemPaySupportClick(txId: String?) { + modelScope.launch { + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId) + .getOrElse { error("CardInfo must be not null") } + val email = FeedbackEmailType.Visa.Withdrawal( + walletMetaInfo = metaInfo, + providerName = dataState.selectedProvider?.name.orEmpty(), + txId = txId.orEmpty(), + ) + sendFeedbackEmailUseCase(email) + } + } + private fun onFailedTxEmailClick(errorMessage: String) { modelScope.launch { val transaction = dataState.swapDataModel?.transaction