From 37023a15b42863f791dfc27b8ecb964119d416c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Mar 2026 15:33:44 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 118 +++++++------- .../DefaultTangemPayWithdrawRepository.kt | 149 +++++++++++------- .../tangempay/model/TangemPayDetailsModel.kt | 9 -- .../subscribers/TangemPayMainSubscriber.kt | 6 + 4 files changed, 153 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 0d9a4eba92..595d7ccdcb 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -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(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + 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(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = appPreferencesStore.getObjectMapSync( + 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(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + 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" diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt index 7bf21432a7..08686ded72 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -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() - private val withdrawPollingMutex = Mutex() + private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val pollingJobs = mutableMapOf() + 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) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index c9e17d71f5..4ada6b4b01 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -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) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 1dcc11ff2a..04f33d8338 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -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() }