Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-28 18:13:46 +04:00
parent 9163a956aa
commit b70933a1e0
47 changed files with 848 additions and 74 deletions

View file

@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.datasource.local.visa.TangemPayStorage
@ -19,6 +20,7 @@ import javax.inject.Singleton
private const val DEFAULT_KEY = "tangem_pay_default_key" private const val DEFAULT_KEY = "tangem_pay_default_key"
private const val ORDER_ID_KEY = "tangem_pay_order_id_key" private const val ORDER_ID_KEY = "tangem_pay_order_id_key"
private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key"
@Singleton @Singleton
internal class DefaultTangemPayStorage @Inject constructor( internal class DefaultTangemPayStorage @Inject constructor(
@ -114,9 +116,38 @@ internal class DefaultTangemPayStorage @Inject constructor(
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
} }
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
value = orders,
)
}
}
override suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? {
val orders = appPreferencesStore.getObjectMapSync<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
return orders[createWithdrawOrderIdKey(userWalletId)]
}
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
.minus(createWithdrawOrderIdKey(userWalletId))
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
value = orders,
)
}
}
private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue
private fun createKey(address: String): String = "${DEFAULT_KEY}_$address" private fun createKey(address: String): String = "${DEFAULT_KEY}_$address"
private fun createOrderIdKey(address: String): String = "${ORDER_ID_KEY}_$address" private fun createOrderIdKey(address: String): String = "${ORDER_ID_KEY}_$address"
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"
} }

View file

@ -11,6 +11,7 @@ import com.tangem.common.authentication.AuthenticationManager
import com.tangem.common.authentication.keystore.KeystoreManager import com.tangem.common.authentication.keystore.KeystoreManager
import com.tangem.common.core.* import com.tangem.common.core.*
import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.services.secure.SecureStorage import com.tangem.common.services.secure.SecureStorage
import com.tangem.common.usersCode.UserCodeRepository import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
@ -45,6 +46,7 @@ import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
@ -524,6 +526,15 @@ internal class DefaultTangemSdkManager(
} }
} }
override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String> {
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = TangemPaySignWithdrawalHashTask(cardId = cardId, hash = hash.hexToBytes()),
cardId = cardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
}
// endregion // endregion
companion object { companion object {

View file

@ -219,5 +219,9 @@ class MockTangemSdkManager(
error("Not implemented") error("Not implemented")
} }
override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String> {
error("Not implemented")
}
// endregion // endregion
} }

View file

@ -0,0 +1,121 @@
package com.tangem.tap.domain.tasks.visa
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
class TangemPaySignWithdrawalHashTask(
private val cardId: String,
private val hash: ByteArray,
) : CardSessionRunnable<String> {
override fun run(session: CardSession, callback: CompletionCallback<String>) {
val card = session.environment.card ?: run {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
if (card.cardId != cardId) {
callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError))
return
}
proceedSign(card, session, callback)
}
private fun proceedSign(card: Card, session: CardSession, callback: CompletionCallback<String>) {
val derivationPath = VisaUtilities.customDerivationPath
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError))
return
}
val derivationTask = DeriveWalletPublicKeyTask(
walletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
)
derivationTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
signData(
targetWalletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
session = session,
extendedPublicKey = result.data,
callback = callback,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun signData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
extendedPublicKey: ExtendedPublicKey?,
session: CardSession,
callback: CompletionCallback<String>,
) {
val signTask = SignHashCommand(
hash = hash,
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = result.data.signature,
hash = hash,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
scanCard(
session = session,
callback = callback,
signedData = rsvSignature,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun scanCard(signedData: String, session: CardSession, callback: CompletionCallback<String>) {
val scanTask = ScanTask()
scanTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(signedData))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -328,6 +328,7 @@ internal class ChildFactory @Inject constructor(
cryptoAmount = tangemPayInput.cryptoAmount, cryptoAmount = tangemPayInput.cryptoAmount,
fiatAmount = tangemPayInput.fiatAmount, fiatAmount = tangemPayInput.fiatAmount,
depositAddress = tangemPayInput.depositAddress, depositAddress = tangemPayInput.depositAddress,
isWithdrawal = tangemPayInput.isWithdrawal,
) )
}, },
), ),

View file

@ -197,6 +197,7 @@ sealed class AppRoute(val path: String) : Route {
val cryptoAmount: SerializedBigDecimal, val cryptoAmount: SerializedBigDecimal,
val fiatAmount: SerializedBigDecimal, val fiatAmount: SerializedBigDecimal,
val depositAddress: String, val depositAddress: String,
val isWithdrawal: Boolean,
) )
} }

View file

@ -166,4 +166,16 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String, @Header("Authorization") authHeader: String,
@Body body: FreezeUnfreezeCardRequest, @Body body: FreezeUnfreezeCardRequest,
): ApiResponse<FreezeUnfreezeCardResponse> ): ApiResponse<FreezeUnfreezeCardResponse>
@POST("v1/customer/card/withdraw/data")
suspend fun getWithdrawData(
@Header("Authorization") authHeader: String,
@Body body: WithdrawDataRequest,
): ApiResponse<WithdrawDataResponse>
@POST("v1/customer/card/withdraw")
suspend fun withdraw(
@Header("Authorization") authHeader: String,
@Body body: WithdrawRequest,
): ApiResponse<WithdrawResponse>
} }

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawDataRequest(
@Json(name = "amount_in_cents") val amountInCents: String,
@Json(name = "recipient_address") val recipientAddress: String,
)

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawRequest(
@Json(name = "amount_in_cents") val amountInCents: String,
@Json(name = "recipient_address") val recipientAddress: String,
@Json(name = "admin_signature") val adminSignature: String,
@Json(name = "admin_salt") val adminSalt: String,
@Json(name = "sender_address") val senderAddress: String,
)

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawDataResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "hash") val hash: String,
@Json(name = "salt") val salt: String,
@Json(name = "sender_address") val senderAddress: String,
)
}

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "order_id") val orderId: String,
@Json(name = "status") val status: String,
@Json(name = "type") val type: String,
)
}

View file

@ -145,6 +145,8 @@ object PreferencesKeys {
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
} }
val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") }
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
// endregion // endregion

View file

@ -24,5 +24,11 @@ interface TangemPayStorage {
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId) suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId)
suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean?
suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String)
suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String?
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId)
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
} }

View file

@ -32,6 +32,7 @@
<string name="account_details_archive_description">You are archiving this account, but you can always get it back.</string> <string name="account_details_archive_description">You are archiving this account, but you can always get it back.</string>
<string name="account_details_archiving">Archiving...</string> <string name="account_details_archiving">Archiving...</string>
<string name="account_details_title">Account</string> <string name="account_details_title">Account</string>
<string name="account_edit_failure_dialog_title">Cant edit the account</string>
<string name="account_edit_success_message">Account saved</string> <string name="account_edit_success_message">Account saved</string>
<string name="account_for_rewards">Account for rewards</string> <string name="account_for_rewards">Account for rewards</string>
<string name="account_form_account_index">Account #%s — used for address derivation.</string> <string name="account_form_account_index">Account #%s — used for address derivation.</string>
@ -444,7 +445,7 @@
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string> <string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of service</string> <string name="disclaimer_title">Terms of service</string>
<string name="domain_receive_assets_default_address">Default Address</string> <string name="domain_receive_assets_default_address">Default Address</string>
<string name="domain_receive_assets_legacy_address">Legacy Address</string> <string name="domain_receive_assets_legacy_address">Legacy %s Address</string>
<string name="domain_receive_assets_navigation_title">Receive assets</string> <string name="domain_receive_assets_navigation_title">Receive assets</string>
<string name="domain_receive_assets_network_name_address">%s address</string> <string name="domain_receive_assets_network_name_address">%s address</string>
<string name="domain_receive_assets_onboarding_description">Sending assets in other networks will result in permanent loss.</string> <string name="domain_receive_assets_onboarding_description">Sending assets in other networks will result in permanent loss.</string>
@ -1441,9 +1442,21 @@
<string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string> <string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string>
<string name="tangempay_card_details_title">Card details</string> <string name="tangempay_card_details_title">Card details</string>
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string> <string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
<string name="tangempay_card_details_withdraw">Withdraw</string>
<string name="tangempay_card_details_withdraw_error_title">Withdraw unavailable now</string>
<string name="tangempay_card_details_withdraw_in_progress_description">You can\'t initiate swap or new withdrawal till the current one is finished</string>
<string name="tangempay_card_details_withdraw_in_progress_title">Withdrawal in progress</string>
<string name="tangempay_failed_to_issue_card">Failed to issue card</string>
<string name="tangempay_failed_to_issue_card_retry_description">A technical error has occurred, please try again by clicking the button below.</string>
<string name="tangempay_failed_to_issue_card_support_description">A technical error has occurred, please contact support.</string>
<string name="tangempay_issue_card_notification_description">It usually takes up to 15 minutes</string> <string name="tangempay_issue_card_notification_description">It usually takes up to 15 minutes</string>
<string name="tangempay_issue_card_notification_title">Setting up your Tangem Card</string> <string name="tangempay_issue_card_notification_title">Setting up your Tangem Card</string>
<string name="tangempay_issuing_your_card">Issuing your card</string>
<string name="tangempay_issuing_your_card_description">Were getting your card ready. This may take a little time.</string>
<string name="tangempay_kyc_card_ready_notification_title">Tangem Pay</string> <string name="tangempay_kyc_card_ready_notification_title">Tangem Pay</string>
<string name="tangempay_kyc_failed_description">We could not verify your profile. If you have any questions, please contact support.</string>
<string name="tangempay_kyc_failed_title">Unfortunately, we couldn\'t verify your identity </string>
<string name="tangempay_kyc_in_progress">KYC in progress</string>
<string name="tangempay_kyc_in_progress_notification_button">View Status</string> <string name="tangempay_kyc_in_progress_notification_button">View Status</string>
<string name="tangempay_kyc_in_progress_notification_title">KYC in progress for Tangem Pay</string> <string name="tangempay_kyc_in_progress_notification_title">KYC in progress for Tangem Pay</string>
<string name="tangempay_onboarding_get_card_button_text">Get card</string> <string name="tangempay_onboarding_get_card_button_text">Get card</string>
@ -1459,6 +1472,7 @@
<string name="tangempay_service_unavailable_description">Were fixing a technical issue. Please try again later.</string> <string name="tangempay_service_unavailable_description">Were fixing a technical issue. Please try again later.</string>
<string name="tangempay_service_unavailable_title">Service temporarily unavailable</string> <string name="tangempay_service_unavailable_title">Service temporarily unavailable</string>
<string name="tangempay_service_unreachable_try_later">The service is currently unreachable. Please try again later.</string> <string name="tangempay_service_unreachable_try_later">The service is currently unreachable. Please try again later.</string>
<string name="tangempay_sync_needed">Sync needed</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay is temporarily unavailable</string> <string name="tangempay_temporarily_unavailable">Tangem Pay is temporarily unavailable</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Use your card or ring to restore access to your payment account</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Use your card or ring to restore access to your payment account</string>

View file

@ -16,7 +16,7 @@ import com.tangem.domain.models.ReceiveAddressModel.NameService
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayTopUpData import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPayTopUpDataFactory import com.tangem.domain.pay.TangemPaySwapDataFactory
import timber.log.Timber import timber.log.Timber
import java.math.BigDecimal import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
@ -30,11 +30,11 @@ private const val TOKEN_NAME = "USDC"
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
private const val TOKEN_DECIMALS = 6 private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPayTopUpDataFactory @Inject constructor( internal class DefaultTangemPaySwapDataFactory @Inject constructor(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
private val tangemPayWalletsManager: TangemPayWalletsManager, private val tangemPayWalletsManager: TangemPayWalletsManager,
excludedBlockchains: ExcludedBlockchains, excludedBlockchains: ExcludedBlockchains,
) : TangemPayTopUpDataFactory { ) : TangemPaySwapDataFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains) CryptoCurrencyFactory(excludedBlockchains)

View file

@ -27,4 +27,13 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
.mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") } .mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") }
.bind() .bind()
} }
override suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, String> {
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)
}
}
} }

View file

@ -1,18 +1,15 @@
package com.tangem.data.pay.di package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory import com.tangem.data.pay.DefaultTangemPaySwapDataFactory
import com.tangem.data.pay.repository.DefaultKycRepository import com.tangem.data.pay.repository.*
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.domain.pay.TangemPayTopUpDataFactory import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -43,7 +40,15 @@ internal interface TangemPayDataModule {
@Binds @Binds
@Singleton @Singleton
fun bindDataForReceiveFactory(factory: DefaultTangemPayTopUpDataFactory): TangemPayTopUpDataFactory fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository
@Binds
@Singleton
fun bindCustomerOrderRepository(repository: DefaultCustomerOrderRepository): CustomerOrderRepository
@Binds
@Singleton
fun bindTangemPaySwapDataFactory(factory: DefaultTangemPaySwapDataFactory): TangemPaySwapDataFactory
@Binds @Binds
@Singleton @Singleton
@ -51,13 +56,21 @@ internal interface TangemPayDataModule {
impl: DefaultGetTangemPayCurrencyStatusUseCase, impl: DefaultGetTangemPayCurrencyStatusUseCase,
): GetTangemPayCurrencyStatusUseCase ): GetTangemPayCurrencyStatusUseCase
@Binds
@Singleton
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
companion object { companion object {
@Provides @Provides
@Singleton @Singleton
fun provideTangemPayMainScreenCustomerInfoUseCase( fun provideTangemPayMainScreenCustomerInfoUseCase(
repository: OnboardingRepository, repository: OnboardingRepository,
customerOrderRepository: CustomerOrderRepository,
): TangemPayMainScreenCustomerInfoUseCase { ): TangemPayMainScreenCustomerInfoUseCase {
return TangemPayMainScreenCustomerInfoUseCase(repository = repository) return TangemPayMainScreenCustomerInfoUseCase(
repository = repository,
customerOrderRepository = customerOrderRepository,
)
} }
@Provides @Provides

View file

@ -0,0 +1,46 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.visa.error.VisaApiError
import javax.inject.Inject
internal class DefaultCustomerOrderRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayStorage: TangemPayStorage,
) : CustomerOrderRepository {
override suspend fun getOrderStatus(
userWalletId: UserWalletId,
orderId: String,
): Either<VisaApiError, OrderStatus> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
}.map { response ->
when (response.result?.status) {
null -> OrderStatus.UNKNOWN
OrderStatus.NEW.apiName -> OrderStatus.NEW
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
else -> OrderStatus.CANCELED
}
}
}
override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean {
val orderId = tangemPayStorage.getWithdrawOrderId(userWalletId)
if (orderId == null) return false
val status = getOrderStatus(userWalletId, orderId).getOrNull()
val hasActiveOrder = status == OrderStatus.NEW || status == OrderStatus.PROCESSING
if (!hasActiveOrder) tangemPayStorage.deleteWithdrawOrder(userWalletId)
return hasActiveOrder
}
}

View file

@ -14,7 +14,6 @@ import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayCardFrozenState
@ -170,23 +169,6 @@ internal class DefaultOnboardingRepository @Inject constructor(
} }
} }
override suspend fun getOrderStatus(
userWalletId: UserWalletId,
orderId: String,
): Either<VisaApiError, OrderStatus> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
}.map { response ->
when (response.result?.status) {
null -> OrderStatus.UNKNOWN
OrderStatus.NEW.apiName -> OrderStatus.NEW
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
else -> OrderStatus.CANCELED
}
}
}
override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> { override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> {
val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId) val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId)
if (hasTangemPay == true) { if (hasTangemPay == true) {

View file

@ -42,8 +42,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
tangemPayApi.getCardBalance(authHeader) tangemPayApi.getCardBalance(authHeader)
}.result ?: error("Cannot get card balance") }.result ?: error("Cannot get card balance")
TangemPayCardBalance( TangemPayCardBalance(
balance = result.fiat.availableBalance, fiatBalance = result.fiat.availableBalance,
currencyCode = result.fiat.currency, currencyCode = result.fiat.currency,
cryptoBalance = result.crypto.balance,
chainId = result.crypto.chainId,
depositAddress = result.crypto.depositAddress,
contractAddress = result.crypto.tokenContractAddress,
) )
} }
} }

View file

@ -0,0 +1,106 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest
import com.tangem.datasource.api.pay.models.request.WithdrawRequest
import com.tangem.datasource.local.visa.TangemPayStorage
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.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.repository.TangemPaySwapRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.Currency
import java.util.Locale
import javax.inject.Inject
@Suppress("LongParameterList")
internal class DefaultTangemPaySwapRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val authDataSource: TangemPayAuthDataSource,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val quotesFetcher: QuotesFetcher,
private val tangemPayStorage: TangemPayStorage,
) : TangemPaySwapRepository {
override suspend fun withdraw(
userWalletId: UserWalletId,
receiverAddress: String,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
): Either<UniversalError, Unit> {
val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId)
if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError)
return requestHelper.makeSafeRequest(userWalletId) { authHeader ->
val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress)
tangemPayApi.getWithdrawData(authHeader = authHeader, body = request)
}.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)
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)
}
}
}
private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? {
val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null
val amountInDollars = cryptoAmount.multiply(fiatRate)
val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits
return amountInDollars
.setScale(defaultFractionDigits, RoundingMode.HALF_UP)
.movePointRight(defaultFractionDigits)
.longValueExact()
.toString()
}
private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? {
val quotes = quotesFetcher.fetch(
fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode,
currencyId = cryptoCurrencyId.value,
field = QuotesFetcher.Field.PRICE,
).getOrNull()
return quotes?.quotes[cryptoCurrencyId.value]?.price
}
private fun getCardId(userWalletId: UserWalletId): String {
val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
} else {
userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId }
} ?: error("No User Wallet found")
return if (userWallet is UserWallet.Cold) {
userWallet.cardId
} else {
TODO("[REDACTED_JIRA]")
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.data.pay.usecase
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.repository.TangemPaySwapRepository
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
import java.math.BigDecimal
import javax.inject.Inject
internal class DefaultTangemPayWithdrawUseCase @Inject constructor(
private val repository: TangemPaySwapRepository,
) : TangemPayWithdrawUseCase {
override suspend fun invoke(
userWalletId: UserWalletId,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
receiverCexAddress: String,
): Either<UniversalError, Unit> {
return repository.withdraw(
userWalletId = userWalletId,
cryptoAmount = cryptoAmount,
receiverAddress = receiverCexAddress,
cryptoCurrencyId = cryptoCurrencyId,
)
}
}

View file

@ -62,6 +62,9 @@ sealed class VisaApiError(
data object RefreshTokenExpired : VisaApiError(104004001) data object RefreshTokenExpired : VisaApiError(104004001)
data object NotPaeraCustomer : VisaApiError(104004002) data object NotPaeraCustomer : VisaApiError(104004002)
data object WithdrawalDataError : VisaApiError(104004003)
data object SignWithdrawError : VisaApiError(104004004)
data object WithdrawError : VisaApiError(104004005)
companion object { companion object {
fun fromBackendError(backendErrorCode: Int): VisaApiError { fun fromBackendError(backendErrorCode: Int): VisaApiError {

View file

@ -7,7 +7,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import java.math.BigDecimal import java.math.BigDecimal
interface TangemPayTopUpDataFactory { interface TangemPaySwapDataFactory {
fun create( fun create(
depositAddress: String, depositAddress: String,

View file

@ -9,4 +9,6 @@ interface TangemPayAuthDataSource {
suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens> suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens>
suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, String>
} }

View file

@ -3,6 +3,10 @@ package com.tangem.domain.pay.model
import java.math.BigDecimal import java.math.BigDecimal
data class TangemPayCardBalance( data class TangemPayCardBalance(
val balance: BigDecimal, val fiatBalance: BigDecimal,
val currencyCode: String, val currencyCode: String,
val cryptoBalance: BigDecimal,
val chainId: Int,
val depositAddress: String,
val contractAddress: String,
) )

View file

@ -0,0 +1,13 @@
package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.visa.error.VisaApiError
interface CustomerOrderRepository {
suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderStatus>
suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean
}

View file

@ -4,7 +4,6 @@ import arrow.core.Either
import com.tangem.core.error.UniversalError import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -21,8 +20,10 @@ interface OnboardingRepository {
suspend fun createOrder(userWalletId: UserWalletId): Job suspend fun createOrder(userWalletId: UserWalletId): Job
suspend fun clearOrderId(userWalletId: UserWalletId) suspend fun clearOrderId(userWalletId: UserWalletId)
suspend fun getOrderId(userWalletId: UserWalletId): String? suspend fun getOrderId(userWalletId: UserWalletId): String?
suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderStatus>
suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
} }

View file

@ -0,0 +1,17 @@
package com.tangem.domain.pay.repository
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 java.math.BigDecimal
interface TangemPaySwapRepository {
suspend fun withdraw(
userWalletId: UserWalletId,
receiverAddress: String,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
): Either<UniversalError, Unit>
}

View file

@ -9,6 +9,7 @@ import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayCustomerInfoError import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import timber.log.Timber import timber.log.Timber
@ -21,6 +22,7 @@ private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
*/ */
class TangemPayMainScreenCustomerInfoUseCase( class TangemPayMainScreenCustomerInfoUseCase(
private val repository: OnboardingRepository, private val repository: OnboardingRepository,
private val customerOrderRepository: CustomerOrderRepository,
) { ) {
suspend operator fun invoke( suspend operator fun invoke(
@ -74,7 +76,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
userWalletId: UserWalletId, userWalletId: UserWalletId,
orderId: String, orderId: String,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> { ): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return repository.getOrderStatus(userWalletId, orderId = orderId) return customerOrderRepository.getOrderStatus(userWalletId, orderId = orderId)
.fold( .fold(
ifLeft = { error -> ifLeft = { error ->
error.mapErrorForCustomer().left() error.mapErrorForCustomer().left()

View file

@ -0,0 +1,17 @@
package com.tangem.domain.tangempay
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 java.math.BigDecimal
interface TangemPayWithdrawUseCase {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
receiverCexAddress: String,
): Either<UniversalError, Unit>
}

View file

@ -20,6 +20,7 @@ interface SwapComponent : ComposableContentComponent {
val cryptoAmount: BigDecimal, val cryptoAmount: BigDecimal,
val fiatAmount: BigDecimal, val fiatAmount: BigDecimal,
val depositAddress: String, val depositAddress: String,
val isWithdrawal: Boolean,
) )
} }

View file

@ -1,7 +1,13 @@
package com.tangem.feature.swap.domain.models.ui package com.tangem.feature.swap.domain.models.ui
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import java.math.BigDecimal import java.math.BigDecimal
sealed class SwapTransactionState { sealed class SwapTransactionState {
@ -16,6 +22,31 @@ sealed class SwapTransactionState {
val timestamp: Long, val timestamp: Long,
) : SwapTransactionState() ) : SwapTransactionState()
data class TangemPayWithdrawalData(
val cryptoAmount: BigDecimal,
val cryptoCurrencyId: CryptoCurrency.RawID,
val cexAddress: String,
val fromAmount: String?,
val fromAmountValue: BigDecimal?,
val toAmount: String?,
val toAmountValue: BigDecimal?,
val storeData: StoreTransactionData,
) : SwapTransactionState() {
data class StoreTransactionData(
val currencyToSend: CryptoCurrencyStatus,
val currencyToGet: CryptoCurrencyStatus,
val fromAccount: Account.CryptoPortfolio?,
val toAccount: Account.CryptoPortfolio?,
val amount: SwapAmount,
val swapProvider: SwapProvider,
val swapDataModel: SwapDataModel,
val txExternalUrl: String?,
val txExternalId: String?,
val averageDuration: Int?,
)
}
data object DemoMode : SwapTransactionState() data object DemoMode : SwapTransactionState()
sealed class Error : SwapTransactionState() { sealed class Error : SwapTransactionState() {

View file

@ -61,7 +61,8 @@ interface SwapInteractor {
* @param currencyToSend [Currency] * @param currencyToSend [Currency]
* @param currencyToGet [Currency] * @param currencyToGet [Currency]
* @param amountToSwap amount to swap * @param amountToSwap amount to swap
* @param fee for tx * @param fee for tx (can be null only for tangem pay withdrawal)
* @return [SwapTransactionState] * @return [SwapTransactionState]
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -75,8 +76,9 @@ interface SwapInteractor {
toAccount: Account.CryptoPortfolio?, toAccount: Account.CryptoPortfolio?,
amountToSwap: String, amountToSwap: String,
includeFeeInAmount: IncludeFeeInAmount, includeFeeInAmount: IncludeFeeInAmount,
fee: TxFee, fee: TxFee?,
expressOperationType: ExpressOperationType, expressOperationType: ExpressOperationType,
isTangemPayWithdrawal: Boolean,
): SwapTransactionState ): SwapTransactionState
// suspend fun updateQuotesStateWithSelectedFee( // suspend fun updateQuotesStateWithSelectedFee(
@ -115,6 +117,21 @@ interface SwapInteractor {
fun getNativeToken(networkId: String): CryptoCurrency fun getNativeToken(networkId: String): CryptoCurrency
@Suppress("LongParameterList")
suspend fun storeSwapTransaction(
currencyToSend: CryptoCurrencyStatus,
currencyToGet: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
toAccount: Account.CryptoPortfolio?,
amount: SwapAmount,
swapProvider: SwapProvider,
swapDataModel: SwapDataModel,
timestamp: Long,
txExternalUrl: String? = null,
txExternalId: String? = null,
averageDuration: Int? = null,
)
interface Factory { interface Factory {
fun create(selectedWalletId: UserWalletId): SwapInteractor fun create(selectedWalletId: UserWalletId): SwapInteractor
} }

View file

@ -709,6 +709,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
return result return result
} }
@Suppress("NullableToStringCall")
override suspend fun onSwap( override suspend fun onSwap(
swapProvider: SwapProvider, swapProvider: SwapProvider,
swapData: SwapDataModel?, swapData: SwapDataModel?,
@ -718,8 +719,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toAccount: Account.CryptoPortfolio?, toAccount: Account.CryptoPortfolio?,
amountToSwap: String, amountToSwap: String,
includeFeeInAmount: IncludeFeeInAmount, includeFeeInAmount: IncludeFeeInAmount,
fee: TxFee, fee: TxFee?,
expressOperationType: ExpressOperationType, expressOperationType: ExpressOperationType,
isTangemPayWithdrawal: Boolean,
): SwapTransactionState { ): SwapTransactionState {
Timber.i( Timber.i(
""" """
@ -757,6 +759,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
txFee = fee, txFee = fee,
swapProvider = swapProvider, swapProvider = swapProvider,
expressOperationType = expressOperationType, expressOperationType = expressOperationType,
isTangemPayWithdrawal = isTangemPayWithdrawal,
) )
} }
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
@ -773,6 +776,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
amountToSwap = amountToSwap, amountToSwap = amountToSwap,
) )
} else { } else {
if (fee == null) return SwapTransactionState.Error.UnknownError
onSwapDex( onSwapDex(
provider = swapProvider, provider = swapProvider,
networkId = currencyToSend.currency.network.backendId, networkId = currencyToSend.currency.network.backendId,
@ -978,16 +982,17 @@ internal class SwapInteractorImpl @AssistedInject constructor(
).getOrNull() ?: error("failed to create extras") ).getOrNull() ?: error("failed to create extras")
} }
@Suppress("LongMethod") @Suppress("LongMethod", "CanBeNonNullable")
private suspend fun onSwapCex( private suspend fun onSwapCex(
currencyToSend: CryptoCurrencyStatus, currencyToSend: CryptoCurrencyStatus,
currencyToGet: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?, fromAccount: Account.CryptoPortfolio?,
toAccount: Account.CryptoPortfolio?, toAccount: Account.CryptoPortfolio?,
amount: SwapAmount, amount: SwapAmount,
txFee: TxFee, txFee: TxFee?,
swapProvider: SwapProvider, swapProvider: SwapProvider,
expressOperationType: ExpressOperationType, expressOperationType: ExpressOperationType,
isTangemPayWithdrawal: Boolean,
): SwapTransactionState { ): SwapTransactionState {
val exchangeData = repository.getExchangeData( val exchangeData = repository.getExchangeData(
userWallet = userWallet, userWallet = userWallet,
@ -1010,6 +1015,33 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val exchangeDataCex = val exchangeDataCex =
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError
if (isTangemPayWithdrawal) {
return SwapTransactionState.TangemPayWithdrawalData(
cryptoAmount = amount.value,
cryptoCurrencyId = requireNotNull(currencyToSend.currency.id.rawCurrencyId),
cexAddress = exchangeDataCex.txTo,
fromAmount = amount.toStringWithRightOffset(),
fromAmountValue = amount.value,
toAmount = amountFormatter.formatSwapAmountToUI(
exchangeData.toTokenAmount,
currencyToGet.currency.symbol,
),
toAmountValue = exchangeData.toTokenAmount.value,
storeData = SwapTransactionState.TangemPayWithdrawalData.StoreTransactionData(
currencyToSend = currencyToSend,
currencyToGet = currencyToGet,
fromAccount = fromAccount,
toAccount = toAccount,
amount = amount,
swapProvider = swapProvider,
swapDataModel = exchangeData,
txExternalUrl = "",
txExternalId = exchangeDataCex.externalTxId,
averageDuration = null,
),
)
}
val userWallet = userWallet val userWallet = userWallet
if (userWallet is UserWallet.Cold && isDemoCardUseCase(userWallet.scanResponse.card.cardId)) { if (userWallet is UserWallet.Cold && isDemoCardUseCase(userWallet.scanResponse.card.cardId)) {
return SwapTransactionState.Error.UnknownError return SwapTransactionState.Error.UnknownError
@ -1017,7 +1049,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val txData = createTransferTransactionUseCase( val txData = createTransferTransactionUseCase(
amount = amount.value.convertToSdkAmount(currencyToSend), amount = amount.value.convertToSdkAmount(currencyToSend),
fee = txFee.fee, fee = requireNotNull(txFee).fee,
memo = exchangeDataCex.txExtraId, memo = exchangeDataCex.txExtraId,
destination = exchangeDataCex.txTo, destination = exchangeDataCex.txTo,
userWalletId = userWalletId, userWalletId = userWalletId,
@ -1087,7 +1119,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
) )
} }
private suspend fun storeSwapTransaction( override suspend fun storeSwapTransaction(
currencyToSend: CryptoCurrencyStatus, currencyToSend: CryptoCurrencyStatus,
currencyToGet: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?, fromAccount: Account.CryptoPortfolio?,
@ -1096,9 +1128,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
swapProvider: SwapProvider, swapProvider: SwapProvider,
swapDataModel: SwapDataModel, swapDataModel: SwapDataModel,
timestamp: Long, timestamp: Long,
txExternalUrl: String? = null, txExternalUrl: String?,
txExternalId: String? = null, txExternalId: String?,
averageDuration: Int? = null, averageDuration: Int?,
) { ) {
swapTransactionRepository.storeTransaction( swapTransactionRepository.storeTransaction(
userWalletId = userWalletId, userWalletId = userWalletId,

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.common.routing) implementation(projects.common.routing)
implementation(projects.common.ui) implementation(projects.common.ui)
implementation(projects.core.decompose) // For Route supertype implementation(projects.core.decompose) // For Route supertype
implementation(projects.core.error)
/** Domain modules **/ /** Domain modules **/
implementation(projects.domain.models) implementation(projects.domain.models)

View file

@ -45,6 +45,7 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
@ -115,6 +116,7 @@ internal class SwapModel @Inject constructor(
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles,
private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase, private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase,
private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase,
) : Model() { ) : Model() {
private val params = paramsContainer.require<SwapComponent.Params>() private val params = paramsContainer.require<SwapComponent.Params>()
@ -555,6 +557,7 @@ internal class SwapModel @Inject constructor(
selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
isReverseSwapPossible = isReverseSwapPossible(), isReverseSwapPossible = isReverseSwapPossible(),
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
hideFee = tangemPayInput?.isWithdrawal == true,
) )
if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) {
analyticsEventHandler.send( analyticsEventHandler.send(
@ -702,7 +705,7 @@ internal class SwapModel @Inject constructor(
val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) val fromCurrency = requireNotNull(dataState.fromCryptoCurrency)
val fee = dataState.selectedFee val fee = dataState.selectedFee
if (fee == null) { if (fee == null && tangemPayInput?.isWithdrawal != true) {
makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
return return
} }
@ -719,13 +722,18 @@ internal class SwapModel @Inject constructor(
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
fee = fee, fee = fee,
expressOperationType = ExpressOperationType.SWAP, expressOperationType = ExpressOperationType.SWAP,
isTangemPayWithdrawal = tangemPayInput?.isWithdrawal == true,
) )
}.onSuccess { }.onSuccess { swapTransactionState ->
when (it) { when (swapTransactionState) {
is SwapTransactionState.TxSent -> { is SwapTransactionState.TxSent -> {
if (fee == null) {
makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
return@onSuccess
}
sendSuccessSwapEvent(fromCurrency.currency, fee.feeType) sendSuccessSwapEvent(fromCurrency.currency, fee.feeType)
val url = getExplorerTransactionUrlUseCase( val url = getExplorerTransactionUrlUseCase(
txHash = it.txHash, txHash = swapTransactionState.txHash,
networkId = fromCurrency.currency.network.id, networkId = fromCurrency.currency.network.id,
).getOrElse { ).getOrElse {
Timber.i("tx hash explore not supported") Timber.i("tx hash explore not supported")
@ -735,11 +743,11 @@ internal class SwapModel @Inject constructor(
updateWalletBalance() updateWalletBalance()
uiState = stateBuilder.createSuccessState( uiState = stateBuilder.createSuccessState(
uiState = uiState, uiState = uiState,
swapTransactionState = it, swapTransactionState = swapTransactionState,
dataState = dataState, dataState = dataState,
txUrl = url, txUrl = url,
onExploreClick = { onExploreClick = {
if (it.txHash.isNotEmpty()) { if (swapTransactionState.txHash.isNotEmpty()) {
urlOpener.openUrl(url) urlOpener.openUrl(url)
} }
analyticsEventHandler.send( analyticsEventHandler.send(
@ -747,7 +755,7 @@ internal class SwapModel @Inject constructor(
) )
}, },
onStatusClick = { onStatusClick = {
val txExternalUrl = it.txExternalUrl val txExternalUrl = swapTransactionState.txExternalUrl
if (!txExternalUrl.isNullOrBlank()) { if (!txExternalUrl.isNullOrBlank()) {
urlOpener.openUrl(txExternalUrl) urlOpener.openUrl(txExternalUrl)
analyticsEventHandler.send( analyticsEventHandler.send(
@ -771,12 +779,15 @@ internal class SwapModel @Inject constructor(
startLoadingQuotesFromLastState() startLoadingQuotesFromLastState()
uiState = stateBuilder.createErrorTransactionAlert( uiState = stateBuilder.createErrorTransactionAlert(
uiState = uiState, uiState = uiState,
error = it, error = swapTransactionState,
onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
onSupportClick = ::onFailedTxEmailClick, onSupportClick = ::onFailedTxEmailClick,
isReverseSwapPossible = isReverseSwapPossible(), isReverseSwapPossible = isReverseSwapPossible(),
) )
} }
is SwapTransactionState.TangemPayWithdrawalData -> {
processTangemPayWithdrawal(swapTransactionState = swapTransactionState)
}
} }
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
@ -786,6 +797,40 @@ internal class SwapModel @Inject constructor(
} }
} }
private suspend fun processTangemPayWithdrawal(swapTransactionState: SwapTransactionState.TangemPayWithdrawalData) {
tangemPayWithdrawUseCase(
userWalletId = userWalletId,
cryptoAmount = swapTransactionState.cryptoAmount,
cryptoCurrencyId = swapTransactionState.cryptoCurrencyId,
receiverCexAddress = swapTransactionState.cexAddress,
)
.onLeft {
startLoadingQuotesFromLastState()
makeDefaultAlert()
}
.onRight {
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 = "",
timestamp = System.currentTimeMillis(),
txExternalId = swapTransactionState.storeData.txExternalId,
averageDuration = null,
)
uiState = stateBuilder.createTangemPayWithdrawalSuccessState(
uiState = uiState,
swapTransactionState = swapTransactionState,
dataState = dataState,
)
swapRouter.openScreen(SwapNavScreen.Success)
}
}
private fun sendSuccessEvent() { private fun sendSuccessEvent() {
val provider = dataState.selectedProvider ?: return val provider = dataState.selectedProvider ?: return
val fee = dataState.selectedFee?.feeType ?: return val fee = dataState.selectedFee?.feeType ?: return
@ -804,6 +849,7 @@ internal class SwapModel @Inject constructor(
) )
} }
@Suppress("LongMethod")
private fun givePermissionsToSwap() { private fun givePermissionsToSwap() {
modelScope.launch(dispatchers.main) { modelScope.launch(dispatchers.main) {
runCatching { runCatching {
@ -840,8 +886,8 @@ internal class SwapModel @Inject constructor(
spenderAddress = approveDataModel.spenderAddress, spenderAddress = approveDataModel.spenderAddress,
), ),
) )
}.onSuccess { }.onSuccess { swapTransactionState ->
when (it) { when (swapTransactionState) {
is SwapTransactionState.TxSent -> { is SwapTransactionState.TxSent -> {
sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType)
updateWalletBalance() updateWalletBalance()
@ -852,7 +898,7 @@ internal class SwapModel @Inject constructor(
is SwapTransactionState.Error -> { is SwapTransactionState.Error -> {
uiState = stateBuilder.createErrorTransactionAlert( uiState = stateBuilder.createErrorTransactionAlert(
uiState = uiState, uiState = uiState,
error = it, error = swapTransactionState,
onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
onSupportClick = ::onFailedTxEmailClick, onSupportClick = ::onFailedTxEmailClick,
isReverseSwapPossible = isReverseSwapPossible(), isReverseSwapPossible = isReverseSwapPossible(),
@ -865,6 +911,9 @@ internal class SwapModel @Inject constructor(
isReverseSwapPossible = isReverseSwapPossible(), isReverseSwapPossible = isReverseSwapPossible(),
) )
} }
is SwapTransactionState.TangemPayWithdrawalData -> {
processTangemPayWithdrawal(swapTransactionState = swapTransactionState)
}
} }
}.onFailure { makeDefaultAlert() } }.onFailure { makeDefaultAlert() }
}.onFailure { }.onFailure {
@ -1475,7 +1524,17 @@ internal class SwapModel @Inject constructor(
} else { } else {
groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value } groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }
?.providers ?.providers
}.orEmpty() }
?.filterForTangemPayWithdrawal()
.orEmpty()
}
private fun List<SwapProvider>.filterForTangemPayWithdrawal(): List<SwapProvider> {
return if (tangemPayInput?.isWithdrawal == true) {
filter { it.type == ExchangeProviderType.CEX }
} else {
this
}
} }
private fun Map<SwapProvider, SwapState>.getLastLoadedSuccessStates(): SuccessLoadedSwapData { private fun Map<SwapProvider, SwapState>.getLastLoadedSuccessStates(): SuccessLoadedSwapData {

View file

@ -92,12 +92,14 @@ internal class SwapNotificationsFactory(
return updatedNotifications.toPersistentList() return updatedNotifications.toPersistentList()
} }
@Suppress("LongParameterList")
fun getConfirmationStateNotifications( fun getConfirmationStateNotifications(
quoteModel: SwapState.QuotesLoadedState, quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency, fromToken: CryptoCurrency,
feeCryptoCurrencyStatus: CryptoCurrencyStatus?, feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
selectedFeeType: FeeType, selectedFeeType: FeeType,
providerName: String, providerName: String,
hideFee: Boolean,
): ImmutableList<NotificationUM> { ): ImmutableList<NotificationUM> {
val warnings = buildList { val warnings = buildList {
maybeAddRentExemptionError(quoteModel) maybeAddRentExemptionError(quoteModel)
@ -105,7 +107,7 @@ internal class SwapNotificationsFactory(
maybeAddNeedReserveToCreateAccountWarning(quoteModel) maybeAddNeedReserveToCreateAccountWarning(quoteModel)
maybeAddPermissionNeededWarning(quoteModel, fromToken, providerName) maybeAddPermissionNeededWarning(quoteModel, fromToken, providerName)
maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType) maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType)
maybeAddUnableCoverFeeWarning(quoteModel, fromToken) maybeAddUnableCoverFeeWarning(quoteModel, fromToken, hideFee)
maybeAddTransactionInProgressWarning(quoteModel) maybeAddTransactionInProgressWarning(quoteModel)
} }
return warnings.toPersistentList() return warnings.toPersistentList()
@ -284,7 +286,9 @@ internal class SwapNotificationsFactory(
private fun MutableList<NotificationUM>.maybeAddUnableCoverFeeWarning( private fun MutableList<NotificationUM>.maybeAddUnableCoverFeeWarning(
quoteModel: SwapState.QuotesLoadedState, quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency, fromToken: CryptoCurrency,
hideFee: Boolean,
) { ) {
if (hideFee) return
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return
val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.permissionState !is PermissionDataState.PermissionLoading && quoteModel.permissionState !is PermissionDataState.PermissionLoading &&

View file

@ -277,6 +277,7 @@ internal class StateBuilder(
selectedFeeType: FeeType, selectedFeeType: FeeType,
isReverseSwapPossible: Boolean, isReverseSwapPossible: Boolean,
needApplyFCARestrictions: Boolean, needApplyFCARestrictions: Boolean,
hideFee: Boolean,
): SwapStateHolder { ): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
@ -286,8 +287,9 @@ internal class StateBuilder(
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
selectedFeeType = selectedFeeType, selectedFeeType = selectedFeeType,
providerName = swapProvider.name, providerName = swapProvider.name,
hideFee = hideFee,
) )
val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val feeState = if (hideFee) FeeItemState.Empty else createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
val isInsufficientFunds = isInsufficientFundsCondition(quoteModel) val isInsufficientFunds = isInsufficientFundsCondition(quoteModel)
@ -853,6 +855,45 @@ internal class StateBuilder(
) )
} }
fun createTangemPayWithdrawalSuccessState(
uiState: SwapStateHolder,
swapTransactionState: SwapTransactionState.TangemPayWithdrawalData,
dataState: SwapProcessDataState,
): SwapStateHolder {
val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency)
val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency)
val fromAmount = swapTransactionState.fromAmountValue ?: BigDecimal.ZERO
val toAmount = swapTransactionState.toAmountValue ?: BigDecimal.ZERO
val providerState = uiState.providerState as ProviderState.Content
val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount))
val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount))
val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName
return uiState.copy(
successState = SwapSuccessStateHolder(
timestamp = System.currentTimeMillis(),
txUrl = "",
providerName = stringReference(providerState.name),
providerType = stringReference(providerState.type),
showStatusButton = shouldShowStatus,
providerIcon = providerState.iconUrl,
rate = providerState.subtitle,
fee = TextReference.EMPTY,
fromTitle = getFromCardAccountTitle(fromAccount = dataState.fromAccount),
toTitle = getToCardAccountTitle(toAccount = dataState.toAccount),
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
fromTokenFiatAmount = stringReference(fromFiatAmount),
toTokenFiatAmount = stringReference(toFiatAmount),
fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency),
toTokenIconState = iconStateConverter.convert(toCryptoCurrency),
onExploreButtonClick = {},
onStatusButtonClick = {},
),
)
}
fun createErrorTransactionAlert( fun createErrorTransactionAlert(
uiState: SwapStateHolder, uiState: SwapStateHolder,
error: SwapTransactionState.Error, error: SwapTransactionState.Error,

View file

@ -1,5 +1,5 @@
package com.tangem.features.tangempay.entity package com.tangem.features.tangempay.entity
internal enum class TangemPayDetailsErrorType { internal enum class TangemPayDetailsErrorType {
Receive, Receive, Withdraw, WithdrawInProgress
} }

View file

@ -73,9 +73,14 @@ internal class TangemPayDetailsStateFactory(
actionButtons = persistentListOf( actionButtons = persistentListOf(
ActionButtonConfig( ActionButtonConfig(
text = resourceReference(id = R.string.tangempay_card_details_add_funds), text = resourceReference(id = R.string.tangempay_card_details_add_funds),
iconResId = R.drawable.ic_arrow_down_24, iconResId = R.drawable.ic_plus_24,
onClick = intents::onClickAddFunds, onClick = intents::onClickAddFunds,
), ),
ActionButtonConfig(
text = resourceReference(id = R.string.tangempay_card_details_withdraw),
iconResId = R.drawable.ic_arrow_up_24,
onClick = intents::onClickWithdraw,
),
), ),
), ),
addToWalletBlockState = null, addToWalletBlockState = null,

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.pay.TangemPayTopUpDataFactory import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.features.tangempay.components.TangemPayAddFundsComponent import com.tangem.features.tangempay.components.TangemPayAddFundsComponent
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter
@ -16,7 +16,7 @@ import javax.inject.Inject
internal class TangemPayAddFundsModel @Inject constructor( internal class TangemPayAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val tangemPayTopUpDataFactory: TangemPayTopUpDataFactory, private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
) : Model() { ) : Model() {
private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>() private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>()
@ -24,7 +24,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
val uiState: TangemPayAddFundsUM = getInitialState() val uiState: TangemPayAddFundsUM = getInitialState()
private fun getInitialState(): TangemPayAddFundsUM { private fun getInitialState(): TangemPayAddFundsUM {
val data = tangemPayTopUpDataFactory.create( val data = tangemPaySwapDataFactory.create(
depositAddress = params.depositAddress, depositAddress = params.depositAddress,
chainId = params.chainId, chainId = params.chainId,
cryptoBalance = params.cryptoBalance, cryptoBalance = params.cryptoBalance,

View file

@ -18,7 +18,9 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.TangemPayTopUpData import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.TangemPayTxHistoryItem
@ -64,6 +66,8 @@ internal class TangemPayDetailsModel @Inject constructor(
private val uiMessageSender: UiMessageSender, private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener, private val cardDetailsEventListener: CardDetailsEventListener,
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener, private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val orderRepository: CustomerOrderRepository,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener { ) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -199,22 +203,63 @@ internal class TangemPayDetailsModel @Inject constructor(
} }
override fun onClickAddFunds() { override fun onClickAddFunds() {
if (params.config.depositAddress == null || balance == null) { val currentBalance = balance
val depositAddress = params.config.depositAddress
if (currentBalance == null || depositAddress == null) {
showBottomSheetError(TangemPayDetailsErrorType.Receive) showBottomSheetError(TangemPayDetailsErrorType.Receive)
} else { } else {
bottomSheetNavigation.activate( bottomSheetNavigation.activate(
TangemPayDetailsNavigation.AddFunds( TangemPayDetailsNavigation.AddFunds(
walletId = params.userWalletId, walletId = params.userWalletId,
fiatBalance = requireNotNull(balance).balance, fiatBalance = currentBalance.fiatBalance,
// Using the fiat balance as crypto balance. This will be changed when back returns crypto balance cryptoBalance = currentBalance.cryptoBalance,
cryptoBalance = requireNotNull(balance).balance, depositAddress = depositAddress,
depositAddress = requireNotNull(params.config.depositAddress),
chainId = params.config.chainId, chainId = params.config.chainId,
), ),
) )
} }
} }
override fun onClickWithdraw() {
val currentBalance = balance
val depositAddress = params.config.depositAddress
if (currentBalance == null || depositAddress == null) {
showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
} else {
modelScope.launch {
val hasActiveWithdrawal = orderRepository.hasWithdrawOrder(userWalletId = params.userWalletId)
if (hasActiveWithdrawal) {
showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress)
} else {
val data = tangemPaySwapDataFactory.create(
depositAddress = depositAddress,
chainId = params.config.chainId,
cryptoBalance = currentBalance.cryptoBalance,
fiatBalance = currentBalance.fiatBalance,
).getOrNull()
if (data != null) {
router.push(
AppRoute.Swap(
currencyFrom = data.currency,
userWalletId = data.walletId,
isInitialReverseOrder = false,
screenSource = AnalyticsParam.ScreensSources.TangemPay.value,
tangemPayInput = AppRoute.Swap.TangemPayInput(
cryptoAmount = data.cryptoBalance,
fiatAmount = data.fiatBalance,
depositAddress = data.depositAddress,
isWithdrawal = true,
),
),
)
} else {
showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
}
}
}
}
}
private fun fetchBalance(): Job { private fun fetchBalance(): Job {
return modelScope.launch { return modelScope.launch {
val result = try { val result = try {
@ -294,6 +339,7 @@ internal class TangemPayDetailsModel @Inject constructor(
cryptoAmount = data.cryptoBalance, cryptoAmount = data.cryptoBalance,
fiatAmount = data.fiatBalance, fiatAmount = data.fiatBalance,
depositAddress = data.depositAddress, depositAddress = data.depositAddress,
isWithdrawal = false,
), ),
), ),
) )

View file

@ -35,7 +35,7 @@ internal class DetailsBalanceTransformer(
private fun getBalanceText(balance: TangemPayCardBalance): String { private fun getBalanceText(balance: TangemPayCardBalance): String {
val currency = Currency.getInstance(balance.currencyCode) val currency = Currency.getInstance(balance.currencyCode)
return balance.balance.format { return balance.fiatBalance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
} }
} }

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
internal interface TangemPayDetailIntents { internal interface TangemPayDetailIntents {
fun onRefreshSwipe(refreshState: ShowRefreshState) fun onRefreshSwipe(refreshState: ShowRefreshState)
fun onClickAddFunds() fun onClickAddFunds()
fun onClickWithdraw()
fun onClickChangePin() fun onClickChangePin()
fun onClickFreezeCard() fun onClickFreezeCard()
fun onClickUnfreezeCard() fun onClickUnfreezeCard()

View file

@ -25,6 +25,32 @@ internal object TangemPayMessagesFactory {
onClick { closeBs() } onClick { closeBs() }
} }
} }
TangemPayDetailsErrorType.Withdraw -> bottomSheetMessage {
infoBlock {
icon(R.drawable.img_attention_20) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention
}
title = TextReference.Res(R.string.tangempay_card_details_withdraw_error_title)
body = TextReference.Res(R.string.tangempay_card_details_receive_error_description)
}
primaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
}
}
TangemPayDetailsErrorType.WithdrawInProgress -> bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_clock_24) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Informative
}
title = TextReference.Res(R.string.tangempay_card_details_withdraw_in_progress_title)
body = TextReference.Res(R.string.tangempay_card_details_withdraw_in_progress_description)
}
secondaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
}
}
} }
} }

View file

@ -160,5 +160,7 @@ interface TangemSdkManager {
): CompletionResult<VisaSignedDataByCustomerWallet> ): CompletionResult<VisaSignedDataByCustomerWallet>
suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult<TangemPayInitialCredentials> suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult<TangemPayInitialCredentials>
suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String>
// endregion // endregion
} }