Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-04 19:22:23 +01:00
parent c95c3abe5f
commit 84f5d99917
7 changed files with 104 additions and 86 deletions

View file

@ -7,10 +7,13 @@ import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingFeatureToggles: StakingFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingRepository {
override fun getStakingAvailability(blockchainId: String): StakingAvailability {
@ -24,13 +27,15 @@ internal class DefaultStakingRepository(
}
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo {
val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow()
return withContext(dispatchers.io) {
val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow()
return StakingEntryInfo(
interestRate = yield.apy,
periodInDays = yield.metadata.cooldownPeriod.days,
tokenSymbol = yield.token.symbol,
)
StakingEntryInfo(
interestRate = yield.apy,
periodInDays = yield.metadata.cooldownPeriod.days,
tokenSymbol = yield.token.symbol,
)
}
}
companion object {

View file

@ -4,6 +4,7 @@ import com.tangem.data.staking.DefaultStakingRepository
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,10 +20,12 @@ internal object StakingDataModule {
fun provideStakingRepository(
stakeKitApi: StakeKitApi,
stakingFeatureToggles: StakingFeatureToggles,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
stakingFeatureToggles = stakingFeatureToggles,
dispatchers = coroutineDispatcherProvider,
)
}
}

View file

@ -11,12 +11,16 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.SavedSwapTransactionListConverter
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class DefaultSwapTransactionRepository(
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : SwapTransactionRepository {
private val converter = SavedSwapTransactionListConverter()
@ -70,28 +74,30 @@ class DefaultSwapTransactionRepository(
cryptoCurrencyId: CryptoCurrency.ID,
scanResponse: ScanResponse,
): Flow<List<SavedSwapTransactionListModel>?> {
val txStatuses = appPreferencesStore.getObjectMap<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
return appPreferencesStore.getObjectList<SavedSwapTransactionListModelInner>(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
).map { savedTransactions ->
val currencyTxs = savedTransactions
?.filter {
it.userWalletId == userWalletId.stringValue &&
(
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
return withContext(dispatchers.io) {
val txStatuses = appPreferencesStore.getObjectMap<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
appPreferencesStore.getObjectList<SavedSwapTransactionListModelInner>(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
).map { savedTransactions ->
val currencyTxs = savedTransactions
?.filter {
it.userWalletId == userWalletId.stringValue &&
(
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
currencyTxs?.mapNotNull {
converter.convertBack(
value = it,
scanResponse = scanResponse,
txStatuses = txStatuses,
)
}
currencyTxs?.mapNotNull {
converter.convertBack(
value = it,
scanResponse = scanResponse,
txStatuses = txStatuses,
)
}
}.flowOn(dispatchers.io)
}
}

View file

@ -51,9 +51,13 @@ internal class SwapDataModule {
@Provides
@Singleton
fun provideSwapTransactionRepository(appPreferencesStore: AppPreferencesStore): SwapTransactionRepository {
fun provideSwapTransactionRepository(
appPreferencesStore: AppPreferencesStore,
dispatcherProvider: CoroutineDispatcherProvider,
): SwapTransactionRepository {
return DefaultSwapTransactionRepository(
appPreferencesStore = appPreferencesStore,
dispatchers = dispatcherProvider,
)
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
@ -41,6 +42,6 @@ internal class TokenDetailsFragment : ComposeFragment() {
setSystemBarsColor(systemBarsColor)
}
TokenDetailsScreen(state = viewModel.uiState)
TokenDetailsScreen(state = viewModel.uiState.collectAsStateWithLifecycle().value)
}
}

View file

@ -18,8 +18,8 @@ import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListMode
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
import com.tangem.utils.Provider
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf

View file

@ -1,8 +1,5 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import androidx.paging.cachedIn
import arrow.core.getOrElse
@ -16,8 +13,8 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModel
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -140,7 +137,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState },
currentStateProvider = Provider { uiState.value },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
stakingAvailabilityProvider = Provider {
getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value)
@ -162,7 +159,7 @@ internal class TokenDetailsViewModel @Inject constructor(
clickIntents = this,
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
analyticsEventsHandlerProvider = Provider { analyticsEventsHandler },
currentStateProvider = Provider { uiState },
currentStateProvider = Provider { uiState.value },
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
@ -179,8 +176,8 @@ internal class TokenDetailsViewModel @Inject constructor(
TokenDetailsCurrencyStatusAnalyticsSender(analyticsEventsHandler)
}
var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency))
private set
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
val uiState: StateFlow<TokenDetailsState> = internalUiState
init {
deepLinksRegistry.registerWithViewModel(
@ -222,7 +219,7 @@ internal class TokenDetailsViewModel @Inject constructor(
getBalanceHidingSettingsUseCase()
.flowWithLifecycle(owner.lifecycle)
.onEach {
uiState = stateFactory.getStateWithUpdatedHidden(
internalUiState.value = stateFactory.getStateWithUpdatedHidden(
isBalanceHidden = it.isBalanceHidden,
)
}
@ -236,13 +233,15 @@ internal class TokenDetailsViewModel @Inject constructor(
)
.conflate()
.distinctUntilChanged()
.onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) }
.flowOn(dispatchers.io)
.onEach {
internalUiState.value = stateFactory.getManageButtonsState(actions = it.states)
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
}
private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) {
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
getCurrencyWarningsUseCase.invoke(
userWalletId = userWalletId,
currencyStatus = cryptoCurrencyStatus,
@ -252,8 +251,8 @@ internal class TokenDetailsViewModel @Inject constructor(
.distinctUntilChanged()
.onEach {
val updatedState = stateFactory.getStateWithNotifications(it)
notificationsAnalyticsSender.send(uiState, updatedState.notifications)
uiState = updatedState
notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications)
internalUiState.value = updatedState
}
.launchIn(viewModelScope)
.saveIn(warningsJobHolder)
@ -261,7 +260,7 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private fun subscribeOnCurrencyStatusUpdates() {
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
@ -269,7 +268,7 @@ internal class TokenDetailsViewModel @Inject constructor(
)
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
uiState = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
maybeCurrencyStatus.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(currencyStatus = status)
@ -277,14 +276,14 @@ internal class TokenDetailsViewModel @Inject constructor(
}
currencyStatusAnalyticsSender.send(maybeCurrencyStatus)
}
.flowOn(dispatchers.io)
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
}
}
private fun subscribeOnExchangeTransactionsUpdates() {
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
swapTxStatusTaskScheduler.cancelTask()
exchangeStatusFactory.invoke()
.distinctUntilChanged()
@ -296,8 +295,8 @@ internal class TokenDetailsViewModel @Inject constructor(
PeriodicTask(
delay = EXCHANGE_STATUS_UPDATE_DELAY,
task = {
runCatching(dispatchers.io) {
exchangeStatusFactory.updateSwapTxStatuses(uiState.swapTxs)
runCatching {
exchangeStatusFactory.updateSwapTxStatuses(internalUiState.value.swapTxs)
}
},
onSuccess = ::updateSwapTx,
@ -305,20 +304,20 @@ internal class TokenDetailsViewModel @Inject constructor(
),
)
}
.flowOn(dispatchers.io)
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(swapTxJobHolder)
}
}
private fun updateSwapTx(swapTxs: PersistentList<SwapTransactionsState>) {
val config = uiState.bottomSheetConfig
val config = internalUiState.value.bottomSheetConfig
val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig
val currentTx = swapTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId }
if (currentTx?.activeStatus == ExchangeStatus.Finished) {
updateNetworkToSwapBalance(currentTx.toCryptoCurrency)
}
uiState = uiState.copy(
internalUiState.value = internalUiState.value.copy(
swapTxs = swapTxs,
bottomSheetConfig = currentTx?.let(
stateFactory::updateStateWithExchangeStatusBottomSheet,
@ -341,7 +340,7 @@ internal class TokenDetailsViewModel @Inject constructor(
* @param showItemsLoading - show loading items placeholder.
*/
private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) {
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
userWalletId = userWalletId,
currency = cryptoCurrency,
@ -349,9 +348,9 @@ internal class TokenDetailsViewModel @Inject constructor(
// if countEither is left, handling error state run inside getLoadingTxHistoryState
if (showItemsLoading || txHistoryItemsCountEither.isLeft()) {
uiState = stateFactory.getLoadingTxHistoryState(
internalUiState.value = stateFactory.getLoadingTxHistoryState(
itemsCountEither = txHistoryItemsCountEither,
pendingTransactions = uiState.pendingTxs,
pendingTransactions = internalUiState.value.pendingTxs,
)
}
@ -362,24 +361,24 @@ internal class TokenDetailsViewModel @Inject constructor(
refresh = refresh,
).map { it.cachedIn(viewModelScope) }
uiState = stateFactory.getLoadedTxHistoryState(maybeTxHistory)
internalUiState.value = stateFactory.getLoadedTxHistoryState(maybeTxHistory)
}
}
}
private fun updateStakingInfo() {
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value)
if (stakingAvailability is StakingAvailability.Available) {
val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId)
uiState = stateFactory.getStateWithStaking(stakingInfo)
internalUiState.value = stateFactory.getStateWithStaking(stakingInfo)
}
}
}
private fun updateTopBarMenu() {
viewModelScope.launch(dispatchers.main) {
uiState = stateFactory.getStateWithUpdatedMenu(
internalUiState.value = stateFactory.getStateWithUpdatedMenu(
cardTypesResolver = userWallet.scanResponse.cardTypesResolver,
isBitcoin = isBitcoin(cryptoCurrency.network.id.value),
)
@ -429,7 +428,7 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onReloadClick() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReload(cryptoCurrency.symbol))
uiState = stateFactory.getLoadingTxHistoryState()
internalUiState.value = stateFactory.getLoadingTxHistoryState()
updateTxHistory(refresh = true, showItemsLoading = true)
}
@ -475,7 +474,7 @@ internal class TokenDetailsViewModel @Inject constructor(
feeCurrencyStatus: CryptoCurrencyStatus?,
transactionInfo: TransactionInfo?,
) {
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
val maybeCoinStatus = getNetworkCoinStatusUseCase(
userWalletId = userWalletId,
networkId = tokenCurrency.network.id,
@ -507,11 +506,11 @@ internal class TokenDetailsViewModel @Inject constructor(
if (handleUnavailabilityReason(unavailabilityReason)) return
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol))
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
uiState = stateFactory.getStateWithReceiveBottomSheet(
internalUiState.value = stateFactory.getStateWithReceiveBottomSheet(
currency = cryptoCurrency,
networkAddress = networkAddress,
sendCopyAnalyticsEvent = {
@ -568,7 +567,7 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onDismissDialog() {
uiState = stateFactory.getStateWithClosedDialog()
internalUiState.value = stateFactory.getStateWithClosedDialog()
}
override fun onHideClick() {
@ -576,7 +575,7 @@ internal class TokenDetailsViewModel @Inject constructor(
viewModelScope.launch {
val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(userWalletId, cryptoCurrency)
uiState = if (hasLinkedTokens) {
internalUiState.value = if (hasLinkedTokens) {
stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency)
} else {
stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency)
@ -600,10 +599,10 @@ internal class TokenDetailsViewModel @Inject constructor(
private fun openExplorer() {
val currencyStatus = cryptoCurrencyStatus ?: return
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
when (val addresses = currencyStatus.value.networkAddress) {
is NetworkAddress.Selectable -> {
uiState = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
internalUiState.value = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
}
is NetworkAddress.Single -> {
router.openUrl(
@ -622,11 +621,11 @@ internal class TokenDetailsViewModel @Inject constructor(
private fun showErrorIfDemoModeOrElse(action: () -> Unit) {
viewModelScope.launch(dispatchers.main) {
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
uiState = stateFactory.getStateWithClosedBottomSheet()
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
internalUiState.value = stateFactory.getStateAndTriggerEvent(
state = internalUiState.value,
errorMessage = resourceReference(id = R.string.alert_demo_feature_disabled),
setUiState = { uiState = it },
setUiState = { internalUiState.value = it },
)
} else {
action()
@ -643,7 +642,7 @@ internal class TokenDetailsViewModel @Inject constructor(
addressType = AddressType.valueOf(addressModel.type.name),
),
)
uiState = stateFactory.getStateWithClosedBottomSheet()
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
}
}
@ -660,9 +659,9 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onRefreshSwipe() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Refreshed(cryptoCurrency.symbol))
uiState = stateFactory.getRefreshingState()
internalUiState.value = stateFactory.getRefreshingState()
viewModelScope.launch(dispatchers.io) {
viewModelScope.launch(dispatchers.main) {
listOf(
async {
fetchCurrencyStatusUseCase(
@ -674,32 +673,32 @@ internal class TokenDetailsViewModel @Inject constructor(
async {
updateTxHistory(
refresh = true,
showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content,
showItemsLoading = internalUiState.value.txHistoryState !is TxHistoryState.Content,
)
subscribeOnExchangeTransactionsUpdates()
},
).awaitAll()
uiState = stateFactory.getRefreshedState()
internalUiState.value = stateFactory.getRefreshedState()
}.saveIn(refreshStateJobHolder)
}
override fun onDismissBottomSheet() {
if (uiState.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) {
if (internalUiState.value.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) {
viewModelScope.launch(dispatchers.main) {
uiState = exchangeStatusFactory.removeTransactionOnBottomSheetClosed()
internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed()
}
}
uiState = stateFactory.getStateWithClosedBottomSheet()
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
}
override fun onCloseRentInfoNotification() {
uiState = stateFactory.getStateWithRemovedRentNotification()
internalUiState.value = stateFactory.getStateWithRemovedRentNotification()
}
override fun onSwapTransactionClick(txId: String) {
val swapTxState = uiState.swapTxs.first { it.txId == txId }
val swapTxState = internalUiState.value.swapTxs.first { it.txId == txId }
analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTxStatusOpened(cryptoCurrency.symbol))
uiState = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState)
internalUiState.value = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState)
}
override fun onGoToProviderClick(url: String) {
@ -747,7 +746,7 @@ internal class TokenDetailsViewModel @Inject constructor(
ifLeft = { e ->
when (e) {
is AssociateAssetError.NotEnoughBalance -> {
uiState = stateFactory.getStateWithErrorDialog(
internalUiState.value = stateFactory.getStateWithErrorDialog(
resourceReference(
id = R.string.warning_hedera_token_association_not_enough_hbar_message,
formatArgs = wrappedList(e.feeCurrency.symbol),
@ -757,7 +756,7 @@ internal class TokenDetailsViewModel @Inject constructor(
is AssociateAssetError.DataError -> Timber.e(e.message)
}
},
ifRight = { uiState = stateFactory.getStateWithRemovedHederaAssociateNotification() },
ifRight = { internalUiState.value = stateFactory.getStateWithRemovedHederaAssociateNotification() },
)
}
}
@ -765,7 +764,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false
uiState = stateFactory.getStateWithActionButtonErrorDialog(unavailabilityReason)
internalUiState.value = stateFactory.getStateWithActionButtonErrorDialog(unavailabilityReason)
return true
}