Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-13 15:33:44 +04:00
parent 6c7c9bc3cb
commit 37023a15b4
4 changed files with 153 additions and 129 deletions

View file

@ -62,23 +62,19 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
withContext(dispatcherProvider.io) {
appPreferencesStore
.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
}
appPreferencesStore
.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
}
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId))
.takeIf { !it.isNullOrEmpty() }
}
return appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId),
)
.takeIf { !it.isNullOrEmpty() }
}
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
}
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
}
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) =
@ -92,58 +88,57 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
?.decodeToString(throwOnInvalidSequence = true)
?.let(tokensAdapter::fromJson)
return withContext(dispatcherProvider.io) {
val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
?.decodeToString(throwOnInvalidSequence = true)
?.let(tokensAdapter::fromJson)
return authTokens?.let { tokens ->
if (tokens.idempotencyKey == null) {
val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
storeAuthTokens(customerWalletAddress, newAuthTokens)
newAuthTokens
} else {
tokens
authTokens?.let { tokens ->
if (tokens.idempotencyKey == null) {
val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
storeAuthTokens(customerWalletAddress, newAuthTokens)
newAuthTokens
} else {
tokens
}
}
}
}
override suspend fun clearAuthTokens(customerWalletAddress: String) {
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
withContext(dispatcherProvider.io) {
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
}
}
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
}
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
}
override suspend fun getOrderId(customerWalletAddress: String): String? {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
.takeIf { !it.isNullOrEmpty() }
}
return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
.takeIf { !it.isNullOrEmpty() }
}
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
) == true
}
return appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
) == true
}
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
}
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
}
override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) {
override suspend fun clearOrderId(customerWalletAddress: String) {
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
}
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) {
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), isPaeraCustomer)
appPreferencesStore.store(
PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId),
isPaeraCustomer,
)
}
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
@ -152,7 +147,9 @@ internal class DefaultTangemPayStorage @Inject constructor(
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
val orders = mutablePreferences.getObjectMap<String>(
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
)
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
@ -179,7 +176,9 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
val orders = appPreferencesStore.getObjectMapSync<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
val orders = appPreferencesStore.getObjectMapSync<String>(
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
)
return orders[createWithdrawOrderIdKey(userWalletId)]
}
@ -191,7 +190,9 @@ internal class DefaultTangemPayStorage @Inject constructor(
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
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,
@ -221,40 +222,33 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
}
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
}
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
) == true
}
return appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
) == true
}
override suspend fun storeTangemPayEligibility(eligibility: Boolean) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility)
}
appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility)
}
override suspend fun getTangemPayEligibility(): Boolean {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false)
}
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false)
}
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
withContext(dispatcherProvider.io) {
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
}
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
}
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"

View file

@ -23,7 +23,13 @@ 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.*
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.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -36,6 +42,8 @@ import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
private const val TAG = "TangemPaySwapRepository"
private const val MAX_POLLING_ATTEMPTS = 6
private data class PollingKey(val userWalletId: String, val orderId: String)
@Suppress("LongParameterList")
internal class DefaultTangemPayWithdrawRepository @Inject constructor(
@ -48,9 +56,9 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
private val orderRepository: CustomerOrderRepository,
) : TangemPayWithdrawRepository {
private val withdrawPollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val withdrawPollingJobs = mutableMapOf<String, Job>()
private val withdrawPollingMutex = Mutex()
private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val pollingJobs = mutableMapOf<PollingKey, Job>()
private val pollingMutex = Mutex()
override suspend fun withdraw(
userWallet: UserWallet,
@ -121,6 +129,76 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
)
} else {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
startPollingForOrder(userWallet, orderId, exchangeData)
}
}
}
private fun startPollingForOrder(
userWallet: UserWallet,
orderId: String,
exchangeData: TangemPayWithdrawExchangeState,
) {
pollingScope.launch {
startWithdrawOrderPolling(userWallet, orderId, exchangeData)
}
}
private suspend fun startWithdrawOrderPolling(
userWallet: UserWallet,
orderId: String,
exchangeData: TangemPayWithdrawExchangeState,
) {
val key = PollingKey(userWallet.walletId.stringValue, orderId)
pollingMutex.withLock {
if (pollingJobs.containsKey(key)) return@withLock
val pollingJob = pollingScope.launch {
try {
var attemptCount = 0
while (isActive && attemptCount < MAX_POLLING_ATTEMPTS) {
delay(duration = 3.seconds)
attemptCount++
val result = orderRepository.getOrderData(
userWalletId = userWallet.walletId,
orderId = orderId,
)
val txHash = result.getOrNull()?.withdrawTxHash?.ifEmpty { null }
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
txHash = txHash,
exchangeData = exchangeData,
orderId = orderId,
)
pollingMutex.withLock { pollingJobs.remove(key) }
return@launch
}
}
if (attemptCount >= MAX_POLLING_ATTEMPTS) {
Timber.tag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts")
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId)
pollingMutex.withLock { pollingJobs.remove(key) }
}
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
Timber.tag(TAG).e(exception)
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId)
pollingMutex.withLock { pollingJobs.remove(key) }
}
}
pollingJobs[key] = pollingJob
}
}
private fun stopPolling(userWalletId: String, orderId: String) {
pollingScope.launch {
pollingMutex.withLock {
val key = PollingKey(userWalletId, orderId)
pollingJobs[key]?.cancel()
pollingJobs.remove(key)
}
}
}
@ -140,7 +218,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
txHash = txHash,
payInExtraId = exchangeData.payInExtraId,
).also {
tangemPayStorage.deleteWithdrawOrder(userWallet.walletId, orderId)
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId)
stopPolling(userWalletId = userWallet.walletId.stringValue, orderId = orderId)
}
}
@ -157,14 +236,12 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
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)
}
try {
pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state)
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
Timber.tag(TAG).e(exception)
}
}
}
@ -189,51 +266,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData, orderId = orderId)
} else {
startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData)
}
return
}
private suspend fun startWithdrawOrderPolling(
userWallet: UserWallet,
orderId: String,
exchangeData: TangemPayWithdrawExchangeState,
) {
withdrawPollingMutex.withLock {
if (withdrawPollingJobs.containsKey(orderId)) return
val pollingJob = withdrawPollingScope.launch {
try {
while (isActive) {
delay(duration = 5.seconds)
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,
orderId = orderId,
)
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
.onLeft { error ->
Timber.tag(TAG).e("getOrderData error ${error.errorCode}")
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
}
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
Timber.tag(TAG).e(exception)
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
}
}
withdrawPollingJobs[orderId] = pollingJob
startPollingForOrder(userWallet, orderId, exchangeData)
}
}

View file

@ -125,7 +125,6 @@ internal class TangemPayDetailsModel @Inject constructor(
modelScope.launch {
expressTransactionsEventListener.send(ExpressTransactionsEvent.Update)
}
subscribeToWithdrawOrder()
}
fun onPause() {
@ -150,14 +149,6 @@ internal class TangemPayDetailsModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeToWithdrawOrder() {
modelScope.launch {
val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull()
?: return@launch
tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet)
}
}
override fun onClickPinCode() {
analytics.send(TangemPayAnalyticsEvents.PinCodeClicked())
if (!params.config.isPinSet) {

View file

@ -6,6 +6,7 @@ import com.tangem.domain.pay.model.MainCustomerInfoContentState
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
@ -19,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
@Suppress("LongParameterList")
@ -28,10 +30,14 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
private val clickIntents: WalletClickIntents,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val tangemPayWithdrawRepository: TangemPayWithdrawRepository,
private val analytics: WalletTangemPayAnalyticsEventSender,
) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> {
coroutineScope.launch {
tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet)
}
return subscribeOnTangemPayInfoUpdates()
}