Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-27 15:13:54 +08:00
parent 422afbc17f
commit 5384cf75ad
12 changed files with 961 additions and 0 deletions

View file

@ -0,0 +1,62 @@
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import arrow.core.Either
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import dagger.hilt.android.scopes.ViewModelScoped
import java.math.BigDecimal
import javax.inject.Inject
@ViewModelScoped
internal class TokenListAnalyticsSender @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun send(maybeTokenList: Either<TokenListError, TokenList>) {
val tokenList = (maybeTokenList as? Either.Right)?.value ?: return
createCardBalanceState(tokenList)?.let {
analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it))
}
}
private fun createCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState? {
return when (val fiatBalance = tokenList.totalFiatBalance) {
is TokenList.FiatBalance.Failed -> fiatBalance.toCardBalanceState(tokenList)
is TokenList.FiatBalance.Loaded -> fiatBalance.toCardBalanceState()
TokenList.FiatBalance.Loading -> null
}
}
private fun TokenList.FiatBalance.Failed.toCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState {
val currenciesStatuses = when (tokenList) {
is TokenList.Empty -> emptyList()
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
}
return when {
currenciesStatuses.isEmpty() -> AnalyticsParam.CardBalanceState.Empty
currenciesStatuses.any { it.value is CryptoCurrencyStatus.NoQuote } -> {
AnalyticsParam.CardBalanceState.NoRate
}
else -> AnalyticsParam.CardBalanceState.BlockchainError
}
}
private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState? {
return if (amount > BigDecimal.ZERO) {
AnalyticsParam.CardBalanceState.Full
} else if (amount.isZero()) {
AnalyticsParam.CardBalanceState.Empty
} else {
null
}
}
}

View file

@ -0,0 +1,203 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.Either
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOf
import timber.log.Timber
import javax.inject.Inject
@ViewModelScoped
internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
) {
private var readyForRateAppNotification = false
fun create(clickIntents: WalletClickIntentsV2): Flow<ImmutableList<WalletNotification>> {
val userWallet = getSelectedWalletSyncUseCase().fold(
ifLeft = {
Timber.e("Failed to get selected wallet $it")
return flowOf(value = persistentListOf())
},
ifRight = { it },
)
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
return combine(
flow = getTokenListUseCase(userWallet.walletId).conflate(),
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
// flow4 = getMissedAddressCryptoCurrenciesUseCase(userWallet.walletId).conflate(),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup ->
// maybeTokenList.onRight { Timber.e(it.toString()) }
// maybeMissedAddressCurrencies.onRight { Timber.e(it.toString()) }
readyForRateAppNotification = true
buildList {
addCriticalNotifications(cardTypesResolver)
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents)
addRateTheAppNotification(isReadyToShowRating, clickIntents)
}.toImmutableList()
}
}
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
addIf(
element = WalletNotification.Critical.DevCard,
condition = !cardTypesResolver.isReleaseFirmwareType(),
)
addIf(
element = WalletNotification.Critical.FailedCardValidation,
condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(),
)
cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures ->
addIf(
element = WalletNotification.Warning.LowSignatures(count = remainingSignatures),
condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT,
)
}
}
private fun MutableList<WalletNotification>.addInformationalNotifications(
cardTypesResolver: CardTypesResolver,
maybeTokenList: Either<TokenListError, TokenList>,
clickIntents: WalletClickIntentsV2,
) {
addIf(
element = WalletNotification.Informational.DemoCard,
condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
)
addMissingAddressesNotification(maybeTokenList, clickIntents)
}
private fun MutableList<WalletNotification>.addMissingAddressesNotification(
maybeTokenList: Either<TokenListError, TokenList>,
clickIntents: WalletClickIntentsV2,
) {
val currencies = maybeTokenList.getMissingAddressCurrencies()
addIf(
element = WalletNotification.Informational.MissingAddresses(
missingAddressesCount = currencies.count(),
onGenerateClick = {
clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies)
},
),
condition = currencies.isNotEmpty(),
)
}
private fun Either<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
return fold(
ifLeft = { emptyList() },
ifRight = { tokenList ->
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
currencies
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency)
},
)
}
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver,
tokenList: Either<TokenListError, TokenList>,
isNeedToBackup: Boolean,
clickIntents: WalletClickIntentsV2,
) {
addIf(
element = WalletNotification.Warning.MissingBackup(
onStartBackupClick = clickIntents::onAddBackupCardClick,
),
condition = isNeedToBackup,
)
addIf(
element = WalletNotification.Warning.TestNetCard,
condition = cardTypesResolver.isTestCard(),
)
addIf(
element = WalletNotification.Warning.SomeNetworksUnreachable,
condition = tokenList.hasUnreachableNetworks(),
)
}
private fun Either<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
return fold(
ifLeft = { false },
ifRight = { tokenList ->
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
currencies.any { it.value is CryptoCurrencyStatus.Unreachable }
},
)
}
private fun MutableList<WalletNotification>.addRateTheAppNotification(
isReadyToShowRating: Boolean,
clickIntents: WalletClickIntentsV2,
) {
addIf(
element = WalletNotification.RateApp(
onLikeClick = clickIntents::onLikeAppClick,
onDislikeClick = clickIntents::onDislikeAppClick,
onCloseClick = clickIntents::onCloseRateAppWarningClick,
),
condition = isReadyToShowRating && readyForRateAppNotification,
)
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) {
add(element = element)
if (element is WalletNotification.Critical || element is WalletNotification.Warning) {
readyForRateAppNotification = false
}
}
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
}
}

View file

@ -0,0 +1,184 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.Either
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
@ViewModelScoped
internal class GetSingleWalletWarningsFactory @Inject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
) {
private var readyForRateAppNotification = false
fun create(clickIntents: WalletClickIntentsV2): Flow<ImmutableList<WalletNotification>> {
val userWallet = getSelectedWalletSyncUseCase().fold(
ifLeft = {
Timber.e("Failed to get selected wallet $it")
return flowOf(value = persistentListOf())
},
ifRight = { it },
)
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
return combine(
flow = getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
) { primaryCurrencyStatus, isReadyToShowRating, isNeedToBackup ->
readyForRateAppNotification = true
buildList {
addCriticalNotifications(cardTypesResolver)
addInformationalNotifications(cardTypesResolver)
addWarningNotifications(
userWallet,
cardTypesResolver,
primaryCurrencyStatus,
isNeedToBackup,
clickIntents,
)
addRateTheAppNotification(isReadyToShowRating, clickIntents)
}.toImmutableList()
}
}
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
addIf(
element = WalletNotification.Critical.DevCard,
condition = !cardTypesResolver.isReleaseFirmwareType(),
)
addIf(
element = WalletNotification.Critical.FailedCardValidation,
condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(),
)
cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures ->
addIf(
element = WalletNotification.Warning.LowSignatures(count = remainingSignatures),
condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT,
)
}
}
private fun MutableList<WalletNotification>.addInformationalNotifications(cardTypesResolver: CardTypesResolver) {
addIf(
element = WalletNotification.Informational.DemoCard,
condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
)
}
private suspend fun MutableList<WalletNotification>.addWarningNotifications(
userWallet: UserWallet,
cardTypesResolver: CardTypesResolver,
maybePrimaryCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>,
isNeedToBackup: Boolean,
clickIntents: WalletClickIntentsV2,
) {
val cryptoCurrencyStatus = maybePrimaryCurrencyStatus.fold(ifLeft = { null }, ifRight = { it })
addIf(
element = WalletNotification.Warning.MissingBackup(
onStartBackupClick = clickIntents::onAddBackupCardClick,
),
condition = isNeedToBackup,
)
addIf(
element = WalletNotification.Warning.TestNetCard,
condition = cardTypesResolver.isTestCard(),
)
addIf(
element = WalletNotification.Warning.NetworksUnreachable,
condition = cryptoCurrencyStatus?.value is CryptoCurrencyStatus.Unreachable,
)
addNoAccountWarning(cryptoCurrencyStatus)
addIf(
element = WalletNotification.Warning.NumberOfSignedHashesIncorrect(
onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick,
),
condition = hasSignedHashes(userWallet, cryptoCurrencyStatus),
)
}
private fun MutableList<WalletNotification>.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) {
val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount
if (noAccountStatus != null) {
add(
element = WalletNotification.Informational.NoAccount(
network = cryptoCurrencyStatus.currency.name,
amount = noAccountStatus.amountToCreateAccount.toString(),
symbol = cryptoCurrencyStatus.currency.symbol,
),
)
}
}
private suspend fun hasSignedHashes(
selectedWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus?,
): Boolean {
return cryptoCurrencyStatus?.currency?.network?.let {
hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it)
.conflate()
.distinctUntilChanged()
.firstOrNull()
} ?: false
}
private fun MutableList<WalletNotification>.addRateTheAppNotification(
isReadyToShowRating: Boolean,
clickIntents: WalletClickIntentsV2,
) {
addIf(
element = WalletNotification.RateApp(
onLikeClick = clickIntents::onLikeAppClick,
onDislikeClick = clickIntents::onDislikeAppClick,
onCloseClick = clickIntents::onCloseRateAppWarningClick,
),
condition = isReadyToShowRating && readyForRateAppNotification,
)
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) {
add(element = element)
if (element is WalletNotification.Critical || element is WalletNotification.Warning) {
readyForRateAppNotification = false
}
}
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.Either
import com.tangem.common.extensions.isZero
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import javax.inject.Inject
internal class WalletWithFundsChecker @Inject constructor(
private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase,
) {
suspend fun check(maybeTokenList: Either<TokenListError, TokenList>) {
val tokenList = (maybeTokenList as? Either.Right)?.value ?: return
val hasNonZeroWallets = when (tokenList) {
is TokenList.GroupedByNetwork -> {
tokenList.groups
.flatMap(NetworkGroup::currencies)
.hasNonZeroWallets()
}
is TokenList.Ungrouped -> tokenList.currencies.hasNonZeroWallets()
is TokenList.Empty -> false
}
if (hasNonZeroWallets) setWalletWithFundsFoundUseCase()
}
private fun List<CryptoCurrencyStatus>.hasNonZeroWallets(): Boolean {
return any {
val amount = it.value.amount ?: return@any false
!amount.isZero()
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetWarningsTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlin.coroutines.CoroutineContext
internal class MultiWalletWarningsSubscriber(
private val userWalletId: UserWalletId,
private val stateHolder: WalletStateHolderV2,
private val clickIntents: WalletClickIntentsV2,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
) : WalletSubscriber<ImmutableList<WalletNotification>>(name = "multi_wallet_warnings") {
override fun create(
coroutineScope: CoroutineScope,
uiDispatcher: CoroutineContext,
): Flow<ImmutableList<WalletNotification>> {
return getMultiWalletWarningsFactory.create(clickIntents)
.conflate()
.distinctUntilChanged()
.onEach {
stateHolder.update(
SetWarningsTransformer(userWalletId = userWalletId, warnings = it),
)
}
}
}

View file

@ -0,0 +1,92 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import arrow.core.Either
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetPrimaryCurrencyTransformer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import java.math.BigDecimal
import kotlin.coroutines.CoroutineContext
internal class PrimaryCurrencySubscriber(
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val stateHolder: WalletStateHolderV2,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) : WalletSubscriber<Either<CurrencyStatusError, CryptoCurrencyStatus>>(name = "primary_currency") {
override fun create(
coroutineScope: CoroutineScope,
uiDispatcher: CoroutineContext,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId)
.conflate()
.distinctUntilChanged()
.onEach(::updateContent)
.onEach(::sendAnalyticsEvent)
.onEach(::checkWalletWithFunds)
}
private fun updateContent(maybeCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>) {
val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return
stateHolder.update(
SetPrimaryCurrencyTransformer(
status = status.value,
userWallet = userWallet,
appCurrency = appCurrency,
),
)
}
private fun sendAnalyticsEvent(maybeCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>) {
val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return
val fiatAmount = status.value.fiatAmount
val cardBalanceState = when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.NoAmount,
-> createCardBalanceState(fiatAmount)
is CryptoCurrencyStatus.NoQuote -> AnalyticsParam.CardBalanceState.NoRate
is CryptoCurrencyStatus.Unreachable -> AnalyticsParam.CardBalanceState.BlockchainError
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.Custom,
-> null
}
cardBalanceState?.let {
analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it))
}
}
private fun createCardBalanceState(fiatAmount: BigDecimal?): AnalyticsParam.CardBalanceState? {
return when {
fiatAmount == null -> null
fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
}
private suspend fun checkWalletWithFunds(maybeCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>) {
val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return
if (status.value.amount?.isZero() == false) setWalletWithFundsFoundUseCase()
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetCryptoCurrencyActionsTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlin.coroutines.CoroutineContext
internal class SingleWalletButtonsSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateHolderV2,
private val clickIntents: WalletClickIntentsV2,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
) : WalletSubscriber<TokenActionsState>(name = "single_wallet_buttons") {
override fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow<TokenActionsState> {
return channelFlow {
getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status ->
getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status)
.conflate()
.distinctUntilChanged()
.firstOrNull()
?.let { send(it) }
}
}
.onEach(::updateContent)
}
private fun updateContent(tokenActionsState: TokenActionsState) {
stateHolder.update(
SetCryptoCurrencyActionsTransformer(
tokenActionsState = tokenActionsState,
userWallet = userWallet,
clickIntents = clickIntents,
),
)
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetWarningsTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlin.coroutines.CoroutineContext
/**
[REDACTED_AUTHOR]
*/
internal class SingleWalletNotificationsSubscriber(
private val userWalletId: UserWalletId,
private val stateHolder: WalletStateHolderV2,
private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory,
private val clickIntents: WalletClickIntentsV2,
) : WalletSubscriber<ImmutableList<WalletNotification>>(name = "single_wallet_warnings") {
override fun create(
coroutineScope: CoroutineScope,
uiDispatcher: CoroutineContext,
): Flow<ImmutableList<WalletNotification>> {
return getSingleWalletWarningsFactory.create(clickIntents)
.conflate()
.distinctUntilChanged()
.onEach {
stateHolder.update(
SetWarningsTransformer(userWalletId = userWalletId, warnings = it),
)
}
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlin.coroutines.CoroutineContext
@Suppress("LongParameterList")
internal class SingleWalletWithTokenListSubscriber(
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val stateHolder: WalletStateHolderV2,
private val clickIntents: WalletClickIntentsV2,
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getCardTokensListUseCase: GetCardTokensListUseCase,
) : WalletSubscriber<Either<TokenListError, TokenList>>(name = "single_wallet_with_token_list") {
override fun create(
coroutineScope: CoroutineScope,
uiDispatcher: CoroutineContext,
): Flow<Either<TokenListError, TokenList>> {
return getCardTokensListUseCase(userWalletId = userWallet.walletId)
.conflate()
.distinctUntilChanged()
.onEach(::updateContent)
.onEach(tokenListAnalyticsSender::send)
.onEach(walletWithFundsChecker::check)
}
private fun updateContent(maybeTokenList: Either<TokenListError, TokenList>) {
stateHolder.update(
maybeTokenList.fold(
ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) },
ifRight = {
SetTokenListTransformer(
tokenList = it,
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
)
},
),
)
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlin.coroutines.CoroutineContext
typealias MaybeTokenListFlow = Flow<Either<TokenListError, TokenList>>
@Suppress("LongParameterList")
internal class TokenListSubscriber(
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val stateHolder: WalletStateHolderV2,
private val clickIntents: WalletClickIntentsV2,
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getTokenListUseCase: GetTokenListUseCase,
) : WalletSubscriber<Either<TokenListError, TokenList>>(name = "token_list") {
override fun create(
coroutineScope: CoroutineScope,
uiDispatcher: CoroutineContext,
): Flow<Either<TokenListError, TokenList>> {
return getTokenListUseCase(userWalletId = userWallet.walletId)
.conflate()
.distinctUntilChanged()
.onEach(::updateContent)
.onEach(tokenListAnalyticsSender::send)
.onEach(walletWithFundsChecker::check)
}
private fun updateContent(maybeTokenList: Either<TokenListError, TokenList>) {
stateHolder.update(
maybeTokenList.fold(
ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) },
ifRight = {
SetTokenListTransformer(
tokenList = it,
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
)
},
),
)
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import androidx.paging.PagingData
import androidx.paging.cachedIn
import arrow.core.Either
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest
import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountTransformer
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlin.coroutines.CoroutineContext
typealias MaybeTxHistoryCount = Either<TxHistoryStateError, Int>
typealias MaybeTxHistoryItems = Either<TxHistoryListError, Flow<PagingData<TxHistoryItem>>>
@Suppress("LongParameterList")
internal class TxHistorySubscriber(
private val userWallet: UserWallet,
private val isRefresh: Boolean,
private val stateHolder: WalletStateHolderV2,
private val clickIntents: WalletClickIntentsV2,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
) : WalletSubscriber<PagingData<TxHistoryItem>>(name = "tx_history") {
override fun create(
coroutineScope: CoroutineScope,
uiDispatcher: CoroutineContext,
): Flow<PagingData<TxHistoryItem>> {
return flow {
getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status ->
val maybeTxHistoryItemCount = txHistoryItemsCountUseCase(
userWalletId = userWallet.walletId,
currency = status.currency,
)
setLoadingTxHistoryState(maybeTxHistoryItemCount, status)
maybeTxHistoryItemCount.onRight {
val maybeTxHistoryItems = txHistoryItemsUseCase(
userWalletId = userWallet.walletId,
currency = status.currency,
refresh = isRefresh,
).map { it.cachedIn(coroutineScope) }
setLoadedTxHistoryState(maybeTxHistoryItems)
}
}
}
}
private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) {
stateHolder.update(
maybeTxHistoryItemCount.fold(
ifLeft = {
SetTxHistoryCountErrorTransformer(
userWallet = userWallet,
error = it,
pendingTransactions = status.value.pendingTransactions,
clickIntents = clickIntents,
)
},
ifRight = {
SetTxHistoryCountTransformer(
userWalletId = userWallet.walletId,
transactionsCount = it,
clickIntents = clickIntents,
)
},
),
)
}
private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) {
stateHolder.update(
maybeTxHistoryItems.fold(
ifLeft = {
SetTxHistoryItemsErrorTransformer(
userWalletId = userWallet.walletId,
error = it,
clickIntents = clickIntents,
)
},
ifRight = {
SetTxHistoryItemsTransformer(
userWallet = userWallet,
flow = it,
clickIntents = clickIntents,
)
},
),
)
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
/**
* Component for implementation of flow subscription
*
* @property name unique name of subscriber
* [T] - type of flow
*
[REDACTED_AUTHOR]
*/
internal abstract class WalletSubscriber<T>(val name: String) {
protected abstract fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow<T>
fun subscribe(coroutineScope: CoroutineScope, dispatchers: CoroutineDispatcherProvider): Job {
Timber.d("Subscribe on $name")
return create(coroutineScope, dispatchers.main)
.flowOn(dispatchers.main)
.launchIn(coroutineScope)
}
}