Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-24 13:38:12 +04:00
parent 8f4a8b18ef
commit 319fd373b5
7 changed files with 136 additions and 133 deletions

View file

@ -1,7 +1,9 @@
package com.tangem.tap.data
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.data.pay.entity.WithdrawStoreData
import com.tangem.data.pay.util.WithdrawStateConverter
import com.tangem.data.pay.util.WithdrawStoreDataConverter
@ -19,6 +21,7 @@ import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import java.util.UUID
import javax.inject.Inject
@ -137,28 +140,56 @@ internal class DefaultTangemPayStorage @Inject constructor(
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
}
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<TangemPayWithdrawState>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
)
.plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data))
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
value = orders,
)
}
}
override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? {
val orders = appPreferencesStore.getObjectMapSync<WithdrawStoreData>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
)
val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null
return withdrawStateConverter.convert(data)
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType)
val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> = appPreferencesStore.moshi.adapter(mapType)
appPreferencesStore.editData { prefs ->
val walletKey = createWithdrawOrderIdKey(userWalletId)
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson).orEmpty()
val updatedList = currentMap[walletKey].orEmpty() + withdrawStoreDataConverter.convert(data)
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter
.toJson(currentMap + (walletKey to updatedList))
}
}
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) {
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
val orders = appPreferencesStore.getObjectMapSync<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
return orders[createWithdrawOrderIdKey(userWalletId)]
}
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState> {
val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType)
val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> = appPreferencesStore.moshi.adapter(mapType)
val map = appPreferencesStore.data.firstOrNull()
?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty()
return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert)
}
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
.minus(createWithdrawOrderIdKey(userWalletId))
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
value = orders,
)
}
}
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<WithdrawStoreData>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,

View file

@ -161,7 +161,10 @@ object PreferencesKeys {
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
}
val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") }
val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrdersKey") }
val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy {
stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey")
}
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") }
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")

View file

@ -29,11 +29,25 @@ interface TangemPayStorage {
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean)
suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean?
/** Called after creating withdraw order, active order id */
suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String)
/** Called after creating withdraw order, saves order data */
suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState)
suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState?
/** Returns single active order id. Once the order is completed, deletes id from storage.
* Only one active order allowed for a wallet */
suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String?
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId)
/** Returns all withdraw orders saved.
* Once we get tx hash for an order, it gets deleted from this storage */
suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState>?
/** Deletes active withdraw order. Called after order is completed */
suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId)
/** Deletes withdraw order data. Called after getting its tx hash */
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String)
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean

View file

@ -2,7 +2,6 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.datasource.api.pay.TangemPayApi
@ -17,7 +16,6 @@ import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
@ -25,13 +23,7 @@ import com.tangem.domain.visa.error.VisaApiError
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.utils.extensions.addHexPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -40,7 +32,6 @@ import java.math.RoundingMode
import java.util.Currency
import java.util.Locale
import javax.inject.Inject
import kotlin.collections.set
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
@ -113,23 +104,18 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
) {
val orderId = response.result?.orderId
if (orderId != null) {
val orderData = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val withdrawTxHash = orderData?.withdrawTxHash
tangemPayStorage.storeActiveWithdrawOrderId(userWalletId = userWallet.walletId, orderId = orderId)
val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val withdrawTxHash = order?.withdrawTxHash
val storeData = TangemPayWithdrawState(
orderId = orderId,
exchangeData = exchangeData,
)
if (orderData != null && !withdrawTxHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = withdrawTxHash,
orderId = orderId,
exchangeData = exchangeData,
order = orderData,
).onLeft {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
if (order != null && !withdrawTxHash.isNullOrEmpty()) {
finalizeWithdraw(userWallet = userWallet, txHash = withdrawTxHash, exchangeData = exchangeData)
.onLeft {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
} else {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
@ -138,10 +124,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
private suspend fun finalizeWithdraw(
userWallet: UserWallet,
withdrawTxHash: String,
orderId: String,
txHash: String,
exchangeData: TangemPayWithdrawExchangeState,
order: OrderData,
): Either<ExpressDataError, Unit> {
return swapRepository.exchangeSent(
userWallet = userWallet,
@ -149,89 +133,63 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
fromNetwork = exchangeData.fromNetwork,
fromAddress = exchangeData.fromAddress,
payInAddress = exchangeData.payInAddress,
txHash = withdrawTxHash,
txHash = txHash,
payInExtraId = exchangeData.payInExtraId,
)
.onRight {
val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING
if (!isActive) {
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId)
} else {
tangemPayStorage.storeWithdrawOrder(
userWalletId = userWallet.walletId,
data = TangemPayWithdrawState(orderId = orderId, exchangeData = null),
)
}
}
.onLeft { error ->
Timber.tag(TAG).e(error.toString())
}
}
override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean {
val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId)
if (orderExchangeData == null) return false
val exchangeData = orderExchangeData.exchangeData
val orderData = orderRepository
.getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull()
val withdrawTxHash = orderData?.withdrawTxHash
if (exchangeData != null && orderData != null && withdrawTxHash != null) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = withdrawTxHash,
orderId = orderExchangeData.orderId,
exchangeData = exchangeData,
order = orderData,
)
val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWallet.walletId)
if (orderId.isNullOrEmpty()) return false
val orderData = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val isActive = orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING
if (!isActive) {
tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWallet.walletId)
}
return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING
return isActive
}
override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either<VisaApiError, Unit> {
val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right()
val exchangeData = storeData.exchangeData ?: return Unit.right()
val orderId = storeData.orderId
val order = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
?: return Unit.right()
override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) {
tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state ->
withdrawPollingScope.launch {
try {
pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state)
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
Timber.tag(TAG).e(exception)
}
}
}
}
private suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet, data: TangemPayWithdrawState) {
val exchangeData = data.exchangeData ?: return
val orderId = data.orderId
val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
?: return
val txHash = order.withdrawTxHash
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = txHash,
orderId = storeData.orderId,
exchangeData = exchangeData,
order = order,
).onLeft {
startWithdrawOrderPolling(
userWallet = userWallet,
orderId = orderId,
storeData = storeData,
exchangeData = exchangeData,
)
}
finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData)
.onRight {
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId)
}
.onLeft {
startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData)
}
} else {
startWithdrawOrderPolling(
userWallet = userWallet,
orderId = orderId,
storeData = storeData,
exchangeData = exchangeData,
)
startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData)
}
return Unit.right()
return
}
private suspend fun startWithdrawOrderPolling(
userWallet: UserWallet,
orderId: String,
storeData: TangemPayWithdrawState,
exchangeData: TangemPayWithdrawExchangeState,
) {
withdrawPollingMutex.withLock {
@ -239,33 +197,33 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
val pollingJob = withdrawPollingScope.launch {
try {
while (isActive && withdrawPollingJobs.containsKey(orderId)) {
while (isActive) {
delay(duration = 5.seconds)
val orderData = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId)
orderData.onRight { order ->
if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) {
tangemPayStorage.deleteWithdrawOrder(userWallet.walletId)
withdrawPollingJobs.remove(key = orderId)
orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId)
.onRight { order ->
val txHash = order.withdrawTxHash
if (txHash.isNullOrEmpty()) return@onRight
finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData)
.onRight {
tangemPayStorage.deleteWithdrawOrder(
userWalletId = userWallet.walletId,
orderId = orderId,
)
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
.onLeft { error ->
Timber.tag(TAG).e("finalizeWithdraw error: $error")
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
}
.onLeft { error ->
Timber.tag(TAG).e("getOrderData error ${error.errorCode}")
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
val txHash = order.withdrawTxHash
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = txHash,
orderId = storeData.orderId,
exchangeData = exchangeData,
order = order,
).onRight {
withdrawPollingJobs.remove(key = orderId)
return@launch
}
}
}.onLeft { error ->
Timber.tag(TAG).e("error ${error.errorCode}")
}
}
} catch (exception: CancellationException) {
throw exception

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.visa.error.VisaApiError
import java.math.BigDecimal
interface TangemPayWithdrawRepository {
@ -21,5 +20,5 @@ interface TangemPayWithdrawRepository {
suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean
suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either<VisaApiError, Unit>
suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet)
}

View file

@ -153,7 +153,7 @@ internal class TangemPayDetailsModel @Inject constructor(
modelScope.launch {
val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull()
?: return@launch
tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet)
tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet)
}
}

View file

@ -25,14 +25,12 @@ internal class ExpressStateFactory(
fun getStateWithClosedDialog(): ExpressTransactionsBlockState {
val state = currentStateProvider()
val slot = state.dialogSlot ?: return state
return state.copy(dialogSlot = slot.copy(config = slot.config.copy(isShow = false)))
return state.copy(dialogSlot = null)
}
fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState {
val state = currentStateProvider()
val slot = state.bottomSheetSlot ?: return state
return state.copy(bottomSheetSlot = slot.copy(config = slot.config.copy(isShown = false)))
return state.copy(bottomSheetSlot = null)
}
fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState {