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 5708a79d8b..42450edff8 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -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( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - .plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data)) + val orders = mutablePreferences.getObjectMap(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( - 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>> = 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(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + return orders[createWithdrawOrderIdKey(userWalletId)] + } + + override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List { + val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) + val adapter: JsonAdapter>> = 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(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( PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index e0c785a2e8..c70f061ee1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -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") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index bd29637e98..25a33b8970 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -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? + + /** 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 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 cea8e61d60..922b617842 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 @@ -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 { 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 { - 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 diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt index 18b09d331e..6079a4e874 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt @@ -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 + suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) } \ No newline at end of file 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 793549861d..beb2bed482 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 @@ -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) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt index e888745a6f..950bd50f41 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt @@ -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 {