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.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"
|
||||||
}
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
cryptoAmount = tangemPayInput.cryptoAmount,
|
||||||
fiatAmount = tangemPayInput.fiatAmount,
|
fiatAmount = tangemPayInput.fiatAmount,
|
||||||
depositAddress = tangemPayInput.depositAddress,
|
depositAddress = tangemPayInput.depositAddress,
|
||||||
|
isWithdrawal = tangemPayInput.isWithdrawal,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
@ -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")
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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">Can’t 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">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_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">We’re fixing a technical issue. Please try again later.</string>
|
<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_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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -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.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?
|
||||||
}
|
}
|
||||||
|
|
@ -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.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()
|
||||||
|
|
|
||||||
|
|
@ -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 cryptoAmount: BigDecimal,
|
||||||
val fiatAmount: BigDecimal,
|
val fiatAmount: BigDecimal,
|
||||||
val depositAddress: String,
|
val depositAddress: String,
|
||||||
|
val isWithdrawal: Boolean,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue