Updated on 2026-08-14
This commit is contained in:
parent
b210d3b7fd
commit
c9804fa0ed
16 changed files with 166 additions and 51 deletions
|
|
@ -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<Throwable, String> {
|
||||
override suspend fun getWithdrawalSignature(
|
||||
cardId: String,
|
||||
hash: String,
|
||||
): Either<Throwable, WithdrawalSignatureResult> {
|
||||
return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) {
|
||||
is CompletionResult.Failure<*> -> Either.Left(
|
||||
IllegalStateException("TangemPay sign hash failed: ${signResult.error}"),
|
||||
)
|
||||
is CompletionResult.Success<String> -> Either.Right(signResult.data)
|
||||
is CompletionResult.Failure<*> -> {
|
||||
if (signResult.error is TangemSdkError.UserCancelled) {
|
||||
Either.Right(WithdrawalSignatureResult.Cancelled)
|
||||
} else {
|
||||
signResult.error.left()
|
||||
}
|
||||
}
|
||||
is CompletionResult.Success<String> -> {
|
||||
Either.Right(WithdrawalSignatureResult.Success(signResult.data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UniversalError, Unit> {
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UniversalError, Unit> {
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
return repository.withdraw(
|
||||
userWalletId = userWalletId,
|
||||
cryptoAmount = cryptoAmount,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.pay
|
||||
|
||||
enum class WithdrawalResult {
|
||||
Cancelled, Success
|
||||
}
|
||||
|
|
@ -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<Throwable, TangemPayAuthTokens>
|
||||
|
||||
suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, String>
|
||||
suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, WithdrawalSignatureResult>
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
sealed class WithdrawalSignatureResult {
|
||||
|
||||
data class Success(val signature: String) : WithdrawalSignatureResult()
|
||||
|
||||
data object Cancelled : WithdrawalSignatureResult()
|
||||
}
|
||||
|
|
@ -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<UniversalError, Unit>
|
||||
): Either<UniversalError, WithdrawalResult>
|
||||
}
|
||||
|
|
@ -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<UniversalError, Unit>
|
||||
): Either<UniversalError, WithdrawalResult>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,9 @@ internal class SwapTransactionErrorStateConverter(
|
|||
)
|
||||
}
|
||||
SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss)
|
||||
is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.GenericError(
|
||||
onConfirmClick = { onSupportClick(value.txId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue