Updated on 2026-08-14
This commit is contained in:
parent
9163a956aa
commit
b70933a1e0
47 changed files with 848 additions and 74 deletions
|
|
@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi
|
|||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
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.store
|
||||
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 ORDER_ID_KEY = "tangem_pay_order_id_key"
|
||||
private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key"
|
||||
|
||||
@Singleton
|
||||
internal class DefaultTangemPayStorage @Inject constructor(
|
||||
|
|
@ -114,9 +116,38 @@ internal class DefaultTangemPayStorage @Inject constructor(
|
|||
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 createKey(address: String): String = "${DEFAULT_KEY}_$address"
|
||||
|
||||
private fun createOrderIdKey(address: String): String = "${ORDER_ID_KEY}_$address"
|
||||
|
||||
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.common.authentication.AuthenticationManager
|
|||
import com.tangem.common.authentication.keystore.KeystoreManager
|
||||
import com.tangem.common.core.*
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.common.usersCode.UserCodeRepository
|
||||
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.ScanProductTask
|
||||
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.VisaCustomerWalletApproveTask
|
||||
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
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -219,5 +219,9 @@ class MockTangemSdkManager(
|
|||
error("Not implemented")
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String> {
|
||||
error("Not implemented")
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -328,6 +328,7 @@ internal class ChildFactory @Inject constructor(
|
|||
cryptoAmount = tangemPayInput.cryptoAmount,
|
||||
fiatAmount = tangemPayInput.fiatAmount,
|
||||
depositAddress = tangemPayInput.depositAddress,
|
||||
isWithdrawal = tangemPayInput.isWithdrawal,
|
||||
)
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val cryptoAmount: SerializedBigDecimal,
|
||||
val fiatAmount: SerializedBigDecimal,
|
||||
val depositAddress: String,
|
||||
val isWithdrawal: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -166,4 +166,16 @@ interface TangemPayApi {
|
|||
@Header("Authorization") authHeader: String,
|
||||
@Body body: FreezeUnfreezeCardRequest,
|
||||
): 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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -145,6 +145,8 @@ object PreferencesKeys {
|
|||
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
|
||||
}
|
||||
|
||||
val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") }
|
||||
|
||||
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
|
||||
// endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -24,5 +24,11 @@ interface TangemPayStorage {
|
|||
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId)
|
||||
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)
|
||||
}
|
||||
|
|
@ -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_archiving">Archiving...</string>
|
||||
<string name="account_details_title">Account</string>
|
||||
<string name="account_edit_failure_dialog_title">Can’t edit the account</string>
|
||||
<string name="account_edit_success_message">Account saved</string>
|
||||
<string name="account_for_rewards">Account for rewards</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_title">Terms of service</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_network_name_address">%s address</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_title">Card details</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_title">Setting up your Tangem Card</string>
|
||||
<string name="tangempay_issuing_your_card">Issuing your card</string>
|
||||
<string name="tangempay_issuing_your_card_description">We’re 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_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_title">KYC in progress for Tangem Pay</string>
|
||||
<string name="tangempay_onboarding_get_card_button_text">Get card</string>
|
||||
|
|
@ -1459,6 +1472,7 @@
|
|||
<string name="tangempay_service_unavailable_description">We’re fixing a technical issue. Please try again later.</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_sync_needed">Sync needed</string>
|
||||
<string name="tangempay_temporarily_unavailable">Tangem Pay is temporarily unavailable</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>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.domain.models.ReceiveAddressModel.NameService
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayTopUpData
|
||||
import com.tangem.domain.pay.TangemPayTopUpDataFactory
|
||||
import com.tangem.domain.pay.TangemPaySwapDataFactory
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
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_DECIMALS = 6
|
||||
|
||||
internal class DefaultTangemPayTopUpDataFactory @Inject constructor(
|
||||
internal class DefaultTangemPaySwapDataFactory @Inject constructor(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
) : TangemPayTopUpDataFactory {
|
||||
) : TangemPaySwapDataFactory {
|
||||
|
||||
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyFactory(excludedBlockchains)
|
||||
|
|
@ -27,4 +27,13 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
|
|||
.mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") }
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory
|
||||
import com.tangem.data.pay.repository.DefaultKycRepository
|
||||
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.DefaultTangemPaySwapDataFactory
|
||||
import com.tangem.data.pay.repository.*
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.pay.TangemPayTopUpDataFactory
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
|
||||
import com.tangem.domain.pay.TangemPaySwapDataFactory
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -43,7 +40,15 @@ internal interface TangemPayDataModule {
|
|||
|
||||
@Binds
|
||||
@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
|
||||
@Singleton
|
||||
|
|
@ -51,13 +56,21 @@ internal interface TangemPayDataModule {
|
|||
impl: DefaultGetTangemPayCurrencyStatusUseCase,
|
||||
): GetTangemPayCurrencyStatusUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayMainScreenCustomerInfoUseCase(
|
||||
repository: OnboardingRepository,
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(repository = repository)
|
||||
return TangemPayMainScreenCustomerInfoUseCase(
|
||||
repository = repository,
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.CardInfo
|
||||
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.visa.error.VisaApiError
|
||||
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> {
|
||||
val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId)
|
||||
if (hasTangemPay == true) {
|
||||
|
|
|
|||
|
|
@ -42,8 +42,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
tangemPayApi.getCardBalance(authHeader)
|
||||
}.result ?: error("Cannot get card balance")
|
||||
TangemPayCardBalance(
|
||||
balance = result.fiat.availableBalance,
|
||||
fiatBalance = result.fiat.availableBalance,
|
||||
currencyCode = result.fiat.currency,
|
||||
cryptoBalance = result.crypto.balance,
|
||||
chainId = result.crypto.chainId,
|
||||
depositAddress = result.crypto.depositAddress,
|
||||
contractAddress = result.crypto.tokenContractAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,9 @@ sealed class VisaApiError(
|
|||
|
||||
data object RefreshTokenExpired : VisaApiError(104004001)
|
||||
data object NotPaeraCustomer : VisaApiError(104004002)
|
||||
data object WithdrawalDataError : VisaApiError(104004003)
|
||||
data object SignWithdrawError : VisaApiError(104004004)
|
||||
data object WithdrawError : VisaApiError(104004005)
|
||||
|
||||
companion object {
|
||||
fun fromBackendError(backendErrorCode: Int): VisaApiError {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface TangemPayTopUpDataFactory {
|
||||
interface TangemPaySwapDataFactory {
|
||||
|
||||
fun create(
|
||||
depositAddress: String,
|
||||
|
|
@ -9,4 +9,6 @@ interface TangemPayAuthDataSource {
|
|||
suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
|
||||
|
||||
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens>
|
||||
|
||||
suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, String>
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@ package com.tangem.domain.pay.model
|
|||
import java.math.BigDecimal
|
||||
|
||||
data class TangemPayCardBalance(
|
||||
val balance: BigDecimal,
|
||||
val fiatBalance: BigDecimal,
|
||||
val currencyCode: String,
|
||||
val cryptoBalance: BigDecimal,
|
||||
val chainId: Int,
|
||||
val depositAddress: String,
|
||||
val contractAddress: String,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import arrow.core.Either
|
|||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
|
|
@ -21,8 +20,10 @@ interface OnboardingRepository {
|
|||
suspend fun createOrder(userWalletId: UserWalletId): Job
|
||||
|
||||
suspend fun clearOrderId(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getOrderId(userWalletId: UserWalletId): String?
|
||||
suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderStatus>
|
||||
|
||||
suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
|
||||
|
||||
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.pay.model.CustomerInfo
|
|||
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
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.visa.error.VisaApiError
|
||||
import timber.log.Timber
|
||||
|
|
@ -21,6 +22,7 @@ private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
|
|||
*/
|
||||
class TangemPayMainScreenCustomerInfoUseCase(
|
||||
private val repository: OnboardingRepository,
|
||||
private val customerOrderRepository: CustomerOrderRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -74,7 +76,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
orderId: String,
|
||||
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
|
||||
return repository.getOrderStatus(userWalletId, orderId = orderId)
|
||||
return customerOrderRepository.getOrderStatus(userWalletId, orderId = orderId)
|
||||
.fold(
|
||||
ifLeft = { error ->
|
||||
error.mapErrorForCustomer().left()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ interface SwapComponent : ComposableContentComponent {
|
|||
val cryptoAmount: BigDecimal,
|
||||
val fiatAmount: BigDecimal,
|
||||
val depositAddress: String,
|
||||
val isWithdrawal: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
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.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
|
||||
|
||||
sealed class SwapTransactionState {
|
||||
|
|
@ -16,6 +22,31 @@ sealed class SwapTransactionState {
|
|||
val timestamp: Long,
|
||||
) : 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()
|
||||
|
||||
sealed class Error : SwapTransactionState() {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ interface SwapInteractor {
|
|||
* @param currencyToSend [Currency]
|
||||
* @param currencyToGet [Currency]
|
||||
* @param amountToSwap amount to swap
|
||||
* @param fee for tx
|
||||
* @param fee for tx (can be null only for tangem pay withdrawal)
|
||||
|
||||
* @return [SwapTransactionState]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -75,8 +76,9 @@ interface SwapInteractor {
|
|||
toAccount: Account.CryptoPortfolio?,
|
||||
amountToSwap: String,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
fee: TxFee,
|
||||
fee: TxFee?,
|
||||
expressOperationType: ExpressOperationType,
|
||||
isTangemPayWithdrawal: Boolean,
|
||||
): SwapTransactionState
|
||||
|
||||
// suspend fun updateQuotesStateWithSelectedFee(
|
||||
|
|
@ -115,6 +117,21 @@ interface SwapInteractor {
|
|||
|
||||
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 {
|
||||
fun create(selectedWalletId: UserWalletId): SwapInteractor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -709,6 +709,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
return result
|
||||
}
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
override suspend fun onSwap(
|
||||
swapProvider: SwapProvider,
|
||||
swapData: SwapDataModel?,
|
||||
|
|
@ -718,8 +719,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
toAccount: Account.CryptoPortfolio?,
|
||||
amountToSwap: String,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
fee: TxFee,
|
||||
fee: TxFee?,
|
||||
expressOperationType: ExpressOperationType,
|
||||
isTangemPayWithdrawal: Boolean,
|
||||
): SwapTransactionState {
|
||||
Timber.i(
|
||||
"""
|
||||
|
|
@ -757,6 +759,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
txFee = fee,
|
||||
swapProvider = swapProvider,
|
||||
expressOperationType = expressOperationType,
|
||||
isTangemPayWithdrawal = isTangemPayWithdrawal,
|
||||
)
|
||||
}
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
|
|
@ -773,6 +776,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
amountToSwap = amountToSwap,
|
||||
)
|
||||
} else {
|
||||
if (fee == null) return SwapTransactionState.Error.UnknownError
|
||||
onSwapDex(
|
||||
provider = swapProvider,
|
||||
networkId = currencyToSend.currency.network.backendId,
|
||||
|
|
@ -978,16 +982,17 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
).getOrNull() ?: error("failed to create extras")
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CanBeNonNullable")
|
||||
private suspend fun onSwapCex(
|
||||
currencyToSend: CryptoCurrencyStatus,
|
||||
currencyToGet: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
amount: SwapAmount,
|
||||
txFee: TxFee,
|
||||
txFee: TxFee?,
|
||||
swapProvider: SwapProvider,
|
||||
expressOperationType: ExpressOperationType,
|
||||
isTangemPayWithdrawal: Boolean,
|
||||
): SwapTransactionState {
|
||||
val exchangeData = repository.getExchangeData(
|
||||
userWallet = userWallet,
|
||||
|
|
@ -1010,6 +1015,33 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
val exchangeDataCex =
|
||||
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
|
||||
if (userWallet is UserWallet.Cold && isDemoCardUseCase(userWallet.scanResponse.card.cardId)) {
|
||||
return SwapTransactionState.Error.UnknownError
|
||||
|
|
@ -1017,7 +1049,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
|
||||
val txData = createTransferTransactionUseCase(
|
||||
amount = amount.value.convertToSdkAmount(currencyToSend),
|
||||
fee = txFee.fee,
|
||||
fee = requireNotNull(txFee).fee,
|
||||
memo = exchangeDataCex.txExtraId,
|
||||
destination = exchangeDataCex.txTo,
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -1087,7 +1119,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun storeSwapTransaction(
|
||||
override suspend fun storeSwapTransaction(
|
||||
currencyToSend: CryptoCurrencyStatus,
|
||||
currencyToGet: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
|
|
@ -1096,9 +1128,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
swapProvider: SwapProvider,
|
||||
swapDataModel: SwapDataModel,
|
||||
timestamp: Long,
|
||||
txExternalUrl: String? = null,
|
||||
txExternalId: String? = null,
|
||||
averageDuration: Int? = null,
|
||||
txExternalUrl: String?,
|
||||
txExternalId: String?,
|
||||
averageDuration: Int?,
|
||||
) {
|
||||
swapTransactionRepository.storeTransaction(
|
||||
userWalletId = userWalletId,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
implementation(projects.common.routing)
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.core.decompose) // For Route supertype
|
||||
implementation(projects.core.error)
|
||||
|
||||
/** Domain modules **/
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -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.needApplyFCARestrictions
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
|
|
@ -115,6 +116,7 @@ internal class SwapModel @Inject constructor(
|
|||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase,
|
||||
private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<SwapComponent.Params>()
|
||||
|
|
@ -555,6 +557,7 @@ internal class SwapModel @Inject constructor(
|
|||
selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
|
||||
hideFee = tangemPayInput?.isWithdrawal == true,
|
||||
)
|
||||
if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) {
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -702,7 +705,7 @@ internal class SwapModel @Inject constructor(
|
|||
val fromCurrency = requireNotNull(dataState.fromCryptoCurrency)
|
||||
val fee = dataState.selectedFee
|
||||
|
||||
if (fee == null) {
|
||||
if (fee == null && tangemPayInput?.isWithdrawal != true) {
|
||||
makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
|
||||
return
|
||||
}
|
||||
|
|
@ -719,13 +722,18 @@ internal class SwapModel @Inject constructor(
|
|||
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
|
||||
fee = fee,
|
||||
expressOperationType = ExpressOperationType.SWAP,
|
||||
isTangemPayWithdrawal = tangemPayInput?.isWithdrawal == true,
|
||||
)
|
||||
}.onSuccess {
|
||||
when (it) {
|
||||
}.onSuccess { swapTransactionState ->
|
||||
when (swapTransactionState) {
|
||||
is SwapTransactionState.TxSent -> {
|
||||
if (fee == null) {
|
||||
makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
|
||||
return@onSuccess
|
||||
}
|
||||
sendSuccessSwapEvent(fromCurrency.currency, fee.feeType)
|
||||
val url = getExplorerTransactionUrlUseCase(
|
||||
txHash = it.txHash,
|
||||
txHash = swapTransactionState.txHash,
|
||||
networkId = fromCurrency.currency.network.id,
|
||||
).getOrElse {
|
||||
Timber.i("tx hash explore not supported")
|
||||
|
|
@ -735,11 +743,11 @@ internal class SwapModel @Inject constructor(
|
|||
updateWalletBalance()
|
||||
uiState = stateBuilder.createSuccessState(
|
||||
uiState = uiState,
|
||||
swapTransactionState = it,
|
||||
swapTransactionState = swapTransactionState,
|
||||
dataState = dataState,
|
||||
txUrl = url,
|
||||
onExploreClick = {
|
||||
if (it.txHash.isNotEmpty()) {
|
||||
if (swapTransactionState.txHash.isNotEmpty()) {
|
||||
urlOpener.openUrl(url)
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -747,7 +755,7 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
},
|
||||
onStatusClick = {
|
||||
val txExternalUrl = it.txExternalUrl
|
||||
val txExternalUrl = swapTransactionState.txExternalUrl
|
||||
if (!txExternalUrl.isNullOrBlank()) {
|
||||
urlOpener.openUrl(txExternalUrl)
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -771,12 +779,15 @@ internal class SwapModel @Inject constructor(
|
|||
startLoadingQuotesFromLastState()
|
||||
uiState = stateBuilder.createErrorTransactionAlert(
|
||||
uiState = uiState,
|
||||
error = it,
|
||||
error = swapTransactionState,
|
||||
onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
|
||||
onSupportClick = ::onFailedTxEmailClick,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
)
|
||||
}
|
||||
is SwapTransactionState.TangemPayWithdrawalData -> {
|
||||
processTangemPayWithdrawal(swapTransactionState = swapTransactionState)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
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() {
|
||||
val provider = dataState.selectedProvider ?: return
|
||||
val fee = dataState.selectedFee?.feeType ?: return
|
||||
|
|
@ -804,6 +849,7 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun givePermissionsToSwap() {
|
||||
modelScope.launch(dispatchers.main) {
|
||||
runCatching {
|
||||
|
|
@ -840,8 +886,8 @@ internal class SwapModel @Inject constructor(
|
|||
spenderAddress = approveDataModel.spenderAddress,
|
||||
),
|
||||
)
|
||||
}.onSuccess {
|
||||
when (it) {
|
||||
}.onSuccess { swapTransactionState ->
|
||||
when (swapTransactionState) {
|
||||
is SwapTransactionState.TxSent -> {
|
||||
sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType)
|
||||
updateWalletBalance()
|
||||
|
|
@ -852,7 +898,7 @@ internal class SwapModel @Inject constructor(
|
|||
is SwapTransactionState.Error -> {
|
||||
uiState = stateBuilder.createErrorTransactionAlert(
|
||||
uiState = uiState,
|
||||
error = it,
|
||||
error = swapTransactionState,
|
||||
onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
|
||||
onSupportClick = ::onFailedTxEmailClick,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
|
|
@ -865,6 +911,9 @@ internal class SwapModel @Inject constructor(
|
|||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
)
|
||||
}
|
||||
is SwapTransactionState.TangemPayWithdrawalData -> {
|
||||
processTangemPayWithdrawal(swapTransactionState = swapTransactionState)
|
||||
}
|
||||
}
|
||||
}.onFailure { makeDefaultAlert() }
|
||||
}.onFailure {
|
||||
|
|
@ -1475,7 +1524,17 @@ internal class SwapModel @Inject constructor(
|
|||
} else {
|
||||
groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }
|
||||
?.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 {
|
||||
|
|
|
|||
|
|
@ -92,12 +92,14 @@ internal class SwapNotificationsFactory(
|
|||
return updatedNotifications.toPersistentList()
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun getConfirmationStateNotifications(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: CryptoCurrency,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
selectedFeeType: FeeType,
|
||||
providerName: String,
|
||||
hideFee: Boolean,
|
||||
): ImmutableList<NotificationUM> {
|
||||
val warnings = buildList {
|
||||
maybeAddRentExemptionError(quoteModel)
|
||||
|
|
@ -105,7 +107,7 @@ internal class SwapNotificationsFactory(
|
|||
maybeAddNeedReserveToCreateAccountWarning(quoteModel)
|
||||
maybeAddPermissionNeededWarning(quoteModel, fromToken, providerName)
|
||||
maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType)
|
||||
maybeAddUnableCoverFeeWarning(quoteModel, fromToken)
|
||||
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, hideFee)
|
||||
maybeAddTransactionInProgressWarning(quoteModel)
|
||||
}
|
||||
return warnings.toPersistentList()
|
||||
|
|
@ -284,7 +286,9 @@ internal class SwapNotificationsFactory(
|
|||
private fun MutableList<NotificationUM>.maybeAddUnableCoverFeeWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: CryptoCurrency,
|
||||
hideFee: Boolean,
|
||||
) {
|
||||
if (hideFee) return
|
||||
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return
|
||||
val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough &&
|
||||
quoteModel.permissionState !is PermissionDataState.PermissionLoading &&
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ internal class StateBuilder(
|
|||
selectedFeeType: FeeType,
|
||||
isReverseSwapPossible: Boolean,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
hideFee: Boolean,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
|
|
@ -286,8 +287,9 @@ internal class StateBuilder(
|
|||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
selectedFeeType = selectedFeeType,
|
||||
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 toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
|
||||
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(
|
||||
uiState: SwapStateHolder,
|
||||
error: SwapTransactionState.Error,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
internal enum class TangemPayDetailsErrorType {
|
||||
Receive,
|
||||
Receive, Withdraw, WithdrawInProgress
|
||||
}
|
||||
|
|
@ -73,9 +73,14 @@ internal class TangemPayDetailsStateFactory(
|
|||
actionButtons = persistentListOf(
|
||||
ActionButtonConfig(
|
||||
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,
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.tangempay_card_details_withdraw),
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = intents::onClickWithdraw,
|
||||
),
|
||||
),
|
||||
),
|
||||
addToWalletBlockState = null,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
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.entity.TangemPayAddFundsUM
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter
|
||||
|
|
@ -16,7 +16,7 @@ import javax.inject.Inject
|
|||
internal class TangemPayAddFundsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayTopUpDataFactory: TangemPayTopUpDataFactory,
|
||||
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>()
|
||||
|
|
@ -24,7 +24,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
|
|||
val uiState: TangemPayAddFundsUM = getInitialState()
|
||||
|
||||
private fun getInitialState(): TangemPayAddFundsUM {
|
||||
val data = tangemPayTopUpDataFactory.create(
|
||||
val data = tangemPaySwapDataFactory.create(
|
||||
depositAddress = params.depositAddress,
|
||||
chainId = params.chainId,
|
||||
cryptoBalance = params.cryptoBalance,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.pay.TangemPayTopUpData
|
||||
import com.tangem.domain.pay.TangemPaySwapDataFactory
|
||||
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.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
|
|
@ -64,6 +66,8 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
private val uiMessageSender: UiMessageSender,
|
||||
private val cardDetailsEventListener: CardDetailsEventListener,
|
||||
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
|
||||
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
|
||||
private val orderRepository: CustomerOrderRepository,
|
||||
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
|
||||
|
||||
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
|
||||
|
|
@ -199,22 +203,63 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
bottomSheetNavigation.activate(
|
||||
TangemPayDetailsNavigation.AddFunds(
|
||||
walletId = params.userWalletId,
|
||||
fiatBalance = requireNotNull(balance).balance,
|
||||
// Using the fiat balance as crypto balance. This will be changed when back returns crypto balance
|
||||
cryptoBalance = requireNotNull(balance).balance,
|
||||
depositAddress = requireNotNull(params.config.depositAddress),
|
||||
fiatBalance = currentBalance.fiatBalance,
|
||||
cryptoBalance = currentBalance.cryptoBalance,
|
||||
depositAddress = depositAddress,
|
||||
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 {
|
||||
return modelScope.launch {
|
||||
val result = try {
|
||||
|
|
@ -294,6 +339,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
cryptoAmount = data.cryptoBalance,
|
||||
fiatAmount = data.fiatBalance,
|
||||
depositAddress = data.depositAddress,
|
||||
isWithdrawal = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ internal class DetailsBalanceTransformer(
|
|||
|
||||
private fun getBalanceText(balance: TangemPayCardBalance): String {
|
||||
val currency = Currency.getInstance(balance.currencyCode)
|
||||
return balance.balance.format {
|
||||
return balance.fiatBalance.format {
|
||||
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
|
|||
internal interface TangemPayDetailIntents {
|
||||
fun onRefreshSwipe(refreshState: ShowRefreshState)
|
||||
fun onClickAddFunds()
|
||||
fun onClickWithdraw()
|
||||
fun onClickChangePin()
|
||||
fun onClickFreezeCard()
|
||||
fun onClickUnfreezeCard()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,32 @@ internal object TangemPayMessagesFactory {
|
|||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -160,5 +160,7 @@ interface TangemSdkManager {
|
|||
): CompletionResult<VisaSignedDataByCustomerWallet>
|
||||
|
||||
suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult<TangemPayInitialCredentials>
|
||||
|
||||
suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String>
|
||||
// endregion
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue